@reopt-ai/opt-ui 1.12.5 → 1.13.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 (39) hide show
  1. package/CHANGELOG.md +236 -0
  2. package/COMPONENT_CATALOG.md +290 -111
  3. package/dist/core/index.cjs +13 -13
  4. package/dist/core/index.d.cts +2 -2
  5. package/dist/core/index.d.ts +2 -2
  6. package/dist/core/index.js +2 -2
  7. package/dist/docs/02-components/01-core.md +49 -49
  8. package/dist/docs/02-components/02-visuals.md +1 -1
  9. package/dist/docs/02-components/03-shells.md +260 -63
  10. package/dist/docs/02-components/04-surfaces.md +1 -1
  11. package/dist/docs/02-components/index.md +4 -4
  12. package/dist/{field-sidebar-Ben7MQD4.js → field-sidebar-CRdToHRO.js} +3975 -235
  13. package/dist/{field-sidebar-DwtrYSSt.cjs → field-sidebar-CxxSK4JO.cjs} +4338 -244
  14. package/dist/{field-sidebar-Ct8c9L4V.d.cts → field-sidebar-D-rwuN_w.d.cts} +824 -2
  15. package/dist/{field-sidebar-YaWHfLLW.d.ts → field-sidebar-Dz7J4ett.d.ts} +824 -2
  16. package/dist/id-registry.cjs +72 -0
  17. package/dist/id-registry.js +72 -0
  18. package/dist/id-registry.json +153 -0
  19. package/dist/index.cjs +334 -1419
  20. package/dist/index.d.cts +51 -86
  21. package/dist/index.d.ts +51 -86
  22. package/dist/index.js +248 -1356
  23. package/dist/{key-pad-menu-BpQebDyw.cjs → key-pad-menu-0KWOnELe.cjs} +51 -481
  24. package/dist/{key-pad-menu-CwcY_Vps.d.cts → key-pad-menu-BOVu97bj.d.cts} +4 -55
  25. package/dist/{key-pad-menu-CYYtS_Tl.js → key-pad-menu-DMmbQ1jF.js} +52 -416
  26. package/dist/{key-pad-menu-WBJShCJn.d.ts → key-pad-menu-NlZ9zNF8.d.ts} +4 -55
  27. package/dist/meta.cjs +896 -7
  28. package/dist/meta.js +896 -7
  29. package/dist/shells/index.cjs +20 -1
  30. package/dist/shells/index.d.cts +2 -2
  31. package/dist/shells/index.d.ts +2 -2
  32. package/dist/shells/index.js +2 -2
  33. package/dist/{text-truncate-Dlm7JwDf.js → text-truncate-Bd-0WkSP.js} +461 -97
  34. package/dist/{text-truncate-CqdiQuj8.cjs → text-truncate-BeYW6XZu.cjs} +523 -99
  35. package/dist/{text-truncate-CQyfUZvq.d.cts → text-truncate-eMzvU_7k.d.cts} +52 -1
  36. package/dist/{text-truncate-CQyfUZvq.d.ts → text-truncate-eMzvU_7k.d.ts} +52 -1
  37. package/dist/theme/server.cjs +2 -2
  38. package/dist/theme/server.js +2 -2
  39. package/package.json +6 -2
@@ -1,14 +1,182 @@
1
1
  "use client";
2
- import { $n as OPT_TEXT_LABEL, $t as ToolbarButton$1, A as FormRadio$1, An as OPT_BADGE_RED, At as DialogPanel$1, B as FormSubmit$1, Bn as OPT_FOCUS, Bt as TabsRoot$1, Dn as OPT_BADGE_BLUE, E as FormLabel$1, En as OPT_ANIMATE_ENTER_EXIT, F as FormSelect, Fn as OPT_DIVIDER, Ft as SelectRoot$1, Gn as OPT_ITEM_BASE, Gt as CompositeItem, Ht as DisclosureRoot$1, I as FormSelectItem, It as SelectTrigger$1, K as useFormStore, Kn as OPT_SURFACE, Kt as CompositeProvider, L as FormSelectPopover, Ln as OPT_DOT_GREEN, Lt as Tab$1, Mn as OPT_BORDER, Mt as SelectItem$1, Nn as OPT_DISCLOSURE_CONTENT, Nt as SelectLabel$1, On as OPT_BADGE_GREEN, P as FormRoot$1, Pn as OPT_DISCLOSURE_TRIGGER, Pt as SelectPopover$1, Rn as OPT_DOT_RED, Rt as TabList$1, T as FormInput$1, U as FormTextarea, Un as OPT_INPUT, Ut as DisclosureTrigger$1, V as FormSwitch$1, Vn as OPT_FOCUS_VISIBLE, Vt as DisclosureContent$1, Wt as Composite, Xn as OPT_TD, Y as TagInput, Yn as OPT_TABLE_ROW_ACTIVE, Yt as Meter$1, Zn as OPT_TD_MUTED, Zt as Input, an as TooltipProvider$1, ar as _objectWithoutProperties, at as MenuButtonArrow$1, b as FormError$1, bt as ComboboxPopover$1, c as Checkbox, cn as Badge, ct as MenuItemRadio$1, dt as MenuSeparator$1, en as ToolbarContainer$1, er as OPT_TEXT_PRIMARY, et as PopoverContent$1, ft as MenuTrigger$1, h as FormCheckbox$1, hn as Skeleton, ht as ComboboxEmpty, i as FieldToken, in as TooltipAnchor$1, ir as _objectSpread2, j as FormRadioGroup$1, jn as OPT_BADGE_YELLOW, jt as DialogRoot$1, kn as OPT_BADGE_NEUTRAL, kt as DialogHeading$1, ln as EmptyState, lt as MenuPopover$1, m as formatOptTime, mn as StatCard, mt as MenubarRoot$1, n as Highlight, nn as ToolbarSeparator$1, nr as OPT_TEXT_TERTIARY, nt as PopoverTrigger$1, on as Alert, or as Kbd, ot as MenuItem$1, pt as MenubarContainer$1, qn as OPT_SURFACE_POPOVER, qt as CompositeRow, rn as Tooltip$1, rr as OPT_TH, s as Pagination, sn as Spinner, sr as cn, st as MenuItemCheckbox$1, t as TextTruncate, tn as ToolbarRoot$1, tr as OPT_TEXT_SECONDARY, tt as PopoverRoot$1, u as OPT_UI_DEFAULT_LOCALE, un as Button, ut as MenuRoot$1, vt as ComboboxInput$1, wn as OPT_ACTIVE_ITEM, wt as DialogDescription$1, xt as ComboboxRoot$1, yt as ComboboxItem$1, zn as OPT_DOT_YELLOW, zt as TabPanel$1 } from "./text-truncate-Dlm7JwDf.js";
2
+ import { $t as CompositeItem, Bn as OPT_BADGE_NEUTRAL, Bt as SelectItem$1, C as FormCheckbox$1, Ct as ComboboxEmpty, Dt as ComboboxItem$1, En as Skeleton, Et as ComboboxInput$1, Fn as OPT_ACTIVE_ITEM, Ft as DialogFooter, G as FormSelectPopover, Gn as OPT_DISCLOSURE_TRIGGER, Gt as Tab$1, H as FormRoot$1, Hn as OPT_BADGE_YELLOW, Ht as SelectPopover$1, It as DialogHeader, J as FormSubmit$1, Jn as OPT_DOT_GREEN, Jt as TabsRoot$1, Kn as OPT_DIVIDER, Kt as TabList$1, Ln as OPT_ANIMATE_ENTER_EXIT, Lt as DialogHeading$1, Mt as DialogDescription$1, N as FormInput$1, O as FormError$1, Ot as ComboboxPopover$1, P as FormLabel$1, Qn as OPT_FOCUS_VISIBLE, Qt as Composite, R as FormRadio$1, Rn as OPT_BADGE_BLUE, Rt as DialogPanel$1, S as formatOptTime, St as MenubarRoot$1, Tn as StatCard, U as FormSelect, Un as OPT_BORDER, Ut as SelectRoot$1, Vn as OPT_BADGE_RED, Vt as SelectLabel$1, W as FormSelectItem, Wn as OPT_DISCLOSURE_CONTENT, Wt as SelectTrigger$1, Xn as OPT_DOT_YELLOW, Xt as DisclosureRoot$1, Y as FormSwitch$1, Yn as OPT_DOT_RED, Yt as DisclosureContent$1, Z as FormTextarea, Zn as OPT_FOCUS, Zt as DisclosureTrigger$1, _ as DropdownTrigger, _n as Spinner, _r as cn, _t as MenuPopover$1, b as formatOptNumber, bn as EmptyState, bt as MenuTrigger$1, c as ResizableHandle, cr as OPT_TD_MUTED, ct as PopoverContent$1, d as Checkbox, dn as ToolbarRoot$1, dr as OPT_TEXT_SECONDARY, en as CompositeProvider, er as OPT_INPUT, et as useFormStore, fn as ToolbarSeparator$1, fr as OPT_TEXT_TERTIARY, gn as Alert, gr as Kbd, gt as MenuItemRadio$1, h as DropdownMenu, hn as TooltipProvider, hr as _objectWithoutProperties, ht as MenuItemCheckbox$1, i as FieldToken, in as Textarea, ir as OPT_SURFACE_POPOVER, jt as DialogBody, kt as ComboboxRoot$1, l as ResizablePanel, ln as ToolbarButton$1, lr as OPT_TEXT_LABEL, lt as PopoverRoot$1, m as DropdownItem, mn as TooltipAnchor$1, mr as _objectSpread2, mt as MenuItem$1, n as Highlight, nr as OPT_ITEM_BASE, nt as DateRangePicker, o as resolveFieldType, on as Switch, or as OPT_TABLE_ROW_ACTIVE, p as DropdownContent, pn as Tooltip$1, pr as OPT_TH, pt as MenuButtonArrow$1, qt as TabPanel$1, rn as Meter$1, rr as OPT_SURFACE, rt as TagInput, s as Pagination, sn as Input, sr as OPT_TD, t as TextTruncate, tn as CompositeRow, tt as DEFAULT_PRESETS, u as ResizablePanelGroup, un as ToolbarContainer$1, ur as OPT_TEXT_PRIMARY, ut as PopoverTrigger$1, v as OPT_UI_DEFAULT_LOCALE, vn as Badge, vt as MenuRoot$1, xn as Button, xt as MenubarContainer$1, y as formatOptDateTime, yn as DescriptionList, yt as MenuSeparator$1, z as FormRadioGroup$1, zn as OPT_BADGE_GREEN, zt as DialogRoot$1 } from "./text-truncate-Bd-0WkSP.js";
3
3
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
4
  import * as React from "react";
5
5
  import { Fragment as Fragment$1, createContext, forwardRef, memo, useCallback, useContext, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
6
- import { CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandRoot, CommandSeparator } from "@reopt-ai/opt-ui-primitives";
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";
6
+ import { CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandRoot, CommandSeparator, Radio, RadioGroupRoot } from "@reopt-ai/opt-ui-primitives";
7
+ import { ArrowDown, ArrowUp, ArrowUpDown, Braces, ChartLine, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, CircleAlert, CircleCheck, Clock, Columns2, Columns3, Expand, Eye, Filter, Info, Minus, PanelLeftClose, PanelLeftOpen, Play, Plus, RefreshCw, Save, Search, SearchIcon, Square, Table2, TextCursorInput, Trash2, TriangleAlert, Undo2, X, ZoomIn, ZoomOut } from "lucide-react";
8
+ import { generatePalette } from "@reopt-ai/opt-palette";
9
+ import { AreaChart, BarChart, LineChart, PieChart, TimeSeriesChart, formatCountUnit, formatDuration, formatInstant, isDateOnly, isExactMinutes, isRelativeToNow, measureTimeWindow, parseRelativeParts, parseTimeExpression, resolveTimeRange, shiftTimeRange, toAbsoluteRange, zoomTimeRange } from "@reopt-ai/opt-charts";
9
10
  import { matchSorter } from "match-sorter";
10
11
  import Link from "next/link";
11
12
  import { usePathname } from "next/navigation";
13
+ //#region src/core/callout.tsx
14
+ const _excluded$1 = [
15
+ "tone",
16
+ "title",
17
+ "icon",
18
+ "action",
19
+ "size",
20
+ "className",
21
+ "children"
22
+ ];
23
+ const toneStyles = {
24
+ info: "border-info/40 bg-info-subtle",
25
+ success: "border-success/40 bg-success-subtle",
26
+ warning: "border-warning/40 bg-warning-subtle",
27
+ danger: "border-danger/40 bg-danger-subtle",
28
+ neutral: "border-border bg-bg-subtle"
29
+ };
30
+ const iconTone = {
31
+ info: "text-info-fg",
32
+ success: "text-success-fg",
33
+ warning: "text-warning-fg",
34
+ danger: "text-danger-fg",
35
+ neutral: "text-text-tertiary"
36
+ };
37
+ const titleTone = {
38
+ info: "text-info-fg",
39
+ success: "text-success-fg",
40
+ warning: "text-warning-fg",
41
+ danger: "text-danger-fg",
42
+ neutral: "text-text-primary"
43
+ };
44
+ const defaultIcons = {
45
+ info: Info,
46
+ success: CircleCheck,
47
+ warning: TriangleAlert,
48
+ danger: CircleAlert,
49
+ neutral: null
50
+ };
51
+ const sizeStyles$1 = {
52
+ sm: "gap-2 p-2.5 text-xs [&_svg]:size-3.5",
53
+ md: "gap-3 p-3 text-sm [&_svg]:size-4"
54
+ };
55
+ /**
56
+ * Callout — a notice that belongs to the page, not to the moment.
57
+ *
58
+ * `Alert` reports something that just happened and can be dismissed once read.
59
+ * A callout is the opposite: it is part of the layout, it is true for as long
60
+ * as the condition holds, and dismissing it would only hide a fact that is
61
+ * still true. "12 new events to triage" above a list, or "these numbers come
62
+ * from a 10,000-row sample" above a chart — both are read many times and
63
+ * dismissed never.
64
+ *
65
+ * That is why there is no `onDismiss` here, and why `action` takes one control:
66
+ * a notice earns its space by naming the single thing to do about it.
67
+ *
68
+ * @example
69
+ * ```tsx
70
+ * <Callout tone="info" title="정리할 신규 이벤트 12개"
71
+ * action={<Button size="sm" onClick={showNew}>보기</Button>}>
72
+ * 7일 안에 처음 관측된 이벤트입니다.
73
+ * </Callout>
74
+ * ```
75
+ */
76
+ function Callout(_ref) {
77
+ let { tone = "info", title, icon, action, size = "md", className, children } = _ref, props = _objectWithoutProperties(_ref, _excluded$1);
78
+ const DefaultIcon = defaultIcons[tone];
79
+ const glyph = icon === void 0 ? DefaultIcon ? /* @__PURE__ */ jsx(DefaultIcon, { "aria-hidden": "true" }) : null : icon;
80
+ return /* @__PURE__ */ jsxs("div", _objectSpread2(_objectSpread2({
81
+ role: "status",
82
+ className: cn("flex items-start rounded-lg border", toneStyles[tone], sizeStyles$1[size], className)
83
+ }, props), {}, {
84
+ "data-opt-id": "AT7TX",
85
+ "data-opt-slug": "callout.Callout",
86
+ "data-tone": tone,
87
+ children: [
88
+ glyph && /* @__PURE__ */ jsx("span", {
89
+ className: cn("mt-0.5 shrink-0", iconTone[tone]),
90
+ children: glyph
91
+ }),
92
+ /* @__PURE__ */ jsxs("div", {
93
+ className: "min-w-0 flex-1",
94
+ children: [title && /* @__PURE__ */ jsx("p", {
95
+ className: cn("font-medium", titleTone[tone]),
96
+ children: title
97
+ }), children && /* @__PURE__ */ jsx("div", {
98
+ className: cn("text-text-secondary", title && "mt-1"),
99
+ children
100
+ })]
101
+ }),
102
+ action && /* @__PURE__ */ jsx("div", {
103
+ className: "shrink-0 self-center",
104
+ children: action
105
+ })
106
+ ]
107
+ }));
108
+ }
109
+ //#endregion
110
+ //#region src/core/segmented-control.tsx
111
+ const sizeStyles = {
112
+ sm: "h-7 text-[11px] [&_svg]:size-3",
113
+ md: "h-8 text-xs [&_svg]:size-3.5"
114
+ };
115
+ const segmentPadding = {
116
+ sm: "gap-1 px-2",
117
+ md: "gap-1.5 px-2.5"
118
+ };
119
+ /**
120
+ * SegmentedControl — one of a few views, all of them visible.
121
+ *
122
+ * A `Select` hides the alternatives behind a click; a `ToggleGroup` presses
123
+ * buttons that may be on together. Neither is what "이벤트 | 속성" is: exactly
124
+ * one is true, the set is small enough to show, and the reader is choosing a
125
+ * view rather than filtering anything.
126
+ *
127
+ * Built on the radio-group primitive rather than on buttons, so the arrow keys,
128
+ * the group name and the checked state come from the platform. A control that
129
+ * merely looks like a radiogroup is one a keyboard cannot drive.
130
+ *
131
+ * @example
132
+ * ```tsx
133
+ * <SegmentedControl
134
+ * aria-label="카탈로그 탭"
135
+ * options={[
136
+ * { value: "events", label: "이벤트", badge: 128 },
137
+ * { value: "properties", label: "속성", badge: 341 },
138
+ * ]}
139
+ * value={tab}
140
+ * onChange={setTab}
141
+ * />
142
+ * ```
143
+ */
144
+ function SegmentedControl({ options, value, onChange, size = "md", fullWidth = false, disabled = false, "aria-label": ariaLabel, className }) {
145
+ const name = useId();
146
+ return /* @__PURE__ */ jsx(RadioGroupRoot, {
147
+ value,
148
+ onValueChange: (next) => onChange(next),
149
+ disabled,
150
+ orientation: "horizontal",
151
+ name,
152
+ "aria-label": ariaLabel,
153
+ className: cn("inline-flex items-center rounded-lg border border-border bg-bg-subtle p-0.5", fullWidth && "flex w-full", sizeStyles[size], className),
154
+ "data-opt-id": "YKENE",
155
+ "data-opt-slug": "segmented-control.SegmentedControl",
156
+ children: options.map((option) => {
157
+ const selected = option.value === value;
158
+ const itemDisabled = disabled || option.disabled;
159
+ return /* @__PURE__ */ jsxs("label", {
160
+ className: cn("relative inline-flex h-full cursor-pointer items-center justify-center", "rounded-md font-medium whitespace-nowrap transition-colors", "has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring has-[:focus-visible]:ring-offset-1 has-[:focus-visible]:ring-offset-ring-offset", segmentPadding[size], fullWidth && "flex-1", selected ? "bg-surface text-text-primary shadow-sm" : "text-text-tertiary hover:text-text-secondary", itemDisabled && "pointer-events-none opacity-50"),
161
+ "data-selected": selected || void 0,
162
+ children: [
163
+ /* @__PURE__ */ jsx(Radio, {
164
+ value: option.value,
165
+ disabled: itemDisabled,
166
+ className: "absolute inset-0 m-0 h-full w-full cursor-pointer opacity-0"
167
+ }),
168
+ option.icon,
169
+ /* @__PURE__ */ jsx("span", { children: option.label }),
170
+ option.badge != null && /* @__PURE__ */ jsx("span", {
171
+ className: cn("tabular-nums", selected ? "text-text-tertiary" : "text-text-tertiary/70"),
172
+ children: option.badge
173
+ })
174
+ ]
175
+ }, option.value);
176
+ })
177
+ });
178
+ }
179
+ //#endregion
12
180
  //#region src/core/command.tsx
13
181
  const _excluded = ["className"];
14
182
  const _excluded2 = [
@@ -253,7 +421,7 @@ function CommandPaletteTrigger({ commands, onSelect, labels }) {
253
421
  }
254
422
  //#endregion
255
423
  //#region src/shells/dashboard-grid.tsx
256
- const defaultLabels$23 = { ariaLabel: "대시보드 통계" };
424
+ const defaultLabels$34 = { ariaLabel: "대시보드 통계" };
257
425
  const MIN_DASHBOARD_COLUMNS = 1;
258
426
  const MAX_DASHBOARD_COLUMNS = 12;
259
427
  function normalizeDashboardColumns(columns) {
@@ -262,7 +430,7 @@ function normalizeDashboardColumns(columns) {
262
430
  }
263
431
  /** Renders the dashboard grid component. */
264
432
  function DashboardGrid({ stats, columns = 3, labels: customLabels }) {
265
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$23), customLabels);
433
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$34), customLabels);
266
434
  const columnCount = normalizeDashboardColumns(columns);
267
435
  const rowStyle = { "--opt-dashboard-grid-columns": `repeat(${columnCount}, minmax(0, 1fr))` };
268
436
  const rows = [];
@@ -290,13 +458,13 @@ function DashboardGrid({ stats, columns = 3, labels: customLabels }) {
290
458
  }
291
459
  //#endregion
292
460
  //#region src/shells/search-combobox.tsx
293
- const defaultLabels$22 = {
461
+ const defaultLabels$33 = {
294
462
  noResults: "결과가 없습니다.",
295
463
  loading: "Loading…"
296
464
  };
297
465
  /** Renders the search combobox component. */
298
466
  function SearchCombobox({ items, placeholder = "검색...", label = "검색", onSelect, labels: customLabels, async: isAsync = false, onSearch, debounceMs = 300 }) {
299
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$22), customLabels);
467
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$33), customLabels);
300
468
  const inputId = useId();
301
469
  const [query, setQuery] = useState("");
302
470
  const [asyncResults, setAsyncResults] = useState([]);
@@ -394,14 +562,14 @@ function SearchCombobox({ items, placeholder = "검색...", label = "검색", on
394
562
  }
395
563
  //#endregion
396
564
  //#region src/shells/status-select.tsx
397
- const defaultLabels$21 = {
565
+ const defaultLabels$32 = {
398
566
  selectLabel: "상태",
399
567
  emptyMessage: "사용 가능한 상태 옵션이 없습니다."
400
568
  };
401
569
  /** Renders the status select component. */
402
570
  function StatusSelect({ options, value: controlledValue, onChange, label, labels: customLabels }) {
403
571
  var _options$0$value, _options$;
404
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$21), customLabels);
572
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$32), customLabels);
405
573
  const resolvedLabel = label !== null && label !== void 0 ? label : labels.selectLabel;
406
574
  const [internalValue, setInternalValue] = useState((_options$0$value = (_options$ = options[0]) === null || _options$ === void 0 ? void 0 : _options$.value) !== null && _options$0$value !== void 0 ? _options$0$value : "");
407
575
  const value = controlledValue !== null && controlledValue !== void 0 ? controlledValue : internalValue;
