@reopt-ai/opt-ui 1.12.4 → 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.
- package/CHANGELOG.md +236 -0
- package/COMPONENT_CATALOG.md +290 -111
- package/dist/core/index.cjs +13 -13
- package/dist/core/index.d.cts +2 -2
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +2 -2
- package/dist/docs/02-components/01-core.md +49 -49
- package/dist/docs/02-components/02-visuals.md +1 -1
- package/dist/docs/02-components/03-shells.md +260 -63
- package/dist/docs/02-components/04-surfaces.md +1 -1
- package/dist/docs/02-components/index.md +4 -4
- package/dist/{field-sidebar-C8bDzGPs.js → field-sidebar-CRdToHRO.js} +3975 -235
- package/dist/{field-sidebar-DX5wP0qn.cjs → field-sidebar-CxxSK4JO.cjs} +4338 -244
- package/dist/{field-sidebar-Ct8c9L4V.d.cts → field-sidebar-D-rwuN_w.d.cts} +824 -2
- package/dist/{field-sidebar-YaWHfLLW.d.ts → field-sidebar-Dz7J4ett.d.ts} +824 -2
- package/dist/id-registry.cjs +72 -0
- package/dist/id-registry.js +72 -0
- package/dist/id-registry.json +153 -0
- package/dist/index.cjs +334 -1419
- package/dist/index.d.cts +51 -86
- package/dist/index.d.ts +51 -86
- package/dist/index.js +248 -1356
- package/dist/{key-pad-menu-1YN221Oi.cjs → key-pad-menu-0KWOnELe.cjs} +53 -483
- package/dist/{key-pad-menu-CwcY_Vps.d.cts → key-pad-menu-BOVu97bj.d.cts} +4 -55
- package/dist/{key-pad-menu-CUiQ-vKw.js → key-pad-menu-DMmbQ1jF.js} +54 -418
- package/dist/{key-pad-menu-WBJShCJn.d.ts → key-pad-menu-NlZ9zNF8.d.ts} +4 -55
- package/dist/meta.cjs +896 -7
- package/dist/meta.js +896 -7
- package/dist/shells/index.cjs +20 -1
- package/dist/shells/index.d.cts +2 -2
- package/dist/shells/index.d.ts +2 -2
- package/dist/shells/index.js +2 -2
- package/dist/{text-truncate-BFWd2cE_.js → text-truncate-Bd-0WkSP.js} +463 -99
- package/dist/{text-truncate-DsG5UAOY.cjs → text-truncate-BeYW6XZu.cjs} +525 -101
- package/dist/{text-truncate-CQyfUZvq.d.cts → text-truncate-eMzvU_7k.d.cts} +52 -1
- package/dist/{text-truncate-CQyfUZvq.d.ts → text-truncate-eMzvU_7k.d.ts} +52 -1
- package/dist/theme/server.cjs +2 -2
- package/dist/theme/server.js +2 -2
- package/package.json +6 -2
|
@@ -1,14 +1,182 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { $
|
|
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 {
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
773
|
+
const defaultLabels$29 = { ariaLabel: "텍스트 편집기 툴바" };
|
|
606
774
|
function ToolbarButtonWithTooltip({ button, active, onAction }) {
|
|
607
|
-
return /* @__PURE__ */ jsxs(TooltipProvider
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
2029
|
-
emptyMessage: (_customLabels$emptyMe = customLabels === null || customLabels === void 0 ? void 0 : customLabels.emptyMessage) !== null && _customLabels$emptyMe !== void 0 ? _customLabels$emptyMe : defaultLabels$
|
|
2030
|
-
moreItems: (_customLabels$moreIte = customLabels === null || customLabels === void 0 ? void 0 : customLabels.moreItems) !== null && _customLabels$moreIte !== void 0 ? _customLabels$moreIte : defaultLabels$
|
|
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$
|
|
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$
|
|
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/
|
|
2417
|
-
const defaultLabels$
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
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
|
-
*
|
|
2429
|
-
*
|
|
2430
|
-
*
|
|
2431
|
-
*
|
|
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
|
-
|
|
2434
|
-
const
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
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
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
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
|
|
2446
|
-
|
|
2447
|
-
|
|
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
|
|
2452
|
-
|
|
2453
|
-
if (
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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 };
|