@@ -468,7 +636,7 @@ function StatusSelect({ options, value: controlledValue, onChange, label, labels
468
636
  }
469
637
  //#endregion
470
638
  //#region src/shells/app-menubar.tsx
471
- const defaultLabels$20 = { ariaLabel: "Application menu" };
639
+ const defaultLabels$31 = { ariaLabel: "Application menu" };
472
640
  function MenuSection({ menu }) {
473
641
  return /* @__PURE__ */ jsxs(MenuRoot$1, { children: [/* @__PURE__ */ jsxs(MenuTrigger$1, { children: [menu.label, /* @__PURE__ */ jsx(MenuButtonArrow$1, {})] }), /* @__PURE__ */ jsx(MenuPopover$1, { children: menu.items.map((item, i) => {
474
642
  if (item.type === "separator") return /* @__PURE__ */ jsx(MenuSeparator$1, {}, `sep-${i}`);
@@ -512,7 +680,7 @@ function MenuSection({ menu }) {
512
680
  }
513
681
  /** Renders the app menubar component. */
514
682
  function AppMenubar({ menus, labels: customLabels }) {
515
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$20), customLabels);
683
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$31), customLabels);
516
684
  if (menus.length === 0) return null;
517
685
  return /* @__PURE__ */ jsx(MenubarRoot$1, {
518
686
  "data-opt-id": "BGKHW",
@@ -551,10 +719,10 @@ const sectionStyles = {
551
719
  Templates: "text-text-tertiary"
552
720
  };
553
721
  const defaultSectionStyle = "text-text-secondary";
554
- const defaultLabels$19 = { ariaLabel: "문서 네비게이션" };
722
+ const defaultLabels$30 = { ariaLabel: "문서 네비게이션" };
555
723
  /** Renders the sidebar nav component. */
556
724
  function SidebarNav({ items, labels: customLabels }) {
557
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$19), customLabels);
725
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$30), customLabels);
558
726
  const pathname = usePathname();
559
727
  const activeItem = items.find((item) => item.href === pathname);
560
728
  const [activeId, setActiveId] = useState(activeItem ? domId(activeItem.id) : void 0);
@@ -602,9 +770,9 @@ function SidebarNav({ items, labels: customLabels }) {
602
770
  }
603
771
  //#endregion
604
772
  //#region src/shells/editor-toolbar.tsx
605
- const defaultLabels$18 = { ariaLabel: "텍스트 편집기 툴바" };
773
+ const defaultLabels$29 = { ariaLabel: "텍스트 편집기 툴바" };
606
774
  function ToolbarButtonWithTooltip({ button, active, onAction }) {
607
- return /* @__PURE__ */ jsxs(TooltipProvider$1, { children: [/* @__PURE__ */ jsx(TooltipAnchor$1, { children: /* @__PURE__ */ jsx(ToolbarButton$1, {
775
+ return /* @__PURE__ */ jsxs(TooltipProvider, { children: [/* @__PURE__ */ jsx(TooltipAnchor$1, { children: /* @__PURE__ */ jsx(ToolbarButton$1, {
608
776
  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"}`,
609
777
  onClick: () => onAction === null || onAction === void 0 ? void 0 : onAction(button.id),
610
778
  children: button.icon
@@ -612,7 +780,7 @@ function ToolbarButtonWithTooltip({ button, active, onAction }) {
612
780
  }
613
781
  /** Renders the editor toolbar component. */
614
782
  function EditorToolbar({ buttons, onAction, activeButtons = [], labels: customLabels }) {
615
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$18), customLabels);
783
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$29), customLabels);
616
784
  const groups = [];
617
785
  let current = [];
618
786
  for (const btn of buttons) {
@@ -645,14 +813,14 @@ function EditorToolbar({ buttons, onAction, activeButtons = [], labels: customLa
645
813
  }
646
814
  //#endregion
647
815
  //#region src/shells/content-tabs.tsx
648
- const defaultLabels$17 = {
816
+ const defaultLabels$28 = {
649
817
  ariaLabel: "콘텐츠 탭",
650
818
  emptyMessage: "표시할 탭이 없습니다."
651
819
  };
652
820
  /** Renders the content tabs component. */
653
821
  function ContentTabs({ tabs, defaultTabId, labels: customLabels }) {
654
822
  var _tabs$, _tabs$find;
655
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$17), customLabels);
823
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$28), customLabels);
656
824
  const [selectedTabId, setSelectedTabId] = useState(defaultTabId !== null && defaultTabId !== void 0 ? defaultTabId : (_tabs$ = tabs[0]) === null || _tabs$ === void 0 ? void 0 : _tabs$.id);
657
825
  if (tabs.length === 0) return /* @__PURE__ */ jsx("div", {
658
826
  className: "text-text-secondary text-sm",
@@ -684,13 +852,13 @@ function ContentTabs({ tabs, defaultTabId, labels: customLabels }) {
684
852
  }
685
853
  //#endregion
686
854
  //#region src/shells/faq-accordion.tsx
687
- const defaultLabels$16 = {
855
+ const defaultLabels$27 = {
688
856
  ariaLabel: "자주 묻는 질문",
689
857
  emptyMessage: "등록된 FAQ가 없습니다."
690
858
  };
691
859
  /** Renders the faq accordion component. */
692
860
  function FaqAccordion({ items, labels: customLabels }) {
693
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$16), customLabels);
861
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$27), customLabels);
694
862
  if (items.length === 0) return /* @__PURE__ */ jsx("div", {
695
863
  className: "text-text-secondary text-sm",
696
864
  "data-opt-id": "QRBHV",
@@ -730,14 +898,14 @@ function FaqAccordion({ items, labels: customLabels }) {
730
898
  }
731
899
  //#endregion
732
900
  //#region src/shells/settings-form.tsx
733
- const defaultLabels$15 = {
901
+ const defaultLabels$26 = {
734
902
  formAriaLabel: "설정 폼",
735
903
  submitButton: "저장",
736
904
  successMessage: "설정이 저장되었습니다."
737
905
  };
738
906
  /** Renders the settings form component. */
739
907
  function SettingsForm({ fields, onSubmit, labels: customLabels }) {
740
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$15), customLabels);
908
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$26), customLabels);
741
909
  const [submitted, setSubmitted] = useState(false);
742
910
  const defaultValues = {};
743
911
  for (const field of fields) {
@@ -1063,7 +1231,7 @@ function ProjectSwitcher({ projects, value, onChange, placeholder = "프로젝
1063
1231
  ProjectSwitcher.displayName = "ProjectSwitcher";
1064
1232
  //#endregion
1065
1233
  //#region src/shells/env-panel.tsx
1066
- const defaultLabels$14 = {
1234
+ const defaultLabels$25 = {
1067
1235
  varCount: (count) => `${count}개 변수`,
1068
1236
  envVarsLabel: (env) => `${env} 환경 변수`,
1069
1237
  secretBadge: "secret"
@@ -1075,7 +1243,7 @@ const envDot = {
1075
1243
  };
1076
1244
  /** Renders the env panel component. */
1077
1245
  function EnvPanel({ groups, labels: customLabels }) {
1078
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$14), customLabels);
1246
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$25), customLabels);
1079
1247
  return /* @__PURE__ */ jsx("div", {
1080
1248
  className: `rounded-lg ${OPT_BORDER} divide-y ${OPT_DIVIDER} bg-surface`,
1081
1249
  "data-opt-id": "ZBDWK",
@@ -1148,7 +1316,7 @@ function EnvPanel({ groups, labels: customLabels }) {
1148
1316
  }
1149
1317
  //#endregion
1150
1318
  //#region src/shells/branch-select.tsx
1151
- const defaultLabels$13 = {
1319
+ const defaultLabels$24 = {
1152
1320
  selectLabel: "브랜치",
1153
1321
  emptySelectionLabel: "선택...",
1154
1322
  emptyMessage: "사용 가능한 브랜치가 없습니다.",
@@ -1157,7 +1325,7 @@ const defaultLabels$13 = {
1157
1325
  /** Renders the branch select component. */
1158
1326
  function BranchSelect({ branches, value: controlledValue, onChange, label, labels: customLabels }) {
1159
1327
  var _ref, _defaultBranch$name, _branches$, _selected$name;
1160
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$13), customLabels);
1328
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$24), customLabels);
1161
1329
  const resolvedLabel = label !== null && label !== void 0 ? label : labels.selectLabel;
1162
1330
  const defaultBranch = branches.find((b) => b.isDefault);
1163
1331
  const [internalValue, setInternalValue] = useState((_ref = (_defaultBranch$name = defaultBranch === null || defaultBranch === void 0 ? void 0 : defaultBranch.name) !== null && _defaultBranch$name !== void 0 ? _defaultBranch$name : (_branches$ = branches[0]) === null || _branches$ === void 0 ? void 0 : _branches$.name) !== null && _ref !== void 0 ? _ref : "");
@@ -1266,7 +1434,7 @@ function BranchSelect({ branches, value: controlledValue, onChange, label, label
1266
1434
  BranchSelect.displayName = "BranchSelect";
1267
1435
  //#endregion
1268
1436
  //#region src/shells/domain-table.tsx
1269
- const defaultLabels$12 = {
1437
+ const defaultLabels$23 = {
1270
1438
  domain: "Domain",
1271
1439
  status: "Status",
1272
1440
  ssl: "SSL",
@@ -1296,7 +1464,7 @@ const sslLabel = {
1296
1464
  };
1297
1465
  /** Renders the domain table component. */
1298
1466
  function DomainTable({ domains, title = "도메인", labels: customLabels }) {
1299
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$12), customLabels);
1467
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$23), customLabels);
1300
1468
  if (domains.length === 0) return /* @__PURE__ */ jsxs("div", {
1301
1469
  className: "gap-group flex flex-col",
1302
1470
  "data-opt-id": "A7SRM",
@@ -1414,14 +1582,14 @@ function DomainTable({ domains, title = "도메인", labels: customLabels }) {
1414
1582
  }
1415
1583
  //#endregion
1416
1584
  //#region src/shells/data-table.tsx
1417
- const defaultLabels$11 = {
1585
+ const defaultLabels$22 = {
1418
1586
  selectAll: "Select all",
1419
1587
  selectRow: (key) => `Select row ${key}`,
1420
1588
  resizeColumn: (header) => `Resize ${header}`,
1421
1589
  expandRow: (key) => `Expand row ${key}`,
1422
1590
  collapseRow: (key) => `Collapse row ${key}`
1423
1591
  };
1424
- const MIN_COLUMN_WIDTH = 40;
1592
+ const MIN_COLUMN_WIDTH$1 = 40;
1425
1593
  const DEFAULT_COLUMN_RESIZE_MAX = 1200;
1426
1594
  const alignmentClass = {
1427
1595
  left: "text-left",
@@ -1459,10 +1627,10 @@ function clampPercent(ratio) {
1459
1627
  return Math.min(100, Math.max(0, Math.round(ratio * 100)));
1460
1628
  }
1461
1629
  function getResizeHandleValue(currentWidth, configuredWidth) {
1462
- if (typeof currentWidth === "number" && Number.isFinite(currentWidth)) return Math.max(MIN_COLUMN_WIDTH, Math.round(currentWidth));
1630
+ if (typeof currentWidth === "number" && Number.isFinite(currentWidth)) return Math.max(MIN_COLUMN_WIDTH$1, Math.round(currentWidth));
1463
1631
  const pxWidth = configuredWidth === null || configuredWidth === void 0 ? void 0 : configuredWidth.trim().match(/^(\d+(?:\.\d+)?)px$/);
1464
- if (pxWidth) return Math.max(MIN_COLUMN_WIDTH, Math.round(Number(pxWidth[1])));
1465
- return MIN_COLUMN_WIDTH;
1632
+ if (pxWidth) return Math.max(MIN_COLUMN_WIDTH$1, Math.round(Number(pxWidth[1])));
1633
+ return MIN_COLUMN_WIDTH$1;
1466
1634
  }
1467
1635
  function DataTableProgressBar({ value, barColor, label }) {
1468
1636
  const percent = clampPercent(value);
@@ -1482,7 +1650,7 @@ function DataTableProgressBar({ value, barColor, label }) {
1482
1650
  }
1483
1651
  /** Renders the data table component. */
1484
1652
  function DataTable({ data, columns, keyExtractor, title, ariaLabel, onRowClick, emptyMessage = "No data available", activeRowId, sortable = false, selectable = false, selectedKeys, onSelectionChange, expandable = false, renderExpanded, pageSize, stickyHeader = false, resizableColumns = false, onSort: onSortExternal, labels: customLabels, className }) {
1485
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$11), customLabels);
1653
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$22), customLabels);
1486
1654
  const [sortCol, setSortCol] = useState(null);
1487
1655
  const [sortDir, setSortDir] = useState(null);
1488
1656
  const [expandedKeys, setExpandedKeys] = useState(/* @__PURE__ */ new Set());
@@ -1617,7 +1785,7 @@ function DataTable({ data, columns, keyExtractor, title, ariaLabel, onRowClick,
1617
1785
  role: "separator",
1618
1786
  "aria-orientation": "vertical",
1619
1787
  "aria-label": labels.resizeColumn(col.header),
1620
- "aria-valuemin": MIN_COLUMN_WIDTH,
1788
+ "aria-valuemin": MIN_COLUMN_WIDTH$1,
1621
1789
  "aria-valuemax": resizeMax,
1622
1790
  "aria-valuenow": resizeValue,
1623
1791
  "aria-valuetext": `${resizeValue} pixels`,
@@ -1628,14 +1796,14 @@ function DataTable({ data, columns, keyExtractor, title, ariaLabel, onRowClick,
1628
1796
  e.preventDefault();
1629
1797
  e.stopPropagation();
1630
1798
  if (e.key === "Home" || e.key === "End") {
1631
- setColWidths((prev) => _objectSpread2(_objectSpread2({}, prev), {}, { [col.id]: e.key === "Home" ? MIN_COLUMN_WIDTH : resizeMax }));
1799
+ setColWidths((prev) => _objectSpread2(_objectSpread2({}, prev), {}, { [col.id]: e.key === "Home" ? MIN_COLUMN_WIDTH$1 : resizeMax }));
1632
1800
  return;
1633
1801
  }
1634
1802
  const delta = e.shiftKey ? 40 : 10;
1635
1803
  const direction = e.key === "ArrowRight" ? 1 : -1;
1636
1804
  setColWidths((prev) => {
1637
1805
  const current = getResizeHandleValue(prev[col.id], col.width);
1638
- return _objectSpread2(_objectSpread2({}, prev), {}, { [col.id]: Math.max(MIN_COLUMN_WIDTH, current + direction * delta) });
1806
+ return _objectSpread2(_objectSpread2({}, prev), {}, { [col.id]: Math.max(MIN_COLUMN_WIDTH$1, current + direction * delta) });
1639
1807
  });
1640
1808
  },
1641
1809
  onMouseDown: (e) => {
@@ -1644,7 +1812,7 @@ function DataTable({ data, columns, keyExtractor, title, ariaLabel, onRowClick,
1644
1812
  const startX = e.clientX;
1645
1813
  const startW = e.target.parentElement.offsetWidth;
1646
1814
  const onMove = (ev) => {
1647
- const newW = Math.max(MIN_COLUMN_WIDTH, startW + ev.clientX - startX);
1815
+ const newW = Math.max(MIN_COLUMN_WIDTH$1, startW + ev.clientX - startX);
1648
1816
  setColWidths((prev) => _objectSpread2(_objectSpread2({}, prev), {}, { [col.id]: newW }));
1649
1817
  };
1650
1818
  const onUp = () => {
@@ -1802,7 +1970,7 @@ const toast = Object.assign((options) => toastStore.add(options), {
1802
1970
  clear: () => toastStore.clear()
1803
1971
  });
1804
1972
  const ToastContext = createContext(null);
1805
- const defaultLabels$10 = { regionLabel: "Notifications" };
1973
+ const defaultLabels$21 = { regionLabel: "Notifications" };
1806
1974
  const positionStyles = {
1807
1975
  "top-right": "top-4 right-4",
1808
1976
  "top-left": "top-4 left-4",
@@ -1811,7 +1979,7 @@ const positionStyles = {
1811
1979
  };
1812
1980
  /** Renders the toast provider component. */
1813
1981
  function ToastProvider({ children, position = "top-right", defaultDuration = 5e3, stackLimit = 5, labels: customLabels }) {
1814
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$10), customLabels);
1982
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$21), customLabels);
1815
1983
  const allToasts = useSyncExternalStore(toastStore.subscribe, toastStore.getSnapshot, toastStore.getSnapshot);
1816
1984
  const visibleToasts = allToasts.length > stackLimit ? allToasts.slice(-stackLimit) : allToasts;
1817
1985
  const addToast = useCallback((t) => toastStore.add(t), []);
@@ -1915,7 +2083,7 @@ function NotificationToast({ toast, onDismiss, defaultDuration = 5e3 }) {
1915
2083
  }
1916
2084
  //#endregion
1917
2085
  //#region src/shells/failure-list.tsx
1918
- const defaultLabels$9 = {
2086
+ const defaultLabels$20 = {
1919
2087
  title: "최근 실패",
1920
2088
  emptyMessage: "실패 항목이 없습니다",
1921
2089
  moreItems: "{count}개 더 보기",
@@ -2014,20 +2182,20 @@ function FailureItemRow({ failure, isLast, onFailureClick, renderActions, merged
2014
2182
  function FailureList({ failures, title, maxItems, grouped = false, onFailureClick, onShowMore, labels: customLabels, locale = "ko-KR", loading = false, renderActions, className }) {
2015
2183
  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;
2016
2184
  const mergedTimeAgo = {
2017
- justNow: (_customLabels$timeAgo = customLabels === null || customLabels === void 0 || (_customLabels$timeAgo2 = customLabels.timeAgo) === null || _customLabels$timeAgo2 === void 0 ? void 0 : _customLabels$timeAgo2.justNow) !== null && _customLabels$timeAgo !== void 0 ? _customLabels$timeAgo : defaultLabels$9.timeAgo.justNow,
2018
- minutesAgo: (_customLabels$timeAgo3 = customLabels === null || customLabels === void 0 || (_customLabels$timeAgo4 = customLabels.timeAgo) === null || _customLabels$timeAgo4 === void 0 ? void 0 : _customLabels$timeAgo4.minutesAgo) !== null && _customLabels$timeAgo3 !== void 0 ? _customLabels$timeAgo3 : defaultLabels$9.timeAgo.minutesAgo,
2019
- hoursAgo: (_customLabels$timeAgo5 = customLabels === null || customLabels === void 0 || (_customLabels$timeAgo6 = customLabels.timeAgo) === null || _customLabels$timeAgo6 === void 0 ? void 0 : _customLabels$timeAgo6.hoursAgo) !== null && _customLabels$timeAgo5 !== void 0 ? _customLabels$timeAgo5 : defaultLabels$9.timeAgo.hoursAgo,
2020
- daysAgo: (_customLabels$timeAgo7 = customLabels === null || customLabels === void 0 || (_customLabels$timeAgo8 = customLabels.timeAgo) === null || _customLabels$timeAgo8 === void 0 ? void 0 : _customLabels$timeAgo8.daysAgo) !== null && _customLabels$timeAgo7 !== void 0 ? _customLabels$timeAgo7 : defaultLabels$9.timeAgo.daysAgo
2185
+ 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$20.timeAgo.justNow,
2186
+ 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$20.timeAgo.minutesAgo,
2187
+ 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$20.timeAgo.hoursAgo,
2188
+ 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$20.timeAgo.daysAgo
2021
2189
  };
2022
2190
  const mergedSeverityLabels = {
2023
- critical: (_customLabels$severit = customLabels === null || customLabels === void 0 || (_customLabels$severit2 = customLabels.severityLabels) === null || _customLabels$severit2 === void 0 ? void 0 : _customLabels$severit2.critical) !== null && _customLabels$severit !== void 0 ? _customLabels$severit : defaultLabels$9.severityLabels.critical,
2024
- error: (_customLabels$severit3 = customLabels === null || customLabels === void 0 || (_customLabels$severit4 = customLabels.severityLabels) === null || _customLabels$severit4 === void 0 ? void 0 : _customLabels$severit4.error) !== null && _customLabels$severit3 !== void 0 ? _customLabels$severit3 : defaultLabels$9.severityLabels.error,
2025
- warning: (_customLabels$severit5 = customLabels === null || customLabels === void 0 || (_customLabels$severit6 = customLabels.severityLabels) === null || _customLabels$severit6 === void 0 ? void 0 : _customLabels$severit6.warning) !== null && _customLabels$severit5 !== void 0 ? _customLabels$severit5 : defaultLabels$9.severityLabels.warning
2191
+ 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$20.severityLabels.critical,
2192
+ 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$20.severityLabels.error,
2193
+ 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$20.severityLabels.warning
2026
2194
  };
2027
2195
  const labels = {
2028
- title: (_customLabels$title = customLabels === null || customLabels === void 0 ? void 0 : customLabels.title) !== null && _customLabels$title !== void 0 ? _customLabels$title : defaultLabels$9.title,
2029
- emptyMessage: (_customLabels$emptyMe = customLabels === null || customLabels === void 0 ? void 0 : customLabels.emptyMessage) !== null && _customLabels$emptyMe !== void 0 ? _customLabels$emptyMe : defaultLabels$9.emptyMessage,
2030
- moreItems: (_customLabels$moreIte = customLabels === null || customLabels === void 0 ? void 0 : customLabels.moreItems) !== null && _customLabels$moreIte !== void 0 ? _customLabels$moreIte : defaultLabels$9.moreItems
2196
+ title: (_customLabels$title = customLabels === null || customLabels === void 0 ? void 0 : customLabels.title) !== null && _customLabels$title !== void 0 ? _customLabels$title : defaultLabels$20.title,
2197
+ emptyMessage: (_customLabels$emptyMe = customLabels === null || customLabels === void 0 ? void 0 : customLabels.emptyMessage) !== null && _customLabels$emptyMe !== void 0 ? _customLabels$emptyMe : defaultLabels$20.emptyMessage,
2198
+ moreItems: (_customLabels$moreIte = customLabels === null || customLabels === void 0 ? void 0 : customLabels.moreItems) !== null && _customLabels$moreIte !== void 0 ? _customLabels$moreIte : defaultLabels$20.moreItems
2031
2199
  };
2032
2200
  const displayTitle = title !== null && title !== void 0 ? title : labels.title;
2033
2201
  const allItems = maxItems ? failures.slice(0, maxItems) : failures;
@@ -2103,7 +2271,7 @@ function renderGrouped(items, remainingCount, rowProps, labels, onShowMore) {
2103
2271
  }
2104
2272
  //#endregion
2105
2273
  //#region src/shells/key-value-editor.tsx
2106
- const defaultLabels$8 = {
2274
+ const defaultLabels$19 = {
2107
2275
  keyPlaceholder: "키",
2108
2276
  valuePlaceholder: "값",
2109
2277
  addButton: "추가",
@@ -2117,7 +2285,7 @@ function generateId() {
2117
2285
  }
2118
2286
  /** Renders the key value editor component. */
2119
2287
  function KeyValueEditor({ value: controlledValue, defaultValue, onChange, showSecretToggle = false, maxEntries, readOnly = false, labels: customLabels, renderActions, validateKey, className }) {
2120
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$8), customLabels);
2288
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$19), customLabels);
2121
2289
  const [internalEntries, setInternalEntries] = useState(() => defaultValue !== null && defaultValue !== void 0 ? defaultValue : []);
2122
2290
  const entries = controlledValue !== null && controlledValue !== void 0 ? controlledValue : internalEntries;
2123
2291
  const keyInputRef = useRef(null);
@@ -2413,53 +2581,3759 @@ function WorkflowCanvas({ steps, connections = [], rows = 9, cols = 9, cellSize
2413
2581
  }
2414
2582
  WorkflowCanvas.displayName = "WorkflowCanvas";
2415
2583
  //#endregion
2416
- //#region src/shells/event-timeline.tsx
2417
- const defaultLabels$7 = {
2418
- emptyTitle: "No events yet",
2419
- emptyDescription: "Events appear here as soon as they arrive.",
2420
- expand: "Show properties",
2421
- collapse: "Hide properties",
2422
- loadMore: "Load more",
2423
- gap: (duration) => `+${duration}`,
2424
- properties: "Event properties",
2425
- openGroup: (title) => `Open ${title}`
2584
+ //#region src/shells/flyout.tsx
2585
+ const defaultLabels$18 = { close: "Close panel" };
2586
+ const sizeClass = {
2587
+ sm: "sm:w-[22rem]",
2588
+ md: "sm:w-[30rem]",
2589
+ lg: "sm:w-[42rem]"
2426
2590
  };
2427
2591
  /**
2428
- * A line of text and the time it happened have to read as one row. Past about
2429
- * this width the eye stops connecting them, so the rail is capped rather than
2430
- * stretched to whatever the page allows. Override with `className` where a
2431
- * layout genuinely needs the full width.
2592
+ * Flyout a detail panel that does not take the page away.
2593
+ *
2594
+ * Navigating from a list to a record and back costs the reader their scroll
2595
+ * position, their filters and their place in the scan. A flyout keeps the list
2596
+ * on screen and shows the record beside it, so comparing three rows is three
2597
+ * clicks rather than six navigations.
2598
+ *
2599
+ * Deliberately **not** a modal: no focus trap, no `aria-modal`, no inert page.
2600
+ * The list behind it is still the point, and a reader who tabs out of the panel
2601
+ * to change a filter is doing the expected thing. Escape closes; focus returns
2602
+ * to whatever opened it.
2603
+ *
2604
+ * @example
2605
+ * ```tsx
2606
+ * <Flyout open={Boolean(selected)} onClose={clear} title={selected?.name}>
2607
+ * <DescriptionList items={fields} />
2608
+ * </Flyout>
2609
+ * ```
2432
2610
  */
2433
- const TIMELINE_WIDTH = "max-w-5xl";
2434
- const toneDot = {
2435
- default: "bg-text-tertiary",
2436
- accent: "bg-accent",
2437
- success: "bg-success",
2438
- warning: "bg-warning",
2439
- danger: "bg-danger"
2611
+ function Flyout({ open, onClose, title, description, type = "overlay", side = "right", size = "md", actions, footer, scrim = false, closeOnOutsideClick = true, hideCloseButton = false, labels: customLabels, className, children }) {
2612
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$18), customLabels);
2613
+ const panelRef = useRef(null);
2614
+ const openerRef = useRef(null);
2615
+ useEffect(() => {
2616
+ var _panelRef$current;
2617
+ if (!open) return;
2618
+ openerRef.current = document.activeElement;
2619
+ (_panelRef$current = panelRef.current) === null || _panelRef$current === void 0 || _panelRef$current.focus({ preventScroll: true });
2620
+ return () => {
2621
+ var _openerRef$current, _openerRef$current$fo;
2622
+ (_openerRef$current = openerRef.current) === null || _openerRef$current === void 0 || (_openerRef$current$fo = _openerRef$current.focus) === null || _openerRef$current$fo === void 0 || _openerRef$current$fo.call(_openerRef$current, { preventScroll: true });
2623
+ };
2624
+ }, [open]);
2625
+ useEffect(() => {
2626
+ if (!open) return;
2627
+ const handleKeyDown = (event) => {
2628
+ if (event.key === "Escape") {
2629
+ event.stopPropagation();
2630
+ onClose();
2631
+ }
2632
+ };
2633
+ document.addEventListener("keydown", handleKeyDown);
2634
+ return () => document.removeEventListener("keydown", handleKeyDown);
2635
+ }, [open, onClose]);
2636
+ useEffect(() => {
2637
+ if (!open || !closeOnOutsideClick || type !== "overlay") return;
2638
+ const handlePointerDown = (event) => {
2639
+ const panel = panelRef.current;
2640
+ if (panel && !panel.contains(event.target)) onClose();
2641
+ };
2642
+ const timer = window.setTimeout(() => {
2643
+ document.addEventListener("pointerdown", handlePointerDown);
2644
+ }, 0);
2645
+ return () => {
2646
+ window.clearTimeout(timer);
2647
+ document.removeEventListener("pointerdown", handlePointerDown);
2648
+ };
2649
+ }, [
2650
+ open,
2651
+ closeOnOutsideClick,
2652
+ type,
2653
+ onClose
2654
+ ]);
2655
+ const handleClose = useCallback(() => onClose(), [onClose]);
2656
+ if (!open) return null;
2657
+ const header = (title || actions || !hideCloseButton) && /* @__PURE__ */ jsxs("header", {
2658
+ className: cn("border-border-subtle flex items-start gap-2 border-b px-4 py-3"),
2659
+ children: [
2660
+ /* @__PURE__ */ jsxs("div", {
2661
+ className: "flex min-w-0 grow flex-col gap-0.5",
2662
+ children: [title && /* @__PURE__ */ jsx("h2", {
2663
+ className: cn("text-text-primary", "truncate text-base font-semibold"),
2664
+ children: title
2665
+ }), description && /* @__PURE__ */ jsx("p", {
2666
+ className: cn("text-text-secondary", "truncate text-xs"),
2667
+ children: description
2668
+ })]
2669
+ }),
2670
+ actions,
2671
+ !hideCloseButton && /* @__PURE__ */ jsx("button", {
2672
+ type: "button",
2673
+ onClick: handleClose,
2674
+ "aria-label": labels.close,
2675
+ className: cn("outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-ring-offset", "text-text-tertiary", "hover:text-text-primary shrink-0 rounded p-1 transition-colors"),
2676
+ children: /* @__PURE__ */ jsx(X, {
2677
+ className: "size-4",
2678
+ "aria-hidden": "true"
2679
+ })
2680
+ })
2681
+ ]
2682
+ });
2683
+ const panel = /* @__PURE__ */ jsxs("aside", {
2684
+ ref: panelRef,
2685
+ role: "dialog",
2686
+ "aria-modal": false,
2687
+ "aria-label": typeof title === "string" ? title : void 0,
2688
+ tabIndex: -1,
2689
+ className: cn(OPT_FOCUS_VISIBLE, "flex min-h-0 flex-col", type === "overlay" && [
2690
+ "rounded-xl bg-surface-raised shadow-md",
2691
+ "fixed inset-y-0 z-40 w-full",
2692
+ sizeClass[size],
2693
+ side === "right" ? "right-0 border-l" : "left-0 border-r",
2694
+ "border-border"
2695
+ ], type === "inline" && ["border border-border", "rounded-xl bg-surface"], className),
2696
+ "data-opt-id": "85XVE",
2697
+ "data-opt-slug": "flyout.Flyout",
2698
+ children: [
2699
+ header,
2700
+ /* @__PURE__ */ jsx("div", {
2701
+ className: "min-h-0 grow overflow-y-auto px-4 py-4",
2702
+ children
2703
+ }),
2704
+ footer && /* @__PURE__ */ jsx("footer", {
2705
+ className: "border-border-subtle border-t px-4 py-3",
2706
+ children: footer
2707
+ })
2708
+ ]
2709
+ });
2710
+ if (type === "inline") return panel;
2711
+ return /* @__PURE__ */ jsxs(Fragment, { children: [scrim && /* @__PURE__ */ jsx("div", {
2712
+ className: "bg-scrim fixed inset-0 z-30",
2713
+ "aria-hidden": "true"
2714
+ }), panel] });
2715
+ }
2716
+ Flyout.displayName = "Flyout";
2717
+ //#endregion
2718
+ //#region src/shells/query-results-table.tsx
2719
+ const defaultLabels$17 = {
2720
+ emptyMessage: "No results",
2721
+ loadingMessage: "Running query...",
2722
+ rowsSuffix: "row(s)",
2723
+ rowNumber: "#",
2724
+ sortAscending: (column) => `Sort ${column} ascending`,
2725
+ sortDescending: (column) => `Sort ${column} descending`,
2726
+ clearSort: (column) => `Clear sort on ${column}`,
2727
+ resizeColumn: (column) => `Resize ${column}`,
2728
+ openRow: (index) => `Open row ${index}`,
2729
+ rowDetailsTitle: (index) => `Row ${index}`,
2730
+ nullValue: "NULL",
2731
+ trueValue: "true",
2732
+ falseValue: "false"
2440
2733
  };
2441
- function toMillis(timestamp) {
2442
- const parsed = Date.parse(timestamp);
2443
- return Number.isNaN(parsed) ? 0 : parsed;
2734
+ const MIN_COLUMN_WIDTH = 64;
2735
+ const DEFAULT_COLUMN_WIDTH = 160;
2736
+ const WIDE_COLUMN_WIDTH = 360;
2737
+ const OVERSCAN$1 = 8;
2738
+ function normalizeColumns(columns) {
2739
+ return columns.map((column) => typeof column === "string" ? { name: column } : column);
2444
2740
  }
2445
- function stringifyValue(value) {
2446
- if (value === null || value === void 0) return "—";
2447
- if (typeof value === "string") return value;
2448
- if (typeof value === "number" || typeof value === "boolean") return String(value);
2449
- return JSON.stringify(value);
2741
+ function isNumericType(type) {
2742
+ const resolved = resolveFieldType(type);
2743
+ return resolved === "number" || resolved === "percent" || resolved === "currency" || resolved === "duration";
2450
2744
  }
2451
- function PropertyTable({ properties, ariaLabel }) {
2452
- const entries = Object.entries(properties);
2453
- if (entries.length === 0) return null;
2454
- return /* @__PURE__ */ jsx("dl", {
2455
- className: cn(OPT_BORDER, "gap-element bg-bg-subtle mt-2 grid grid-cols-1 rounded-lg p-2.5 sm:grid-cols-2"),
2456
- "aria-label": ariaLabel,
2457
- children: entries.map(([key, value]) => /* @__PURE__ */ jsxs("div", {
2458
- className: "flex min-w-0 flex-col",
2459
- children: [/* @__PURE__ */ jsx("dt", {
2460
- className: cn(OPT_TEXT_TERTIARY, "truncate text-xs"),
2461
- children: key
2462
- }), /* @__PURE__ */ jsx("dd", {
2745
+ function compareCells(a, b) {
2746
+ if (a == null && b == null) return 0;
2747
+ if (a == null) return 1;
2748
+ if (b == null) return -1;
2749
+ if (typeof a === "number" && typeof b === "number") return a - b;
2750
+ if (typeof a === "boolean" && typeof b === "boolean") return Number(a) - Number(b);
2751
+ const left = typeof a === "string" ? a : JSON.stringify(a);
2752
+ const right = typeof b === "string" ? b : JSON.stringify(b);
2753
+ const asNumbers = Number(left) - Number(right);
2754
+ if (Number.isFinite(asNumbers) && left.trim() !== "" && right.trim() !== "") return asNumbers;
2755
+ return left.localeCompare(right);
2756
+ }
2757
+ /** Render a cell value the way a query tool should: nothing is silently blank. */
2758
+ function formatQueryCell(value, labels) {
2759
+ if (value === null || value === void 0) return {
2760
+ text: labels.nullValue,
2761
+ isNull: true
2762
+ };
2763
+ if (typeof value === "boolean") return {
2764
+ text: value ? labels.trueValue : labels.falseValue,
2765
+ isNull: false
2766
+ };
2767
+ if (typeof value === "string") return {
2768
+ text: value,
2769
+ isNull: false
2770
+ };
2771
+ if (typeof value === "number" || typeof value === "bigint") return {
2772
+ text: String(value),
2773
+ isNull: false
2774
+ };
2775
+ try {
2776
+ return {
2777
+ text: JSON.stringify(value),
2778
+ isNull: false
2779
+ };
2780
+ } catch (_unused) {
2781
+ return {
2782
+ text: String(value),
2783
+ isNull: false
2784
+ };
2785
+ }
2786
+ }
2787
+ /**
2788
+ * QueryResultsTable — the answer, in the shape the database gave it.
2789
+ *
2790
+ * A result set is the one table the reader has not seen before, so the
2791
+ * header carries the type of each column and the cells never lie by omission:
2792
+ * `NULL` is written out, booleans are words, objects are their JSON. Sorting is
2793
+ * local because the rows are already here — a second round trip for an ORDER
2794
+ * BY the reader could have typed is a poor trade. Rows are windowed, so the
2795
+ * 10,000-row cap a backend applies is a scroll, not a stall.
2796
+ *
2797
+ * @example
2798
+ * ```tsx
2799
+ * <QueryResultsTable
2800
+ * columns={result.columns}
2801
+ * rows={result.rows}
2802
+ * maxHeight="fill"
2803
+ * footer={<span>{result.elapsed} ms</span>}
2804
+ * />
2805
+ * ```
2806
+ */
2807
+ function QueryResultsTable({ columns: rawColumns, rows, maxHeight = 400, emptyMessage, loading = false, error, sortable = true, sort: controlledSort, onSortChange, rowHeight = 32, rowDetails = true, footer, labels: customLabels, className }) {
2808
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$17), customLabels);
2809
+ const columns = useMemo(() => normalizeColumns(rawColumns), [rawColumns]);
2810
+ const fill = maxHeight === "fill";
2811
+ const [localSort, setLocalSort] = useState(null);
2812
+ const sort = controlledSort === void 0 ? localSort : controlledSort;
2813
+ const setSort = useCallback((next) => {
2814
+ if (controlledSort === void 0) setLocalSort(next);
2815
+ onSortChange === null || onSortChange === void 0 || onSortChange(next);
2816
+ }, [controlledSort, onSortChange]);
2817
+ useEffect(() => {
2818
+ if (sort && !columns.some((column) => column.name === sort.column)) setSort(null);
2819
+ }, [
2820
+ columns,
2821
+ sort,
2822
+ setSort
2823
+ ]);
2824
+ const sortedRows = useMemo(() => {
2825
+ if (!sort) return rows;
2826
+ const factor = sort.direction === "asc" ? 1 : -1;
2827
+ return rows.map((row, index) => ({
2828
+ row,
2829
+ index
2830
+ })).sort((a, b) => compareCells(a.row[sort.column], b.row[sort.column]) * factor || a.index - b.index).map((entry) => entry.row);
2831
+ }, [rows, sort]);
2832
+ const [widths, setWidths] = useState({});
2833
+ useEffect(() => {
2834
+ setWidths({});
2835
+ }, [columns]);
2836
+ const defaultWidths = useMemo(() => {
2837
+ const out = {};
2838
+ const sample = rows.slice(0, 200);
2839
+ const cellLabels = {
2840
+ nullValue: labels.nullValue,
2841
+ trueValue: labels.trueValue,
2842
+ falseValue: labels.falseValue
2843
+ };
2844
+ for (const column of columns) {
2845
+ let longest = column.name.length;
2846
+ for (const row of sample) {
2847
+ const { text } = formatQueryCell(row[column.name], cellLabels);
2848
+ if (text.length > longest) longest = text.length;
2849
+ if (longest > 100) break;
2850
+ }
2851
+ out[column.name] = longest > 100 ? WIDE_COLUMN_WIDTH : Math.min(WIDE_COLUMN_WIDTH, Math.max(DEFAULT_COLUMN_WIDTH * .6, longest * 8 + 32));
2852
+ }
2853
+ return out;
2854
+ }, [
2855
+ columns,
2856
+ rows,
2857
+ labels.nullValue,
2858
+ labels.trueValue,
2859
+ labels.falseValue
2860
+ ]);
2861
+ const resizeRef = useRef(null);
2862
+ const startResize = (event, column) => {
2863
+ var _ref, _widths$column, _target$setPointerCap;
2864
+ event.preventDefault();
2865
+ event.stopPropagation();
2866
+ const startWidth = (_ref = (_widths$column = widths[column]) !== null && _widths$column !== void 0 ? _widths$column : defaultWidths[column]) !== null && _ref !== void 0 ? _ref : DEFAULT_COLUMN_WIDTH;
2867
+ resizeRef.current = {
2868
+ column,
2869
+ startX: event.clientX,
2870
+ startWidth
2871
+ };
2872
+ const target = event.currentTarget;
2873
+ (_target$setPointerCap = target.setPointerCapture) === null || _target$setPointerCap === void 0 || _target$setPointerCap.call(target, event.pointerId);
2874
+ const move = (moveEvent) => {
2875
+ const current = resizeRef.current;
2876
+ if (!current) return;
2877
+ const next = Math.max(MIN_COLUMN_WIDTH, current.startWidth + (moveEvent.clientX - current.startX));
2878
+ setWidths((prev) => _objectSpread2(_objectSpread2({}, prev), {}, { [current.column]: next }));
2879
+ };
2880
+ const stop = () => {
2881
+ resizeRef.current = null;
2882
+ window.removeEventListener("pointermove", move);
2883
+ window.removeEventListener("pointerup", stop);
2884
+ };
2885
+ window.addEventListener("pointermove", move);
2886
+ window.addEventListener("pointerup", stop);
2887
+ };
2888
+ const scrollRef = useRef(null);
2889
+ const [viewport, setViewport] = useState({
2890
+ top: 0,
2891
+ height: 0
2892
+ });
2893
+ const onScroll = useCallback(() => {
2894
+ const el = scrollRef.current;
2895
+ if (!el) return;
2896
+ setViewport({
2897
+ top: el.scrollTop,
2898
+ height: el.clientHeight
2899
+ });
2900
+ }, []);
2901
+ useEffect(() => {
2902
+ const el = scrollRef.current;
2903
+ if (!el) return;
2904
+ setViewport({
2905
+ top: el.scrollTop,
2906
+ height: el.clientHeight
2907
+ });
2908
+ if (typeof ResizeObserver === "undefined") return;
2909
+ const observer = new ResizeObserver(() => {
2910
+ setViewport({
2911
+ top: el.scrollTop,
2912
+ height: el.clientHeight
2913
+ });
2914
+ });
2915
+ observer.observe(el);
2916
+ return () => observer.disconnect();
2917
+ }, [
2918
+ loading,
2919
+ error,
2920
+ rows.length
2921
+ ]);
2922
+ const total = sortedRows.length;
2923
+ const visibleCount = viewport.height > 0 ? Math.ceil(viewport.height / rowHeight) : 40;
2924
+ const start = Math.max(0, Math.floor(viewport.top / rowHeight) - OVERSCAN$1);
2925
+ const end = Math.min(total, start + visibleCount + 16);
2926
+ const topPad = start * rowHeight;
2927
+ const bottomPad = Math.max(0, (total - end) * rowHeight);
2928
+ const [selectedRow, setSelectedRow] = useState(null);
2929
+ useEffect(() => {
2930
+ setSelectedRow(null);
2931
+ }, [rows]);
2932
+ const cycleSort = (column) => {
2933
+ if (!sortable) return;
2934
+ if (!sort || sort.column !== column) setSort({
2935
+ column,
2936
+ direction: "asc"
2937
+ });
2938
+ else if (sort.direction === "asc") setSort({
2939
+ column,
2940
+ direction: "desc"
2941
+ });
2942
+ else setSort(null);
2943
+ };
2944
+ const sortLabel = (column) => {
2945
+ if (!sort || sort.column !== column) return labels.sortAscending(column);
2946
+ if (sort.direction === "asc") return labels.sortDescending(column);
2947
+ return labels.clearSort(column);
2948
+ };
2949
+ const frame = cn("border-border flex flex-col overflow-hidden rounded-lg border", fill && "h-full min-h-0", className);
2950
+ if (error) return /* @__PURE__ */ jsx("div", {
2951
+ className: cn("border-danger bg-danger-subtle rounded-lg border p-4", fill && "h-full min-h-0 overflow-auto", className),
2952
+ role: "alert",
2953
+ "aria-live": "assertive",
2954
+ "data-opt-id": "PNC1V",
2955
+ "data-opt-slug": "query-results-table.QueryResultsTable",
2956
+ children: /* @__PURE__ */ jsx("pre", {
2957
+ className: "text-danger-fg font-mono text-sm whitespace-pre-wrap",
2958
+ children: error
2959
+ })
2960
+ });
2961
+ if (loading) return /* @__PURE__ */ jsxs("div", {
2962
+ className: cn("border-border flex flex-col items-center justify-center rounded-lg border p-8 text-center", fill && "h-full min-h-0", className),
2963
+ role: "status",
2964
+ "aria-live": "polite",
2965
+ "aria-busy": "true",
2966
+ "data-opt-id": "PNC1V",
2967
+ "data-opt-slug": "query-results-table.QueryResultsTable",
2968
+ children: [/* @__PURE__ */ jsx("div", {
2969
+ className: "border-border border-t-accent inline-block h-5 w-5 animate-spin rounded-full border-2",
2970
+ "aria-hidden": "true"
2971
+ }), /* @__PURE__ */ jsx("p", {
2972
+ className: "text-text-tertiary mt-2 text-sm",
2973
+ children: labels.loadingMessage
2974
+ })]
2975
+ });
2976
+ if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2977
+ className: cn("border-border flex flex-col items-center justify-center rounded-lg border p-8 text-center", fill && "h-full min-h-0", className),
2978
+ "data-opt-id": "PNC1V",
2979
+ "data-opt-slug": "query-results-table.QueryResultsTable",
2980
+ children: /* @__PURE__ */ jsx("p", {
2981
+ className: "text-text-tertiary text-sm",
2982
+ children: emptyMessage !== null && emptyMessage !== void 0 ? emptyMessage : labels.emptyMessage
2983
+ })
2984
+ });
2985
+ const selected = selectedRow == null ? null : sortedRows[selectedRow];
2986
+ return /* @__PURE__ */ jsxs("div", {
2987
+ className: frame,
2988
+ "data-opt-id": "PNC1V",
2989
+ "data-opt-slug": "query-results-table.QueryResultsTable",
2990
+ children: [
2991
+ /* @__PURE__ */ jsx("div", {
2992
+ ref: scrollRef,
2993
+ onScroll,
2994
+ className: cn("overflow-auto", fill && "min-h-0 flex-1"),
2995
+ style: fill ? void 0 : { maxHeight },
2996
+ children: /* @__PURE__ */ jsxs("table", {
2997
+ className: "w-max min-w-full table-fixed border-separate border-spacing-0 text-sm",
2998
+ "aria-rowcount": total,
2999
+ children: [/* @__PURE__ */ jsx("thead", {
3000
+ className: "bg-surface-inset sticky top-0 z-10",
3001
+ children: /* @__PURE__ */ jsxs("tr", { children: [/* @__PURE__ */ jsx("th", {
3002
+ scope: "col",
3003
+ className: cn("text-text-tertiary w-12 px-3 py-2 text-left text-xs font-medium", "border-border-subtle border-b"),
3004
+ children: labels.rowNumber
3005
+ }), columns.map((column) => {
3006
+ var _ref2, _widths$column$name, _column$type, _column$type2;
3007
+ const width = (_ref2 = (_widths$column$name = widths[column.name]) !== null && _widths$column$name !== void 0 ? _widths$column$name : defaultWidths[column.name]) !== null && _ref2 !== void 0 ? _ref2 : DEFAULT_COLUMN_WIDTH;
3008
+ const active = (sort === null || sort === void 0 ? void 0 : sort.column) === column.name;
3009
+ const ariaSort = !active ? "none" : (sort === null || sort === void 0 ? void 0 : sort.direction) === "asc" ? "ascending" : "descending";
3010
+ const numeric = isNumericType(column.type);
3011
+ return /* @__PURE__ */ jsxs("th", {
3012
+ scope: "col",
3013
+ "aria-sort": sortable ? ariaSort : void 0,
3014
+ style: {
3015
+ width,
3016
+ minWidth: width,
3017
+ maxWidth: width
3018
+ },
3019
+ className: cn("group/th relative px-0 py-0 text-left text-xs font-medium", "border-border-subtle border-b"),
3020
+ children: [/* @__PURE__ */ jsxs("div", {
3021
+ className: cn("flex items-center gap-1.5 px-3 py-2", numeric && "flex-row-reverse text-right"),
3022
+ children: [column.type ? /* @__PURE__ */ jsx(FieldToken, {
3023
+ type: resolveFieldType(column.type),
3024
+ size: "xs",
3025
+ title: column.type
3026
+ }) : null, sortable ? /* @__PURE__ */ jsxs("button", {
3027
+ type: "button",
3028
+ className: cn("text-text-tertiary hover:text-text-primary flex min-w-0 flex-1 cursor-pointer items-center gap-1 truncate font-mono", numeric && "flex-row-reverse", active && "text-text-primary"),
3029
+ "aria-label": sortLabel(column.name),
3030
+ title: (_column$type = column.type) !== null && _column$type !== void 0 ? _column$type : column.name,
3031
+ onClick: () => cycleSort(column.name),
3032
+ children: [/* @__PURE__ */ jsx("span", {
3033
+ className: "truncate",
3034
+ children: column.name
3035
+ }), active ? (sort === null || sort === void 0 ? void 0 : sort.direction) === "asc" ? /* @__PURE__ */ jsx(ArrowUp, {
3036
+ className: "size-3 shrink-0",
3037
+ "aria-hidden": true
3038
+ }) : /* @__PURE__ */ jsx(ArrowDown, {
3039
+ className: "size-3 shrink-0",
3040
+ "aria-hidden": true
3041
+ }) : /* @__PURE__ */ jsx(ArrowUpDown, {
3042
+ className: "size-3 shrink-0 opacity-0 group-hover/th:opacity-60",
3043
+ "aria-hidden": true
3044
+ })]
3045
+ }) : /* @__PURE__ */ jsx("span", {
3046
+ className: "text-text-tertiary min-w-0 flex-1 truncate font-mono",
3047
+ title: (_column$type2 = column.type) !== null && _column$type2 !== void 0 ? _column$type2 : column.name,
3048
+ children: column.name
3049
+ })]
3050
+ }), /* @__PURE__ */ jsx("div", {
3051
+ role: "separator",
3052
+ "aria-orientation": "vertical",
3053
+ "aria-label": labels.resizeColumn(column.name),
3054
+ onPointerDown: (event) => startResize(event, column.name),
3055
+ className: cn("absolute top-0 right-0 h-full w-1.5 cursor-col-resize select-none", "hover:bg-accent/60 active:bg-accent")
3056
+ })]
3057
+ }, column.name);
3058
+ })] })
3059
+ }), /* @__PURE__ */ jsxs("tbody", { children: [
3060
+ topPad > 0 && /* @__PURE__ */ jsx("tr", {
3061
+ "aria-hidden": "true",
3062
+ children: /* @__PURE__ */ jsx("td", {
3063
+ colSpan: columns.length + 1,
3064
+ style: { height: topPad }
3065
+ })
3066
+ }),
3067
+ sortedRows.slice(start, end).map((row, offset) => {
3068
+ const index = start + offset;
3069
+ const isSelected = selectedRow === index;
3070
+ return /* @__PURE__ */ jsxs("tr", {
3071
+ "aria-rowindex": index + 1,
3072
+ "aria-selected": rowDetails ? isSelected : void 0,
3073
+ style: { height: rowHeight },
3074
+ className: cn("group/row border-border-subtle hover:bg-bg-subtle", isSelected && "bg-accent-subtle"),
3075
+ children: [/* @__PURE__ */ jsx("td", {
3076
+ className: cn(OPT_TEXT_TERTIARY, "border-border-subtle relative border-b px-3 font-mono text-xs tabular-nums"),
3077
+ children: rowDetails ? /* @__PURE__ */ jsxs("button", {
3078
+ type: "button",
3079
+ className: cn("flex w-full cursor-pointer items-center gap-1 text-left", "text-text-tertiary hover:text-text-primary"),
3080
+ "aria-label": labels.openRow(index + 1),
3081
+ onClick: () => setSelectedRow(index),
3082
+ children: [/* @__PURE__ */ jsx("span", { children: index + 1 }), /* @__PURE__ */ jsx(Expand, {
3083
+ className: "size-3 opacity-0 group-hover/row:opacity-100",
3084
+ "aria-hidden": true
3085
+ })]
3086
+ }) : index + 1
3087
+ }), columns.map((column) => {
3088
+ const { text, isNull } = formatQueryCell(row[column.name], labels);
3089
+ const numeric = isNumericType(column.type);
3090
+ return /* @__PURE__ */ jsx("td", {
3091
+ title: text,
3092
+ className: cn("border-border-subtle border-b px-3 font-mono text-xs whitespace-nowrap", "max-w-0 truncate", isNull ? "text-text-tertiary italic" : "text-text-primary", numeric && "text-right tabular-nums"),
3093
+ children: text
3094
+ }, column.name);
3095
+ })]
3096
+ }, index);
3097
+ }),
3098
+ bottomPad > 0 && /* @__PURE__ */ jsx("tr", {
3099
+ "aria-hidden": "true",
3100
+ children: /* @__PURE__ */ jsx("td", {
3101
+ colSpan: columns.length + 1,
3102
+ style: { height: bottomPad }
3103
+ })
3104
+ })
3105
+ ] })]
3106
+ })
3107
+ }),
3108
+ /* @__PURE__ */ jsxs("div", {
3109
+ className: cn("border-border-subtle bg-surface-inset flex flex-none items-center justify-between gap-3 border-t px-3 py-1.5"),
3110
+ children: [/* @__PURE__ */ jsxs("span", {
3111
+ className: "text-text-tertiary text-xs tabular-nums",
3112
+ children: [
3113
+ rows.length.toLocaleString(),
3114
+ " ",
3115
+ labels.rowsSuffix
3116
+ ]
3117
+ }), footer ? /* @__PURE__ */ jsx("span", {
3118
+ className: "text-text-tertiary flex items-center gap-3 text-xs",
3119
+ children: footer
3120
+ }) : null]
3121
+ }),
3122
+ rowDetails && /* @__PURE__ */ jsx(Flyout, {
3123
+ open: selected != null,
3124
+ onClose: () => setSelectedRow(null),
3125
+ title: labels.rowDetailsTitle((selectedRow !== null && selectedRow !== void 0 ? selectedRow : 0) + 1),
3126
+ size: "md",
3127
+ children: selected ? /* @__PURE__ */ jsx(DescriptionList, {
3128
+ layout: "stack",
3129
+ compressed: true,
3130
+ items: columns.map((column) => {
3131
+ const { text } = formatQueryCell(selected[column.name], labels);
3132
+ return {
3133
+ id: column.name,
3134
+ term: /* @__PURE__ */ jsxs("span", {
3135
+ className: "flex items-center gap-1.5 font-mono",
3136
+ children: [column.type ? /* @__PURE__ */ jsx(FieldToken, {
3137
+ type: resolveFieldType(column.type),
3138
+ size: "xs",
3139
+ title: column.type
3140
+ }) : null, column.name]
3141
+ }),
3142
+ description: /* @__PURE__ */ jsx("span", {
3143
+ className: "font-mono text-xs break-all",
3144
+ children: text
3145
+ }),
3146
+ copyValue: text,
3147
+ wrap: true
3148
+ };
3149
+ })
3150
+ }) : null
3151
+ })
3152
+ ]
3153
+ });
3154
+ }
3155
+ QueryResultsTable.displayName = "QueryResultsTable";
3156
+ //#endregion
3157
+ //#region src/shells/schema-tree.tsx
3158
+ const defaultLabels$16 = {
3159
+ heading: "Schema",
3160
+ searchPlaceholder: "Find a table or column",
3161
+ searchLabel: "Find a table or column",
3162
+ empty: "Nothing matches.",
3163
+ loading: "Loading schema…",
3164
+ insert: (name) => `Insert ${name} at cursor`,
3165
+ preview: (table) => `Preview ${table}`,
3166
+ toggle: (table) => `Toggle ${table}`,
3167
+ columnCount: (count) => `${count} columns`
3168
+ };
3169
+ /**
3170
+ * SchemaTree — what can be queried, and what it is called.
3171
+ *
3172
+ * A SQL editor without a schema beside it is an exam: the reader must already
3173
+ * know that the column is `created_at` and not `timestamp`. The tree answers
3174
+ * that at a glance, and its two actions are the two things people do with the
3175
+ * answer — put the name in the query, or look at the rows to see what the
3176
+ * name means.
3177
+ *
3178
+ * Search matches tables *and* columns, because "where is `utm_source`" is
3179
+ * asked more often than "what is in `sessions`".
3180
+ *
3181
+ * @example
3182
+ * ```tsx
3183
+ * <SchemaTree
3184
+ * tables={schema.tables}
3185
+ * onInsert={(name) => editor.current?.insertText(name)}
3186
+ * onPreview={(table) => run(`SELECT * FROM ${table} LIMIT 100`)}
3187
+ * />
3188
+ * ```
3189
+ */
3190
+ function SchemaTree({ tables, loading = false, defaultExpanded, onInsert, onPreview, header, className, labels: customLabels }) {
3191
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$16), customLabels);
3192
+ const [search, setSearch] = useState("");
3193
+ const [expanded, setExpanded] = useState(() => new Set(defaultExpanded !== null && defaultExpanded !== void 0 ? defaultExpanded : tables.slice(0, 1).map((t) => t.name)));
3194
+ const needle = search.trim().toLowerCase();
3195
+ const visible = useMemo(() => {
3196
+ if (!needle) return tables;
3197
+ return tables.flatMap((table) => {
3198
+ const tableMatches = table.name.toLowerCase().includes(needle);
3199
+ const columns = table.columns.filter((column) => column.name.toLowerCase().includes(needle));
3200
+ if (tableMatches) return [table];
3201
+ if (columns.length > 0) return [_objectSpread2(_objectSpread2({}, table), {}, { columns })];
3202
+ return [];
3203
+ });
3204
+ }, [tables, needle]);
3205
+ const isOpen = (table) => needle ? true : expanded.has(table);
3206
+ const toggle = (table) => {
3207
+ setExpanded((prev) => {
3208
+ const next = new Set(prev);
3209
+ if (next.has(table)) next.delete(table);
3210
+ else next.add(table);
3211
+ return next;
3212
+ });
3213
+ };
3214
+ return /* @__PURE__ */ jsxs("aside", {
3215
+ className: cn("flex min-h-0 flex-col", className),
3216
+ "aria-label": labels.heading,
3217
+ "data-opt-id": "WXX9P",
3218
+ "data-opt-slug": "schema-tree.SchemaTree",
3219
+ children: [
3220
+ header,
3221
+ /* @__PURE__ */ jsxs("div", {
3222
+ className: cn("flex items-center gap-2 border-b px-3 py-2", "focus-within:border-accent transition-colors", OPT_BORDER),
3223
+ children: [/* @__PURE__ */ jsx(Search, {
3224
+ className: "text-text-tertiary size-3.5 shrink-0",
3225
+ "aria-hidden": true
3226
+ }), /* @__PURE__ */ jsx("input", {
3227
+ type: "search",
3228
+ "aria-label": labels.searchLabel,
3229
+ placeholder: labels.searchPlaceholder,
3230
+ className: cn("text-text-primary placeholder:text-text-tertiary min-w-0 flex-1", "bg-transparent text-sm outline-none focus-visible:outline-none"),
3231
+ value: search,
3232
+ onChange: (event) => setSearch(event.target.value)
3233
+ })]
3234
+ }),
3235
+ /* @__PURE__ */ jsx("div", {
3236
+ className: "min-h-0 flex-1 overflow-y-auto py-1",
3237
+ children: loading ? /* @__PURE__ */ jsxs("p", {
3238
+ className: cn(OPT_TEXT_TERTIARY, "flex items-center gap-2 px-3 py-4 text-xs"),
3239
+ children: [/* @__PURE__ */ jsx(Spinner, {
3240
+ size: "sm",
3241
+ "aria-hidden": true
3242
+ }), labels.loading]
3243
+ }) : visible.length === 0 ? /* @__PURE__ */ jsx("p", {
3244
+ className: cn(OPT_TEXT_TERTIARY, "px-3 py-4 text-xs"),
3245
+ children: labels.empty
3246
+ }) : /* @__PURE__ */ jsx("ul", {
3247
+ role: "tree",
3248
+ "aria-label": labels.heading,
3249
+ children: visible.map((table) => {
3250
+ var _table$description;
3251
+ const open = isOpen(table.name);
3252
+ return /* @__PURE__ */ jsxs("li", {
3253
+ role: "treeitem",
3254
+ "aria-expanded": open,
3255
+ "aria-label": table.name,
3256
+ children: [/* @__PURE__ */ jsxs("div", {
3257
+ className: "hover:bg-bg-subtle group/table flex items-center gap-1 px-2 py-1",
3258
+ children: [/* @__PURE__ */ jsxs("button", {
3259
+ type: "button",
3260
+ className: "flex min-w-0 flex-1 cursor-pointer items-center gap-1.5 text-left",
3261
+ "aria-label": labels.toggle(table.name),
3262
+ "aria-expanded": open,
3263
+ onClick: () => toggle(table.name),
3264
+ title: (_table$description = table.description) !== null && _table$description !== void 0 ? _table$description : table.name,
3265
+ children: [
3266
+ /* @__PURE__ */ jsx(ChevronRight, {
3267
+ className: cn("text-text-tertiary size-3.5 shrink-0 transition-transform", open && "rotate-90"),
3268
+ "aria-hidden": true
3269
+ }),
3270
+ /* @__PURE__ */ jsx(Table2, {
3271
+ className: "text-text-tertiary size-3.5 shrink-0",
3272
+ "aria-hidden": true
3273
+ }),
3274
+ /* @__PURE__ */ jsx("span", {
3275
+ className: "text-text-primary truncate font-mono text-xs font-medium",
3276
+ children: /* @__PURE__ */ jsx(Highlight, {
3277
+ text: table.name,
3278
+ search: needle
3279
+ })
3280
+ }),
3281
+ table.kind ? /* @__PURE__ */ jsx("span", {
3282
+ className: cn(OPT_TEXT_TERTIARY, "shrink-0 text-[10px] uppercase"),
3283
+ children: table.kind
3284
+ }) : null,
3285
+ /* @__PURE__ */ jsx("span", {
3286
+ className: cn(OPT_TEXT_TERTIARY, "ml-auto shrink-0 text-[10px] tabular-nums"),
3287
+ children: table.columns.length
3288
+ })
3289
+ ]
3290
+ }), /* @__PURE__ */ jsxs("span", {
3291
+ className: cn("flex shrink-0 items-center gap-0.5", "opacity-0 transition-opacity", "group-hover/table:opacity-100 focus-within:opacity-100"),
3292
+ children: [onInsert && /* @__PURE__ */ jsx("button", {
3293
+ type: "button",
3294
+ className: "text-text-tertiary hover:text-text-primary cursor-pointer rounded-sm p-0.5",
3295
+ "aria-label": labels.insert(table.name),
3296
+ title: labels.insert(table.name),
3297
+ onClick: () => onInsert(table.name, "table", table.name),
3298
+ children: /* @__PURE__ */ jsx(TextCursorInput, {
3299
+ className: "size-3.5",
3300
+ "aria-hidden": true
3301
+ })
3302
+ }), onPreview && /* @__PURE__ */ jsx("button", {
3303
+ type: "button",
3304
+ className: "text-text-tertiary hover:text-text-primary cursor-pointer rounded-sm p-0.5",
3305
+ "aria-label": labels.preview(table.name),
3306
+ title: labels.preview(table.name),
3307
+ onClick: () => onPreview(table.name),
3308
+ children: /* @__PURE__ */ jsx(Eye, {
3309
+ className: "size-3.5",
3310
+ "aria-hidden": true
3311
+ })
3312
+ })]
3313
+ })]
3314
+ }), open && /* @__PURE__ */ jsx("ul", {
3315
+ role: "group",
3316
+ className: "pb-1",
3317
+ children: table.columns.map((column) => {
3318
+ var _column$type, _ref, _column$description;
3319
+ return /* @__PURE__ */ jsxs("li", {
3320
+ role: "treeitem",
3321
+ "aria-label": column.name,
3322
+ className: "hover:bg-bg-subtle group/column flex items-center gap-1.5 py-0.5 pr-2 pl-9",
3323
+ children: [
3324
+ /* @__PURE__ */ jsx(FieldToken, {
3325
+ type: resolveFieldType(column.type),
3326
+ size: "xs",
3327
+ title: (_column$type = column.type) !== null && _column$type !== void 0 ? _column$type : column.name
3328
+ }),
3329
+ /* @__PURE__ */ jsx("span", {
3330
+ className: "text-text-secondary min-w-0 flex-1 truncate font-mono text-xs",
3331
+ title: (_ref = (_column$description = column.description) !== null && _column$description !== void 0 ? _column$description : column.type) !== null && _ref !== void 0 ? _ref : column.name,
3332
+ children: /* @__PURE__ */ jsx(Highlight, {
3333
+ text: column.name,
3334
+ search: needle
3335
+ })
3336
+ }),
3337
+ column.type ? /* @__PURE__ */ jsx("span", {
3338
+ className: cn("text-text-tertiary", "hidden shrink-0 font-mono text-[10px] group-hover/column:inline"),
3339
+ children: column.type
3340
+ }) : null,
3341
+ onInsert && /* @__PURE__ */ jsx("button", {
3342
+ type: "button",
3343
+ className: cn("text-text-tertiary hover:text-text-primary cursor-pointer rounded-sm p-0.5", "opacity-0 group-hover/column:opacity-100 focus-visible:opacity-100"),
3344
+ "aria-label": labels.insert(column.name),
3345
+ title: labels.insert(column.name),
3346
+ onClick: () => onInsert(column.name, "column", table.name),
3347
+ children: /* @__PURE__ */ jsx(TextCursorInput, {
3348
+ className: "size-3.5",
3349
+ "aria-hidden": true
3350
+ })
3351
+ })
3352
+ ]
3353
+ }, column.name);
3354
+ })
3355
+ })]
3356
+ }, table.name);
3357
+ })
3358
+ })
3359
+ })
3360
+ ]
3361
+ });
3362
+ }
3363
+ SchemaTree.displayName = "SchemaTree";
3364
+ //#endregion
3365
+ //#region src/shells/query-toolbar.tsx
3366
+ const defaultLabels$15 = {
3367
+ run: "Run",
3368
+ cancel: "Cancel",
3369
+ runStatement: "Run statement at cursor",
3370
+ runSelection: "Run selection",
3371
+ runAll: "Run everything",
3372
+ runMenu: "More ways to run",
3373
+ save: "Save",
3374
+ changedHint: "Changes since the last run",
3375
+ currentHint: "This is what last ran",
3376
+ runningHint: "Running…"
3377
+ };
3378
+ /**
3379
+ * QueryToolbar — one button that always means "give me the answer".
3380
+ *
3381
+ * The run button carries state so the reader does not have to remember it:
3382
+ * a changed query shows as the accent colour, an unchanged one as neutral, and
3383
+ * a running one becomes Cancel. The alternatives (run selection, run the
3384
+ * statement at the cursor, run all) sit behind a split so the primary action
3385
+ * stays a single unambiguous target, which is what a keyboard shortcut is
3386
+ * bound to.
3387
+ *
3388
+ * @example
3389
+ * ```tsx
3390
+ * <QueryToolbar
3391
+ * onRun={runAtCursor}
3392
+ * onCancel={cancel}
3393
+ * running={isRunning}
3394
+ * runState={sql === lastRun ? "current" : "changed"}
3395
+ * onSave={openSaveDialog}
3396
+ * >
3397
+ * <QueryFiltersMenu … />
3398
+ * </QueryToolbar>
3399
+ * ```
3400
+ */
3401
+ function QueryToolbar({ onRun, onCancel, running = false, runState = "idle", runDisabledReason, onRunSelection, hasSelection = false, onRunAll, multiStatement = false, onSave, saveDisabled = false, children, trailing, modifierKey, className, labels: customLabels }) {
3402
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$15), customLabels);
3403
+ const showCancel = running && Boolean(onCancel);
3404
+ const disabled = Boolean(runDisabledReason) || running && !onCancel;
3405
+ const hint = running ? labels.runningHint : runState === "changed" ? labels.changedHint : runState === "current" ? labels.currentHint : void 0;
3406
+ const modifier = modifierKey !== null && modifierKey !== void 0 ? modifierKey : typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform) ? "⌘" : "Ctrl";
3407
+ const showRunMenu = Boolean(onRunSelection || onRunAll && multiStatement);
3408
+ return /* @__PURE__ */ jsxs("div", {
3409
+ role: "toolbar",
3410
+ "aria-label": labels.run,
3411
+ className: cn("bg-surface border-border flex min-h-11 flex-wrap items-center gap-2 border-b px-2 py-1.5", className),
3412
+ "data-opt-id": "9VWEE",
3413
+ "data-opt-slug": "query-toolbar.QueryToolbar",
3414
+ children: [
3415
+ /* @__PURE__ */ jsxs("div", {
3416
+ className: "flex items-center",
3417
+ children: [/* @__PURE__ */ jsxs(Button, {
3418
+ variant: showCancel ? "secondary" : "primary",
3419
+ size: "sm",
3420
+ onClick: showCancel ? onCancel : onRun,
3421
+ disabled,
3422
+ title: runDisabledReason !== null && runDisabledReason !== void 0 ? runDisabledReason : hint,
3423
+ "aria-label": showCancel ? labels.cancel : labels.run,
3424
+ className: cn(showRunMenu && "rounded-r-none", !showCancel && runState === "current" && "bg-bg-muted text-text-primary hover:bg-bg-subtle"),
3425
+ "data-run-state": running ? "running" : runState,
3426
+ children: [
3427
+ showCancel ? /* @__PURE__ */ jsx(Square, {
3428
+ className: "size-3.5",
3429
+ "aria-hidden": true
3430
+ }) : /* @__PURE__ */ jsx(Play, {
3431
+ className: "size-3.5",
3432
+ "aria-hidden": true
3433
+ }),
3434
+ /* @__PURE__ */ jsx("span", { children: showCancel ? labels.cancel : labels.run }),
3435
+ !showCancel && /* @__PURE__ */ jsxs(Kbd, {
3436
+ hint: true,
3437
+ className: "ml-1 hidden sm:inline-flex",
3438
+ children: [modifier, "↵"]
3439
+ })
3440
+ ]
3441
+ }), showRunMenu && /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownTrigger, {
3442
+ "aria-label": labels.runMenu,
3443
+ disabled: disabled || running,
3444
+ className: cn("bg-accent text-accent-fg hover:bg-accent/90 border-accent-fg/20 inline-flex h-8 cursor-pointer items-center rounded-r-lg border-l px-1.5", "disabled:cursor-not-allowed disabled:opacity-50", runState === "current" && "bg-bg-muted text-text-primary"),
3445
+ children: /* @__PURE__ */ jsx(ChevronDown, {
3446
+ className: "size-3.5",
3447
+ "aria-hidden": true
3448
+ })
3449
+ }), /* @__PURE__ */ jsxs(DropdownContent, { children: [
3450
+ /* @__PURE__ */ jsxs(DropdownItem, {
3451
+ onClick: onRun,
3452
+ children: [/* @__PURE__ */ jsx("span", {
3453
+ className: "flex-1",
3454
+ children: labels.runStatement
3455
+ }), /* @__PURE__ */ jsxs(Kbd, {
3456
+ hint: true,
3457
+ children: [modifier, "↵"]
3458
+ })]
3459
+ }),
3460
+ onRunSelection && /* @__PURE__ */ jsx(DropdownItem, {
3461
+ onClick: onRunSelection,
3462
+ disabled: !hasSelection,
3463
+ children: labels.runSelection
3464
+ }),
3465
+ onRunAll && multiStatement && /* @__PURE__ */ jsxs(DropdownItem, {
3466
+ onClick: onRunAll,
3467
+ children: [/* @__PURE__ */ jsx("span", {
3468
+ className: "flex-1",
3469
+ children: labels.runAll
3470
+ }), /* @__PURE__ */ jsxs(Kbd, {
3471
+ hint: true,
3472
+ children: [modifier, "⇧↵"]
3473
+ })]
3474
+ })
3475
+ ] })] })]
3476
+ }),
3477
+ children ? /* @__PURE__ */ jsx("div", {
3478
+ className: "flex min-w-0 flex-wrap items-center gap-2",
3479
+ children
3480
+ }) : null,
3481
+ /* @__PURE__ */ jsxs("div", {
3482
+ className: "ml-auto flex items-center gap-2",
3483
+ children: [trailing, onSave && /* @__PURE__ */ jsxs(Button, {
3484
+ variant: "secondary",
3485
+ size: "sm",
3486
+ onClick: onSave,
3487
+ disabled: saveDisabled,
3488
+ children: [/* @__PURE__ */ jsx(Save, {
3489
+ className: "size-3.5",
3490
+ "aria-hidden": true
3491
+ }), /* @__PURE__ */ jsx("span", { children: labels.save })]
3492
+ })]
3493
+ })
3494
+ ]
3495
+ });
3496
+ }
3497
+ QueryToolbar.displayName = "QueryToolbar";
3498
+ //#endregion
3499
+ //#region src/shells/query-output-pane.tsx
3500
+ const defaultLabels$14 = {
3501
+ results: "Results",
3502
+ chart: "Chart",
3503
+ split: "Both",
3504
+ viewLabel: "Output view",
3505
+ emptyTitle: "Results will appear here",
3506
+ emptyDescription: "Run the query to see what it returns.",
3507
+ emptyHint: null,
3508
+ loadingTitle: "Running query",
3509
+ loadingDescription: (seconds) => `${seconds}s`,
3510
+ errorTitle: "The query failed",
3511
+ cancelled: "The query was cancelled.",
3512
+ cancel: "Cancel",
3513
+ rows: (count) => `${formatOptNumber(count)} rows`,
3514
+ elapsed: (ms) => ms >= 1e3 ? `${(ms / 1e3).toFixed(2)} s` : `${ms} ms`,
3515
+ bytesRead: (bytes) => `${formatBytes(bytes)} read`,
3516
+ rowsRead: (count) => `${formatOptNumber(count)} rows scanned`,
3517
+ truncated: "Result capped by the backend",
3518
+ dismissError: "Dismiss"
3519
+ };
3520
+ function formatBytes(bytes) {
3521
+ if (bytes < 1024) return `${bytes} B`;
3522
+ const units = [
3523
+ "KB",
3524
+ "MB",
3525
+ "GB",
3526
+ "TB"
3527
+ ];
3528
+ let value = bytes / 1024;
3529
+ let unit = 0;
3530
+ while (value >= 1024 && unit < units.length - 1) {
3531
+ value /= 1024;
3532
+ unit++;
3533
+ }
3534
+ return `${value.toFixed(value >= 100 ? 0 : 1)} ${units[unit]}`;
3535
+ }
3536
+ /**
3537
+ * QueryOutputPane — where the answer lands, and what it cost.
3538
+ *
3539
+ * The pane owns the run lifecycle's *presentation*: nothing yet, running (with
3540
+ * a clock and a way out), failed (with the backend's words, not a paraphrase),
3541
+ * and done. Done is a table, a chart, or both side by side — the split is a
3542
+ * real resizable pair, because comparing a number to its bar is the point of
3543
+ * showing both. The status bar keeps the cost visible: rows, time, bytes
3544
+ * scanned, and whether the backend truncated the result, so a reader never
3545
+ * mistakes a capped answer for a complete one.
3546
+ *
3547
+ * @example
3548
+ * ```tsx
3549
+ * <QueryOutputPane
3550
+ * view={view} onViewChange={setView}
3551
+ * status={status} error={error} stats={stats}
3552
+ * results={<QueryResultsTable … maxHeight="fill" />}
3553
+ * chart={<QueryResultChart … />}
3554
+ * />
3555
+ * ```
3556
+ */
3557
+ function QueryOutputPane({ view, onViewChange, results, chart, status, error, runningSeconds = 0, onCancel, stats, actions, statusExtra, banner, onRetry, className, labels: customLabels }) {
3558
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$14), customLabels);
3559
+ const hasChart = chart != null;
3560
+ const effectiveView = hasChart ? view : "results";
3561
+ const options = [{
3562
+ value: "results",
3563
+ label: labels.results,
3564
+ icon: /* @__PURE__ */ jsx(Table2, { "aria-hidden": true })
3565
+ }, ...hasChart ? [{
3566
+ value: "chart",
3567
+ label: labels.chart,
3568
+ icon: /* @__PURE__ */ jsx(ChartLine, { "aria-hidden": true })
3569
+ }, {
3570
+ value: "split",
3571
+ label: labels.split,
3572
+ icon: /* @__PURE__ */ jsx(Columns2, { "aria-hidden": true })
3573
+ }] : []];
3574
+ let body;
3575
+ if (status === "idle") {
3576
+ var _labels$emptyHint;
3577
+ body = /* @__PURE__ */ jsx("div", {
3578
+ className: "flex flex-1 items-center justify-center p-6",
3579
+ children: /* @__PURE__ */ jsx(EmptyState, {
3580
+ title: labels.emptyTitle,
3581
+ description: labels.emptyDescription,
3582
+ tone: "subdued",
3583
+ titleSize: "sm",
3584
+ footer: (_labels$emptyHint = labels.emptyHint) !== null && _labels$emptyHint !== void 0 ? _labels$emptyHint : /* @__PURE__ */ jsx("span", {
3585
+ className: "flex items-center gap-1.5",
3586
+ children: /* @__PURE__ */ jsx(Kbd, { children: "⌘↵" })
3587
+ })
3588
+ })
3589
+ });
3590
+ } else if (status === "running") body = /* @__PURE__ */ jsxs("div", {
3591
+ className: "flex flex-1 flex-col items-center justify-center gap-3 p-6",
3592
+ role: "status",
3593
+ "aria-live": "polite",
3594
+ "aria-busy": "true",
3595
+ children: [
3596
+ /* @__PURE__ */ jsx(Spinner, { "aria-hidden": true }),
3597
+ /* @__PURE__ */ jsxs("p", {
3598
+ className: "text-text-secondary text-sm",
3599
+ children: [labels.loadingTitle, /* @__PURE__ */ jsx("span", {
3600
+ className: cn(OPT_TEXT_TERTIARY, "ml-2 tabular-nums"),
3601
+ children: labels.loadingDescription(runningSeconds)
3602
+ })]
3603
+ }),
3604
+ onCancel && /* @__PURE__ */ jsx(Button, {
3605
+ variant: "secondary",
3606
+ size: "sm",
3607
+ onClick: onCancel,
3608
+ children: labels.cancel
3609
+ })
3610
+ ]
3611
+ });
3612
+ else if (status === "error") body = /* @__PURE__ */ jsxs("div", {
3613
+ className: "flex flex-1 flex-col gap-3 overflow-auto p-4",
3614
+ role: "alert",
3615
+ "aria-live": "assertive",
3616
+ children: [/* @__PURE__ */ jsxs("div", {
3617
+ className: "flex items-start justify-between gap-3",
3618
+ children: [/* @__PURE__ */ jsx("p", {
3619
+ className: "text-danger-fg text-sm font-medium",
3620
+ children: labels.errorTitle
3621
+ }), onRetry && /* @__PURE__ */ jsx(Button, {
3622
+ variant: "ghost",
3623
+ size: "sm",
3624
+ onClick: onRetry,
3625
+ "aria-label": labels.dismissError,
3626
+ children: /* @__PURE__ */ jsx(X, {
3627
+ className: "size-3.5",
3628
+ "aria-hidden": true
3629
+ })
3630
+ })]
3631
+ }), /* @__PURE__ */ jsx("pre", {
3632
+ className: "bg-danger-subtle text-danger-fg max-h-64 overflow-auto rounded-lg p-3 font-mono text-xs whitespace-pre-wrap",
3633
+ children: error
3634
+ })]
3635
+ });
3636
+ else if (status === "cancelled") body = /* @__PURE__ */ jsx("div", {
3637
+ className: "flex flex-1 items-center justify-center p-6",
3638
+ children: /* @__PURE__ */ jsx(EmptyState, {
3639
+ title: labels.cancelled,
3640
+ tone: "subdued",
3641
+ titleSize: "sm"
3642
+ })
3643
+ });
3644
+ else if (effectiveView === "split") body = /* @__PURE__ */ jsxs(ResizablePanelGroup, {
3645
+ direction: "horizontal",
3646
+ className: "min-h-0 flex-1",
3647
+ children: [
3648
+ /* @__PURE__ */ jsx(ResizablePanel, {
3649
+ defaultSize: 50,
3650
+ minSize: 20,
3651
+ className: "min-h-0 p-2",
3652
+ children: results
3653
+ }),
3654
+ /* @__PURE__ */ jsx(ResizableHandle, { withHandle: true }),
3655
+ /* @__PURE__ */ jsx(ResizablePanel, {
3656
+ defaultSize: 50,
3657
+ minSize: 20,
3658
+ className: "min-h-0 p-2",
3659
+ children: chart
3660
+ })
3661
+ ]
3662
+ });
3663
+ else if (effectiveView === "chart") body = /* @__PURE__ */ jsx("div", {
3664
+ className: "min-h-0 flex-1 overflow-auto p-2",
3665
+ children: chart
3666
+ });
3667
+ else body = /* @__PURE__ */ jsx("div", {
3668
+ className: "min-h-0 flex-1 p-2",
3669
+ children: results
3670
+ });
3671
+ const showStats = status === "success" && stats;
3672
+ return /* @__PURE__ */ jsxs("section", {
3673
+ className: cn("bg-surface flex h-full min-h-0 flex-col", className),
3674
+ "aria-label": labels.results,
3675
+ "data-opt-id": "E82K9",
3676
+ "data-opt-slug": "query-output-pane.QueryOutputPane",
3677
+ children: [
3678
+ /* @__PURE__ */ jsxs("div", {
3679
+ className: "border-border flex min-h-10 flex-none items-center justify-between gap-2 border-b px-2 py-1",
3680
+ children: [options.length > 1 ? /* @__PURE__ */ jsx(SegmentedControl, {
3681
+ "aria-label": labels.viewLabel,
3682
+ options,
3683
+ value: effectiveView,
3684
+ onChange: onViewChange,
3685
+ size: "sm"
3686
+ }) : /* @__PURE__ */ jsx("span", {
3687
+ className: "text-text-secondary px-1 text-xs font-medium",
3688
+ children: labels.results
3689
+ }), actions ? /* @__PURE__ */ jsx("div", {
3690
+ className: "flex items-center gap-1.5",
3691
+ children: actions
3692
+ }) : null]
3693
+ }),
3694
+ banner ? /* @__PURE__ */ jsx("div", {
3695
+ className: "flex-none px-2 pt-2",
3696
+ children: banner
3697
+ }) : null,
3698
+ /* @__PURE__ */ jsx("div", {
3699
+ className: "flex min-h-0 flex-1 flex-col",
3700
+ children: body
3701
+ }),
3702
+ /* @__PURE__ */ jsxs("div", {
3703
+ className: cn("border-border-subtle bg-surface-inset flex min-h-7 flex-none flex-wrap items-center gap-x-3 gap-y-1 border-t px-3 py-1 text-xs", OPT_TEXT_TERTIARY),
3704
+ children: [showStats ? /* @__PURE__ */ jsxs(Fragment, { children: [
3705
+ stats.rowCount != null && /* @__PURE__ */ jsx("span", {
3706
+ className: "tabular-nums",
3707
+ children: labels.rows(stats.rowCount)
3708
+ }),
3709
+ stats.elapsed != null && /* @__PURE__ */ jsx("span", {
3710
+ className: "tabular-nums",
3711
+ children: labels.elapsed(stats.elapsed)
3712
+ }),
3713
+ stats.rowsRead != null && /* @__PURE__ */ jsx("span", {
3714
+ className: "tabular-nums",
3715
+ children: labels.rowsRead(stats.rowsRead)
3716
+ }),
3717
+ stats.bytesRead != null && /* @__PURE__ */ jsx("span", {
3718
+ className: "tabular-nums",
3719
+ children: labels.bytesRead(stats.bytesRead)
3720
+ }),
3721
+ stats.truncated && /* @__PURE__ */ jsx("span", {
3722
+ className: "text-warning-fg",
3723
+ children: labels.truncated
3724
+ })
3725
+ ] }) : null, statusExtra ? /* @__PURE__ */ jsx("span", {
3726
+ className: "ml-auto",
3727
+ children: statusExtra
3728
+ }) : null]
3729
+ })
3730
+ ]
3731
+ });
3732
+ }
3733
+ QueryOutputPane.displayName = "QueryOutputPane";
3734
+ //#endregion
3735
+ //#region src/shells/query-result-chart.tsx
3736
+ const defaultLabels$13 = {
3737
+ typeLabel: "Chart type",
3738
+ line: "Line",
3739
+ bar: "Bar",
3740
+ area: "Area",
3741
+ pie: "Pie",
3742
+ number: "Number",
3743
+ xAxis: "X axis",
3744
+ yAxis: "Values",
3745
+ stacked: "Stack",
3746
+ noNumeric: "Nothing to chart",
3747
+ noNumericDescription: "The result has no numeric column. Aggregate something — a count, a sum — to draw it.",
3748
+ pickSeries: "Choose a value column",
3749
+ pickSeriesDescription: "Pick at least one numeric column to plot.",
3750
+ chartLabel: "Query result chart"
3751
+ };
3752
+ const NUMERIC_TYPES = /* @__PURE__ */ new Set([
3753
+ "number",
3754
+ "percent",
3755
+ "currency",
3756
+ "duration"
3757
+ ]);
3758
+ function isNumericValue(value) {
3759
+ if (typeof value === "number") return Number.isFinite(value);
3760
+ if (typeof value === "string" && value.trim() !== "") return Number.isFinite(Number(value));
3761
+ return false;
3762
+ }
3763
+ /**
3764
+ * Which columns can be values. The declared type decides when the backend
3765
+ * gave one; a sample of the rows decides otherwise, so a `count()` that came
3766
+ * back as a string still charts.
3767
+ */
3768
+ function numericResultColumns(columns, rows) {
3769
+ const sample = rows.slice(0, 50);
3770
+ return columns.filter((column) => {
3771
+ if (column.type) return NUMERIC_TYPES.has(resolveFieldType(column.type));
3772
+ if (sample.length === 0) return false;
3773
+ let seen = 0;
3774
+ for (const row of sample) {
3775
+ const value = row[column.name];
3776
+ if (value == null) continue;
3777
+ seen++;
3778
+ if (!isNumericValue(value)) return false;
3779
+ }
3780
+ return seen > 0;
3781
+ });
3782
+ }
3783
+ /** A starting configuration for a fresh result: first non-numeric column as x, the numerics as y. */
3784
+ function defaultQueryChartConfig(columns, rows) {
3785
+ const numeric = numericResultColumns(columns, rows);
3786
+ const numericNames = new Set(numeric.map((column) => column.name));
3787
+ const x = columns.find((column) => !numericNames.has(column.name));
3788
+ const y = numeric.slice(0, 3).map((column) => column.name);
3789
+ if (rows.length === 1 && y.length >= 1 && !x) return {
3790
+ type: "number",
3791
+ yColumns: y.slice(0, 1)
3792
+ };
3793
+ const xType = resolveFieldType(x === null || x === void 0 ? void 0 : x.type);
3794
+ return {
3795
+ type: xType === "date" || xType === "datetime" ? "line" : "bar",
3796
+ xColumn: x === null || x === void 0 ? void 0 : x.name,
3797
+ yColumns: y
3798
+ };
3799
+ }
3800
+ function toNumber(value) {
3801
+ if (typeof value === "number") return value;
3802
+ if (typeof value === "string") return Number(value);
3803
+ return NaN;
3804
+ }
3805
+ function toLabel(value) {
3806
+ if (value == null) return "—";
3807
+ if (typeof value === "string") return value;
3808
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
3809
+ try {
3810
+ return JSON.stringify(value);
3811
+ } catch (_unused) {
3812
+ return String(value);
3813
+ }
3814
+ }
3815
+ /**
3816
+ * QueryResultChart — the same rows, as a picture.
3817
+ *
3818
+ * A result set is not a chart until someone says which column is the axis and
3819
+ * which are the values. This makes that a two-control decision with sensible
3820
+ * defaults: the first text or date column is the axis, the numeric columns are
3821
+ * the values, and a single-row single-number answer is just the number, large.
3822
+ * The configuration is the caller's, so it can be saved alongside the SQL.
3823
+ *
3824
+ * @example
3825
+ * ```tsx
3826
+ * const [config, setConfig] = useState(() => defaultQueryChartConfig(columns, rows));
3827
+ * <QueryResultChart columns={columns} rows={rows} config={config} onConfigChange={setConfig} />
3828
+ * ```
3829
+ */
3830
+ function QueryResultChart({ columns, rows, config, onConfigChange, showControls = true, height, maxPoints = 1e3, className, labels: customLabels }) {
3831
+ var _config$xColumn, _config$xColumn2, _config$yColumns$;
3832
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$13), customLabels);
3833
+ const numeric = useMemo(() => numericResultColumns(columns, rows), [columns, rows]);
3834
+ const numericNames = useMemo(() => new Set(numeric.map((column) => column.name)), [numeric]);
3835
+ useEffect(() => {
3836
+ const yColumns = config.yColumns.filter((name) => numericNames.has(name));
3837
+ const xValid = config.xColumn == null || columns.some((column) => column.name === config.xColumn);
3838
+ if (yColumns.length !== config.yColumns.length || !xValid) onConfigChange(_objectSpread2(_objectSpread2({}, config), {}, {
3839
+ yColumns,
3840
+ xColumn: xValid ? config.xColumn : void 0
3841
+ }));
3842
+ }, [
3843
+ columns,
3844
+ numericNames,
3845
+ config,
3846
+ onConfigChange
3847
+ ]);
3848
+ const data = useMemo(() => {
3849
+ const xColumn = config.xColumn;
3850
+ return rows.slice(0, maxPoints).map((row, index) => {
3851
+ const point = { name: xColumn ? toLabel(row[xColumn]) : String(index + 1) };
3852
+ for (const y of config.yColumns) {
3853
+ const value = toNumber(row[y]);
3854
+ point[y] = Number.isFinite(value) ? value : 0;
3855
+ }
3856
+ return point;
3857
+ });
3858
+ }, [
3859
+ rows,
3860
+ config.xColumn,
3861
+ config.yColumns,
3862
+ maxPoints
3863
+ ]);
3864
+ const series = useMemo(() => config.yColumns.map((name) => _objectSpread2({
3865
+ dataKey: name,
3866
+ name
3867
+ }, config.stacked && config.type !== "line" ? { stackId: "stack" } : {})), [
3868
+ config.yColumns,
3869
+ config.stacked,
3870
+ config.type
3871
+ ]);
3872
+ const typeOptions = [
3873
+ {
3874
+ value: "line",
3875
+ label: labels.line
3876
+ },
3877
+ {
3878
+ value: "bar",
3879
+ label: labels.bar
3880
+ },
3881
+ {
3882
+ value: "area",
3883
+ label: labels.area
3884
+ },
3885
+ {
3886
+ value: "pie",
3887
+ label: labels.pie
3888
+ },
3889
+ {
3890
+ value: "number",
3891
+ label: labels.number
3892
+ }
3893
+ ];
3894
+ let chart;
3895
+ const chartHeight = height !== null && height !== void 0 ? height : "100%";
3896
+ if (numeric.length === 0) chart = /* @__PURE__ */ jsx(EmptyState, {
3897
+ title: labels.noNumeric,
3898
+ description: labels.noNumericDescription,
3899
+ tone: "subdued",
3900
+ titleSize: "sm"
3901
+ });
3902
+ else if (config.yColumns.length === 0) chart = /* @__PURE__ */ jsx(EmptyState, {
3903
+ title: labels.pickSeries,
3904
+ description: labels.pickSeriesDescription,
3905
+ tone: "subdued",
3906
+ titleSize: "sm"
3907
+ });
3908
+ else if (config.type === "number") {
3909
+ var _rows$;
3910
+ const first = config.yColumns[0];
3911
+ const value = toNumber((_rows$ = rows[0]) === null || _rows$ === void 0 ? void 0 : _rows$[first]);
3912
+ chart = /* @__PURE__ */ jsxs("div", {
3913
+ className: "flex h-full flex-col items-center justify-center gap-1",
3914
+ children: [/* @__PURE__ */ jsx("span", {
3915
+ className: "text-text-primary text-4xl font-semibold tabular-nums",
3916
+ children: Number.isFinite(value) ? formatOptNumber(value) : "—"
3917
+ }), /* @__PURE__ */ jsx("span", {
3918
+ className: cn(OPT_TEXT_TERTIARY, "font-mono text-xs"),
3919
+ children: first
3920
+ })]
3921
+ });
3922
+ } else if (config.type === "pie") {
3923
+ const first = config.yColumns[0];
3924
+ chart = /* @__PURE__ */ jsx(PieChart, {
3925
+ data: data.map((point) => {
3926
+ var _point$first;
3927
+ return {
3928
+ name: point.name,
3929
+ value: Number((_point$first = point[first]) !== null && _point$first !== void 0 ? _point$first : 0)
3930
+ };
3931
+ }),
3932
+ height: chartHeight,
3933
+ showLegend: true,
3934
+ "aria-label": labels.chartLabel
3935
+ });
3936
+ } else if (config.type === "bar") chart = /* @__PURE__ */ jsx(BarChart, {
3937
+ data,
3938
+ series,
3939
+ stacked: config.stacked,
3940
+ height: chartHeight,
3941
+ showLegend: series.length > 1,
3942
+ "aria-label": labels.chartLabel
3943
+ });
3944
+ else if (config.type === "area") chart = /* @__PURE__ */ jsx(AreaChart, {
3945
+ data,
3946
+ series,
3947
+ height: chartHeight,
3948
+ showLegend: series.length > 1,
3949
+ "aria-label": labels.chartLabel
3950
+ });
3951
+ else chart = /* @__PURE__ */ jsx(LineChart, {
3952
+ data,
3953
+ series,
3954
+ height: chartHeight,
3955
+ showLegend: series.length > 1,
3956
+ "aria-label": labels.chartLabel
3957
+ });
3958
+ const singleSeries = config.type === "pie" || config.type === "number";
3959
+ return /* @__PURE__ */ jsxs("div", {
3960
+ className: cn("flex h-full min-h-0 flex-col gap-2", className),
3961
+ "data-opt-id": "5DSW7",
3962
+ "data-opt-slug": "query-result-chart.QueryResultChart",
3963
+ children: [showControls && numeric.length > 0 && /* @__PURE__ */ jsxs("div", {
3964
+ className: "flex flex-none flex-wrap items-center gap-2",
3965
+ children: [
3966
+ /* @__PURE__ */ jsx(SegmentedControl, {
3967
+ "aria-label": labels.typeLabel,
3968
+ options: typeOptions,
3969
+ value: config.type,
3970
+ onChange: (type) => onConfigChange(_objectSpread2(_objectSpread2({}, config), {}, { type })),
3971
+ size: "sm"
3972
+ }),
3973
+ config.type !== "number" && /* @__PURE__ */ jsxs(SelectRoot$1, {
3974
+ value: (_config$xColumn = config.xColumn) !== null && _config$xColumn !== void 0 ? _config$xColumn : "",
3975
+ onValueChange: (value) => onConfigChange(_objectSpread2(_objectSpread2({}, config), {}, { xColumn: typeof value === "string" && value ? value : void 0 })),
3976
+ children: [/* @__PURE__ */ jsxs(SelectTrigger$1, {
3977
+ className: "h-7 text-xs",
3978
+ minWidth: 120,
3979
+ "aria-label": labels.xAxis,
3980
+ children: [/* @__PURE__ */ jsx("span", {
3981
+ className: cn("text-text-tertiary", "mr-1"),
3982
+ children: labels.xAxis
3983
+ }), /* @__PURE__ */ jsx("span", {
3984
+ className: "font-mono",
3985
+ children: (_config$xColumn2 = config.xColumn) !== null && _config$xColumn2 !== void 0 ? _config$xColumn2 : "—"
3986
+ })]
3987
+ }), /* @__PURE__ */ jsx(SelectPopover$1, { children: columns.map((column) => /* @__PURE__ */ jsxs(SelectItem$1, {
3988
+ value: column.name,
3989
+ children: [/* @__PURE__ */ jsx(FieldToken, {
3990
+ type: resolveFieldType(column.type),
3991
+ size: "xs",
3992
+ decorative: true
3993
+ }), /* @__PURE__ */ jsx("span", {
3994
+ className: "font-mono text-xs",
3995
+ children: column.name
3996
+ })]
3997
+ }, column.name)) })]
3998
+ }),
3999
+ /* @__PURE__ */ jsxs(SelectRoot$1, {
4000
+ multiple: !singleSeries,
4001
+ value: singleSeries ? (_config$yColumns$ = config.yColumns[0]) !== null && _config$yColumns$ !== void 0 ? _config$yColumns$ : "" : config.yColumns,
4002
+ onValueChange: (value) => onConfigChange(_objectSpread2(_objectSpread2({}, config), {}, { yColumns: Array.isArray(value) ? value : value ? [value] : [] })),
4003
+ children: [/* @__PURE__ */ jsxs(SelectTrigger$1, {
4004
+ className: "h-7 text-xs",
4005
+ minWidth: 140,
4006
+ "aria-label": labels.yAxis,
4007
+ children: [/* @__PURE__ */ jsx("span", {
4008
+ className: cn("text-text-tertiary", "mr-1"),
4009
+ children: labels.yAxis
4010
+ }), /* @__PURE__ */ jsx("span", {
4011
+ className: "truncate font-mono",
4012
+ children: config.yColumns.length > 0 ? config.yColumns.join(", ") : "—"
4013
+ })]
4014
+ }), /* @__PURE__ */ jsx(SelectPopover$1, { children: numeric.map((column) => /* @__PURE__ */ jsxs(SelectItem$1, {
4015
+ value: column.name,
4016
+ children: [/* @__PURE__ */ jsx(FieldToken, {
4017
+ type: resolveFieldType(column.type),
4018
+ size: "xs",
4019
+ decorative: true
4020
+ }), /* @__PURE__ */ jsx("span", {
4021
+ className: "font-mono text-xs",
4022
+ children: column.name
4023
+ })]
4024
+ }, column.name)) })]
4025
+ }),
4026
+ (config.type === "bar" || config.type === "area") && config.yColumns.length > 1 && /* @__PURE__ */ jsx(Checkbox, {
4027
+ size: "sm",
4028
+ label: labels.stacked,
4029
+ checked: Boolean(config.stacked),
4030
+ onChange: (stacked) => onConfigChange(_objectSpread2(_objectSpread2({}, config), {}, { stacked }))
4031
+ })
4032
+ ]
4033
+ }), /* @__PURE__ */ jsx("div", {
4034
+ className: "min-h-0 flex-1",
4035
+ children: chart
4036
+ })]
4037
+ });
4038
+ }
4039
+ QueryResultChart.displayName = "QueryResultChart";
4040
+ //#endregion
4041
+ //#region src/shells/query-filters-menu.tsx
4042
+ const OPERATOR_LABELS = {
4043
+ is: "is",
4044
+ isNot: "is not",
4045
+ contains: "contains",
4046
+ notContains: "does not contain",
4047
+ startsWith: "starts with",
4048
+ endsWith: "ends with",
4049
+ regex: "matches regex",
4050
+ gt: ">",
4051
+ gte: "≥",
4052
+ lt: "<",
4053
+ lte: "≤",
4054
+ isNull: "is null",
4055
+ isNotNull: "is not null"
4056
+ };
4057
+ const defaultLabels$12 = {
4058
+ trigger: "Filters",
4059
+ title: "Query filters",
4060
+ dateRange: "Time range",
4061
+ dateRangePlaceholder: "Select date range",
4062
+ properties: "Property filters",
4063
+ addProperty: "Add filter",
4064
+ removeProperty: (field) => `Remove filter on ${field}`,
4065
+ field: "Field",
4066
+ operator: "Operator",
4067
+ value: "Value",
4068
+ reset: "Reset",
4069
+ insertPlaceholder: "Insert {filters}",
4070
+ missingPlaceholderTitle: "Filters are set but not applied",
4071
+ missingPlaceholder: "Put {filters} in the WHERE clause of your query so these conditions reach the database.",
4072
+ placeholderHint: "Use {filters} in the WHERE clause to apply these to the query.",
4073
+ valuesLoading: "Loading values…",
4074
+ noValues: "No matching values. Enter is fine too.",
4075
+ operators: OPERATOR_LABELS,
4076
+ activeCount: (count) => `${count} active`
4077
+ };
4078
+ /** Operators without a value. */
4079
+ const VALUELESS_OPERATORS = /* @__PURE__ */ new Set(["isNull", "isNotNull"]);
4080
+ const OPERATOR_ORDER = [
4081
+ "is",
4082
+ "isNot",
4083
+ "contains",
4084
+ "notContains",
4085
+ "startsWith",
4086
+ "endsWith",
4087
+ "regex",
4088
+ "gt",
4089
+ "gte",
4090
+ "lt",
4091
+ "lte",
4092
+ "isNull",
4093
+ "isNotNull"
4094
+ ];
4095
+ /** True when any filter would change the query. */
4096
+ function hasActiveQueryFilters(filters) {
4097
+ var _filters$dateRange, _filters$dateRange2;
4098
+ return Boolean(((_filters$dateRange = filters.dateRange) === null || _filters$dateRange === void 0 ? void 0 : _filters$dateRange.start) || ((_filters$dateRange2 = filters.dateRange) === null || _filters$dateRange2 === void 0 ? void 0 : _filters$dateRange2.end)) || filters.properties.length > 0;
4099
+ }
4100
+ /** How many filters are set, for the badge. */
4101
+ function countActiveQueryFilters(filters) {
4102
+ var _filters$dateRange3, _filters$dateRange4;
4103
+ return (((_filters$dateRange3 = filters.dateRange) === null || _filters$dateRange3 === void 0 ? void 0 : _filters$dateRange3.start) || ((_filters$dateRange4 = filters.dateRange) === null || _filters$dateRange4 === void 0 ? void 0 : _filters$dateRange4.end) ? 1 : 0) + filters.properties.length;
4104
+ }
4105
+ /** An empty filter set. */
4106
+ const EMPTY_QUERY_FILTERS = {
4107
+ dateRange: null,
4108
+ properties: []
4109
+ };
4110
+ let filterSeq = 0;
4111
+ function nextFilterId() {
4112
+ filterSeq += 1;
4113
+ return `qf-${Date.now().toString(36)}-${filterSeq}`;
4114
+ }
4115
+ /**
4116
+ * QueryFiltersMenu — conditions the reader would rather not type.
4117
+ *
4118
+ * A time range and a few equality filters are the WHERE clause most queries
4119
+ * share, and retyping them for every query is how a dashboard's filters end
4120
+ * up disagreeing with its queries. This holds them as data and hands them to
4121
+ * the SQL through a `{filters}` placeholder, the way PostHog does — the query
4122
+ * says *where* they apply, the menu says *what* they are. When the placeholder
4123
+ * is missing the menu says so, loudly, because silently unapplied filters are
4124
+ * the worst kind.
4125
+ *
4126
+ * @example
4127
+ * ```tsx
4128
+ * <QueryFiltersMenu
4129
+ * value={filters} onChange={setFilters}
4130
+ * fields={schema.columns}
4131
+ * placeholderPresent={sqlUsesPlaceholder(sql, "filters")}
4132
+ * onInsertPlaceholder={() => editor.current?.insertText("{filters}")}
4133
+ * />
4134
+ * ```
4135
+ */
4136
+ function QueryFiltersMenu({ value, onChange, fields, placeholderPresent = false, onInsertPlaceholder, onLoadValues, presets = DEFAULT_PRESETS, disabled = false, className, labels: customLabels }) {
4137
+ var _value$dateRange;
4138
+ const labels = _objectSpread2(_objectSpread2(_objectSpread2({}, defaultLabels$12), customLabels), {}, { operators: _objectSpread2(_objectSpread2({}, OPERATOR_LABELS), customLabels === null || customLabels === void 0 ? void 0 : customLabels.operators) });
4139
+ const [open, setOpen] = useState(false);
4140
+ const headingId = useId();
4141
+ const active = countActiveQueryFilters(value);
4142
+ const [valueCache, setValueCache] = useState({});
4143
+ const fieldsInUse = value.properties.map((filter) => filter.field).join("\0");
4144
+ useEffect(() => {
4145
+ if (!open || !onLoadValues) return;
4146
+ const missing = [...new Set(fieldsInUse.split("\0"))].filter((field) => field && !valueCache[field]);
4147
+ if (missing.length === 0) return;
4148
+ setValueCache((prev) => {
4149
+ const next = _objectSpread2({}, prev);
4150
+ for (const field of missing) next[field] = {
4151
+ loading: true,
4152
+ values: []
4153
+ };
4154
+ return next;
4155
+ });
4156
+ for (const field of missing) onLoadValues(field).then((values) => {
4157
+ setValueCache((prev) => _objectSpread2(_objectSpread2({}, prev), {}, { [field]: {
4158
+ loading: false,
4159
+ values
4160
+ } }));
4161
+ }).catch(() => {
4162
+ setValueCache((prev) => _objectSpread2(_objectSpread2({}, prev), {}, { [field]: {
4163
+ loading: false,
4164
+ values: []
4165
+ } }));
4166
+ });
4167
+ }, [
4168
+ open,
4169
+ onLoadValues,
4170
+ fieldsInUse,
4171
+ valueCache
4172
+ ]);
4173
+ const missing = active > 0 && !placeholderPresent;
4174
+ const updateProperty = (id, patch) => {
4175
+ onChange(_objectSpread2(_objectSpread2({}, value), {}, { properties: value.properties.map((filter) => filter.id === id ? _objectSpread2(_objectSpread2({}, filter), patch) : filter) }));
4176
+ };
4177
+ const addProperty = () => {
4178
+ var _first$name;
4179
+ const first = fields[0];
4180
+ onChange(_objectSpread2(_objectSpread2({}, value), {}, { properties: [...value.properties, {
4181
+ id: nextFilterId(),
4182
+ field: (_first$name = first === null || first === void 0 ? void 0 : first.name) !== null && _first$name !== void 0 ? _first$name : "",
4183
+ operator: "is",
4184
+ value: ""
4185
+ }] }));
4186
+ };
4187
+ const removeProperty = (id) => {
4188
+ onChange(_objectSpread2(_objectSpread2({}, value), {}, { properties: value.properties.filter((filter) => filter.id !== id) }));
4189
+ };
4190
+ return /* @__PURE__ */ jsxs(PopoverRoot$1, {
4191
+ open,
4192
+ onOpenChange: setOpen,
4193
+ placement: "bottom-start",
4194
+ "data-opt-id": "CEHKK",
4195
+ "data-opt-slug": "query-filters-menu.QueryFiltersMenu",
4196
+ children: [/* @__PURE__ */ jsxs(PopoverTrigger$1, {
4197
+ disabled,
4198
+ className: cn("border-border bg-surface text-text-primary hover:bg-bg-subtle h-8 gap-1.5 rounded-lg border px-2.5 text-xs font-medium", missing && "border-warning text-warning-fg", className),
4199
+ "aria-label": labels.trigger,
4200
+ children: [
4201
+ /* @__PURE__ */ jsx(Filter, {
4202
+ className: "size-3.5",
4203
+ "aria-hidden": true
4204
+ }),
4205
+ /* @__PURE__ */ jsx("span", { children: labels.trigger }),
4206
+ active > 0 && /* @__PURE__ */ jsx("span", {
4207
+ className: cn("rounded-full px-1.5 text-[10px] tabular-nums", missing ? "bg-warning-subtle text-warning-fg" : "bg-accent text-accent-fg"),
4208
+ "aria-label": labels.activeCount(active),
4209
+ children: active
4210
+ })
4211
+ ]
4212
+ }), /* @__PURE__ */ jsxs(PopoverContent$1, {
4213
+ "aria-labelledby": headingId,
4214
+ className: "flex w-[min(28rem,calc(100vw-2rem))] flex-col gap-4",
4215
+ children: [
4216
+ /* @__PURE__ */ jsx("h3", {
4217
+ id: headingId,
4218
+ className: "text-text-primary text-sm font-semibold",
4219
+ children: labels.title
4220
+ }),
4221
+ missing ? /* @__PURE__ */ jsx(Callout, {
4222
+ tone: "warning",
4223
+ size: "sm",
4224
+ title: labels.missingPlaceholderTitle,
4225
+ action: onInsertPlaceholder ? /* @__PURE__ */ jsx(Button, {
4226
+ size: "sm",
4227
+ variant: "secondary",
4228
+ onClick: () => {
4229
+ onInsertPlaceholder();
4230
+ setOpen(false);
4231
+ },
4232
+ children: labels.insertPlaceholder
4233
+ }) : void 0,
4234
+ children: labels.missingPlaceholder
4235
+ }) : /* @__PURE__ */ jsx("p", {
4236
+ className: cn(OPT_TEXT_TERTIARY, "text-xs"),
4237
+ children: labels.placeholderHint
4238
+ }),
4239
+ /* @__PURE__ */ jsxs("div", {
4240
+ className: "flex flex-col gap-1.5",
4241
+ children: [/* @__PURE__ */ jsx("span", {
4242
+ className: "text-text-secondary text-xs font-medium",
4243
+ children: labels.dateRange
4244
+ }), /* @__PURE__ */ jsx(DateRangePicker, {
4245
+ value: (_value$dateRange = value.dateRange) !== null && _value$dateRange !== void 0 ? _value$dateRange : {
4246
+ start: null,
4247
+ end: null
4248
+ },
4249
+ onChange: (range) => onChange(_objectSpread2(_objectSpread2({}, value), {}, { dateRange: range.start || range.end ? range : null })),
4250
+ presets,
4251
+ placeholder: labels.dateRangePlaceholder
4252
+ })]
4253
+ }),
4254
+ /* @__PURE__ */ jsxs("div", {
4255
+ className: "flex flex-col gap-2",
4256
+ children: [
4257
+ /* @__PURE__ */ jsx("span", {
4258
+ className: "text-text-secondary text-xs font-medium",
4259
+ children: labels.properties
4260
+ }),
4261
+ value.properties.map((filter) => {
4262
+ const valueless = VALUELESS_OPERATORS.has(filter.operator);
4263
+ const field = fields.find((entry) => entry.name === filter.field);
4264
+ return /* @__PURE__ */ jsxs("div", {
4265
+ className: "flex items-center gap-1.5",
4266
+ children: [
4267
+ /* @__PURE__ */ jsxs(SelectRoot$1, {
4268
+ value: filter.field,
4269
+ onValueChange: (next) => typeof next === "string" && updateProperty(filter.id, { field: next }),
4270
+ children: [/* @__PURE__ */ jsxs(SelectTrigger$1, {
4271
+ className: "h-8 w-auto shrink-0 text-xs",
4272
+ minWidth: 120,
4273
+ "aria-label": labels.field,
4274
+ children: [(field === null || field === void 0 ? void 0 : field.type) ? /* @__PURE__ */ jsx(FieldToken, {
4275
+ type: resolveFieldType(field.type),
4276
+ size: "xs",
4277
+ decorative: true
4278
+ }) : null, /* @__PURE__ */ jsx("span", {
4279
+ className: "truncate font-mono",
4280
+ children: filter.field || labels.field
4281
+ })]
4282
+ }), /* @__PURE__ */ jsx(SelectPopover$1, { children: fields.map((entry) => /* @__PURE__ */ jsxs(SelectItem$1, {
4283
+ value: entry.name,
4284
+ children: [/* @__PURE__ */ jsx(FieldToken, {
4285
+ type: resolveFieldType(entry.type),
4286
+ size: "xs",
4287
+ decorative: true
4288
+ }), /* @__PURE__ */ jsx("span", {
4289
+ className: "font-mono text-xs",
4290
+ children: entry.name
4291
+ })]
4292
+ }, entry.name)) })]
4293
+ }),
4294
+ /* @__PURE__ */ jsxs(SelectRoot$1, {
4295
+ value: filter.operator,
4296
+ onValueChange: (next) => typeof next === "string" && updateProperty(filter.id, {
4297
+ operator: next,
4298
+ value: VALUELESS_OPERATORS.has(next) ? "" : filter.value
4299
+ }),
4300
+ children: [/* @__PURE__ */ jsx(SelectTrigger$1, {
4301
+ className: "h-8 w-auto shrink-0 text-xs",
4302
+ minWidth: 110,
4303
+ "aria-label": labels.operator,
4304
+ children: labels.operators[filter.operator]
4305
+ }), /* @__PURE__ */ jsx(SelectPopover$1, { children: OPERATOR_ORDER.map((operator) => /* @__PURE__ */ jsx(SelectItem$1, {
4306
+ value: operator,
4307
+ children: labels.operators[operator]
4308
+ }, operator)) })]
4309
+ }),
4310
+ !valueless && onLoadValues ? /* @__PURE__ */ jsx(ValueSuggestInput, {
4311
+ value: filter.value,
4312
+ onChange: (next) => updateProperty(filter.id, { value: next }),
4313
+ entry: valueCache[filter.field],
4314
+ label: labels.value,
4315
+ loadingLabel: labels.valuesLoading,
4316
+ emptyLabel: labels.noValues
4317
+ }) : !valueless ? /* @__PURE__ */ jsx(Input, {
4318
+ size: "sm",
4319
+ "aria-label": labels.value,
4320
+ placeholder: labels.value,
4321
+ value: filter.value,
4322
+ onChange: (event) => updateProperty(filter.id, { value: event.target.value }),
4323
+ className: "min-w-0 flex-1"
4324
+ }) : null,
4325
+ /* @__PURE__ */ jsx(Button, {
4326
+ variant: "ghost",
4327
+ size: "sm",
4328
+ "aria-label": labels.removeProperty(filter.field),
4329
+ onClick: () => removeProperty(filter.id),
4330
+ children: /* @__PURE__ */ jsx(Trash2, {
4331
+ className: "size-3.5",
4332
+ "aria-hidden": true
4333
+ })
4334
+ })
4335
+ ]
4336
+ }, filter.id);
4337
+ }),
4338
+ /* @__PURE__ */ jsxs(Button, {
4339
+ variant: "secondary",
4340
+ size: "sm",
4341
+ onClick: addProperty,
4342
+ disabled: fields.length === 0,
4343
+ className: "self-start",
4344
+ children: [/* @__PURE__ */ jsx(Plus, {
4345
+ className: "size-3.5",
4346
+ "aria-hidden": true
4347
+ }), labels.addProperty]
4348
+ })
4349
+ ]
4350
+ }),
4351
+ /* @__PURE__ */ jsxs("div", {
4352
+ className: "flex items-center justify-between gap-2",
4353
+ children: [/* @__PURE__ */ jsx(Button, {
4354
+ variant: "ghost",
4355
+ size: "sm",
4356
+ disabled: active === 0,
4357
+ onClick: () => onChange(EMPTY_QUERY_FILTERS),
4358
+ children: labels.reset
4359
+ }), onInsertPlaceholder && !missing && !placeholderPresent && /* @__PURE__ */ jsx(Button, {
4360
+ variant: "secondary",
4361
+ size: "sm",
4362
+ onClick: () => {
4363
+ onInsertPlaceholder();
4364
+ setOpen(false);
4365
+ },
4366
+ children: labels.insertPlaceholder
4367
+ })]
4368
+ })
4369
+ ]
4370
+ })]
4371
+ });
4372
+ }
4373
+ QueryFiltersMenu.displayName = "QueryFiltersMenu";
4374
+ /**
4375
+ * A value box with the field's known values underneath. Free text is still
4376
+ * accepted: the list is a memory aid, not a constraint, because the query may
4377
+ * well be asking about a value that is not in the sample.
4378
+ */
4379
+ function ValueSuggestInput({ value, onChange, entry, label, loadingLabel, emptyLabel }) {
4380
+ var _entry$values;
4381
+ const needle = value.trim().toLowerCase();
4382
+ const matches = ((_entry$values = entry === null || entry === void 0 ? void 0 : entry.values) !== null && _entry$values !== void 0 ? _entry$values : []).filter((candidate) => !needle || candidate.toLowerCase().includes(needle)).slice(0, 50);
4383
+ return /* @__PURE__ */ jsx("div", {
4384
+ className: "relative min-w-0 flex-1 basis-32",
4385
+ children: /* @__PURE__ */ jsxs(ComboboxRoot$1, {
4386
+ value,
4387
+ onValueChange: onChange,
4388
+ children: [/* @__PURE__ */ jsx(ComboboxInput$1, {
4389
+ "aria-label": label,
4390
+ placeholder: label,
4391
+ onInput: (event) => onChange(event.currentTarget.value),
4392
+ className: cn("border-border bg-surface text-text-primary placeholder:text-text-tertiary h-8 w-full rounded-lg border px-2.5 font-mono text-xs", "focus-visible:ring-ring focus-visible:ring-offset-ring-offset outline-none focus-visible:ring-2 focus-visible:ring-offset-1")
4393
+ }), /* @__PURE__ */ jsx(ComboboxPopover$1, {
4394
+ className: "max-h-64 overflow-auto",
4395
+ children: (entry === null || entry === void 0 ? void 0 : entry.loading) ? /* @__PURE__ */ jsx("div", {
4396
+ className: cn(OPT_TEXT_TERTIARY, "px-3 py-2 text-xs"),
4397
+ role: "status",
4398
+ "aria-live": "polite",
4399
+ children: loadingLabel
4400
+ }) : matches.length === 0 ? /* @__PURE__ */ jsx(ComboboxEmpty, { children: emptyLabel }) : matches.map((candidate) => /* @__PURE__ */ jsx(ComboboxItem$1, {
4401
+ value: candidate,
4402
+ children: /* @__PURE__ */ jsx("span", {
4403
+ className: "truncate font-mono text-xs",
4404
+ children: candidate
4405
+ })
4406
+ }, candidate))
4407
+ })]
4408
+ })
4409
+ });
4410
+ }
4411
+ //#endregion
4412
+ //#region src/shells/query-variables-menu.tsx
4413
+ const defaultLabels$11 = {
4414
+ trigger: "Variables",
4415
+ title: "Query variables",
4416
+ hint: "Reference a variable in the SQL as {variables.name}. The value is substituted before the query runs.",
4417
+ name: "Name",
4418
+ value: "Value",
4419
+ add: "Add variable",
4420
+ remove: (name) => `Remove ${name}`,
4421
+ insert: (name) => `Insert {variables.${name}} at cursor`,
4422
+ empty: "No variables yet.",
4423
+ invalidName: "Letters, digits and underscores only, starting with a letter.",
4424
+ activeCount: (count) => `${count} defined`
4425
+ };
4426
+ const NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
4427
+ /** True when the name can be referenced from SQL. */
4428
+ function isValidQueryVariableName(name) {
4429
+ return NAME_PATTERN.test(name);
4430
+ }
4431
+ let variableSeq = 0;
4432
+ function nextVariableId() {
4433
+ variableSeq += 1;
4434
+ return `qv-${Date.now().toString(36)}-${variableSeq}`;
4435
+ }
4436
+ /**
4437
+ * QueryVariablesMenu — the parts of a query that change between runs.
4438
+ *
4439
+ * A saved query with a literal `'2026-09-01'` in it is a query that lies a
4440
+ * week later. Variables move that value out of the text and into a small
4441
+ * form, so the same SQL answers the same question for a different week — or
4442
+ * a different plan, or a different country — without an edit. The menu owns
4443
+ * only the names and values; substitution is the caller's, because how a
4444
+ * value is quoted is the dialect's business.
4445
+ *
4446
+ * @example
4447
+ * ```tsx
4448
+ * <QueryVariablesMenu
4449
+ * value={variables} onChange={setVariables}
4450
+ * onInsert={(name) => editor.current?.insertText(`{variables.${name}}`)}
4451
+ * />
4452
+ * ```
4453
+ */
4454
+ function QueryVariablesMenu({ value, onChange, onInsert, disabled = false, disabledReason, className, labels: customLabels }) {
4455
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$11), customLabels);
4456
+ const [open, setOpen] = useState(false);
4457
+ const headingId = useId();
4458
+ const update = (id, patch) => {
4459
+ onChange(value.map((variable) => variable.id === id ? _objectSpread2(_objectSpread2({}, variable), patch) : variable));
4460
+ };
4461
+ const add = () => {
4462
+ let index = value.length + 1;
4463
+ let name = `var${index}`;
4464
+ const taken = new Set(value.map((variable) => variable.name));
4465
+ while (taken.has(name)) {
4466
+ index += 1;
4467
+ name = `var${index}`;
4468
+ }
4469
+ onChange([...value, {
4470
+ id: nextVariableId(),
4471
+ name,
4472
+ value: ""
4473
+ }]);
4474
+ };
4475
+ return /* @__PURE__ */ jsxs(PopoverRoot$1, {
4476
+ open,
4477
+ onOpenChange: setOpen,
4478
+ placement: "bottom-start",
4479
+ "data-opt-id": "9FP6Z",
4480
+ "data-opt-slug": "query-variables-menu.QueryVariablesMenu",
4481
+ children: [/* @__PURE__ */ jsxs(PopoverTrigger$1, {
4482
+ disabled,
4483
+ title: disabled ? disabledReason : void 0,
4484
+ className: cn("border-border bg-surface text-text-primary hover:bg-bg-subtle h-8 gap-1.5 rounded-lg border px-2.5 text-xs font-medium", className),
4485
+ "aria-label": labels.trigger,
4486
+ children: [
4487
+ /* @__PURE__ */ jsx(Braces, {
4488
+ className: "size-3.5",
4489
+ "aria-hidden": true
4490
+ }),
4491
+ /* @__PURE__ */ jsx("span", { children: labels.trigger }),
4492
+ value.length > 0 && /* @__PURE__ */ jsx("span", {
4493
+ className: "bg-accent text-accent-fg rounded-full px-1.5 text-[10px] tabular-nums",
4494
+ "aria-label": labels.activeCount(value.length),
4495
+ children: value.length
4496
+ })
4497
+ ]
4498
+ }), /* @__PURE__ */ jsxs(PopoverContent$1, {
4499
+ "aria-labelledby": headingId,
4500
+ className: "flex w-[min(26rem,calc(100vw-2rem))] flex-col gap-3",
4501
+ children: [
4502
+ /* @__PURE__ */ jsx("h3", {
4503
+ id: headingId,
4504
+ className: "text-text-primary text-sm font-semibold",
4505
+ children: labels.title
4506
+ }),
4507
+ /* @__PURE__ */ jsx("p", {
4508
+ className: cn(OPT_TEXT_TERTIARY, "text-xs"),
4509
+ children: labels.hint
4510
+ }),
4511
+ value.length === 0 ? /* @__PURE__ */ jsx("p", {
4512
+ className: cn(OPT_TEXT_TERTIARY, "text-xs"),
4513
+ children: labels.empty
4514
+ }) : /* @__PURE__ */ jsx("ul", {
4515
+ className: "flex flex-col gap-2",
4516
+ children: value.map((variable) => {
4517
+ const valid = isValidQueryVariableName(variable.name);
4518
+ return /* @__PURE__ */ jsxs("li", {
4519
+ className: "flex items-start gap-1.5",
4520
+ children: [
4521
+ /* @__PURE__ */ jsx(Input, {
4522
+ size: "sm",
4523
+ "aria-label": labels.name,
4524
+ placeholder: labels.name,
4525
+ value: variable.name,
4526
+ error: valid ? void 0 : labels.invalidName,
4527
+ onChange: (event) => update(variable.id, { name: event.target.value }),
4528
+ className: "w-32 font-mono"
4529
+ }),
4530
+ /* @__PURE__ */ jsx(Input, {
4531
+ size: "sm",
4532
+ "aria-label": labels.value,
4533
+ placeholder: labels.value,
4534
+ value: variable.value,
4535
+ onChange: (event) => update(variable.id, { value: event.target.value }),
4536
+ className: "min-w-0 flex-1 font-mono"
4537
+ }),
4538
+ onInsert && /* @__PURE__ */ jsx(Button, {
4539
+ variant: "ghost",
4540
+ size: "sm",
4541
+ disabled: !valid,
4542
+ "aria-label": labels.insert(variable.name),
4543
+ title: labels.insert(variable.name),
4544
+ onClick: () => {
4545
+ onInsert(variable.name);
4546
+ setOpen(false);
4547
+ },
4548
+ children: /* @__PURE__ */ jsx(TextCursorInput, {
4549
+ className: "size-3.5",
4550
+ "aria-hidden": true
4551
+ })
4552
+ }),
4553
+ /* @__PURE__ */ jsx(Button, {
4554
+ variant: "ghost",
4555
+ size: "sm",
4556
+ "aria-label": labels.remove(variable.name),
4557
+ onClick: () => onChange(value.filter((entry) => entry.id !== variable.id)),
4558
+ children: /* @__PURE__ */ jsx(Trash2, {
4559
+ className: "size-3.5",
4560
+ "aria-hidden": true
4561
+ })
4562
+ })
4563
+ ]
4564
+ }, variable.id);
4565
+ })
4566
+ }),
4567
+ /* @__PURE__ */ jsxs(Button, {
4568
+ variant: "secondary",
4569
+ size: "sm",
4570
+ onClick: add,
4571
+ className: "self-start",
4572
+ children: [/* @__PURE__ */ jsx(Plus, {
4573
+ className: "size-3.5",
4574
+ "aria-hidden": true
4575
+ }), labels.add]
4576
+ })
4577
+ ]
4578
+ })]
4579
+ });
4580
+ }
4581
+ QueryVariablesMenu.displayName = "QueryVariablesMenu";
4582
+ //#endregion
4583
+ //#region src/shells/query-history-list.tsx
4584
+ const defaultLabels$10 = {
4585
+ heading: "History",
4586
+ empty: "Nothing has run yet.",
4587
+ loading: "Loading history…",
4588
+ clear: "Clear history",
4589
+ rerun: (query) => `Run again: ${query}`,
4590
+ load: (query) => `Load into editor: ${query}`,
4591
+ rows: (count) => `${formatOptNumber(count)} rows`,
4592
+ elapsed: (ms) => ms >= 1e3 ? `${(ms / 1e3).toFixed(1)} s` : `${ms} ms`,
4593
+ failed: "Failed",
4594
+ succeeded: "Succeeded",
4595
+ ranAt: (iso) => formatOptDateTime(iso)
4596
+ };
4597
+ function firstLine(query) {
4598
+ const line = query.split("\n").map((entry) => entry.trim()).find((entry) => entry.length > 0 && !entry.startsWith("--"));
4599
+ return line !== null && line !== void 0 ? line : query.trim();
4600
+ }
4601
+ /**
4602
+ * QueryHistoryList — what was asked, and whether it answered.
4603
+ *
4604
+ * Half of query writing is recovering the query from twenty minutes ago that
4605
+ * almost worked. The list keeps each run with its outcome, so a failed
4606
+ * attempt is as easy to get back as a good one, and the cost of a run is
4607
+ * visible before it is repeated.
4608
+ *
4609
+ * @example
4610
+ * ```tsx
4611
+ * <QueryHistoryList items={history} onLoad={(item) => setSql(item.query)} onRerun={(item) => run(item.query)} />
4612
+ * ```
4613
+ */
4614
+ function QueryHistoryList({ items, loading = false, onLoad, onRerun, onClear, header, className, labels: customLabels }) {
4615
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$10), customLabels);
4616
+ return /* @__PURE__ */ jsxs("section", {
4617
+ className: cn("flex min-h-0 flex-col", className),
4618
+ "aria-label": labels.heading,
4619
+ "data-opt-id": "AGV55",
4620
+ "data-opt-slug": "query-history-list.QueryHistoryList",
4621
+ children: [
4622
+ header,
4623
+ onClear && items.length > 0 && /* @__PURE__ */ jsx("div", {
4624
+ className: "flex flex-none justify-end px-2 pt-1",
4625
+ children: /* @__PURE__ */ jsxs(Button, {
4626
+ variant: "ghost",
4627
+ size: "sm",
4628
+ onClick: onClear,
4629
+ children: [/* @__PURE__ */ jsx(Trash2, {
4630
+ className: "size-3.5",
4631
+ "aria-hidden": true
4632
+ }), labels.clear]
4633
+ })
4634
+ }),
4635
+ /* @__PURE__ */ jsx("div", {
4636
+ className: "min-h-0 flex-1 overflow-y-auto",
4637
+ children: loading ? /* @__PURE__ */ jsx("p", {
4638
+ className: cn(OPT_TEXT_TERTIARY, "px-3 py-4 text-xs"),
4639
+ children: labels.loading
4640
+ }) : items.length === 0 ? /* @__PURE__ */ jsx("p", {
4641
+ className: cn(OPT_TEXT_TERTIARY, "px-3 py-4 text-xs"),
4642
+ children: labels.empty
4643
+ }) : /* @__PURE__ */ jsx("ul", {
4644
+ className: "divide-border-subtle divide-y",
4645
+ children: items.map((item) => {
4646
+ const summary = firstLine(item.query);
4647
+ return /* @__PURE__ */ jsxs("li", {
4648
+ className: "group/history hover:bg-bg-subtle flex items-start gap-2 px-3 py-2",
4649
+ children: [
4650
+ /* @__PURE__ */ jsx("span", {
4651
+ className: cn("mt-1.5 inline-block size-2 shrink-0 rounded-full", item.status === "success" ? "bg-success" : "bg-danger"),
4652
+ role: "img",
4653
+ "aria-label": item.status === "success" ? labels.succeeded : labels.failed
4654
+ }),
4655
+ /* @__PURE__ */ jsxs("button", {
4656
+ type: "button",
4657
+ className: "flex min-w-0 flex-1 cursor-pointer flex-col gap-0.5 text-left",
4658
+ "aria-label": labels.load(summary),
4659
+ title: item.query,
4660
+ onClick: () => onLoad === null || onLoad === void 0 ? void 0 : onLoad(item),
4661
+ disabled: !onLoad,
4662
+ children: [/* @__PURE__ */ jsx("span", {
4663
+ className: "text-text-primary truncate font-mono text-xs",
4664
+ children: summary
4665
+ }), /* @__PURE__ */ jsxs("span", {
4666
+ className: cn(OPT_TEXT_TERTIARY, "flex flex-wrap items-center gap-x-2 text-[11px] tabular-nums"),
4667
+ children: [
4668
+ /* @__PURE__ */ jsx("time", {
4669
+ dateTime: item.ranAt,
4670
+ children: labels.ranAt(item.ranAt)
4671
+ }),
4672
+ item.status === "success" && item.rowCount != null && /* @__PURE__ */ jsx("span", { children: labels.rows(item.rowCount) }),
4673
+ item.elapsed != null && /* @__PURE__ */ jsx("span", { children: labels.elapsed(item.elapsed) }),
4674
+ item.status === "error" && item.error && /* @__PURE__ */ jsx("span", {
4675
+ className: "text-danger-fg truncate",
4676
+ children: item.error
4677
+ })
4678
+ ]
4679
+ })]
4680
+ }),
4681
+ onRerun && /* @__PURE__ */ jsx(Button, {
4682
+ variant: "ghost",
4683
+ size: "sm",
4684
+ className: "opacity-0 group-hover/history:opacity-100 focus-visible:opacity-100",
4685
+ "aria-label": labels.rerun(summary),
4686
+ title: labels.rerun(summary),
4687
+ onClick: () => onRerun(item),
4688
+ children: /* @__PURE__ */ jsx(Play, {
4689
+ className: "size-3.5",
4690
+ "aria-hidden": true
4691
+ })
4692
+ })
4693
+ ]
4694
+ }, item.id);
4695
+ })
4696
+ })
4697
+ })
4698
+ ]
4699
+ });
4700
+ }
4701
+ QueryHistoryList.displayName = "QueryHistoryList";
4702
+ //#endregion
4703
+ //#region src/shells/save-query-dialog.tsx
4704
+ const defaultLabels$9 = {
4705
+ createTitle: "Save query",
4706
+ updateTitle: "Update query",
4707
+ description: "A saved query can be reopened from the sidebar and shared with the project.",
4708
+ name: "Name",
4709
+ namePlaceholder: "Weekly signups by channel",
4710
+ descriptionField: "Description",
4711
+ descriptionPlaceholder: "What this answers, and any caveats.",
4712
+ isPublic: "Share with the project",
4713
+ isPublicHint: "Everyone in the project can open and run it. Only you can edit it.",
4714
+ save: "Save",
4715
+ cancel: "Cancel",
4716
+ nameRequired: "Give the query a name.",
4717
+ candidate: (index, total) => `Statement ${index} of ${total}`,
4718
+ selection: "Selection",
4719
+ previous: "Previous statement",
4720
+ next: "Next statement"
4721
+ };
4722
+ /**
4723
+ * SaveQueryDialog — give the query a name it can be found by.
4724
+ *
4725
+ * Saving is the moment a query stops being scratch work, so the dialog asks
4726
+ * for the things a colleague will need: a name, a line about what it answers,
4727
+ * and whether they may see it. When the editor holds several statements the
4728
+ * dialog pages through them rather than guessing, because the one at the
4729
+ * cursor is usually — not always — the one meant.
4730
+ *
4731
+ * @example
4732
+ * ```tsx
4733
+ * <SaveQueryDialog
4734
+ * open={saving} onOpenChange={setSaving}
4735
+ * candidates={statements.map((s) => s.text)}
4736
+ * initialCandidate={currentIndex}
4737
+ * onSubmit={(values) => saveMutation.mutate(values)}
4738
+ * />
4739
+ * ```
4740
+ */
4741
+ function SaveQueryDialog({ open, onOpenChange, onSubmit, candidates, initialCandidate = 0, candidateIsSelection = false, initialValues, mode = "create", saving = false, error, labels: customLabels }) {
4742
+ var _initialValues$name, _initialValues$descri, _initialValues$isPubl, _candidates$safeIndex;
4743
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$9), customLabels);
4744
+ const [name, setName] = useState((_initialValues$name = initialValues === null || initialValues === void 0 ? void 0 : initialValues.name) !== null && _initialValues$name !== void 0 ? _initialValues$name : "");
4745
+ const [description, setDescription] = useState((_initialValues$descri = initialValues === null || initialValues === void 0 ? void 0 : initialValues.description) !== null && _initialValues$descri !== void 0 ? _initialValues$descri : "");
4746
+ const [isPublic, setIsPublic] = useState((_initialValues$isPubl = initialValues === null || initialValues === void 0 ? void 0 : initialValues.isPublic) !== null && _initialValues$isPubl !== void 0 ? _initialValues$isPubl : false);
4747
+ const [index, setIndex] = useState(initialCandidate);
4748
+ const [touched, setTouched] = useState(false);
4749
+ useEffect(() => {
4750
+ var _initialValues$name2, _initialValues$descri2, _initialValues$isPubl2;
4751
+ if (!open) return;
4752
+ setName((_initialValues$name2 = initialValues === null || initialValues === void 0 ? void 0 : initialValues.name) !== null && _initialValues$name2 !== void 0 ? _initialValues$name2 : "");
4753
+ setDescription((_initialValues$descri2 = initialValues === null || initialValues === void 0 ? void 0 : initialValues.description) !== null && _initialValues$descri2 !== void 0 ? _initialValues$descri2 : "");
4754
+ setIsPublic((_initialValues$isPubl2 = initialValues === null || initialValues === void 0 ? void 0 : initialValues.isPublic) !== null && _initialValues$isPubl2 !== void 0 ? _initialValues$isPubl2 : false);
4755
+ setIndex(Math.min(initialCandidate, Math.max(0, candidates.length - 1)));
4756
+ setTouched(false);
4757
+ }, [
4758
+ open,
4759
+ initialValues === null || initialValues === void 0 ? void 0 : initialValues.name,
4760
+ initialValues === null || initialValues === void 0 ? void 0 : initialValues.description,
4761
+ initialValues === null || initialValues === void 0 ? void 0 : initialValues.isPublic,
4762
+ initialCandidate,
4763
+ candidates.length
4764
+ ]);
4765
+ const safeIndex = Math.min(index, Math.max(0, candidates.length - 1));
4766
+ const sql = (_candidates$safeIndex = candidates[safeIndex]) !== null && _candidates$safeIndex !== void 0 ? _candidates$safeIndex : "";
4767
+ const multi = candidates.length > 1;
4768
+ const nameError = touched && name.trim() === "" ? labels.nameRequired : void 0;
4769
+ const submit = (event) => {
4770
+ event.preventDefault();
4771
+ setTouched(true);
4772
+ if (name.trim() === "" || sql.trim() === "") return;
4773
+ onSubmit({
4774
+ name: name.trim(),
4775
+ description: description.trim(),
4776
+ isPublic,
4777
+ sql
4778
+ });
4779
+ };
4780
+ return /* @__PURE__ */ jsx(DialogRoot$1, {
4781
+ open,
4782
+ onOpenChange,
4783
+ "data-opt-id": "ZGRHN",
4784
+ "data-opt-slug": "save-query-dialog.SaveQueryDialog",
4785
+ children: /* @__PURE__ */ jsx(DialogPanel$1, {
4786
+ size: "md",
4787
+ children: /* @__PURE__ */ jsxs("form", {
4788
+ onSubmit: submit,
4789
+ className: "contents",
4790
+ children: [
4791
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [/* @__PURE__ */ jsx(DialogHeading$1, { children: mode === "update" ? labels.updateTitle : labels.createTitle }), /* @__PURE__ */ jsx(DialogDescription$1, { children: labels.description })] }),
4792
+ /* @__PURE__ */ jsx(DialogBody, { children: /* @__PURE__ */ jsxs("div", {
4793
+ className: "flex flex-col gap-4",
4794
+ children: [
4795
+ /* @__PURE__ */ jsx(Input, {
4796
+ label: labels.name,
4797
+ placeholder: labels.namePlaceholder,
4798
+ value: name,
4799
+ onChange: (event) => setName(event.target.value),
4800
+ onBlur: () => setTouched(true),
4801
+ error: nameError,
4802
+ required: true,
4803
+ fullWidth: true,
4804
+ autoFocus: true
4805
+ }),
4806
+ /* @__PURE__ */ jsx(Textarea, {
4807
+ label: labels.descriptionField,
4808
+ placeholder: labels.descriptionPlaceholder,
4809
+ value: description,
4810
+ onChange: (event) => setDescription(event.target.value),
4811
+ rows: 2,
4812
+ fullWidth: true
4813
+ }),
4814
+ /* @__PURE__ */ jsx(Switch, {
4815
+ label: labels.isPublic,
4816
+ hint: labels.isPublicHint,
4817
+ checked: isPublic,
4818
+ onChange: setIsPublic,
4819
+ size: "sm"
4820
+ }),
4821
+ /* @__PURE__ */ jsxs("div", {
4822
+ className: "flex flex-col gap-1.5",
4823
+ children: [/* @__PURE__ */ jsxs("div", {
4824
+ className: "flex items-center justify-between",
4825
+ children: [/* @__PURE__ */ jsx("span", {
4826
+ className: cn(OPT_TEXT_TERTIARY, "text-xs"),
4827
+ children: candidateIsSelection ? labels.selection : multi ? labels.candidate(safeIndex + 1, candidates.length) : null
4828
+ }), multi && /* @__PURE__ */ jsxs("span", {
4829
+ className: "flex items-center gap-1",
4830
+ children: [/* @__PURE__ */ jsx(Button, {
4831
+ type: "button",
4832
+ variant: "ghost",
4833
+ size: "sm",
4834
+ "aria-label": labels.previous,
4835
+ disabled: safeIndex === 0,
4836
+ onClick: () => setIndex((current) => current - 1),
4837
+ children: /* @__PURE__ */ jsx(ChevronLeft, {
4838
+ className: "size-3.5",
4839
+ "aria-hidden": true
4840
+ })
4841
+ }), /* @__PURE__ */ jsx(Button, {
4842
+ type: "button",
4843
+ variant: "ghost",
4844
+ size: "sm",
4845
+ "aria-label": labels.next,
4846
+ disabled: safeIndex === candidates.length - 1,
4847
+ onClick: () => setIndex((current) => current + 1),
4848
+ children: /* @__PURE__ */ jsx(ChevronRight, {
4849
+ className: "size-3.5",
4850
+ "aria-hidden": true
4851
+ })
4852
+ })]
4853
+ })]
4854
+ }), /* @__PURE__ */ jsx("pre", {
4855
+ className: "bg-bg-subtle text-text-secondary max-h-40 overflow-auto rounded-lg p-3 font-mono text-xs whitespace-pre-wrap",
4856
+ children: sql
4857
+ })]
4858
+ }),
4859
+ error ? /* @__PURE__ */ jsx("p", {
4860
+ className: "text-danger-fg text-sm",
4861
+ role: "alert",
4862
+ children: error
4863
+ }) : null
4864
+ ]
4865
+ }) }),
4866
+ /* @__PURE__ */ jsxs(DialogFooter, { children: [/* @__PURE__ */ jsx(Button, {
4867
+ type: "button",
4868
+ variant: "secondary",
4869
+ onClick: () => onOpenChange(false),
4870
+ disabled: saving,
4871
+ children: labels.cancel
4872
+ }), /* @__PURE__ */ jsx(Button, {
4873
+ type: "submit",
4874
+ variant: "primary",
4875
+ loading: saving,
4876
+ children: labels.save
4877
+ })] })
4878
+ ]
4879
+ })
4880
+ })
4881
+ });
4882
+ }
4883
+ SaveQueryDialog.displayName = "SaveQueryDialog";
4884
+ //#endregion
4885
+ //#region src/lib/use-media-query.ts
4886
+ /**
4887
+ * Whether a media query matches, kept in sync with the browser.
4888
+ *
4889
+ * `useSyncExternalStore` rather than an effect: a layout that switches on width
4890
+ * must not render the wrong shape for a frame first. The server snapshot is the
4891
+ * caller's `serverFallback`, because the server does not know the viewport and
4892
+ * guessing "narrow" collapses a wide layout on every first paint.
4893
+ *
4894
+ * Internal. Exported from `lib` for the components in this package, not from
4895
+ * the package barrel — a hook that answers "is this viewport wide" invites
4896
+ * layout decisions that belong in CSS.
4897
+ */
4898
+ function useMediaQuery(query, serverFallback = false) {
4899
+ return useSyncExternalStore((onChange) => {
4900
+ if (typeof window === "undefined") return () => {};
4901
+ const list = window.matchMedia(query);
4902
+ list.addEventListener("change", onChange);
4903
+ return () => list.removeEventListener("change", onChange);
4904
+ }, () => typeof window === "undefined" ? serverFallback : window.matchMedia(query).matches, () => serverFallback);
4905
+ }
4906
+ //#endregion
4907
+ //#region src/shells/query-workspace.tsx
4908
+ const defaultLabels$8 = {
4909
+ sidebar: "Schema and saved queries",
4910
+ openSidebar: "Show sidebar",
4911
+ closeSidebar: "Hide sidebar",
4912
+ editor: "Query editor",
4913
+ output: "Query output"
4914
+ };
4915
+ const STORAGE_PREFIX = "opt-query-workspace:";
4916
+ function subscribeNoop() {
4917
+ return () => {};
4918
+ }
4919
+ function readLayout(key) {
4920
+ if (!key || typeof window === "undefined") return void 0;
4921
+ try {
4922
+ const raw = window.localStorage.getItem(STORAGE_PREFIX + key);
4923
+ if (!raw) return void 0;
4924
+ const parsed = JSON.parse(raw);
4925
+ if (parsed && typeof parsed === "object") return parsed;
4926
+ } catch (_unused) {}
4927
+ }
4928
+ function writeLayout(key, layout) {
4929
+ if (!key || typeof window === "undefined") return;
4930
+ try {
4931
+ window.localStorage.setItem(STORAGE_PREFIX + key, JSON.stringify(layout));
4932
+ } catch (_unused2) {}
4933
+ }
4934
+ /**
4935
+ * QueryWorkspace — three panes, one job.
4936
+ *
4937
+ * The shape every SQL tool converges on: what can be queried on the left,
4938
+ * the query on top, the answer below. The panes resize and the sidebar
4939
+ * collapses, and both are remembered per `storageKey`, because the layout a
4940
+ * reader settles into is part of their tool. The workspace is only a frame —
4941
+ * the toolbar, editor and output are slots, so the same frame serves a full
4942
+ * page and an embedded card.
4943
+ *
4944
+ * @example
4945
+ * ```tsx
4946
+ * <QueryWorkspace
4947
+ * storageKey="project-query"
4948
+ * sidebar={<SchemaTree … />}
4949
+ * toolbar={<QueryToolbar … />}
4950
+ * editor={<SqlEditor height="fill" … />}
4951
+ * output={<QueryOutputPane … />}
4952
+ * />
4953
+ * ```
4954
+ */
4955
+ function QueryWorkspace({ sidebar, sidebarOpen: controlledSidebarOpen, onSidebarOpenChange, header, toolbar, editor, output, storageKey, defaultSidebarSize = 22, defaultEditorSize = 40, className, labels: customLabels }) {
4956
+ var _ref;
4957
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$8), customLabels);
4958
+ const compact = useMediaQuery("(max-width: 767px)", false);
4959
+ const [localOpen, setLocalOpen] = useState(null);
4960
+ const sidebarOpen = (_ref = controlledSidebarOpen !== null && controlledSidebarOpen !== void 0 ? controlledSidebarOpen : localOpen) !== null && _ref !== void 0 ? _ref : !compact;
4961
+ const setSidebarOpen = useCallback((next) => {
4962
+ if (controlledSidebarOpen === void 0) setLocalOpen(next);
4963
+ onSidebarOpenChange === null || onSidebarOpenChange === void 0 || onSidebarOpenChange(next);
4964
+ }, [controlledSidebarOpen, onSidebarOpenChange]);
4965
+ const sidebarRef = useRef(null);
4966
+ useEffect(() => {
4967
+ const panel = sidebarRef.current;
4968
+ if (!panel) return;
4969
+ if (sidebarOpen) panel.expand();
4970
+ else panel.collapse();
4971
+ }, [sidebarOpen]);
4972
+ const mounted = useSyncExternalStore(subscribeNoop, () => true, () => false);
4973
+ const [horizontalLayout] = useState(() => readLayout(storageKey ? `${storageKey}:h` : void 0));
4974
+ const [verticalLayout] = useState(() => readLayout(storageKey ? `${storageKey}:v` : void 0));
4975
+ const column = /* @__PURE__ */ jsxs("div", {
4976
+ className: "flex h-full min-h-0 min-w-0 flex-col",
4977
+ children: [
4978
+ header ? /* @__PURE__ */ jsx("div", {
4979
+ className: "flex-none",
4980
+ children: header
4981
+ }) : null,
4982
+ toolbar ? /* @__PURE__ */ jsxs("div", {
4983
+ className: "flex flex-none items-stretch",
4984
+ children: [sidebar && !sidebarOpen ? /* @__PURE__ */ jsx("div", {
4985
+ className: "border-border flex items-center border-b pl-1",
4986
+ children: /* @__PURE__ */ jsx(Button, {
4987
+ variant: "ghost",
4988
+ size: "sm",
4989
+ "aria-label": labels.openSidebar,
4990
+ title: labels.openSidebar,
4991
+ onClick: () => setSidebarOpen(true),
4992
+ children: /* @__PURE__ */ jsx(PanelLeftOpen, {
4993
+ className: "size-4",
4994
+ "aria-hidden": true
4995
+ })
4996
+ })
4997
+ }) : null, /* @__PURE__ */ jsx("div", {
4998
+ className: "min-w-0 flex-1",
4999
+ children: toolbar
5000
+ })]
5001
+ }) : null,
5002
+ /* @__PURE__ */ jsxs(ResizablePanelGroup, {
5003
+ direction: "vertical",
5004
+ className: "min-h-0 flex-1",
5005
+ defaultLayout: verticalLayout,
5006
+ onLayoutChanged: (layout) => writeLayout(storageKey ? `${storageKey}:v` : void 0, layout),
5007
+ children: [
5008
+ /* @__PURE__ */ jsx(ResizablePanel, {
5009
+ id: "editor",
5010
+ defaultSize: defaultEditorSize,
5011
+ minSize: 15,
5012
+ className: "flex min-h-0 flex-col overflow-hidden",
5013
+ "aria-label": labels.editor,
5014
+ children: /* @__PURE__ */ jsx("div", {
5015
+ className: "flex min-h-0 flex-1 flex-col p-2",
5016
+ children: editor
5017
+ })
5018
+ }),
5019
+ /* @__PURE__ */ jsx(ResizableHandle, { withHandle: true }),
5020
+ /* @__PURE__ */ jsx(ResizablePanel, {
5021
+ id: "output",
5022
+ defaultSize: 100 - defaultEditorSize,
5023
+ minSize: 15,
5024
+ className: "flex min-h-0 flex-col overflow-hidden",
5025
+ "aria-label": labels.output,
5026
+ children: output
5027
+ })
5028
+ ]
5029
+ })
5030
+ ]
5031
+ });
5032
+ return /* @__PURE__ */ jsx("div", {
5033
+ className: cn("bg-surface border-border flex h-full min-h-0 w-full overflow-hidden rounded-lg border", className),
5034
+ "data-opt-id": "HCNR2",
5035
+ "data-opt-slug": "query-workspace.QueryWorkspace",
5036
+ children: compact ? /* @__PURE__ */ jsxs("div", {
5037
+ className: "flex h-full min-h-0 w-full flex-col",
5038
+ children: [sidebar && sidebarOpen ? /* @__PURE__ */ jsxs("div", {
5039
+ className: "bg-bg-subtle border-border flex max-h-[50%] min-h-0 flex-col overflow-hidden border-b",
5040
+ "aria-label": labels.sidebar,
5041
+ children: [/* @__PURE__ */ jsx("div", {
5042
+ className: "border-border flex flex-none items-center justify-end border-b px-1 py-0.5",
5043
+ children: /* @__PURE__ */ jsx(Button, {
5044
+ variant: "ghost",
5045
+ size: "sm",
5046
+ "aria-label": labels.closeSidebar,
5047
+ title: labels.closeSidebar,
5048
+ onClick: () => setSidebarOpen(false),
5049
+ children: /* @__PURE__ */ jsx(PanelLeftClose, {
5050
+ className: "size-4",
5051
+ "aria-hidden": true
5052
+ })
5053
+ })
5054
+ }), /* @__PURE__ */ jsx("div", {
5055
+ className: "flex min-h-0 flex-1 flex-col",
5056
+ children: sidebar
5057
+ })]
5058
+ }) : null, /* @__PURE__ */ jsx("div", {
5059
+ className: "flex min-h-0 min-w-0 flex-1 flex-col",
5060
+ children: column
5061
+ })]
5062
+ }) : !mounted ? /* @__PURE__ */ jsxs("div", {
5063
+ className: "flex h-full min-h-0 w-full",
5064
+ children: [sidebar && sidebarOpen ? /* @__PURE__ */ jsx("div", {
5065
+ className: "bg-bg-subtle border-border flex min-h-0 w-[22%] flex-col overflow-hidden border-r",
5066
+ "aria-label": labels.sidebar,
5067
+ children: /* @__PURE__ */ jsx("div", {
5068
+ className: "flex min-h-0 flex-1 flex-col",
5069
+ children: sidebar
5070
+ })
5071
+ }) : null, /* @__PURE__ */ jsxs("div", {
5072
+ className: "flex min-h-0 min-w-0 flex-1 flex-col",
5073
+ children: [
5074
+ header ? /* @__PURE__ */ jsx("div", {
5075
+ className: "flex-none",
5076
+ children: header
5077
+ }) : null,
5078
+ toolbar ? /* @__PURE__ */ jsx("div", {
5079
+ className: "flex-none",
5080
+ children: toolbar
5081
+ }) : null,
5082
+ /* @__PURE__ */ jsx("div", {
5083
+ className: "flex min-h-0 flex-col p-2",
5084
+ style: { flexBasis: `${defaultEditorSize}%` },
5085
+ children: editor
5086
+ }),
5087
+ /* @__PURE__ */ jsx("div", {
5088
+ className: "flex min-h-0 flex-1 flex-col",
5089
+ children: output
5090
+ })
5091
+ ]
5092
+ })]
5093
+ }) : sidebar ? /* @__PURE__ */ jsxs(ResizablePanelGroup, {
5094
+ direction: "horizontal",
5095
+ className: "min-h-0 flex-1",
5096
+ defaultLayout: horizontalLayout,
5097
+ onLayoutChanged: (layout) => writeLayout(storageKey ? `${storageKey}:h` : void 0, layout),
5098
+ children: [
5099
+ /* @__PURE__ */ jsxs(ResizablePanel, {
5100
+ id: "sidebar",
5101
+ panelRef: sidebarRef,
5102
+ defaultSize: defaultSidebarSize,
5103
+ minSize: 12,
5104
+ maxSize: 45,
5105
+ collapsible: true,
5106
+ collapsedSize: 0,
5107
+ onResize: (size) => {
5108
+ const collapsed = size.asPercentage === 0;
5109
+ if (collapsed === sidebarOpen) setSidebarOpen(!collapsed);
5110
+ },
5111
+ className: "bg-bg-subtle flex min-h-0 flex-col overflow-hidden",
5112
+ "aria-label": labels.sidebar,
5113
+ children: [/* @__PURE__ */ jsx("div", {
5114
+ className: "border-border flex flex-none items-center justify-end border-b px-1 py-0.5",
5115
+ children: /* @__PURE__ */ jsx(Button, {
5116
+ variant: "ghost",
5117
+ size: "sm",
5118
+ "aria-label": labels.closeSidebar,
5119
+ title: labels.closeSidebar,
5120
+ onClick: () => setSidebarOpen(false),
5121
+ children: /* @__PURE__ */ jsx(PanelLeftClose, {
5122
+ className: "size-4",
5123
+ "aria-hidden": true
5124
+ })
5125
+ })
5126
+ }), /* @__PURE__ */ jsx("div", {
5127
+ className: "flex min-h-0 flex-1 flex-col",
5128
+ children: sidebar
5129
+ })]
5130
+ }),
5131
+ /* @__PURE__ */ jsx(ResizableHandle, {}),
5132
+ /* @__PURE__ */ jsx(ResizablePanel, {
5133
+ id: "main",
5134
+ defaultSize: 100 - defaultSidebarSize,
5135
+ minSize: 40,
5136
+ className: "flex min-h-0 flex-col overflow-hidden",
5137
+ children: column
5138
+ })
5139
+ ]
5140
+ }) : column
5141
+ });
5142
+ }
5143
+ QueryWorkspace.displayName = "QueryWorkspace";
5144
+ const THEME_REGISTRY = {
5145
+ default: {
5146
+ id: "default",
5147
+ label: "Default",
5148
+ description: "Blue accent, balanced radius & shadow",
5149
+ palette: {
5150
+ seed: "#2563eb",
5151
+ harmony: "split-complementary",
5152
+ radiusScale: "medium",
5153
+ shadowIntensity: "subtle",
5154
+ neutralTemperature: "cool"
5155
+ },
5156
+ switcherSwatches: [
5157
+ "hsl(221 83% 53%)",
5158
+ "hsl(142 71% 45%)",
5159
+ "hsl(38 92% 50%)"
5160
+ ]
5161
+ },
5162
+ minimal: {
5163
+ id: "minimal",
5164
+ label: "Minimal",
5165
+ description: "Monochrome, no shadow, sharp edges",
5166
+ palette: {
5167
+ seed: "#333333",
5168
+ harmony: "analogous",
5169
+ radiusScale: "sharp",
5170
+ shadowIntensity: "none",
5171
+ neutralTemperature: "cool"
5172
+ },
5173
+ switcherSwatches: [
5174
+ "hsl(0 0% 20%)",
5175
+ "hsl(0 0% 45%)",
5176
+ "hsl(0 0% 70%)"
5177
+ ]
5178
+ },
5179
+ natural: {
5180
+ id: "natural",
5181
+ label: "Natural",
5182
+ description: "Always light — sage accent, warm cream",
5183
+ forceLightOnly: true,
5184
+ palette: {
5185
+ seed: "#5a9376",
5186
+ harmony: "complementary",
5187
+ radiusScale: "medium",
5188
+ shadowIntensity: "subtle",
5189
+ neutralTemperature: "warm"
5190
+ },
5191
+ switcherSwatches: [
5192
+ "hsl(155 38% 45%)",
5193
+ "hsl(25 65% 55%)",
5194
+ "hsl(45 75% 50%)"
5195
+ ]
5196
+ },
5197
+ pro: {
5198
+ id: "pro",
5199
+ label: "Pro",
5200
+ description: "Neon blue accent, mono headings, ultra-dense",
5201
+ palette: {
5202
+ seed: "#0080ff",
5203
+ harmony: "tetradic",
5204
+ radiusScale: "sharp",
5205
+ shadowIntensity: "subtle",
5206
+ neutralTemperature: "cool"
5207
+ },
5208
+ switcherSwatches: [
5209
+ "hsl(210 100% 55%)",
5210
+ "hsl(170 90% 48%)",
5211
+ "hsl(290 85% 60%)"
5212
+ ]
5213
+ },
5214
+ "mono-dark": {
5215
+ id: "mono-dark",
5216
+ label: "Mono Dark",
5217
+ description: "OLED-friendly black, neon cyan accent, mono headings",
5218
+ palette: {
5219
+ seed: "#00d4ff",
5220
+ harmony: "tetradic",
5221
+ radiusScale: "sharp",
5222
+ shadowIntensity: "subtle",
5223
+ neutralTemperature: "cool"
5224
+ },
5225
+ switcherSwatches: [
5226
+ "hsl(190 100% 50%)",
5227
+ "hsl(0 0% 8%)",
5228
+ "hsl(280 90% 65%)"
5229
+ ]
5230
+ }
5231
+ };
5232
+ /** All registered preset ids in declaration order. */
5233
+ const THEME_PRESET_IDS = Object.keys(THEME_REGISTRY);
5234
+ /** Look up a descriptor by id. Returns `undefined` for unknown ids. */
5235
+ function getThemeDescriptor(id) {
5236
+ return THEME_REGISTRY[id];
5237
+ }
5238
+ /** Type guard for a known preset id. */
5239
+ function isThemePresetId(id) {
5240
+ return id in THEME_REGISTRY;
5241
+ }
5242
+ /** Preset ids that suppress dark mode generation. */
5243
+ const FORCE_LIGHT_PRESET_IDS = new Set(THEME_PRESET_IDS.filter((id) => THEME_REGISTRY[id].forceLightOnly));
5244
+ /**
5245
+ * Map from removed preset ids to the closest surviving preset. Boot script and
5246
+ * `readStoredTheme` consult this so users with persisted `corporate`/`playful`
5247
+ * values silently land on a working preset on next page load instead of
5248
+ * showing the global default fallback for an unknown id.
5249
+ *
5250
+ * Keep this around indefinitely — localStorage values are forever and the
5251
+ * cost of two extra entries is negligible.
5252
+ */
5253
+ const DEPRECATED_PRESET_MIGRATIONS = Object.freeze({
5254
+ corporate: "default",
5255
+ playful: "default"
5256
+ });
5257
+ /**
5258
+ * Resolve a possibly-deprecated preset id to a registered one. Returns the
5259
+ * input unchanged when it is already valid; returns the migration target when
5260
+ * it is a known deprecated id; returns `null` when the id is unknown
5261
+ * altogether (caller should fall back to its default).
5262
+ */
5263
+ function migrateDeprecatedPresetId(id) {
5264
+ if (!id) return null;
5265
+ if (isThemePresetId(id)) return id;
5266
+ if (id in DEPRECATED_PRESET_MIGRATIONS) return DEPRECATED_PRESET_MIGRATIONS[id];
5267
+ return null;
5268
+ }
5269
+ //#endregion
5270
+ //#region src/lib/theme/theme-types.ts
5271
+ /**
5272
+ * Author canonical theme overrides with token-name autocomplete.
5273
+ * ThemeOverrides remains broad for backward compatibility with older custom
5274
+ * variables; new themes should use this helper as the governed entry point.
5275
+ */
5276
+ function defineThemeOverrides(overrides) {
5277
+ return overrides;
5278
+ }
5279
+ /** Construct a `PresetThemeSpec` — convenience for the common case. */
5280
+ function presetTheme(preset, overrides) {
5281
+ return overrides ? {
5282
+ kind: "preset",
5283
+ preset,
5284
+ overrides
5285
+ } : {
5286
+ kind: "preset",
5287
+ preset
5288
+ };
5289
+ }
5290
+ /** Construct a `GeneratedThemeSpec` from an inline `PaletteConfig`. */
5291
+ function generatedTheme(palette, options) {
5292
+ return _objectSpread2({
5293
+ kind: "generated",
5294
+ palette
5295
+ }, options);
5296
+ }
5297
+ /** True when `spec` references a registered preset id. */
5298
+ function isPresetSpec(spec) {
5299
+ return spec.kind === "preset";
5300
+ }
5301
+ /** True when `spec` carries an inline `PaletteConfig`. */
5302
+ function isGeneratedSpec(spec) {
5303
+ return spec.kind === "generated";
5304
+ }
5305
+ /** True when `spec` wraps a fully-formed `ThemeDescriptor`. */
5306
+ function isDescriptorSpec(spec) {
5307
+ return spec.kind === "descriptor";
5308
+ }
5309
+ //#endregion
5310
+ //#region src/lib/theme/theme-serialize.ts
5311
+ /**
5312
+ * theme-serialize.ts
5313
+ *
5314
+ * Pure helpers that turn a `ThemeSpec` into the artifacts the runtime needs:
5315
+ * resolved descriptor + mode, computed token maps for generated palettes, and
5316
+ * `<style>` CSS strings ready to be injected into `<head>`.
5317
+ *
5318
+ * Kept side-effect free so the boot script and the provider can share the
5319
+ * same logic without dragging React into the client bundle's critical path.
5320
+ */
5321
+ /** Element id used for the global generated theme style tag. */
5322
+ const GLOBAL_THEME_STYLE_ID = "opt-global-theme";
5323
+ /** localStorage key for the persisted ThemeSpec (generated mode). */
5324
+ const THEME_SPEC_STORAGE_KEY = "opt-theme-spec";
5325
+ /**
5326
+ * localStorage key for the cached, pre-serialized token maps that match the
5327
+ * spec under `THEME_SPEC_STORAGE_KEY`. The boot script reads this directly so
5328
+ * it can inline tokens without invoking opt-palette at startup.
5329
+ */
5330
+ const THEME_TOKENS_STORAGE_KEY = "opt-theme-tokens";
5331
+ /** Stable id used for unnamed generated themes. */
5332
+ const GENERATED_THEME_ID_DEFAULT = "generated";
5333
+ /** Cached token maps keyed by spec hash. Lazy-built per process. */
5334
+ const generatedCache = /* @__PURE__ */ new Map();
5335
+ /** Build a stable cache key for a `PaletteConfig`. */
5336
+ function paletteCacheKey(config) {
5337
+ return JSON.stringify(config);
5338
+ }
5339
+ /** Run opt-palette with caching. */
5340
+ function generatePaletteCached(config) {
5341
+ const key = paletteCacheKey(config);
5342
+ let cached = generatedCache.get(key);
5343
+ if (!cached) {
5344
+ cached = generatePalette(config);
5345
+ generatedCache.set(key, cached);
5346
+ }
5347
+ return cached;
5348
+ }
5349
+ /** Apply sparse override maps onto a base token record. */
5350
+ function applyOverrides(base, overrides, mode) {
5351
+ if (!overrides) return base;
5352
+ const merged = _objectSpread2({}, base);
5353
+ if (overrides.shared) Object.assign(merged, overrides.shared);
5354
+ const modeOverrides = mode === "light" ? overrides.light : overrides.dark;
5355
+ if (modeOverrides) Object.assign(merged, modeOverrides);
5356
+ return merged;
5357
+ }
5358
+ /**
5359
+ * Stable id used in `<html data-theme>` and the `<style>` selector.
5360
+ * - preset → the preset id (force-light) or `${id}` / `${id}-dark`
5361
+ * - generated → `spec.id ?? "generated"` with `-dark` suffix when applicable
5362
+ * - descriptor → `descriptor.id` with same suffix logic
5363
+ */
5364
+ function getCompoundIdForSpec(spec, resolvedMode, forceLightOnly) {
5365
+ const baseId = baseIdFor(spec);
5366
+ if (forceLightOnly) return baseId;
5367
+ return resolvedMode === "dark" ? `${baseId}-dark` : baseId;
5368
+ }
5369
+ function baseIdFor(spec) {
5370
+ var _spec$id;
5371
+ if (isPresetSpec(spec)) return spec.preset;
5372
+ if (isDescriptorSpec(spec)) return spec.descriptor.id;
5373
+ return (_spec$id = spec.id) !== null && _spec$id !== void 0 ? _spec$id : GENERATED_THEME_ID_DEFAULT;
5374
+ }
5375
+ /** Whether the given spec emits dark variants at all. */
5376
+ function isForceLightSpec(spec) {
5377
+ if (isPresetSpec(spec)) {
5378
+ const desc = THEME_REGISTRY[spec.preset];
5379
+ return (desc === null || desc === void 0 ? void 0 : desc.forceLightOnly) === true;
5380
+ }
5381
+ if (isDescriptorSpec(spec)) return spec.descriptor.forceLightOnly === true;
5382
+ return spec.forceLightOnly === true;
5383
+ }
5384
+ /**
5385
+ * Compute the inline tokens for a `GeneratedThemeSpec` or
5386
+ * `DescriptorThemeSpec` whose descriptor carries a `palette`. Returns `null`
5387
+ * for preset specs (their tokens live in the static CSS files).
5388
+ */
5389
+ function computeGeneratedTokens(spec) {
5390
+ if (isPresetSpec(spec)) return null;
5391
+ let palette;
5392
+ let overrides;
5393
+ let id;
5394
+ let forceLight;
5395
+ if (isGeneratedSpec(spec)) {
5396
+ var _spec$id2;
5397
+ palette = spec.palette;
5398
+ overrides = spec.overrides;
5399
+ id = (_spec$id2 = spec.id) !== null && _spec$id2 !== void 0 ? _spec$id2 : GENERATED_THEME_ID_DEFAULT;
5400
+ forceLight = spec.forceLightOnly === true;
5401
+ } else {
5402
+ palette = spec.descriptor.palette;
5403
+ overrides = spec.overrides;
5404
+ id = spec.descriptor.id;
5405
+ forceLight = spec.descriptor.forceLightOnly === true;
5406
+ }
5407
+ if (!palette) return null;
5408
+ const generated = generatePaletteCached(palette);
5409
+ return {
5410
+ id,
5411
+ light: applyOverrides(generated.light, overrides, "light"),
5412
+ dark: forceLight ? null : applyOverrides(generated.dark, overrides, "dark")
5413
+ };
5414
+ }
5415
+ /**
5416
+ * Serialize a token record into a CSS rule body — `--name:value;...` — without
5417
+ * surrounding selector or braces. Values are not escaped; callers are
5418
+ * responsible for trusted input. Boot script and provider both rely on this
5419
+ * shape.
5420
+ */
5421
+ function serializeTokenBody(tokens) {
5422
+ return Object.entries(tokens).map(([k, v]) => `${k}:${v}`).join(";");
5423
+ }
5424
+ /**
5425
+ * Build a CSS rule scoped to `[data-theme="<id>"]`.
5426
+ */
5427
+ function serializeThemeRule(themeId, tokens) {
5428
+ return `[data-theme="${themeId}"]{${serializeTokenBody(tokens)}}`;
5429
+ }
5430
+ /**
5431
+ * Build the full CSS for a generated theme style tag — covers light + dark
5432
+ * selectors based on the computed tokens. Caller chooses the id used in
5433
+ * the selector (typically the spec base id).
5434
+ */
5435
+ function serializeGeneratedTheme(tokens) {
5436
+ const { id, light, dark } = tokens;
5437
+ let css = serializeThemeRule(id, light);
5438
+ if (dark) css += serializeThemeRule(`${id}-dark`, dark);
5439
+ return css;
5440
+ }
5441
+ /**
5442
+ * Resolve a spec into its descriptor counterpart. Preset specs map to
5443
+ * `THEME_REGISTRY[preset]`. Generated specs are wrapped into a synthetic
5444
+ * descriptor so callers can treat them uniformly.
5445
+ */
5446
+ function resolveDescriptor(spec) {
5447
+ if (isPresetSpec(spec)) {
5448
+ const desc = THEME_REGISTRY[spec.preset];
5449
+ if (!desc) throw new Error(`[opt-ui] Unknown theme preset id: "${spec.preset}". Registered ids: ${Object.keys(THEME_REGISTRY).join(", ")}`);
5450
+ return desc;
5451
+ }
5452
+ if (isDescriptorSpec(spec)) return spec.descriptor;
5453
+ return generatedSpecToDescriptor(spec);
5454
+ }
5455
+ function generatedSpecToDescriptor(spec) {
5456
+ var _spec$id3, _spec$label;
5457
+ return {
5458
+ id: (_spec$id3 = spec.id) !== null && _spec$id3 !== void 0 ? _spec$id3 : GENERATED_THEME_ID_DEFAULT,
5459
+ label: (_spec$label = spec.label) !== null && _spec$label !== void 0 ? _spec$label : "Custom",
5460
+ description: "Generated theme",
5461
+ palette: spec.palette,
5462
+ forceLightOnly: spec.forceLightOnly
5463
+ };
5464
+ }
5465
+ /**
5466
+ * Validate a `ThemeSpec` parsed from localStorage. Returns the spec on
5467
+ * success or `null` if any required field is missing or the preset id is
5468
+ * unknown. Defensive parsing — never throws.
5469
+ */
5470
+ function safeParseStoredSpec(json) {
5471
+ if (!json) return null;
5472
+ try {
5473
+ const parsed = JSON.parse(json);
5474
+ if (!parsed || typeof parsed !== "object" || !("kind" in parsed)) return null;
5475
+ if (parsed.kind === "preset") {
5476
+ const preset = parsed.preset;
5477
+ if (typeof preset !== "string" || !isThemePresetId(preset)) return null;
5478
+ return {
5479
+ kind: "preset",
5480
+ preset
5481
+ };
5482
+ }
5483
+ if (parsed.kind === "generated") {
5484
+ const palette = parsed.palette;
5485
+ if (!isPaletteConfig(palette)) return null;
5486
+ return {
5487
+ kind: "generated",
5488
+ palette,
5489
+ id: typeof parsed.id === "string" ? parsed.id : void 0,
5490
+ label: typeof parsed.label === "string" ? parsed.label : void 0,
5491
+ forceLightOnly: parsed.forceLightOnly === true,
5492
+ overrides: isObjectMap(parsed.overrides) ? parsed.overrides : void 0
5493
+ };
5494
+ }
5495
+ return null;
5496
+ } catch (_unused) {
5497
+ return null;
5498
+ }
5499
+ }
5500
+ function isObjectMap(value) {
5501
+ return value !== null && typeof value === "object" && !Array.isArray(value);
5502
+ }
5503
+ function isPaletteConfig(value) {
5504
+ if (!isObjectMap(value)) return false;
5505
+ const v = value;
5506
+ return typeof v.seed === "string" && typeof v.harmony === "string" && typeof v.radiusScale === "string" && typeof v.shadowIntensity === "string" && typeof v.neutralTemperature === "string";
5507
+ }
5508
+ /**
5509
+ * Parse the cached token payload from localStorage. Returns `null` on any
5510
+ * shape mismatch — caller will recompute from the spec.
5511
+ */
5512
+ function safeParseStoredTokens(json) {
5513
+ if (!json) return null;
5514
+ try {
5515
+ const parsed = JSON.parse(json);
5516
+ if (!parsed || typeof parsed.id !== "string" || !isObjectMap(parsed.light)) return null;
5517
+ return {
5518
+ id: parsed.id,
5519
+ light: parsed.light,
5520
+ dark: isObjectMap(parsed.dark) ? parsed.dark : void 0
5521
+ };
5522
+ } catch (_unused2) {
5523
+ return null;
5524
+ }
5525
+ }
5526
+ /** Build the storage payload that pairs with `opt-theme-spec`. */
5527
+ function buildTokenCache(tokens) {
5528
+ var _tokens$dark;
5529
+ return {
5530
+ id: tokens.id,
5531
+ light: tokens.light,
5532
+ dark: (_tokens$dark = tokens.dark) !== null && _tokens$dark !== void 0 ? _tokens$dark : void 0
5533
+ };
5534
+ }
5535
+ //#endregion
5536
+ //#region src/lib/theme/theme-resolution.ts
5537
+ const THEME_MODE_VALUES = [
5538
+ "light",
5539
+ "dark",
5540
+ "system"
5541
+ ];
5542
+ const LEGACY_THEME_STORAGE_KEY = "opt-theme";
5543
+ const THEME_PRESET_STORAGE_KEY = "opt-preset";
5544
+ const THEME_MODE_STORAGE_KEY = "opt-mode";
5545
+ const DARK_MQ = "(prefers-color-scheme: dark)";
5546
+ /**
5547
+ * Presets that force light mode regardless of system dark/light preference.
5548
+ * Mirrors `FORCE_LIGHT_PRESET_IDS` from `./presets` — kept as alias for
5549
+ * existing imports.
5550
+ */
5551
+ const FORCE_LIGHT_PRESETS = FORCE_LIGHT_PRESET_IDS;
5552
+ /** All valid compound theme names. Useful for static enumeration. */
5553
+ const ALL_COMPOUND_THEMES = THEME_PRESET_IDS.flatMap((id) => FORCE_LIGHT_PRESET_IDS.has(id) ? [id] : [id, `${id}-dark`]);
5554
+ function hasStorageMutation(storage) {
5555
+ return typeof storage.setItem === "function" && typeof storage.removeItem === "function";
5556
+ }
5557
+ /** Handles is theme mode. */
5558
+ function isThemeMode(value) {
5559
+ return typeof value === "string" && THEME_MODE_VALUES.includes(value);
5560
+ }
5561
+ /** Compute compound theme name from preset + resolved mode. */
5562
+ function getCompoundTheme(preset, resolvedMode) {
5563
+ if (FORCE_LIGHT_PRESETS.has(preset)) return preset;
5564
+ return resolvedMode === "dark" ? `${preset}-dark` : preset;
5565
+ }
5566
+ /** Handles resolve theme mode. */
5567
+ function resolveThemeMode(preset, mode, systemDark) {
5568
+ if (FORCE_LIGHT_PRESETS.has(preset)) return "light";
5569
+ if (mode === "system") return systemDark ? "dark" : "light";
5570
+ return mode;
5571
+ }
5572
+ /** Handles read stored theme. */
5573
+ function readStoredTheme(storage, defaultPreset, defaultMode, options = {}) {
5574
+ const legacy = storage.getItem(LEGACY_THEME_STORAGE_KEY);
5575
+ const storedPreset = storage.getItem(THEME_PRESET_STORAGE_KEY);
5576
+ const storedMode = storage.getItem(THEME_MODE_STORAGE_KEY);
5577
+ let preset = defaultPreset;
5578
+ if (typeof legacy === "string") {
5579
+ const migratedLegacy = migrateDeprecatedPresetId(legacy);
5580
+ if (migratedLegacy) {
5581
+ if (options.migrateLegacy && hasStorageMutation(storage)) {
5582
+ storage.setItem(THEME_PRESET_STORAGE_KEY, migratedLegacy);
5583
+ storage.removeItem(LEGACY_THEME_STORAGE_KEY);
5584
+ }
5585
+ preset = migratedLegacy;
5586
+ }
5587
+ } else if (typeof storedPreset === "string") {
5588
+ const migrated = migrateDeprecatedPresetId(storedPreset);
5589
+ if (migrated) {
5590
+ preset = migrated;
5591
+ if (hasStorageMutation(storage) && migrated !== storedPreset && storedPreset in DEPRECATED_PRESET_MIGRATIONS) storage.setItem(THEME_PRESET_STORAGE_KEY, migrated);
5592
+ }
5593
+ }
5594
+ const mode = isThemeMode(storedMode) ? storedMode : defaultMode;
5595
+ return {
5596
+ preset,
5597
+ mode
5598
+ };
5599
+ }
5600
+ /** Normalize a mode against an optional allowed-mode list. */
5601
+ function coerceThemeMode(mode, allowedModes, fallback = "system") {
5602
+ var _allowedModes$;
5603
+ if (!allowedModes || allowedModes.length === 0) return mode;
5604
+ if (allowedModes.includes(mode)) return mode;
5605
+ if (allowedModes.includes(fallback)) return fallback;
5606
+ return (_allowedModes$ = allowedModes[0]) !== null && _allowedModes$ !== void 0 ? _allowedModes$ : fallback;
5607
+ }
5608
+ /**
5609
+ * Build the inline `<head>` boot script that paints the correct theme before
5610
+ * React mounts. Two paths:
5611
+ *
5612
+ * 1. **Generated** — when both `opt-theme-spec` (kind: "generated") and
5613
+ * `opt-theme-tokens` are present and well-formed, a `<style id="…">` tag
5614
+ * is inserted with the cached tokens and `<html data-theme>` is set to the
5615
+ * generated id (with optional `-dark` suffix). This bypasses opt-palette
5616
+ * at startup so first paint is instant.
5617
+ * 2. **Preset (legacy)** — falls through to the existing preset selector,
5618
+ * including legacy `opt-theme` → `opt-preset` migration.
5619
+ *
5620
+ * Returned as a single minified line; intended for `dangerouslySetInnerHTML`.
5621
+ */
5622
+ function createThemeBootScript(defaultPreset = "default", defaultMode = "system", options = {}) {
5623
+ var _options$allowedModes, _allowedModes$2;
5624
+ const presetValues = JSON.stringify(THEME_PRESET_IDS);
5625
+ const modeValues = JSON.stringify(THEME_MODE_VALUES);
5626
+ const allowedModes = ((_options$allowedModes = options.allowedModes) === null || _options$allowedModes === void 0 ? void 0 : _options$allowedModes.length) ? options.allowedModes : THEME_MODE_VALUES;
5627
+ const allowedModeValues = JSON.stringify(allowedModes);
5628
+ const forceLightPresets = JSON.stringify(Array.from(FORCE_LIGHT_PRESETS));
5629
+ const deprecatedMap = JSON.stringify(DEPRECATED_PRESET_MIGRATIONS);
5630
+ const lockedPreset = options.lockedPreset;
5631
+ const defaultPresetLiteral = JSON.stringify(lockedPreset !== null && lockedPreset !== void 0 ? lockedPreset : defaultPreset);
5632
+ const defaultModeLiteral = JSON.stringify(coerceThemeMode(defaultMode, allowedModes, (_allowedModes$2 = allowedModes[0]) !== null && _allowedModes$2 !== void 0 ? _allowedModes$2 : "system"));
5633
+ const lockedPresetLiteral = JSON.stringify(lockedPreset !== null && lockedPreset !== void 0 ? lockedPreset : null);
5634
+ const styleId = JSON.stringify(GLOBAL_THEME_STYLE_ID);
5635
+ const specKey = JSON.stringify(THEME_SPEC_STORAGE_KEY);
5636
+ const tokensKey = JSON.stringify(THEME_TOKENS_STORAGE_KEY);
5637
+ const presetKey = JSON.stringify(THEME_PRESET_STORAGE_KEY);
5638
+ const modeKey = JSON.stringify(THEME_MODE_STORAGE_KEY);
5639
+ const legacyKey = JSON.stringify(LEGACY_THEME_STORAGE_KEY);
5640
+ const darkMq = JSON.stringify(DARK_MQ);
5641
+ return `
5642
+ try {
5643
+ var pv=${presetValues},mv=${modeValues},am=${allowedModeValues},fp=${forceLightPresets},dm=${deprecatedMap},lk=${lockedPresetLiteral};
5644
+ function mp(v){if(typeof v!=="string")return null;if(pv.indexOf(v)>=0)return v;return Object.prototype.hasOwnProperty.call(dm,v)?dm[v]:null;}
5645
+ var p=${defaultPresetLiteral};
5646
+ if(!lk){
5647
+ var lp=localStorage.getItem(${legacyKey}),sp=localStorage.getItem(${presetKey});
5648
+ var lpm=mp(lp),spm=mp(sp);
5649
+ p=lpm||spm||${defaultPresetLiteral};
5650
+ if(lpm){localStorage.setItem(${presetKey},lpm);localStorage.removeItem(${legacyKey});}
5651
+ else if(sp&&spm&&sp!==spm){localStorage.setItem(${presetKey},spm);}
5652
+ }
5653
+ var sm=localStorage.getItem(${modeKey});
5654
+ var m=(typeof sm==="string"&&mv.indexOf(sm)>=0&&am.indexOf(sm)>=0)?sm:${defaultModeLiteral};
5655
+ var spec=null,tk=null;
5656
+ if(!lk){try{var sj=localStorage.getItem(${specKey});if(sj)spec=JSON.parse(sj);}catch(e){}}
5657
+ if(!lk){try{var tj=localStorage.getItem(${tokensKey});if(tj)tk=JSON.parse(tj);}catch(e){}}
5658
+ if(!lk&&spec&&spec.kind==="generated"&&tk&&typeof tk.id==="string"&&tk.light&&typeof tk.light==="object"){
5659
+ var fld=spec.forceLightOnly===true;
5660
+ var dk=!fld&&(m==="dark"||(m==="system"&&matchMedia(${darkMq}).matches));
5661
+ var tt=(dk&&tk.dark)?tk.dark:tk.light;
5662
+ var cid=(dk&&tk.dark)?(tk.id+"-dark"):tk.id;
5663
+ var css="";
5664
+ for(var k in tt){if(Object.prototype.hasOwnProperty.call(tt,k))css+=k+":"+tt[k]+";"}
5665
+ var st=document.getElementById(${styleId});
5666
+ if(!st){st=document.createElement("style");st.id=${styleId};document.head.appendChild(st);}
5667
+ st.textContent='[data-theme="'+cid+'"]{'+css+'}';
5668
+ document.documentElement.setAttribute("data-theme",cid);
5669
+ } else {
5670
+ var d=fp.indexOf(p)<0&&(m==="dark"||(m==="system"&&matchMedia(${darkMq}).matches));
5671
+ document.documentElement.setAttribute("data-theme",d?p+"-dark":p);
5672
+ }
5673
+ } catch(e){}`.replace(/\n\s*/g, "");
5674
+ }
5675
+ //#endregion
5676
+ //#region src/lib/theme/theme-controller-context.tsx
5677
+ const OptThemeControllerContext = createContext(null);
5678
+ /**
5679
+ * Reads the controller exposed by the nearest `OptThemeProvider`. Returns
5680
+ * `null` when used outside a provider — callers should treat that as "no
5681
+ * promotion available" and behave as if scope === "override".
5682
+ */
5683
+ function useOptThemeController() {
5684
+ return useContext(OptThemeControllerContext);
5685
+ }
5686
+ //#endregion
5687
+ //#region src/lib/theme/theme-provider.tsx
5688
+ const THEME_STORAGE_KEYS = /* @__PURE__ */ new Set([
5689
+ LEGACY_THEME_STORAGE_KEY,
5690
+ THEME_PRESET_STORAGE_KEY,
5691
+ THEME_MODE_STORAGE_KEY,
5692
+ THEME_SPEC_STORAGE_KEY,
5693
+ THEME_TOKENS_STORAGE_KEY
5694
+ ]);
5695
+ function getSystemDark() {
5696
+ if (typeof window === "undefined") return false;
5697
+ return window.matchMedia(DARK_MQ).matches;
5698
+ }
5699
+ function subscribeSystemDark(cb) {
5700
+ if (typeof window === "undefined") return () => {};
5701
+ const mql = window.matchMedia(DARK_MQ);
5702
+ mql.addEventListener("change", cb);
5703
+ return () => mql.removeEventListener("change", cb);
5704
+ }
5705
+ /**
5706
+ * Read the persisted ThemeSpec, preferring a generated spec when present.
5707
+ * Falls back to the legacy preset path otherwise. Does not touch localStorage
5708
+ * apart from the legacy migration handled by `readStoredTheme`.
5709
+ */
5710
+ function readStoredSpec(defaultPreset, defaultMode, options = {}) {
5711
+ const generated = safeParseStoredSpec(window.localStorage.getItem(THEME_SPEC_STORAGE_KEY));
5712
+ const { preset, mode } = readStoredTheme(window.localStorage, defaultPreset, defaultMode, options);
5713
+ if (generated && generated.kind === "generated") return {
5714
+ spec: generated,
5715
+ mode,
5716
+ presetShadow: preset
5717
+ };
5718
+ return {
5719
+ spec: presetTheme(preset),
5720
+ mode,
5721
+ presetShadow: preset
5722
+ };
5723
+ }
5724
+ function persistSpec(spec) {
5725
+ if (isPresetSpec(spec)) {
5726
+ window.localStorage.setItem(THEME_PRESET_STORAGE_KEY, spec.preset);
5727
+ window.localStorage.removeItem(THEME_SPEC_STORAGE_KEY);
5728
+ window.localStorage.removeItem(THEME_TOKENS_STORAGE_KEY);
5729
+ return;
5730
+ }
5731
+ if (isGeneratedSpec(spec)) {
5732
+ const tokens = computeGeneratedTokens(spec);
5733
+ window.localStorage.setItem(THEME_SPEC_STORAGE_KEY, JSON.stringify(spec));
5734
+ if (tokens) window.localStorage.setItem(THEME_TOKENS_STORAGE_KEY, JSON.stringify(buildTokenCache(tokens)));
5735
+ else window.localStorage.removeItem(THEME_TOKENS_STORAGE_KEY);
5736
+ return;
5737
+ }
5738
+ const descriptor = spec.descriptor;
5739
+ if (isThemePresetId(descriptor.id) && !descriptor.palette) {
5740
+ window.localStorage.setItem(THEME_PRESET_STORAGE_KEY, descriptor.id);
5741
+ window.localStorage.removeItem(THEME_SPEC_STORAGE_KEY);
5742
+ window.localStorage.removeItem(THEME_TOKENS_STORAGE_KEY);
5743
+ return;
5744
+ }
5745
+ if (descriptor.palette) persistSpec({
5746
+ kind: "generated",
5747
+ id: descriptor.id,
5748
+ label: descriptor.label,
5749
+ palette: descriptor.palette,
5750
+ forceLightOnly: descriptor.forceLightOnly
5751
+ });
5752
+ }
5753
+ function upsertGlobalStyle(css) {
5754
+ if (typeof document === "undefined") return;
5755
+ let node = document.getElementById(GLOBAL_THEME_STYLE_ID);
5756
+ if (!node) {
5757
+ node = document.createElement("style");
5758
+ node.id = GLOBAL_THEME_STYLE_ID;
5759
+ document.head.appendChild(node);
5760
+ }
5761
+ if (node.textContent !== css) node.textContent = css;
5762
+ }
5763
+ function removeGlobalStyle() {
5764
+ var _document$getElementB;
5765
+ if (typeof document === "undefined") return;
5766
+ (_document$getElementB = document.getElementById("opt-global-theme")) === null || _document$getElementB === void 0 || _document$getElementB.remove();
5767
+ }
5768
+ const ThemeContext = createContext(void 0);
5769
+ /** Renders the opt theme provider component. */
5770
+ function OptThemeProvider({ children, defaultTheme, controlledTheme, defaultPreset = "default", defaultMode = "system", lockedPreset, allowedModes, allowThemePromotion = true }) {
5771
+ var _allowedModes$, _ref, _effectiveStackTop$sp;
5772
+ const normalizedDefaultMode = coerceThemeMode(defaultMode, allowedModes, (_allowedModes$ = allowedModes === null || allowedModes === void 0 ? void 0 : allowedModes[0]) !== null && _allowedModes$ !== void 0 ? _allowedModes$ : "system");
5773
+ const initialSpec = (_ref = controlledTheme !== null && controlledTheme !== void 0 ? controlledTheme : lockedPreset ? presetTheme(lockedPreset) : defaultTheme) !== null && _ref !== void 0 ? _ref : presetTheme(defaultPreset);
5774
+ const initialPresetShadow = isPresetSpec(initialSpec) ? initialSpec.preset : lockedPreset !== null && lockedPreset !== void 0 ? lockedPreset : defaultPreset;
5775
+ const [theme, setThemeState] = useState(initialSpec);
5776
+ const [presetShadow, setPresetShadow] = useState(initialPresetShadow);
5777
+ const [mode, setModeState] = useState(normalizedDefaultMode);
5778
+ /**
5779
+ * Transient theme override stack — descendants (typically harnesses in
5780
+ * promote-global mode) push specs that take priority over `controlledTheme`
5781
+ * and persisted state. The stack is intentionally kept out of localStorage.
5782
+ */
5783
+ const [stack, setStack] = useState([]);
5784
+ const isControlled = controlledTheme !== void 0;
5785
+ const isLocked = !isControlled && lockedPreset !== void 0;
5786
+ useEffect(() => {
5787
+ if (!isControlled) return;
5788
+ setThemeState(controlledTheme);
5789
+ if (isPresetSpec(controlledTheme)) setPresetShadow(controlledTheme.preset);
5790
+ }, [controlledTheme, isControlled]);
5791
+ useEffect(() => {
5792
+ if (isControlled) return;
5793
+ if (isLocked) {
5794
+ const locked = lockedPreset !== null && lockedPreset !== void 0 ? lockedPreset : defaultPreset;
5795
+ const { mode: storedMode } = readStoredTheme(window.localStorage, locked, normalizedDefaultMode);
5796
+ const nextMode = coerceThemeMode(storedMode, allowedModes, normalizedDefaultMode);
5797
+ const nextSpec = presetTheme(locked);
5798
+ setThemeState((prev) => themeEqual(prev, nextSpec) ? prev : nextSpec);
5799
+ setPresetShadow((prev) => prev === locked ? prev : locked);
5800
+ setModeState((prev) => prev === nextMode ? prev : nextMode);
5801
+ return;
5802
+ }
5803
+ const next = readStoredSpec(defaultPreset, defaultMode, { migrateLegacy: true });
5804
+ const nextMode = coerceThemeMode(next.mode, allowedModes, normalizedDefaultMode);
5805
+ setThemeState((prev) => themeEqual(prev, next.spec) ? prev : next.spec);
5806
+ setPresetShadow((prev) => prev === next.presetShadow ? prev : next.presetShadow);
5807
+ setModeState((prev) => prev === nextMode ? prev : nextMode);
5808
+ }, [
5809
+ allowedModes,
5810
+ isControlled,
5811
+ isLocked,
5812
+ lockedPreset,
5813
+ normalizedDefaultMode
5814
+ ]);
5815
+ useEffect(() => {
5816
+ if (isControlled) return;
5817
+ function handleStorage(event) {
5818
+ if (event.storageArea && event.storageArea !== window.localStorage) return;
5819
+ if (event.key != null && !THEME_STORAGE_KEYS.has(event.key)) return;
5820
+ if (isLocked) {
5821
+ const locked = lockedPreset !== null && lockedPreset !== void 0 ? lockedPreset : defaultPreset;
5822
+ const { mode: storedMode } = readStoredTheme(window.localStorage, locked, normalizedDefaultMode);
5823
+ const nextMode = coerceThemeMode(storedMode, allowedModes, normalizedDefaultMode);
5824
+ const nextSpec = presetTheme(locked);
5825
+ setThemeState((prev) => themeEqual(prev, nextSpec) ? prev : nextSpec);
5826
+ setPresetShadow((prev) => prev === locked ? prev : locked);
5827
+ setModeState((prev) => prev === nextMode ? prev : nextMode);
5828
+ return;
5829
+ }
5830
+ const next = readStoredSpec(defaultPreset, defaultMode, { migrateLegacy: event.key === LEGACY_THEME_STORAGE_KEY });
5831
+ const nextMode = coerceThemeMode(next.mode, allowedModes, normalizedDefaultMode);
5832
+ setThemeState((prev) => themeEqual(prev, next.spec) ? prev : next.spec);
5833
+ setPresetShadow((prev) => prev === next.presetShadow ? prev : next.presetShadow);
5834
+ setModeState((prev) => prev === nextMode ? prev : nextMode);
5835
+ }
5836
+ window.addEventListener("storage", handleStorage);
5837
+ return () => window.removeEventListener("storage", handleStorage);
5838
+ }, [
5839
+ allowedModes,
5840
+ isControlled,
5841
+ isLocked,
5842
+ defaultMode,
5843
+ defaultPreset,
5844
+ lockedPreset,
5845
+ normalizedDefaultMode
5846
+ ]);
5847
+ const systemDark = useSyncExternalStore(subscribeSystemDark, getSystemDark, () => false);
5848
+ const stackTop = stack.length > 0 ? stack[stack.length - 1] : null;
5849
+ const effectiveStackTop = allowThemePromotion ? stackTop : null;
5850
+ const isPromoted = effectiveStackTop !== null;
5851
+ const effectiveSpec = (_effectiveStackTop$sp = effectiveStackTop === null || effectiveStackTop === void 0 ? void 0 : effectiveStackTop.spec) !== null && _effectiveStackTop$sp !== void 0 ? _effectiveStackTop$sp : theme;
5852
+ const forceLight = isForceLightSpec(effectiveSpec);
5853
+ const resolvedMode = computeResolvedMode(forceLight, mode, systemDark);
5854
+ const compoundId = getCompoundIdForSpec(effectiveSpec, resolvedMode, forceLight);
5855
+ const [hydrated, setHydrated] = useState(false);
5856
+ useEffect(() => {
5857
+ setHydrated(true);
5858
+ }, []);
5859
+ useEffect(() => {
5860
+ if (!hydrated) return;
5861
+ if (document.documentElement.getAttribute("data-theme") !== compoundId) document.documentElement.setAttribute("data-theme", compoundId);
5862
+ }, [compoundId, hydrated]);
5863
+ useEffect(() => {
5864
+ if (isPresetSpec(effectiveSpec)) {
5865
+ removeGlobalStyle();
5866
+ return;
5867
+ }
5868
+ const tokens = computeGeneratedTokens(effectiveSpec);
5869
+ if (!tokens) {
5870
+ removeGlobalStyle();
5871
+ return;
5872
+ }
5873
+ upsertGlobalStyle(serializeGeneratedTheme(tokens));
5874
+ if (!isControlled && !isPromoted && effectiveSpec === theme) try {
5875
+ window.localStorage.setItem(THEME_TOKENS_STORAGE_KEY, JSON.stringify(buildTokenCache(tokens)));
5876
+ } catch (_unused) {}
5877
+ }, [
5878
+ effectiveSpec,
5879
+ theme,
5880
+ isControlled,
5881
+ isPromoted
5882
+ ]);
5883
+ const setTheme = useCallback((next) => {
5884
+ if (isLocked) {
5885
+ if (process.env.NODE_ENV !== "production") console.warn("[OptThemeProvider] setTheme called while lockedPreset is set — ignoring.");
5886
+ return;
5887
+ }
5888
+ if (isControlled) {
5889
+ if (process.env.NODE_ENV !== "production") console.warn("[OptThemeProvider] setTheme called while controlledTheme is set — ignoring.");
5890
+ return;
5891
+ }
5892
+ setThemeState(next);
5893
+ if (isPresetSpec(next)) setPresetShadow(next.preset);
5894
+ try {
5895
+ persistSpec(next);
5896
+ } catch (_unused2) {}
5897
+ }, [isControlled, isLocked]);
5898
+ const setPreset = useCallback((p) => {
5899
+ setTheme(presetTheme(p));
5900
+ }, [setTheme]);
5901
+ const setMode = useCallback((m) => {
5902
+ const nextMode = coerceThemeMode(m, allowedModes, normalizedDefaultMode);
5903
+ setModeState(nextMode);
5904
+ if (!isControlled) try {
5905
+ window.localStorage.setItem(THEME_MODE_STORAGE_KEY, nextMode);
5906
+ } catch (_unused3) {}
5907
+ }, [
5908
+ allowedModes,
5909
+ isControlled,
5910
+ normalizedDefaultMode
5911
+ ]);
5912
+ const controller = useMemo(() => ({
5913
+ pushTheme(id, spec) {
5914
+ if (!allowThemePromotion) return;
5915
+ setStack((prev) => {
5916
+ return [...prev.filter((entry) => entry.id !== id), {
5917
+ id,
5918
+ spec
5919
+ }];
5920
+ });
5921
+ },
5922
+ popTheme(id) {
5923
+ if (!allowThemePromotion) return;
5924
+ setStack((prev) => prev.filter((entry) => entry.id !== id));
5925
+ }
5926
+ }), [allowThemePromotion]);
5927
+ const value = useMemo(() => ({
5928
+ theme: effectiveSpec,
5929
+ setTheme,
5930
+ preset: presetShadow,
5931
+ setPreset,
5932
+ mode,
5933
+ setMode,
5934
+ resolvedMode,
5935
+ isControlled,
5936
+ isGenerated: !isPresetSpec(effectiveSpec),
5937
+ isPromoted
5938
+ }), [
5939
+ effectiveSpec,
5940
+ setTheme,
5941
+ presetShadow,
5942
+ setPreset,
5943
+ mode,
5944
+ setMode,
5945
+ resolvedMode,
5946
+ isControlled,
5947
+ isPromoted
5948
+ ]);
5949
+ return /* @__PURE__ */ jsx(ThemeContext.Provider, {
5950
+ value,
5951
+ children: /* @__PURE__ */ jsx(OptThemeControllerContext.Provider, {
5952
+ value: controller,
5953
+ children
5954
+ })
5955
+ });
5956
+ }
5957
+ /** Provides opt theme. */
5958
+ function useOptTheme() {
5959
+ const ctx = useContext(ThemeContext);
5960
+ if (!ctx) throw new Error("useOptTheme must be used within OptThemeProvider");
5961
+ return ctx;
5962
+ }
5963
+ /** Reads opt theme context when present without requiring a provider. */
5964
+ function useOptionalOptTheme() {
5965
+ return useContext(ThemeContext);
5966
+ }
5967
+ function computeResolvedMode(forceLight, mode, systemDark) {
5968
+ if (forceLight) return "light";
5969
+ if (mode === "system") return systemDark ? "dark" : "light";
5970
+ return mode;
5971
+ }
5972
+ function themeEqual(a, b) {
5973
+ if (a === b) return true;
5974
+ if (a.kind !== b.kind) return false;
5975
+ if (a.kind === "preset" && b.kind === "preset") return a.preset === b.preset && overridesEqual(a.overrides, b.overrides);
5976
+ if (a.kind === "generated" && b.kind === "generated") return a.id === b.id && a.label === b.label && a.forceLightOnly === b.forceLightOnly && JSON.stringify(a.palette) === JSON.stringify(b.palette) && overridesEqual(a.overrides, b.overrides);
5977
+ if (a.kind === "descriptor" && b.kind === "descriptor") return a.descriptor === b.descriptor && overridesEqual(a.overrides, b.overrides);
5978
+ return false;
5979
+ }
5980
+ function overridesEqual(a, b) {
5981
+ if (a === b) return true;
5982
+ if (!a && !b) return true;
5983
+ if (!a || !b) return false;
5984
+ return JSON.stringify(a) === JSON.stringify(b);
5985
+ }
5986
+ //#endregion
5987
+ //#region src/shells/sql-editor.tsx
5988
+ const defaultLabels$7 = {
5989
+ placeholder: "SELECT * FROM events LIMIT 100",
5990
+ runHint: "Ctrl+Enter to run",
5991
+ runButton: "Run Query",
5992
+ editor: "SQL 편집기"
5993
+ };
5994
+ /**
5995
+ * SqlEditor — a query is text until it runs; this is where it is text.
5996
+ *
5997
+ * Built on CodeMirror, loaded on first mount so pages that never open the
5998
+ * editor never pay for it. The editor is created once: schema, theme and
5999
+ * read-only changes are applied through compartments, so a catalogue refresh
6000
+ * or a theme switch does not throw away the cursor and the undo history.
6001
+ *
6002
+ * @example
6003
+ * ```tsx
6004
+ * const editor = useRef<SqlEditorHandle>(null);
6005
+ * <SqlEditor
6006
+ * ref={editor}
6007
+ * value={sql}
6008
+ * onChange={setSql}
6009
+ * onRun={(text, { selection }) => run(selection ?? text)}
6010
+ * schema={{ events: ["id", "name", "created_at"] }}
6011
+ * height="fill"
6012
+ * />
6013
+ * ```
6014
+ */
6015
+ const SqlEditor = React.forwardRef(function SqlEditor({ value, onChange, onRun, onRunAndAdvance, placeholder, readOnly = false, height = 200, schema, completionSources, keymap: customKeymap, extensions: customExtensions, ariaLabel, autoFocus = false, showFooter, lineNumbers = true, theme = "auto", labels: customLabels, className }, ref) {
6016
+ var _themeContext$resolve;
6017
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$7), customLabels);
6018
+ const fill = height === "fill";
6019
+ const footerVisible = showFooter !== null && showFooter !== void 0 ? showFooter : Boolean(onRun);
6020
+ const themeContext = useOptionalOptTheme();
6021
+ const resolvedTheme = theme === "auto" ? (_themeContext$resolve = themeContext === null || themeContext === void 0 ? void 0 : themeContext.resolvedMode) !== null && _themeContext$resolve !== void 0 ? _themeContext$resolve : "dark" : theme;
6022
+ const editorRef = React.useRef(null);
6023
+ const viewRef = React.useRef(null);
6024
+ const completionCompartmentRef = React.useRef(null);
6025
+ const editableCompartmentRef = React.useRef(null);
6026
+ const themeCompartmentRef = React.useRef(null);
6027
+ const buildCompletionRef = React.useRef(null);
6028
+ const buildThemeRef = React.useRef(null);
6029
+ const latestValueRef = React.useRef(value);
6030
+ const onChangeRef = React.useRef(onChange);
6031
+ const onRunRef = React.useRef(onRun);
6032
+ const onRunAndAdvanceRef = React.useRef(onRunAndAdvance);
6033
+ const customKeymapRef = React.useRef(customKeymap);
6034
+ const schemaRef = React.useRef(schema);
6035
+ const completionSourcesRef = React.useRef(completionSources);
6036
+ const autoFocusRef = React.useRef(autoFocus);
6037
+ const themeRef = React.useRef(resolvedTheme);
6038
+ const lineNumbersRef = React.useRef(lineNumbers);
6039
+ latestValueRef.current = value;
6040
+ onChangeRef.current = onChange;
6041
+ onRunRef.current = onRun;
6042
+ onRunAndAdvanceRef.current = onRunAndAdvance;
6043
+ customKeymapRef.current = customKeymap;
6044
+ schemaRef.current = schema;
6045
+ completionSourcesRef.current = completionSources;
6046
+ autoFocusRef.current = autoFocus;
6047
+ themeRef.current = resolvedTheme;
6048
+ lineNumbersRef.current = lineNumbers;
6049
+ const autocompleteEnabled = React.useRef(schema !== void 0 || completionSources !== void 0).current;
6050
+ const readContext = React.useCallback(() => {
6051
+ var _view$state$selection;
6052
+ const view = viewRef.current;
6053
+ const main = view === null || view === void 0 || (_view$state$selection = view.state.selection) === null || _view$state$selection === void 0 ? void 0 : _view$state$selection.main;
6054
+ if (!view || !main) return {
6055
+ selection: null,
6056
+ cursorOffset: 0
6057
+ };
6058
+ return {
6059
+ selection: main.from === main.to ? null : view.state.doc.sliceString(main.from, main.to),
6060
+ cursorOffset: main.head
6061
+ };
6062
+ }, []);
6063
+ React.useImperativeHandle(ref, () => ({
6064
+ focus: () => {
6065
+ var _viewRef$current;
6066
+ return (_viewRef$current = viewRef.current) === null || _viewRef$current === void 0 ? void 0 : _viewRef$current.focus();
6067
+ },
6068
+ insertText: (text) => {
6069
+ var _view$state$selection2, _main$from, _main$to;
6070
+ const view = viewRef.current;
6071
+ if (!view) return;
6072
+ const main = (_view$state$selection2 = view.state.selection) === null || _view$state$selection2 === void 0 ? void 0 : _view$state$selection2.main;
6073
+ const from = (_main$from = main === null || main === void 0 ? void 0 : main.from) !== null && _main$from !== void 0 ? _main$from : view.state.doc.length;
6074
+ const to = (_main$to = main === null || main === void 0 ? void 0 : main.to) !== null && _main$to !== void 0 ? _main$to : from;
6075
+ view.dispatch({
6076
+ changes: {
6077
+ from,
6078
+ to,
6079
+ insert: text
6080
+ },
6081
+ selection: { anchor: from + text.length }
6082
+ });
6083
+ view.focus();
6084
+ },
6085
+ getSelection: () => readContext().selection,
6086
+ getCursorOffset: () => readContext().cursorOffset
6087
+ }), [readContext]);
6088
+ React.useEffect(() => {
6089
+ let view = null;
6090
+ let cancelled = false;
6091
+ async function init() {
6092
+ var _customKeymapRef$curr;
6093
+ const parent = editorRef.current;
6094
+ if (!parent) return;
6095
+ const [viewModule, { Compartment, EditorState }, langSql, { oneDark }, autocomplete, commands] = await Promise.all([
6096
+ import("@codemirror/view"),
6097
+ import("@codemirror/state"),
6098
+ import("@codemirror/lang-sql"),
6099
+ import("@codemirror/theme-one-dark"),
6100
+ autocompleteEnabled ? import("@codemirror/autocomplete") : Promise.resolve(null),
6101
+ import("@codemirror/commands").catch(() => null)
6102
+ ]);
6103
+ if (cancelled || !parent.isConnected) return;
6104
+ const { EditorView, keymap: cmKeymap, placeholder: cmPlaceholder } = viewModule;
6105
+ const completionCompartment = new Compartment();
6106
+ const editableCompartment = new Compartment();
6107
+ const themeCompartment = new Compartment();
6108
+ completionCompartmentRef.current = completionCompartment;
6109
+ editableCompartmentRef.current = editableCompartment;
6110
+ themeCompartmentRef.current = themeCompartment;
6111
+ const buildCompletion = (nextSchema, nextSources) => {
6112
+ if (!autocomplete) return [];
6113
+ const dialect = langSql.PostgreSQL;
6114
+ return [autocomplete.autocompletion({ override: [
6115
+ langSql.schemaCompletionSource({
6116
+ dialect,
6117
+ schema: nextSchema !== null && nextSchema !== void 0 ? nextSchema : {}
6118
+ }),
6119
+ langSql.keywordCompletionSource(dialect),
6120
+ ...nextSources !== null && nextSources !== void 0 ? nextSources : []
6121
+ ] }), cmKeymap.of(autocomplete.completionKeymap)];
6122
+ };
6123
+ buildCompletionRef.current = buildCompletion;
6124
+ const buildTheme = (mode) => mode === "dark" ? oneDark : EditorView.theme({
6125
+ "&": {
6126
+ backgroundColor: "var(--opt-surface)",
6127
+ color: "var(--opt-text-primary)"
6128
+ },
6129
+ ".cm-content": { caretColor: "var(--opt-text-primary)" },
6130
+ ".cm-cursor, .cm-dropCursor": { borderLeftColor: "var(--opt-text-primary)" },
6131
+ ".cm-gutters": {
6132
+ backgroundColor: "var(--opt-bg-subtle)",
6133
+ color: "var(--opt-text-tertiary)",
6134
+ borderRight: "1px solid var(--opt-border-subtle)"
6135
+ },
6136
+ ".cm-activeLine": { backgroundColor: "var(--opt-bg-subtle)" },
6137
+ ".cm-activeLineGutter": { backgroundColor: "var(--opt-bg-muted)" },
6138
+ "&.cm-focused .cm-selectionBackground, ::selection": { backgroundColor: "var(--opt-accent-subtle)" },
6139
+ ".cm-selectionBackground": { backgroundColor: "var(--opt-bg-muted)" },
6140
+ ".cm-tooltip": {
6141
+ backgroundColor: "var(--opt-surface-overlay)",
6142
+ color: "var(--opt-text-primary)",
6143
+ border: "1px solid var(--opt-border)"
6144
+ },
6145
+ ".cm-tooltip-autocomplete ul li[aria-selected]": {
6146
+ backgroundColor: "var(--opt-accent)",
6147
+ color: "var(--opt-accent-fg)"
6148
+ }
6149
+ }, { dark: false });
6150
+ buildThemeRef.current = buildTheme;
6151
+ const runKeymap = [{
6152
+ key: "Shift-Enter",
6153
+ run: (target) => {
6154
+ const handler = onRunAndAdvanceRef.current;
6155
+ if (!handler) return false;
6156
+ handler(target.state.doc.toString(), readContext());
6157
+ return true;
6158
+ }
6159
+ }, {
6160
+ key: "Mod-Enter",
6161
+ run: (target) => {
6162
+ const handler = onRunRef.current;
6163
+ if (!handler) return false;
6164
+ handler(target.state.doc.toString(), readContext());
6165
+ return true;
6166
+ }
6167
+ }];
6168
+ const editingKeymap = commands ? [
6169
+ ...commands.defaultKeymap,
6170
+ ...commands.historyKeymap,
6171
+ commands.indentWithTab
6172
+ ] : [];
6173
+ const extensions = [
6174
+ langSql.PostgreSQL.extension,
6175
+ completionCompartment.of(buildCompletion(schemaRef.current, completionSourcesRef.current)),
6176
+ themeCompartment.of(buildTheme(themeRef.current)),
6177
+ ...commands ? [commands.history()] : [],
6178
+ ...lineNumbersRef.current ? [viewModule.lineNumbers(), viewModule.highlightActiveLineGutter()] : [],
6179
+ viewModule.highlightActiveLine(),
6180
+ cmKeymap.of([
6181
+ ...(_customKeymapRef$curr = customKeymapRef.current) !== null && _customKeymapRef$curr !== void 0 ? _customKeymapRef$curr : [],
6182
+ ...runKeymap,
6183
+ ...editingKeymap
6184
+ ]),
6185
+ EditorView.theme({
6186
+ "&": {
6187
+ height: "100%",
6188
+ fontSize: "13px"
6189
+ },
6190
+ "&.cm-focused": {
6191
+ outline: "2px solid var(--opt-ring)",
6192
+ outlineOffset: "-2px"
6193
+ },
6194
+ ".cm-scroller": {
6195
+ height: "100%",
6196
+ overflow: "auto",
6197
+ fontFamily: "var(--opt-font-mono, ui-monospace, monospace)"
6198
+ }
6199
+ }),
6200
+ cmPlaceholder(placeholder !== null && placeholder !== void 0 ? placeholder : labels.placeholder),
6201
+ EditorView.lineWrapping,
6202
+ EditorView.updateListener.of((update) => {
6203
+ if (update.docChanged) onChangeRef.current(update.state.doc.toString());
6204
+ }),
6205
+ editableCompartment.of(EditorView.editable.of(!readOnly)),
6206
+ EditorView.contentAttributes.of({ "aria-label": ariaLabel !== null && ariaLabel !== void 0 ? ariaLabel : labels.editor }),
6207
+ ...customExtensions !== null && customExtensions !== void 0 ? customExtensions : []
6208
+ ];
6209
+ view = new EditorView({
6210
+ state: EditorState.create({
6211
+ doc: latestValueRef.current,
6212
+ extensions
6213
+ }),
6214
+ parent
6215
+ });
6216
+ viewRef.current = view;
6217
+ if (autoFocusRef.current) view.focus();
6218
+ }
6219
+ init();
6220
+ return () => {
6221
+ cancelled = true;
6222
+ view === null || view === void 0 || view.destroy();
6223
+ if (viewRef.current === view) viewRef.current = null;
6224
+ completionCompartmentRef.current = null;
6225
+ editableCompartmentRef.current = null;
6226
+ themeCompartmentRef.current = null;
6227
+ buildCompletionRef.current = null;
6228
+ buildThemeRef.current = null;
6229
+ };
6230
+ }, []);
6231
+ React.useEffect(() => {
6232
+ const view = viewRef.current;
6233
+ if (view && view.state.doc.toString() !== value) view.dispatch({ changes: {
6234
+ from: 0,
6235
+ to: view.state.doc.length,
6236
+ insert: value
6237
+ } });
6238
+ }, [value]);
6239
+ React.useEffect(() => {
6240
+ const view = viewRef.current;
6241
+ const compartment = completionCompartmentRef.current;
6242
+ const build = buildCompletionRef.current;
6243
+ if (!view || !compartment || !build) return;
6244
+ view.dispatch({ effects: compartment.reconfigure(build(schema, completionSources)) });
6245
+ }, [schema, completionSources]);
6246
+ React.useEffect(() => {
6247
+ const view = viewRef.current;
6248
+ const compartment = themeCompartmentRef.current;
6249
+ const build = buildThemeRef.current;
6250
+ if (!view || !compartment || !build) return;
6251
+ view.dispatch({ effects: compartment.reconfigure(build(resolvedTheme)) });
6252
+ }, [resolvedTheme]);
6253
+ React.useEffect(() => {
6254
+ const view = viewRef.current;
6255
+ const compartment = editableCompartmentRef.current;
6256
+ if (!view || !compartment) return;
6257
+ let cancelled = false;
6258
+ import("@codemirror/view").then(({ EditorView }) => {
6259
+ if (cancelled || viewRef.current !== view) return;
6260
+ view.dispatch({ effects: compartment.reconfigure(EditorView.editable.of(!readOnly)) });
6261
+ });
6262
+ return () => {
6263
+ cancelled = true;
6264
+ };
6265
+ }, [readOnly]);
6266
+ return /* @__PURE__ */ jsxs("div", {
6267
+ className: cn("gap-element flex flex-col", fill && "h-full min-h-0", className),
6268
+ "data-opt-id": "RSGKV",
6269
+ "data-opt-slug": "sql-editor.SqlEditor",
6270
+ children: [/* @__PURE__ */ jsx("div", {
6271
+ ref: editorRef,
6272
+ className: cn("border-border overflow-hidden rounded-lg border", fill && "min-h-0 flex-1"),
6273
+ style: fill ? void 0 : { height }
6274
+ }), footerVisible && /* @__PURE__ */ jsxs("div", {
6275
+ className: "flex flex-none items-center justify-between",
6276
+ children: [/* @__PURE__ */ jsx("span", {
6277
+ className: "text-text-tertiary text-xs",
6278
+ children: labels.runHint
6279
+ }), onRun && /* @__PURE__ */ jsx(Button, {
6280
+ variant: "primary",
6281
+ size: "sm",
6282
+ onClick: () => onRun(value, readContext()),
6283
+ children: labels.runButton
6284
+ })]
6285
+ })]
6286
+ });
6287
+ });
6288
+ SqlEditor.displayName = "SqlEditor";
6289
+ //#endregion
6290
+ //#region src/shells/event-timeline.tsx
6291
+ const defaultLabels$6 = {
6292
+ emptyTitle: "No events yet",
6293
+ emptyDescription: "Events appear here as soon as they arrive.",
6294
+ expand: "Show properties",
6295
+ collapse: "Hide properties",
6296
+ loadMore: "Load more",
6297
+ gap: (duration) => `+${duration}`,
6298
+ properties: "Event properties",
6299
+ openGroup: (title) => `Open ${title}`
6300
+ };
6301
+ /**
6302
+ * A line of text and the time it happened have to read as one row. Past about
6303
+ * this width the eye stops connecting them, so the rail is capped rather than
6304
+ * stretched to whatever the page allows. Override with `className` where a
6305
+ * layout genuinely needs the full width.
6306
+ */
6307
+ const TIMELINE_WIDTH = "max-w-5xl";
6308
+ const toneDot = {
6309
+ default: "bg-text-tertiary",
6310
+ accent: "bg-accent",
6311
+ success: "bg-success",
6312
+ warning: "bg-warning",
6313
+ danger: "bg-danger"
6314
+ };
6315
+ function toMillis(timestamp) {
6316
+ const parsed = Date.parse(timestamp);
6317
+ return Number.isNaN(parsed) ? 0 : parsed;
6318
+ }
6319
+ function stringifyValue(value) {
6320
+ if (value === null || value === void 0) return "—";
6321
+ if (typeof value === "string") return value;
6322
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
6323
+ return JSON.stringify(value);
6324
+ }
6325
+ function PropertyTable({ properties, ariaLabel }) {
6326
+ const entries = Object.entries(properties);
6327
+ if (entries.length === 0) return null;
6328
+ return /* @__PURE__ */ jsx("dl", {
6329
+ className: cn(OPT_BORDER, "gap-element bg-bg-subtle mt-2 grid grid-cols-1 rounded-lg p-2.5 sm:grid-cols-2"),
6330
+ "aria-label": ariaLabel,
6331
+ children: entries.map(([key, value]) => /* @__PURE__ */ jsxs("div", {
6332
+ className: "flex min-w-0 flex-col",
6333
+ children: [/* @__PURE__ */ jsx("dt", {
6334
+ className: cn(OPT_TEXT_TERTIARY, "truncate text-xs"),
6335
+ children: key
6336
+ }), /* @__PURE__ */ jsx("dd", {
2463
6337
  className: cn(OPT_TEXT_PRIMARY, "truncate text-xs"),
2464
6338
  children: stringifyValue(value)
2465
6339
  })]
@@ -2654,7 +6528,7 @@ function GroupBlock({ group, showGaps, order, minGapMs, locale, labels, onItemSe
2654
6528
  * ```
2655
6529
  */
2656
6530
  function EventTimeline({ items, groups, showGaps = true, order = "desc", minGapMs = 1e3, loading = false, hasMore = false, onLoadMore, onItemSelect, locale = OPT_UI_DEFAULT_LOCALE, labels: customLabels, className }) {
2657
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$7), customLabels);
6531
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$6), customLabels);
2658
6532
  const flatItems = items !== null && items !== void 0 ? items : [];
2659
6533
  const hasContent = groups ? groups.length > 0 : flatItems.length > 0;
2660
6534
  if (loading) return /* @__PURE__ */ jsx("div", {
@@ -2714,7 +6588,7 @@ function EventTimeline({ items, groups, showGaps = true, order = "desc", minGapM
2714
6588
  EventTimeline.displayName = "EventTimeline";
2715
6589
  //#endregion
2716
6590
  //#region src/shells/score-breakdown.tsx
2717
- const defaultLabels$6 = {
6591
+ const defaultLabels$5 = {
2718
6592
  title: "Score",
2719
6593
  outOf: (max) => `out of ${max}`,
2720
6594
  weight: (percent) => `${percent} of total`,
@@ -2746,7 +6620,7 @@ function resolveBand(score, bands) {
2746
6620
  */
2747
6621
  function ScoreBreakdown({ score, max = 100, axes, bands = [], formula, loading = false, labels: customLabels, className }) {
2748
6622
  var _band$tone;
2749
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$6), customLabels);
6623
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$5), customLabels);
2750
6624
  const band = resolveBand(score, bands);
2751
6625
  const tone = (_band$tone = band === null || band === void 0 ? void 0 : band.tone) !== null && _band$tone !== void 0 ? _band$tone : "default";
2752
6626
  if (loading) return /* @__PURE__ */ jsxs("section", {
@@ -2828,140 +6702,6 @@ function ScoreBreakdown({ score, max = 100, axes, bands = [], formula, loading =
2828
6702
  }
2829
6703
  ScoreBreakdown.displayName = "ScoreBreakdown";
2830
6704
  //#endregion
2831
- //#region src/shells/flyout.tsx
2832
- const defaultLabels$5 = { close: "Close panel" };
2833
- const sizeClass = {
2834
- sm: "sm:w-[22rem]",
2835
- md: "sm:w-[30rem]",
2836
- lg: "sm:w-[42rem]"
2837
- };
2838
- /**
2839
- * Flyout — a detail panel that does not take the page away.
2840
- *
2841
- * Navigating from a list to a record and back costs the reader their scroll
2842
- * position, their filters and their place in the scan. A flyout keeps the list
2843
- * on screen and shows the record beside it, so comparing three rows is three
2844
- * clicks rather than six navigations.
2845
- *
2846
- * Deliberately **not** a modal: no focus trap, no `aria-modal`, no inert page.
2847
- * The list behind it is still the point, and a reader who tabs out of the panel
2848
- * to change a filter is doing the expected thing. Escape closes; focus returns
2849
- * to whatever opened it.
2850
- *
2851
- * @example
2852
- * ```tsx
2853
- * <Flyout open={Boolean(selected)} onClose={clear} title={selected?.name}>
2854
- * <DescriptionList items={fields} />
2855
- * </Flyout>
2856
- * ```
2857
- */
2858
- function Flyout({ open, onClose, title, description, type = "overlay", side = "right", size = "md", actions, footer, scrim = false, closeOnOutsideClick = true, hideCloseButton = false, labels: customLabels, className, children }) {
2859
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$5), customLabels);
2860
- const panelRef = useRef(null);
2861
- const openerRef = useRef(null);
2862
- useEffect(() => {
2863
- var _panelRef$current;
2864
- if (!open) return;
2865
- openerRef.current = document.activeElement;
2866
- (_panelRef$current = panelRef.current) === null || _panelRef$current === void 0 || _panelRef$current.focus({ preventScroll: true });
2867
- return () => {
2868
- var _openerRef$current, _openerRef$current$fo;
2869
- (_openerRef$current = openerRef.current) === null || _openerRef$current === void 0 || (_openerRef$current$fo = _openerRef$current.focus) === null || _openerRef$current$fo === void 0 || _openerRef$current$fo.call(_openerRef$current, { preventScroll: true });
2870
- };
2871
- }, [open]);
2872
- useEffect(() => {
2873
- if (!open) return;
2874
- const handleKeyDown = (event) => {
2875
- if (event.key === "Escape") {
2876
- event.stopPropagation();
2877
- onClose();
2878
- }
2879
- };
2880
- document.addEventListener("keydown", handleKeyDown);
2881
- return () => document.removeEventListener("keydown", handleKeyDown);
2882
- }, [open, onClose]);
2883
- useEffect(() => {
2884
- if (!open || !closeOnOutsideClick || type !== "overlay") return;
2885
- const handlePointerDown = (event) => {
2886
- const panel = panelRef.current;
2887
- if (panel && !panel.contains(event.target)) onClose();
2888
- };
2889
- const timer = window.setTimeout(() => {
2890
- document.addEventListener("pointerdown", handlePointerDown);
2891
- }, 0);
2892
- return () => {
2893
- window.clearTimeout(timer);
2894
- document.removeEventListener("pointerdown", handlePointerDown);
2895
- };
2896
- }, [
2897
- open,
2898
- closeOnOutsideClick,
2899
- type,
2900
- onClose
2901
- ]);
2902
- const handleClose = useCallback(() => onClose(), [onClose]);
2903
- if (!open) return null;
2904
- const header = (title || actions || !hideCloseButton) && /* @__PURE__ */ jsxs("header", {
2905
- className: cn("border-border-subtle flex items-start gap-2 border-b px-4 py-3"),
2906
- children: [
2907
- /* @__PURE__ */ jsxs("div", {
2908
- className: "flex min-w-0 grow flex-col gap-0.5",
2909
- children: [title && /* @__PURE__ */ jsx("h2", {
2910
- className: cn("text-text-primary", "truncate text-base font-semibold"),
2911
- children: title
2912
- }), description && /* @__PURE__ */ jsx("p", {
2913
- className: cn("text-text-secondary", "truncate text-xs"),
2914
- children: description
2915
- })]
2916
- }),
2917
- actions,
2918
- !hideCloseButton && /* @__PURE__ */ jsx("button", {
2919
- type: "button",
2920
- onClick: handleClose,
2921
- "aria-label": labels.close,
2922
- className: cn("outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-ring-offset", "text-text-tertiary", "hover:text-text-primary shrink-0 rounded p-1 transition-colors"),
2923
- children: /* @__PURE__ */ jsx(X, {
2924
- className: "size-4",
2925
- "aria-hidden": "true"
2926
- })
2927
- })
2928
- ]
2929
- });
2930
- const panel = /* @__PURE__ */ jsxs("aside", {
2931
- ref: panelRef,
2932
- role: "dialog",
2933
- "aria-modal": false,
2934
- "aria-label": typeof title === "string" ? title : void 0,
2935
- tabIndex: -1,
2936
- className: cn(OPT_FOCUS_VISIBLE, "flex min-h-0 flex-col", type === "overlay" && [
2937
- "rounded-xl bg-surface-raised shadow-md",
2938
- "fixed inset-y-0 z-40 w-full",
2939
- sizeClass[size],
2940
- side === "right" ? "right-0 border-l" : "left-0 border-r",
2941
- "border-border"
2942
- ], type === "inline" && ["border border-border", "rounded-xl bg-surface"], className),
2943
- "data-opt-id": "85XVE",
2944
- "data-opt-slug": "flyout.Flyout",
2945
- children: [
2946
- header,
2947
- /* @__PURE__ */ jsx("div", {
2948
- className: "min-h-0 grow overflow-y-auto px-4 py-4",
2949
- children
2950
- }),
2951
- footer && /* @__PURE__ */ jsx("footer", {
2952
- className: "border-border-subtle border-t px-4 py-3",
2953
- children: footer
2954
- })
2955
- ]
2956
- });
2957
- if (type === "inline") return panel;
2958
- return /* @__PURE__ */ jsxs(Fragment, { children: [scrim && /* @__PURE__ */ jsx("div", {
2959
- className: "bg-scrim fixed inset-0 z-30",
2960
- "aria-hidden": "true"
2961
- }), panel] });
2962
- }
2963
- Flyout.displayName = "Flyout";
2964
- //#endregion
2965
6705
  //#region src/lib/query/query-ast.ts
2966
6706
  const EMPTY_QUERY = { clauses: [] };
2967
6707
  /** Whether a query constrains anything at all. */
@@ -3796,7 +7536,7 @@ function TimeRangeControl({ value, onChange, presets, recentRanges, timeZone, lo
3796
7536
  }),
3797
7537
  /* @__PURE__ */ jsxs(SelectRoot$1, {
3798
7538
  value: quickUnit,
3799
- setValue: (next) => setQuickUnit(selectValue(next)),
7539
+ onValueChange: (next) => setQuickUnit(selectValue(next)),
3800
7540
  children: [/* @__PURE__ */ jsx(SelectTrigger$1, {
3801
7541
  minWidth: 110,
3802
7542
  "aria-label": labels.quickSelect
@@ -3856,7 +7596,7 @@ function TimeRangeControl({ value, onChange, presets, recentRanges, timeZone, lo
3856
7596
  children: labels.autoRefresh
3857
7597
  }), /* @__PURE__ */ jsxs(SelectRoot$1, {
3858
7598
  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),
3859
- setValue: (next) => {
7599
+ onValueChange: (next) => {
3860
7600
  const interval = Number(selectValue(next));
3861
7601
  onRefreshChange({
3862
7602
  interval,
@@ -4835,4 +8575,4 @@ function FieldValues({ values, loading, total, sampledRows, onFilter, labels })
4835
8575
  });
4836
8576
  }
4837
8577
  //#endregion
4838
- export { CommandGroup$1 as $, ToastProvider as A, FaqAccordion as B, Flyout as C, KeyValueEditor as D, WorkflowCanvas as E, BranchSelect as F, StatusSelect as G, EditorToolbar as H, EnvPanel as I, CommandPaletteTrigger as J, SearchCombobox as K, ProjectSwitcher as L, useToast as M, DataTable as N, FailureList as O, DomainTable as P, CommandEmpty$1 as Q, DeploymentTimeline as R, withoutField as S, EventTimeline as T, SidebarNav as U, ContentTabs as V, AppMenubar as W, Command as X, CommandPalette as Y, CommandDialog as Z, fieldValues as _, TimeRangeControl as a, removeClause as b, describeClause as c, quoteValue as d, CommandInput$1 as et, parseQuery as f, applyCellAction as g, addClause as h, DEFAULT_TIME_PRESETS as i, CommandShortcut as it, toast as j, NotificationToast as k, formatClause as l, EMPTY_QUERY as m, LogTable as n, CommandList$1 as nt, useAutoRefresh as o, parseQueryWithDiagnostics as p, DashboardGrid as q, TimeSeriesPanel as r, CommandSeparator$1 as rt, QueryBar as s, FieldSidebar as t, CommandItem$1 as tt, formatQuery as u, isEmptyQuery as v, ScoreBreakdown as w, withFieldValue as x, queryFields as y, SettingsForm as z };
8578
+ export { generatedTheme as $, Command as $t, useOptThemeController as A, KeyValueEditor as At, THEME_SPEC_STORAGE_KEY as B, ProjectSwitcher as Bt, ScoreBreakdown as C, QueryOutputPane as Ct, useOptTheme as D, formatQueryCell as Dt, OptThemeProvider as E, QueryResultsTable as Et, getCompoundTheme as F, useToast as Ft, isForceLightSpec as G, EditorToolbar as Gt, buildTokenCache as H, SettingsForm as Ht, readStoredTheme as I, DataTable as It, safeParseStoredTokens as J, StatusSelect as Jt, resolveDescriptor as K, SidebarNav as Kt, resolveThemeMode as L, DomainTable as Lt, FORCE_LIGHT_PRESETS as M, NotificationToast as Mt, coerceThemeMode as N, ToastProvider as Nt, useOptionalOptTheme as O, Flyout as Ot, createThemeBootScript as P, toast as Pt, defineThemeOverrides as Q, CommandPalette as Qt, GENERATED_THEME_ID_DEFAULT as R, BranchSelect as Rt, withoutField as S, numericResultColumns as St, SqlEditor as T, SchemaTree as Tt, computeGeneratedTokens as U, FaqAccordion as Ut, THEME_TOKENS_STORAGE_KEY as V, DeploymentTimeline as Vt, getCompoundIdForSpec as W, ContentTabs as Wt, serializeThemeRule as X, DashboardGrid as Xt, serializeGeneratedTheme as Y, SearchCombobox as Yt, serializeTokenBody as Z, CommandPaletteTrigger as Zt, fieldValues as _, VALUELESS_OPERATORS as _t, TimeRangeControl as a, CommandList$1 as an, THEME_PRESET_IDS as at, removeClause as b, QueryResultChart as bt, describeClause as c, SegmentedControl as cn, isThemePresetId as ct, quoteValue as d, SaveQueryDialog as dt, CommandDialog as en, isDescriptorSpec as et, parseQuery as f, QueryHistoryList as ft, applyCellAction as g, QueryFiltersMenu as gt, addClause as h, EMPTY_QUERY_FILTERS as ht, DEFAULT_TIME_PRESETS as i, CommandItem$1 as in, FORCE_LIGHT_PRESET_IDS as it, ALL_COMPOUND_THEMES as j, FailureList as jt, OptThemeControllerContext as k, WorkflowCanvas as kt, formatClause as l, Callout as ln, QueryWorkspace as lt, EMPTY_QUERY as m, isValidQueryVariableName as mt, LogTable as n, CommandGroup$1 as nn, isPresetSpec as nt, useAutoRefresh as o, CommandSeparator$1 as on, THEME_REGISTRY as ot, parseQueryWithDiagnostics as p, QueryVariablesMenu as pt, safeParseStoredSpec as q, AppMenubar as qt, TimeSeriesPanel as r, CommandInput$1 as rn, presetTheme as rt, QueryBar as s, CommandShortcut as sn, getThemeDescriptor as st, FieldSidebar as t, CommandEmpty$1 as tn, isGeneratedSpec as tt, formatQuery as u, useMediaQuery as ut, isEmptyQuery as v, countActiveQueryFilters as vt, EventTimeline as w, QueryToolbar as wt, withFieldValue as x, defaultQueryChartConfig as xt, queryFields as y, hasActiveQueryFilters as yt, GLOBAL_THEME_STYLE_ID as z, EnvPanel as zt };