@reopt-ai/opt-ui 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/COMPONENT_CATALOG.md +222 -34
  2. package/README.md +70 -6
  3. package/dist/core/index.cjs +170 -155
  4. package/dist/core/index.d.cts +3 -2
  5. package/dist/core/index.d.ts +3 -2
  6. package/dist/core/index.js +3 -3
  7. package/dist/docs/02-components/01-core.md +223 -18
  8. package/dist/docs/02-components/02-visuals.md +1 -1
  9. package/dist/docs/02-components/03-shells.md +17 -16
  10. package/dist/docs/02-components/04-surfaces.md +1 -1
  11. package/dist/docs/02-components/index.md +4 -4
  12. package/dist/{field-sidebar-BP0iWkoV.d.cts → field-sidebar-B5vk3luQ.d.cts} +20 -3
  13. package/dist/{field-sidebar-BP0iWkoV.d.cts.map → field-sidebar-B5vk3luQ.d.cts.map} +1 -1
  14. package/dist/{field-sidebar-BYD_UIjG.js → field-sidebar-B7QkWvMG.js} +16 -8
  15. package/dist/{field-sidebar-BYD_UIjG.js.map → field-sidebar-B7QkWvMG.js.map} +1 -1
  16. package/dist/{field-sidebar-5KYVlpSs.d.ts → field-sidebar-Dk9gJGA_.d.ts} +20 -3
  17. package/dist/{field-sidebar-5KYVlpSs.d.ts.map → field-sidebar-Dk9gJGA_.d.ts.map} +1 -1
  18. package/dist/{field-sidebar-Bqfp6dgg.cjs → field-sidebar-mFHemxBz.cjs} +296 -288
  19. package/dist/{types-D0FlcYnM.d.ts → field-token-CSOrU5gl.d.cts} +66 -2
  20. package/dist/field-token-CSOrU5gl.d.cts.map +1 -0
  21. package/dist/{types-D0FlcYnM.d.cts → field-token-CSOrU5gl.d.ts} +66 -2
  22. package/dist/field-token-CSOrU5gl.d.ts.map +1 -0
  23. package/dist/{pagination-DWbvACuN.js → highlight-C2sfnJN8.js} +263 -11
  24. package/dist/highlight-C2sfnJN8.js.map +1 -0
  25. package/dist/{pagination-DjfxBcjX.cjs → highlight-CGcrYGMC.cjs} +284 -8
  26. package/dist/id-registry.cjs +98 -2
  27. package/dist/id-registry.d.cts.map +1 -1
  28. package/dist/id-registry.d.ts.map +1 -1
  29. package/dist/id-registry.js +98 -2
  30. package/dist/id-registry.js.map +1 -1
  31. package/dist/id-registry.json +202 -4
  32. package/dist/index.cjs +664 -643
  33. package/dist/index.d.cts +5 -4
  34. package/dist/index.d.cts.map +1 -1
  35. package/dist/index.d.ts +5 -4
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +10 -4
  38. package/dist/index.js.map +1 -1
  39. package/dist/{shader-surface-BUPZ0P-n.js → key-pad-menu-BFd79JoS.js} +855 -19
  40. package/dist/key-pad-menu-BFd79JoS.js.map +1 -0
  41. package/dist/{shader-surface-DRrcLrWf.d.cts → key-pad-menu-D8OS_VCT.d.cts} +459 -3
  42. package/dist/key-pad-menu-D8OS_VCT.d.cts.map +1 -0
  43. package/dist/{shader-surface-Fav5K9UC.cjs → key-pad-menu-DoGskWbp.cjs} +1103 -201
  44. package/dist/{shader-surface-BfTP9c1Q.d.ts → key-pad-menu-EEb6Ho5h.d.ts} +459 -3
  45. package/dist/key-pad-menu-EEb6Ho5h.d.ts.map +1 -0
  46. package/dist/meta.cjs +641 -0
  47. package/dist/meta.d.cts.map +1 -1
  48. package/dist/meta.d.ts.map +1 -1
  49. package/dist/meta.js +641 -0
  50. package/dist/meta.js.map +1 -1
  51. package/dist/shells/index.cjs +1 -1
  52. package/dist/shells/index.d.cts +1 -1
  53. package/dist/shells/index.d.ts +1 -1
  54. package/dist/shells/index.js +1 -1
  55. package/package.json +2 -2
  56. package/dist/pagination-DWbvACuN.js.map +0 -1
  57. package/dist/shader-surface-BUPZ0P-n.js.map +0 -1
  58. package/dist/shader-surface-BfTP9c1Q.d.ts.map +0 -1
  59. package/dist/shader-surface-DRrcLrWf.d.cts.map +0 -1
  60. package/dist/types-D0FlcYnM.d.cts.map +0 -1
  61. package/dist/types-D0FlcYnM.d.ts.map +0 -1
@@ -1,10 +1,10 @@
1
1
  "use client";
2
- import { $t as koBreadcrumbLabels, Bn as _objectSpread2, Cn as OPT_FOCUS, En as OPT_INPUT, In as OPT_TEXT_PRIMARY, Ln as OPT_TEXT_SECONDARY, Pn as OPT_TEXTAREA_SIZE, Qt as Button, Rn as OPT_TEXT_TERTIARY, Un as cn, Vn as _objectWithoutProperties, Yt as Spinner, an as RequiredMark, at as MenuPopover$1, cn as OPT_ANIMATE_DRAWER, ct as MenuTrigger$1, hn as OPT_BORDER, in as FieldMessages, kn as OPT_SURFACE, ln as OPT_ANIMATE_ENTER_EXIT, nt as MenuItem$1, on as getDescribedBy, ot as MenuRoot$1, qt as useInputId, rn as useControlledState, st as MenuSeparator$1, wn as OPT_FOCUS_VISIBLE } from "./pagination-DWbvACuN.js";
2
+ import { $t as Spinner, An as OPT_INPUT, Bn as OPT_TEXT_PRIMARY, Dn as OPT_FOCUS, Ft as SelectRoot$1, Gn as _objectWithoutProperties, Hn as OPT_TEXT_TERTIARY, It as SelectTrigger$1, Mt as SelectItem$1, Nn as OPT_SURFACE, On as OPT_FOCUS_VISIBLE, Pt as SelectPopover$1, Rn as OPT_TEXTAREA_SIZE, Vn as OPT_TEXT_SECONDARY, Wn as _objectSpread2, Zt as useInputId, cn as FieldMessages, dt as MenuSeparator$1, fn as OPT_ANIMATE_DRAWER, ft as MenuTrigger$1, ln as RequiredMark, lt as MenuPopover$1, nn as Button, ot as MenuItem$1, pn as OPT_ANIMATE_ENTER_EXIT, qn as cn, rn as koBreadcrumbLabels, sn as useControlledState, un as getDescribedBy, ut as MenuRoot$1, yn as OPT_BORDER } from "./highlight-C2sfnJN8.js";
3
3
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
4
  import * as React from "react";
5
5
  import { createContext, createElement, forwardRef, useCallback, useContext, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
6
6
  import { AccordionContent, AccordionItem, AccordionRoot, AccordionTrigger, DialogDismiss, DialogPanel, DialogRoot, Meter, RadioGroupRoot, Separator, Toggle, ToggleGroup, useDialogClose, useRadioGroupContext } from "@reopt-ai/opt-ui-primitives";
7
- import { ChevronDown, GripVertical } from "lucide-react";
7
+ import { Check, ChevronDown, GripVertical, Pencil, X } from "lucide-react";
8
8
  import { Group, Panel, Separator as Separator$1 } from "react-resizable-panels";
9
9
  import { Mesh, Program, Renderer, Triangle } from "ogl";
10
10
  import { hexToSrgb } from "@reopt-ai/opt-palette";
@@ -17,7 +17,7 @@ const _excluded$17 = [
17
17
  "className",
18
18
  "children"
19
19
  ];
20
- const toneStyles = {
20
+ const toneStyles$1 = {
21
21
  primary: "text-text-primary",
22
22
  secondary: "text-text-secondary",
23
23
  tertiary: "text-text-tertiary",
@@ -28,7 +28,7 @@ function Text(_ref) {
28
28
  let { as, variant = "body", tone = "primary", truncate = false, className, children } = _ref, props = _objectWithoutProperties(_ref, _excluded$17);
29
29
  const Component = as !== null && as !== void 0 ? as : "span";
30
30
  const elementProps = _objectSpread2(_objectSpread2({}, props), {}, {
31
- className: cn(`opt-type-${variant}`, toneStyles[tone], truncate && "truncate", className),
31
+ className: cn(`opt-type-${variant}`, toneStyles$1[tone], truncate && "truncate", className),
32
32
  "data-opt-component": "text",
33
33
  "data-opt-part": "root",
34
34
  "data-opt-id": "NFKP2",
@@ -277,7 +277,7 @@ const _excluded2$8 = [
277
277
  "children",
278
278
  "orientation"
279
279
  ];
280
- const sizeStyles$5 = {
280
+ const sizeStyles$8 = {
281
281
  sm: "h-8 min-w-8 px-2 text-xs gap-1.5",
282
282
  md: "h-10 min-w-10 px-3 text-sm gap-2"
283
283
  };
@@ -285,7 +285,7 @@ const toggleBase = cn("inline-flex cursor-pointer items-center justify-center ro
285
285
  /** A two-state (pressed) button. Use standalone or inside `ToggleGroup`. */
286
286
  function Toggle$1(_ref) {
287
287
  let { size = "md", className, children } = _ref, props = _objectWithoutProperties(_ref, _excluded$15);
288
- return /* @__PURE__ */ jsx(Toggle, _objectSpread2(_objectSpread2({ className: cn(toggleBase, sizeStyles$5[size], className) }, props), {}, {
288
+ return /* @__PURE__ */ jsx(Toggle, _objectSpread2(_objectSpread2({ className: cn(toggleBase, sizeStyles$8[size], className) }, props), {}, {
289
289
  "data-opt-id": "GFCMY",
290
290
  "data-opt-slug": "toggle.Toggle",
291
291
  children
@@ -559,7 +559,7 @@ const _excluded$12 = [
559
559
  "aria-label",
560
560
  "aria-labelledby"
561
561
  ];
562
- const sizeStyles$4 = {
562
+ const sizeStyles$7 = {
563
563
  sm: {
564
564
  track: "h-5 w-9",
565
565
  thumb: "h-4 w-4",
@@ -583,7 +583,7 @@ const Switch = forwardRef((_ref, ref) => {
583
583
  const switchId = (_useInputId = useInputId(id, label, "switch")) !== null && _useInputId !== void 0 ? _useInputId : `switch-${fallbackId}`;
584
584
  const hintText = hint !== null && hint !== void 0 ? hint : description;
585
585
  const hasError = Boolean(error);
586
- const styles = sizeStyles$4[size];
586
+ const styles = sizeStyles$7[size];
587
587
  const labelId = label ? `${switchId}-label` : void 0;
588
588
  const switchLabelledBy = ariaLabelledBy !== null && ariaLabelledBy !== void 0 ? ariaLabelledBy : ariaLabel ? void 0 : labelId;
589
589
  const switchAriaLabel = ariaLabel !== null && ariaLabel !== void 0 ? ariaLabel : switchLabelledBy ? void 0 : "Switch";
@@ -889,7 +889,7 @@ function useImageLoadingStatus(src, { srcSet, sizes, crossOrigin, referrerPolicy
889
889
  ]);
890
890
  return status;
891
891
  }
892
- const sizeStyles$3 = {
892
+ const sizeStyles$6 = {
893
893
  xs: "h-6 w-6 text-[10px]",
894
894
  sm: "h-8 w-8 text-xs",
895
895
  md: "h-10 w-10 text-sm",
@@ -973,7 +973,7 @@ function Avatar(_ref) {
973
973
  return /* @__PURE__ */ jsxs("span", _objectSpread2(_objectSpread2({
974
974
  role: "img",
975
975
  "aria-label": avatarLabel,
976
- className: cn("relative inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full", sizeStyles$3[size], !showImage && bgColor, className)
976
+ className: cn("relative inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full", sizeStyles$6[size], !showImage && bgColor, className)
977
977
  }, props), {}, {
978
978
  "data-opt-id": "SCMM0",
979
979
  "data-opt-slug": "avatar.Avatar",
@@ -996,7 +996,7 @@ function AvatarGroup(_ref5) {
996
996
  className: "ring-surface relative -ml-2 inline-block rounded-full ring-2 first:ml-0",
997
997
  children: child
998
998
  }, index)), remainingCount > 0 && /* @__PURE__ */ jsxs("span", {
999
- className: cn("bg-bg-subtle text-text-secondary ring-surface relative inline-flex items-center justify-center rounded-full font-medium ring-2", sizeStyles$3[size]),
999
+ className: cn("bg-bg-subtle text-text-secondary ring-surface relative inline-flex items-center justify-center rounded-full font-medium ring-2", sizeStyles$6[size]),
1000
1000
  children: ["+", remainingCount]
1001
1001
  })]
1002
1002
  }));
@@ -1025,7 +1025,7 @@ const _excluded2$3 = [
1025
1025
  "indeterminate",
1026
1026
  "className"
1027
1027
  ];
1028
- const sizeStyles$2 = {
1028
+ const sizeStyles$5 = {
1029
1029
  sm: "h-1",
1030
1030
  md: "h-2",
1031
1031
  lg: "h-3"
@@ -1065,7 +1065,7 @@ function Progress(_ref) {
1065
1065
  children: progress.valueText
1066
1066
  })]
1067
1067
  }), /* @__PURE__ */ jsx("div", {
1068
- className: cn("bg-bg-subtle w-full overflow-hidden rounded-full", sizeStyles$2[size]),
1068
+ className: cn("bg-bg-subtle w-full overflow-hidden rounded-full", sizeStyles$5[size]),
1069
1069
  role: "progressbar",
1070
1070
  "aria-valuenow": progress === null || progress === void 0 ? void 0 : progress.clampedValue,
1071
1071
  "aria-valuemin": progress ? 0 : void 0,
@@ -1158,7 +1158,7 @@ const _excluded$7 = [
1158
1158
  "getValueLabel",
1159
1159
  "className"
1160
1160
  ];
1161
- const sizeStyles$1 = {
1161
+ const sizeStyles$4 = {
1162
1162
  sm: "h-1",
1163
1163
  md: "h-2",
1164
1164
  lg: "h-3"
@@ -1196,7 +1196,7 @@ function Meter$1(_ref) {
1196
1196
  max,
1197
1197
  getValueLabel,
1198
1198
  "aria-label": label !== null && label !== void 0 ? label : `${Math.round(percentage)}%`,
1199
- className: cn("bg-bg-subtle w-full overflow-hidden rounded-full", sizeStyles$1[size]),
1199
+ className: cn("bg-bg-subtle w-full overflow-hidden rounded-full", sizeStyles$4[size]),
1200
1200
  children: /* @__PURE__ */ jsx("div", {
1201
1201
  className: cn("h-full rounded-full transition-all duration-300 ease-out", variantStyles[variant]),
1202
1202
  style: { width: `${percentage}%` }
@@ -1793,7 +1793,7 @@ const _excluded$5 = [
1793
1793
  "disabled",
1794
1794
  "className"
1795
1795
  ];
1796
- const sizeStyles = {
1796
+ const sizeStyles$3 = {
1797
1797
  sm: {
1798
1798
  radio: "h-4 w-4",
1799
1799
  dot: "h-2 w-2",
@@ -1861,7 +1861,7 @@ const Radio = forwardRef((_ref, ref) => {
1861
1861
  const size = sizeProp !== null && sizeProp !== void 0 ? sizeProp : groupContext.size;
1862
1862
  const disabled = (_ref2 = disabledProp !== null && disabledProp !== void 0 ? disabledProp : groupContext.disabled) !== null && _ref2 !== void 0 ? _ref2 : primContext === null || primContext === void 0 ? void 0 : primContext.disabled;
1863
1863
  const hintText = hint !== null && hint !== void 0 ? hint : description;
1864
- const styles = sizeStyles[size];
1864
+ const styles = sizeStyles$3[size];
1865
1865
  const isChecked = (primContext === null || primContext === void 0 ? void 0 : primContext.value) === value;
1866
1866
  const radioId = `radio-${useId().replace(/:/g, "")}`;
1867
1867
  const labelId = label ? `${radioId}-label` : void 0;
@@ -3649,6 +3649,842 @@ const ShaderSurfaceComponent = forwardRef(function ShaderSurface({ preset, color
3649
3649
  ShaderSurfaceComponent.displayName = "ShaderSurface";
3650
3650
  const ShaderSurface = ShaderSurfaceComponent;
3651
3651
  //#endregion
3652
- export { CardHeader as $, DropdownTrigger as A, Avatar as B, LoadingOverlay as C, DropdownItem as D, DropdownContent as E, formatOptTime as F, SkeletonCard as G, Textarea as H, StepBuilder as I, Switch as J, SkeletonTable as K, Meter$1 as L, formatOptDateTime as M, formatOptNumber as N, DropdownMenu as O, formatOptShortDate as P, CardFooter as Q, CircularProgress as R, BlockLayout as S, RadioGroup as T, Skeleton as U, AvatarGroup as V, SkeletonAvatar as W, CardContent as X, Card as Y, CardDescription as Z, Section as _, SHADER_PRESETS as a, AccordionTrigger$1 as at, PageHeader as b, getShaderPreset as c, OtpField as ct, DrawerPanel as d, Text as dt, CardTitle as et, DrawerRoot as f, ResizablePanelGroup as g, ResizablePanel as h, SHADER_PARAM_KEYS as i, AccordionRoot$1 as it, ConditionBuilder as j, DropdownSeparator as k, VirtualList as l, Separator$2 as lt, ResizableHandle as m, SHADER_OVERLAY_PARAM_KEYS as n, AccordionContent$1 as nt, SHADER_PRESET_IDS as o, Toggle$1 as ot, Logo as p, SkeletonText as q, SHADER_PARAM_FALLBACK as r, AccordionItem$1 as rt, getShaderFragment as s, ToggleGroup$1 as st, ShaderSurface as t, Breadcrumb as tt, DrawerClose as u, Heading as ut, Container as v, Radio as w, createBlock as x, ScrollArea as y, Progress as z };
3652
+ //#region src/core/facet.tsx
3653
+ const defaultLabels$1 = {
3654
+ empty: "No values in this range.",
3655
+ loading: "Counting…",
3656
+ showMore: (remaining) => `Show ${remaining} more`,
3657
+ showLess: "Show fewer",
3658
+ selectHint: (label, count, share) => `${label}, ${count} (${share})`
3659
+ };
3660
+ function defaultCountFormat(value) {
3661
+ return new Intl.NumberFormat(void 0, { notation: "compact" }).format(value);
3662
+ }
3663
+ function formatShare(share) {
3664
+ const percent = share * 100;
3665
+ if (percent > 0 && percent < .1) return "<0.1%";
3666
+ return `${percent >= 10 ? Math.round(percent) : percent.toFixed(1)}%`;
3667
+ }
3668
+ /**
3669
+ * Facet — one value of a field, its count, and its share of the whole.
3670
+ *
3671
+ * The row is the filter: reading it and acting on it are the same gesture,
3672
+ * which is what separates a facet from a legend.
3673
+ */
3674
+ function Facet({ item, selected = false, onSelect, showShare = true, share, countFormatter = defaultCountFormat, layout = "vertical", labels, className }) {
3675
+ var _ref;
3676
+ const text = _objectSpread2(_objectSpread2({}, defaultLabels$1), labels);
3677
+ const resolvedShare = (_ref = share !== null && share !== void 0 ? share : item.share) !== null && _ref !== void 0 ? _ref : 0;
3678
+ const interactive = onSelect !== void 0 && !item.disabled;
3679
+ const countText = item.count === void 0 ? "" : countFormatter(item.count);
3680
+ const shareText = formatShare(resolvedShare);
3681
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [
3682
+ showShare && layout === "vertical" && /* @__PURE__ */ jsx("span", {
3683
+ "aria-hidden": "true",
3684
+ className: cn("absolute inset-y-0 left-0 rounded-[inherit] transition-[width] duration-300", selected ? "bg-accent/25" : "bg-accent/10"),
3685
+ style: {
3686
+ width: `${Math.max(resolvedShare * 100, resolvedShare > 0 ? 1.5 : 0)}%`,
3687
+ backgroundColor: item.color ? `color-mix(in oklab, ${item.color} ${selected ? 30 : 16}%, transparent)` : void 0
3688
+ }
3689
+ }),
3690
+ /* @__PURE__ */ jsxs("span", {
3691
+ className: "gap-element relative flex min-w-0 flex-1 items-center",
3692
+ children: [
3693
+ item.color && !item.icon && /* @__PURE__ */ jsx("span", {
3694
+ "aria-hidden": "true",
3695
+ className: "size-2 shrink-0 rounded-full",
3696
+ style: { backgroundColor: item.color }
3697
+ }),
3698
+ item.icon && /* @__PURE__ */ jsx("span", {
3699
+ "aria-hidden": "true",
3700
+ className: "shrink-0",
3701
+ children: item.icon
3702
+ }),
3703
+ /* @__PURE__ */ jsx("span", {
3704
+ className: "truncate",
3705
+ children: item.label
3706
+ })
3707
+ ]
3708
+ }),
3709
+ item.count !== void 0 && /* @__PURE__ */ jsx("span", {
3710
+ className: "text-text-tertiary relative shrink-0 tabular-nums",
3711
+ children: countText
3712
+ }),
3713
+ showShare && item.count !== void 0 && /* @__PURE__ */ jsx("span", {
3714
+ className: "text-text-tertiary relative w-11 shrink-0 text-right text-xs tabular-nums",
3715
+ children: shareText
3716
+ })
3717
+ ] });
3718
+ const shellClass = cn("relative flex w-full items-center gap-element overflow-hidden rounded-md px-2 py-1.5 text-left text-sm", layout === "horizontal" && "w-auto", selected ? "text-text-primary font-medium" : "text-text-secondary", interactive && "hover:bg-accent-subtle hover:text-text-primary cursor-pointer transition-colors", item.disabled && "cursor-not-allowed opacity-50", className);
3719
+ if (!interactive) return /* @__PURE__ */ jsx("div", {
3720
+ className: shellClass,
3721
+ "data-selected": selected || void 0,
3722
+ "data-opt-id": "E9H18",
3723
+ "data-opt-slug": "facet.Facet",
3724
+ children: content
3725
+ });
3726
+ return /* @__PURE__ */ jsx("button", {
3727
+ type: "button",
3728
+ className: cn(shellClass, OPT_FOCUS_VISIBLE),
3729
+ "aria-pressed": selected,
3730
+ "aria-label": text.selectHint(typeof item.label === "string" ? item.label : item.id, countText, shareText),
3731
+ disabled: item.disabled,
3732
+ onClick: () => onSelect(item.id),
3733
+ "data-selected": selected || void 0,
3734
+ "data-opt-id": "E9H18",
3735
+ "data-opt-slug": "facet.Facet",
3736
+ children: content
3737
+ });
3738
+ }
3739
+ /**
3740
+ * FacetGroup — the distribution of one field, as a list you can filter with.
3741
+ *
3742
+ * Counts alone do not say whether 4,000 is most of the data or a rounding
3743
+ * error, so every row carries its share of the whole as a bar behind the
3744
+ * label. The tail collapses by default: a high-cardinality field would
3745
+ * otherwise push everything else off the screen.
3746
+ *
3747
+ * @example
3748
+ * ```tsx
3749
+ * <FacetGroup
3750
+ * items={topValues}
3751
+ * selected={filters}
3752
+ * onSelectedChange={setFilters}
3753
+ * total={sampledRows}
3754
+ * max={5}
3755
+ * />
3756
+ * ```
3757
+ */
3758
+ function FacetGroup({ items, selected, onSelectedChange, selection = "multiple", layout = "vertical", showShare = true, countFormatter = defaultCountFormat, max, total, loading = false, emptyState, labels, className, "aria-label": ariaLabel }) {
3759
+ const text = _objectSpread2(_objectSpread2({}, defaultLabels$1), labels);
3760
+ const [uncontrolled, setUncontrolled] = useState([]);
3761
+ const [expanded, setExpanded] = useState(false);
3762
+ const current = selected !== null && selected !== void 0 ? selected : uncontrolled;
3763
+ const denominator = useMemo(() => {
3764
+ if (total !== void 0 && total > 0) return total;
3765
+ const sum = items.reduce((acc, item) => {
3766
+ var _item$count;
3767
+ return acc + ((_item$count = item.count) !== null && _item$count !== void 0 ? _item$count : 0);
3768
+ }, 0);
3769
+ return sum > 0 ? sum : 1;
3770
+ }, [items, total]);
3771
+ const visible = max === void 0 || expanded ? items : items.slice(0, Math.max(max, 1));
3772
+ const remaining = items.length - visible.length;
3773
+ const handleSelect = (id) => {
3774
+ if (selection === "none") return;
3775
+ const next = selection === "single" ? current.includes(id) ? [] : [id] : current.includes(id) ? current.filter((value) => value !== id) : [...current, id];
3776
+ if (selected === void 0) setUncontrolled(next);
3777
+ onSelectedChange === null || onSelectedChange === void 0 || onSelectedChange(next);
3778
+ };
3779
+ if (loading) return /* @__PURE__ */ jsxs("div", {
3780
+ className: cn("flex flex-col gap-element", className),
3781
+ "aria-busy": "true",
3782
+ "aria-label": ariaLabel,
3783
+ "data-opt-id": "G1RNN",
3784
+ "data-opt-slug": "facet.FacetGroup",
3785
+ children: [/* @__PURE__ */ jsx("span", {
3786
+ className: "sr-only",
3787
+ children: text.loading
3788
+ }), Array.from({ length: Math.min(max !== null && max !== void 0 ? max : 4, 6) }, (_, index) => /* @__PURE__ */ jsx("span", {
3789
+ "aria-hidden": "true",
3790
+ className: "bg-bg-subtle h-7 animate-pulse rounded-md"
3791
+ }, index))]
3792
+ });
3793
+ if (items.length === 0) return /* @__PURE__ */ jsx("div", {
3794
+ className: cn("text-text-tertiary px-2 py-1.5 text-sm", className),
3795
+ "aria-label": ariaLabel,
3796
+ "data-opt-id": "G1RNN",
3797
+ "data-opt-slug": "facet.FacetGroup",
3798
+ children: emptyState !== null && emptyState !== void 0 ? emptyState : text.empty
3799
+ });
3800
+ return /* @__PURE__ */ jsxs("div", {
3801
+ className: cn(layout === "vertical" ? "flex flex-col gap-0.5" : "flex flex-wrap items-center gap-element", className),
3802
+ role: selection === "none" ? "list" : "group",
3803
+ "aria-label": ariaLabel,
3804
+ "data-opt-id": "G1RNN",
3805
+ "data-opt-slug": "facet.FacetGroup",
3806
+ children: [visible.map((item) => {
3807
+ var _item$share, _item$count2;
3808
+ return /* @__PURE__ */ jsx(Facet, {
3809
+ item,
3810
+ layout,
3811
+ selected: current.includes(item.id),
3812
+ onSelect: selection === "none" ? void 0 : handleSelect,
3813
+ showShare,
3814
+ share: (_item$share = item.share) !== null && _item$share !== void 0 ? _item$share : ((_item$count2 = item.count) !== null && _item$count2 !== void 0 ? _item$count2 : 0) / denominator,
3815
+ countFormatter,
3816
+ labels
3817
+ }, item.id);
3818
+ }), (remaining > 0 || expanded) && max !== void 0 && /* @__PURE__ */ jsx("button", {
3819
+ type: "button",
3820
+ className: cn("text-text-tertiary hover:text-text-primary rounded-md px-2 py-1 text-left text-xs", "outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-ring-offset"),
3821
+ onClick: () => setExpanded((value) => !value),
3822
+ children: expanded ? text.showLess : text.showMore(remaining)
3823
+ })]
3824
+ });
3825
+ }
3826
+ //#endregion
3827
+ //#region src/core/expression.tsx
3828
+ const toneStyles = {
3829
+ accent: {
3830
+ text: "text-accent",
3831
+ rule: "decoration-accent"
3832
+ },
3833
+ success: {
3834
+ text: "text-success",
3835
+ rule: "decoration-success"
3836
+ },
3837
+ warning: {
3838
+ text: "text-warning",
3839
+ rule: "decoration-warning"
3840
+ },
3841
+ danger: {
3842
+ text: "text-danger",
3843
+ rule: "decoration-danger"
3844
+ },
3845
+ info: {
3846
+ text: "text-info",
3847
+ rule: "decoration-info"
3848
+ },
3849
+ muted: {
3850
+ text: "text-text-secondary",
3851
+ rule: "decoration-border-hover"
3852
+ }
3853
+ };
3854
+ /**
3855
+ * Expression — one clause of a condition, written as a sentence and edited in
3856
+ * place.
3857
+ *
3858
+ * A rule like "when the count of checkout events is above 100 over 5 minutes"
3859
+ * is a sentence before it is a form. Rendering it as one — with only the
3860
+ * variable halves clickable — lets a reader verify the rule by reading it,
3861
+ * which is the step a stack of labelled inputs skips.
3862
+ *
3863
+ * @example
3864
+ * ```tsx
3865
+ * <ExpressionGroup>
3866
+ * <Expression description="when" value="count()" onClick={openAggregation} />
3867
+ * <Expression description="is above" value="100" tone="danger" onClick={openThreshold} />
3868
+ * <Expression description="for the last" value="5 minutes" onClick={openWindow} />
3869
+ * </ExpressionGroup>
3870
+ * ```
3871
+ */
3872
+ function Expression({ description, value, tone = "accent", active = false, invalid = false, disabled = false, onClick, display = "inline", "aria-label": ariaLabel, className }) {
3873
+ const palette = invalid ? toneStyles.danger : toneStyles[tone];
3874
+ const interactive = onClick !== void 0 && !disabled;
3875
+ const body = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
3876
+ className: cn("text-text-secondary text-xs font-semibold tracking-wide uppercase", disabled && "opacity-60"),
3877
+ children: description
3878
+ }), value !== void 0 && value !== null && value !== "" && /* @__PURE__ */ jsx("span", {
3879
+ className: cn("text-sm font-semibold", palette.text, interactive && cn("underline decoration-2 underline-offset-4", palette.rule, active ? "decoration-solid" : "decoration-dotted"), disabled && "opacity-60"),
3880
+ children: value
3881
+ })] });
3882
+ const shell = cn(display === "stacked" ? "flex flex-col items-start gap-0.5" : "inline-flex items-baseline gap-1.5", "rounded-md px-1.5 py-1 text-left", interactive && "hover:bg-accent-subtle cursor-pointer transition-colors", active && "bg-accent-subtle", disabled && "cursor-not-allowed", className);
3883
+ if (!interactive) return /* @__PURE__ */ jsx("span", {
3884
+ className: shell,
3885
+ "data-invalid": invalid || void 0,
3886
+ "data-opt-id": "TXGA8",
3887
+ "data-opt-slug": "expression.Expression",
3888
+ children: body
3889
+ });
3890
+ return /* @__PURE__ */ jsx("button", {
3891
+ type: "button",
3892
+ className: cn(shell, OPT_FOCUS_VISIBLE),
3893
+ onClick,
3894
+ disabled,
3895
+ "aria-expanded": active,
3896
+ "aria-label": ariaLabel,
3897
+ "data-invalid": invalid || void 0,
3898
+ "data-opt-id": "TXGA8",
3899
+ "data-opt-slug": "expression.Expression",
3900
+ children: body
3901
+ });
3902
+ }
3903
+ /**
3904
+ * ExpressionGroup — the clauses of one condition, laid out so they read as a
3905
+ * single sentence rather than as a row of unrelated controls.
3906
+ */
3907
+ function ExpressionGroup({ children, display = "sentence", className, "aria-label": ariaLabel }) {
3908
+ return /* @__PURE__ */ jsx("div", {
3909
+ className: cn(display === "sentence" ? "flex flex-wrap items-baseline gap-x-1 gap-y-1" : "grid grid-flow-col auto-cols-fr items-start gap-group", className),
3910
+ role: "group",
3911
+ "aria-label": ariaLabel,
3912
+ "data-opt-id": "F78CE",
3913
+ "data-opt-slug": "expression.ExpressionGroup",
3914
+ children
3915
+ });
3916
+ }
3917
+ //#endregion
3918
+ //#region src/core/text-truncate.tsx
3919
+ const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
3920
+ /** Measures text in the element's own resolved font, not an assumed one. */
3921
+ function createMeasurer(element) {
3922
+ const context = document.createElement("canvas").getContext("2d");
3923
+ if (!context) return null;
3924
+ const computed = window.getComputedStyle(element);
3925
+ context.font = [
3926
+ computed.fontStyle,
3927
+ computed.fontVariant,
3928
+ computed.fontWeight,
3929
+ `${computed.fontSize}/${computed.lineHeight}`,
3930
+ computed.fontFamily
3931
+ ].join(" ");
3932
+ const spacing = Number.parseFloat(computed.letterSpacing) || 0;
3933
+ return (value) => context.measureText(value).width + spacing * value.length;
3934
+ }
3935
+ /** Largest `n` in `[0, max]` for which `fits(n)` holds. */
3936
+ function largestFitting(max, fits) {
3937
+ let low = 0;
3938
+ let high = max;
3939
+ while (low < high) {
3940
+ const mid = Math.ceil((low + high) / 2);
3941
+ if (fits(mid)) low = mid;
3942
+ else high = mid - 1;
3943
+ }
3944
+ return low;
3945
+ }
3946
+ function truncateToWidth(text, side, ellipsis, available, measure) {
3947
+ if (measure(text) <= available) return text;
3948
+ const chars = Array.from(text);
3949
+ if (side === "end") {
3950
+ const keep = largestFitting(chars.length, (n) => measure(chars.slice(0, n).join("") + ellipsis) <= available);
3951
+ return chars.slice(0, keep).join("") + ellipsis;
3952
+ }
3953
+ if (side === "start") {
3954
+ const keep = largestFitting(chars.length, (n) => measure(ellipsis + chars.slice(chars.length - n).join("")) <= available);
3955
+ return ellipsis + chars.slice(chars.length - keep).join("");
3956
+ }
3957
+ if (side === "startEnd") {
3958
+ const keep = largestFitting(Math.floor(chars.length / 2), (n) => {
3959
+ const head = chars.slice(0, n).join("");
3960
+ const tail = chars.slice(chars.length - n).join("");
3961
+ return measure(head + ellipsis + tail) <= available;
3962
+ });
3963
+ if (keep === 0) return ellipsis;
3964
+ return chars.slice(0, keep).join("") + ellipsis + chars.slice(chars.length - keep).join("");
3965
+ }
3966
+ const span = largestFitting(chars.length, (n) => {
3967
+ const start = Math.floor((chars.length - n) / 2);
3968
+ return measure(ellipsis + chars.slice(start, start + n).join("") + ellipsis) <= available;
3969
+ });
3970
+ if (span === 0) return ellipsis;
3971
+ const start = Math.floor((chars.length - span) / 2);
3972
+ return ellipsis + chars.slice(start, start + span).join("") + ellipsis;
3973
+ }
3974
+ /**
3975
+ * TextTruncate — drops characters from the part of a string that identifies it
3976
+ * least.
3977
+ *
3978
+ * CSS can only cut the end. That is the wrong end for the strings analytics
3979
+ * shows most: two URLs that differ in their query string, two ids that share a
3980
+ * prefix, a stack frame whose filename is at the tail. This measures the real
3981
+ * text in the real font against the real available width and cuts where the
3982
+ * caller says.
3983
+ *
3984
+ * The full string stays in the accessibility tree and in `title`, so
3985
+ * truncation never costs the reader information — only space.
3986
+ *
3987
+ * @example
3988
+ * ```tsx
3989
+ * <TextTruncate text={event.url} truncation="startEnd" />
3990
+ * ```
3991
+ */
3992
+ function TextTruncate({ text, truncation = "startEnd", ellipsis = "…", width, children, className }) {
3993
+ const ref = useRef(null);
3994
+ const [available, setAvailable] = useState(width !== null && width !== void 0 ? width : null);
3995
+ useIsomorphicLayoutEffect(() => {
3996
+ if (width !== void 0) {
3997
+ setAvailable(width);
3998
+ return;
3999
+ }
4000
+ const element = ref.current;
4001
+ if (!element || typeof ResizeObserver === "undefined") return;
4002
+ const observer = new ResizeObserver((entries) => {
4003
+ const entry = entries[0];
4004
+ if (entry) setAvailable(entry.contentRect.width);
4005
+ });
4006
+ observer.observe(element);
4007
+ setAvailable(element.getBoundingClientRect().width);
4008
+ return () => observer.disconnect();
4009
+ }, [width]);
4010
+ const [rendered, setRendered] = useState(text);
4011
+ useIsomorphicLayoutEffect(() => {
4012
+ const element = ref.current;
4013
+ if (!element || available === null || available <= 0) {
4014
+ setRendered(text);
4015
+ return;
4016
+ }
4017
+ const measure = createMeasurer(element);
4018
+ if (!measure) {
4019
+ setRendered(text);
4020
+ return;
4021
+ }
4022
+ setRendered(truncateToWidth(text, truncation, ellipsis, available, measure));
4023
+ }, [
4024
+ text,
4025
+ truncation,
4026
+ ellipsis,
4027
+ available
4028
+ ]);
4029
+ const truncated = rendered !== text;
4030
+ return /* @__PURE__ */ jsxs("span", {
4031
+ ref,
4032
+ className: cn("block w-full overflow-hidden whitespace-nowrap", className),
4033
+ title: truncated ? text : void 0,
4034
+ "data-opt-id": "Y6BGX",
4035
+ "data-opt-slug": "text-truncate.TextTruncate",
4036
+ children: [/* @__PURE__ */ jsx("span", {
4037
+ "aria-hidden": truncated ? "true" : void 0,
4038
+ children: children ? children(rendered) : rendered
4039
+ }), truncated && /* @__PURE__ */ jsx("span", {
4040
+ className: "sr-only",
4041
+ children: text
4042
+ })]
4043
+ });
4044
+ }
4045
+ //#endregion
4046
+ //#region src/core/live-region.tsx
4047
+ /**
4048
+ * LiveRegion — says out loud what a live view changed.
4049
+ *
4050
+ * Data that updates without a page change is invisible to a screen reader
4051
+ * unless something narrates it. This is that narrator: one polite region per
4052
+ * live surface, throttled so a fast stream produces a readable summary rather
4053
+ * than a torrent, and alternating between two nodes so a repeat of the same
4054
+ * words is still announced rather than swallowed as an unchanged region.
4055
+ *
4056
+ * @example
4057
+ * ```tsx
4058
+ * <LiveRegion
4059
+ * message={`${newRows} new events`}
4060
+ * nonce={batchId}
4061
+ * throttleMs={4000}
4062
+ * muted={paused}
4063
+ * />
4064
+ * ```
4065
+ */
4066
+ function LiveRegion({ message, nonce, politeness = "polite", throttleMs = 0, muted = false }) {
4067
+ const [slots, setSlots] = useState(["", ""]);
4068
+ const activeSlot = useRef(0);
4069
+ const pending = useRef(null);
4070
+ const timer = useRef(null);
4071
+ useEffect(() => {
4072
+ if (muted || message === "") return;
4073
+ const flush = () => {
4074
+ const next = pending.current;
4075
+ pending.current = null;
4076
+ timer.current = null;
4077
+ if (next === null) return;
4078
+ const slot = activeSlot.current;
4079
+ activeSlot.current = slot === 0 ? 1 : 0;
4080
+ setSlots(slot === 0 ? [next, ""] : ["", next]);
4081
+ };
4082
+ pending.current = message;
4083
+ if (throttleMs <= 0) {
4084
+ flush();
4085
+ return;
4086
+ }
4087
+ if (timer.current === null) timer.current = setTimeout(flush, throttleMs);
4088
+ return () => {
4089
+ if (timer.current !== null) {
4090
+ clearTimeout(timer.current);
4091
+ timer.current = null;
4092
+ }
4093
+ };
4094
+ }, [
4095
+ message,
4096
+ nonce,
4097
+ throttleMs,
4098
+ muted
4099
+ ]);
4100
+ useEffect(() => {
4101
+ if (!muted) return;
4102
+ pending.current = null;
4103
+ if (timer.current !== null) {
4104
+ clearTimeout(timer.current);
4105
+ timer.current = null;
4106
+ }
4107
+ setSlots(["", ""]);
4108
+ }, [muted]);
4109
+ return /* @__PURE__ */ jsxs("div", {
4110
+ className: "sr-only",
4111
+ "data-opt-id": "0CFGR",
4112
+ "data-opt-slug": "live-region.LiveRegion",
4113
+ children: [/* @__PURE__ */ jsx("div", {
4114
+ role: "status",
4115
+ "aria-live": politeness,
4116
+ "aria-atomic": "true",
4117
+ children: slots[0]
4118
+ }), /* @__PURE__ */ jsx("div", {
4119
+ role: "status",
4120
+ "aria-live": politeness,
4121
+ "aria-atomic": "true",
4122
+ children: slots[1]
4123
+ })]
4124
+ });
4125
+ }
4126
+ //#endregion
4127
+ //#region src/core/color-palette.tsx
4128
+ /** The theme's own chart ramps, so a picker can offer them without a literal. */
4129
+ const THEME_PALETTES = [
4130
+ {
4131
+ id: "categorical",
4132
+ label: "Categorical",
4133
+ kind: "categorical",
4134
+ colors: Array.from({ length: 10 }, (_, index) => `var(--opt-chart-${index + 1})`)
4135
+ },
4136
+ {
4137
+ id: "sequential",
4138
+ label: "Sequential",
4139
+ kind: "sequential",
4140
+ colors: Array.from({ length: 5 }, (_, index) => `var(--opt-chart-sequential-${index + 1})`)
4141
+ },
4142
+ {
4143
+ id: "diverging",
4144
+ label: "Diverging",
4145
+ kind: "diverging",
4146
+ colors: [
4147
+ "var(--opt-chart-diverging-negative)",
4148
+ "var(--opt-chart-diverging-neutral)",
4149
+ "var(--opt-chart-diverging-positive)"
4150
+ ]
4151
+ }
4152
+ ];
4153
+ const sizeStyles$2 = {
4154
+ xs: "h-1.5",
4155
+ sm: "h-2.5",
4156
+ md: "h-4",
4157
+ lg: "h-6"
4158
+ };
4159
+ /**
4160
+ * ColorPaletteDisplay — a palette, shown as the thing it will look like.
4161
+ *
4162
+ * A palette name tells a reader nothing; the colours do. Categorical palettes
4163
+ * draw as discrete blocks and ramps draw as a blend, so the picture itself
4164
+ * states which kind of data the palette is for.
4165
+ */
4166
+ function ColorPaletteDisplay({ palette, render, size = "sm", "aria-label": ariaLabel, className }) {
4167
+ const mode = render !== null && render !== void 0 ? render : palette.kind === "categorical" ? "stops" : "gradient";
4168
+ const colors = palette.colors.length > 0 ? palette.colors : ["transparent"];
4169
+ return /* @__PURE__ */ jsx("span", {
4170
+ className: cn("flex w-full overflow-hidden rounded-sm", sizeStyles$2[size], className),
4171
+ role: "img",
4172
+ "aria-label": ariaLabel !== null && ariaLabel !== void 0 ? ariaLabel : palette.label,
4173
+ style: mode === "gradient" ? { backgroundImage: `linear-gradient(to right, ${colors.join(", ")})` } : void 0,
4174
+ "data-opt-id": "E7T89",
4175
+ "data-opt-slug": "color-palette.ColorPaletteDisplay",
4176
+ children: mode === "stops" && colors.map((color, index) => /* @__PURE__ */ jsx("span", {
4177
+ className: "h-full flex-1",
4178
+ style: { backgroundColor: color }
4179
+ }, `${color}-${index}`))
4180
+ });
4181
+ }
4182
+ /**
4183
+ * ColorPalettePicker — choose the colours a visualisation is drawn in.
4184
+ *
4185
+ * Each option shows its own palette, so the choice is made by looking rather
4186
+ * than by remembering what "Cool" meant last time.
4187
+ *
4188
+ * @example
4189
+ * ```tsx
4190
+ * <ColorPalettePicker
4191
+ * palettes={THEME_PALETTES}
4192
+ * value={paletteId}
4193
+ * onValueChange={setPaletteId}
4194
+ * label="Series colours"
4195
+ * />
4196
+ * ```
4197
+ */
4198
+ function ColorPalettePicker({ palettes, value, onValueChange, label = "Colour palette", hideLabel = false, size = "sm", disabled = false, className }) {
4199
+ var _palettes$find, _ref;
4200
+ const labelId = useId();
4201
+ const selected = (_palettes$find = palettes.find((palette) => palette.id === value)) !== null && _palettes$find !== void 0 ? _palettes$find : palettes[0];
4202
+ return /* @__PURE__ */ jsxs("div", {
4203
+ className: cn("flex flex-col gap-element", className),
4204
+ "data-opt-id": "5R0PG",
4205
+ "data-opt-slug": "color-palette.ColorPalettePicker",
4206
+ children: [/* @__PURE__ */ jsx("span", {
4207
+ id: labelId,
4208
+ className: cn("text-text-primary text-sm font-medium", hideLabel && "sr-only"),
4209
+ children: label
4210
+ }), /* @__PURE__ */ jsxs(SelectRoot$1, {
4211
+ value: (_ref = value !== null && value !== void 0 ? value : selected === null || selected === void 0 ? void 0 : selected.id) !== null && _ref !== void 0 ? _ref : "",
4212
+ setValue: (next) => {
4213
+ if (typeof next === "string") onValueChange === null || onValueChange === void 0 || onValueChange(next);
4214
+ },
4215
+ children: [/* @__PURE__ */ jsx(SelectTrigger$1, {
4216
+ "aria-labelledby": labelId,
4217
+ disabled,
4218
+ className: cn("gap-group", OPT_FOCUS_VISIBLE),
4219
+ children: /* @__PURE__ */ jsxs("span", {
4220
+ className: "gap-group flex min-w-0 flex-1 items-center",
4221
+ children: [/* @__PURE__ */ jsx("span", {
4222
+ className: "text-text-primary shrink-0 truncate text-sm",
4223
+ children: selected === null || selected === void 0 ? void 0 : selected.label
4224
+ }), selected && /* @__PURE__ */ jsx(ColorPaletteDisplay, {
4225
+ palette: selected,
4226
+ size,
4227
+ className: cn("min-w-16 flex-1", "border border-border"),
4228
+ "aria-label": ""
4229
+ })]
4230
+ })
4231
+ }), /* @__PURE__ */ jsx(SelectPopover$1, { children: palettes.map((palette) => /* @__PURE__ */ jsx(SelectItem$1, {
4232
+ value: palette.id,
4233
+ children: /* @__PURE__ */ jsxs("span", {
4234
+ className: "gap-group flex w-full min-w-40 items-center",
4235
+ children: [/* @__PURE__ */ jsx("span", {
4236
+ className: "shrink-0 truncate text-sm",
4237
+ children: palette.label
4238
+ }), /* @__PURE__ */ jsx(ColorPaletteDisplay, {
4239
+ palette,
4240
+ size,
4241
+ className: "min-w-16 flex-1",
4242
+ "aria-label": ""
4243
+ })]
4244
+ })
4245
+ }, palette.id)) })]
4246
+ })]
4247
+ });
4248
+ }
4249
+ //#endregion
4250
+ //#region src/core/inline-edit.tsx
4251
+ const defaultLabels = {
4252
+ edit: (value) => `Edit ${value}`,
4253
+ save: "Save",
4254
+ cancel: "Cancel",
4255
+ invalid: "That value cannot be saved."
4256
+ };
4257
+ const sizeStyles$1 = {
4258
+ sm: "text-sm",
4259
+ md: "text-base",
4260
+ lg: "text-lg font-semibold",
4261
+ xl: "text-2xl font-semibold tracking-tight"
4262
+ };
4263
+ /**
4264
+ * InlineEdit — renames a thing where the thing is, instead of in a dialog.
4265
+ *
4266
+ * A dashboard title, a metric name, a saved segment: each is read far more
4267
+ * often than it is renamed, so the control has to disappear into the text and
4268
+ * reappear on intent. Read mode is a button carrying the text; edit mode is a
4269
+ * field sized to match it, so the words do not move when the mode does.
4270
+ *
4271
+ * Enter commits, Escape reverts, and a rejected save keeps the field open with
4272
+ * what was typed — losing an edit to a validation error is worse than the
4273
+ * error.
4274
+ *
4275
+ * @example
4276
+ * ```tsx
4277
+ * <InlineEdit as="h2" size="xl" value={dashboard.name} onSave={rename} />
4278
+ * ```
4279
+ */
4280
+ function InlineEdit({ value, onSave, as: Tag = "span", size = "md", placeholder, emptyText = "Untitled", disabled = false, maxLength, suffix, labels, className }) {
4281
+ const text = _objectSpread2(_objectSpread2({}, defaultLabels), labels);
4282
+ const [editing, setEditing] = useState(false);
4283
+ const [draft, setDraft] = useState(value);
4284
+ const [rejected, setRejected] = useState(false);
4285
+ const [saving, setSaving] = useState(false);
4286
+ const inputRef = useRef(null);
4287
+ useEffect(() => {
4288
+ if (!editing) setDraft(value);
4289
+ }, [value, editing]);
4290
+ useEffect(() => {
4291
+ if (editing) {
4292
+ var _inputRef$current, _inputRef$current2;
4293
+ (_inputRef$current = inputRef.current) === null || _inputRef$current === void 0 || _inputRef$current.focus();
4294
+ (_inputRef$current2 = inputRef.current) === null || _inputRef$current2 === void 0 || _inputRef$current2.select();
4295
+ }
4296
+ }, [editing]);
4297
+ const commit = async () => {
4298
+ if (saving) return;
4299
+ const next = draft.trim();
4300
+ if (next === value) {
4301
+ setEditing(false);
4302
+ setRejected(false);
4303
+ return;
4304
+ }
4305
+ setSaving(true);
4306
+ try {
4307
+ if (await onSave(next) === false) {
4308
+ setRejected(true);
4309
+ return;
4310
+ }
4311
+ setRejected(false);
4312
+ setEditing(false);
4313
+ } finally {
4314
+ setSaving(false);
4315
+ }
4316
+ };
4317
+ const cancel = () => {
4318
+ setDraft(value);
4319
+ setRejected(false);
4320
+ setEditing(false);
4321
+ };
4322
+ const handleKeyDown = (event) => {
4323
+ if (event.key === "Enter") {
4324
+ event.preventDefault();
4325
+ commit();
4326
+ } else if (event.key === "Escape") {
4327
+ event.preventDefault();
4328
+ cancel();
4329
+ }
4330
+ };
4331
+ if (editing) return /* @__PURE__ */ jsxs("span", {
4332
+ className: cn("inline-flex items-center gap-element", className),
4333
+ "data-opt-id": "RQ0H2",
4334
+ "data-opt-slug": "inline-edit.InlineEdit",
4335
+ children: [
4336
+ /* @__PURE__ */ jsx("input", {
4337
+ ref: inputRef,
4338
+ value: draft,
4339
+ onChange: (event) => {
4340
+ setDraft(event.target.value);
4341
+ setRejected(false);
4342
+ },
4343
+ onKeyDown: handleKeyDown,
4344
+ onBlur: () => void commit(),
4345
+ placeholder,
4346
+ maxLength,
4347
+ disabled: saving,
4348
+ "aria-invalid": rejected || void 0,
4349
+ "aria-errormessage": rejected ? "inline-edit-error" : void 0,
4350
+ className: cn(OPT_INPUT, sizeStyles$1[size], "min-w-0 flex-1")
4351
+ }),
4352
+ rejected && /* @__PURE__ */ jsx("span", {
4353
+ id: "inline-edit-error",
4354
+ role: "alert",
4355
+ className: "sr-only",
4356
+ children: text.invalid
4357
+ }),
4358
+ /* @__PURE__ */ jsx("button", {
4359
+ type: "button",
4360
+ "aria-label": text.save,
4361
+ disabled: saving,
4362
+ onMouseDown: (event) => event.preventDefault(),
4363
+ onClick: () => void commit(),
4364
+ className: cn("text-success hover:bg-success-subtle rounded-md p-1", OPT_FOCUS_VISIBLE),
4365
+ children: /* @__PURE__ */ jsx(Check, {
4366
+ className: "size-4",
4367
+ "aria-hidden": "true"
4368
+ })
4369
+ }),
4370
+ /* @__PURE__ */ jsx("button", {
4371
+ type: "button",
4372
+ "aria-label": text.cancel,
4373
+ onMouseDown: (event) => event.preventDefault(),
4374
+ onClick: cancel,
4375
+ className: cn("text-text-tertiary hover:bg-bg-subtle rounded-md p-1", OPT_FOCUS_VISIBLE),
4376
+ children: /* @__PURE__ */ jsx(X, {
4377
+ className: "size-4",
4378
+ "aria-hidden": "true"
4379
+ })
4380
+ })
4381
+ ]
4382
+ });
4383
+ return /* @__PURE__ */ jsxs("span", {
4384
+ className: cn("inline-flex items-center gap-element", className),
4385
+ "data-opt-id": "RQ0H2",
4386
+ "data-opt-slug": "inline-edit.InlineEdit",
4387
+ children: [/* @__PURE__ */ jsxs("button", {
4388
+ type: "button",
4389
+ disabled,
4390
+ onClick: () => setEditing(true),
4391
+ "aria-label": text.edit(value || emptyText),
4392
+ className: cn("group -mx-1.5 -my-0.5 inline-flex max-w-full items-center gap-element rounded-md px-1.5 py-0.5 text-left", !disabled && "hover:bg-bg-subtle cursor-pointer", disabled && "cursor-default", OPT_FOCUS_VISIBLE),
4393
+ children: [/* @__PURE__ */ jsx(Tag, {
4394
+ className: cn("truncate", sizeStyles$1[size], value ? "text-text-primary" : "text-text-tertiary italic"),
4395
+ children: value || emptyText
4396
+ }), !disabled && /* @__PURE__ */ jsx(Pencil, {
4397
+ "aria-hidden": "true",
4398
+ className: cn("text-text-tertiary size-3.5 shrink-0 opacity-0 transition-opacity", "group-hover:opacity-100 group-focus-visible:opacity-100")
4399
+ })]
4400
+ }), suffix]
4401
+ });
4402
+ }
4403
+ //#endregion
4404
+ //#region src/core/key-pad-menu.tsx
4405
+ const sizeStyles = {
4406
+ sm: "min-h-20 p-2 text-xs [&_[data-keypad-icon]]:size-5",
4407
+ md: "min-h-24 p-3 text-sm [&_[data-keypad-icon]]:size-6"
4408
+ };
4409
+ /**
4410
+ * KeyPadMenu — a choice between things that are easier to recognise than to
4411
+ * name.
4412
+ *
4413
+ * A chart type, a layout, an import source: the picture is the label, and a
4414
+ * dropdown of words makes the reader translate twice. Tiles are real radios or
4415
+ * checkboxes underneath, so arrow keys, screen readers, and form submission
4416
+ * all behave as they would for any other group.
4417
+ *
4418
+ * @example
4419
+ * ```tsx
4420
+ * <KeyPadMenu
4421
+ * label="Chart type"
4422
+ * items={vizTypes}
4423
+ * value={vizType}
4424
+ * onValueChange={setVizType}
4425
+ * />
4426
+ * ```
4427
+ */
4428
+ function KeyPadMenu({ items, label, selection = "single", value, onValueChange, columns = "auto", size = "md", hideLabel = false, className }) {
4429
+ const name = useId();
4430
+ const selected = new Set(value === void 0 ? [] : Array.isArray(value) ? value : [value]);
4431
+ const handleChange = (id) => {
4432
+ if (selection === "multiple") {
4433
+ const next = selected.has(id) ? [...selected].filter((item) => item !== id) : [...selected, id];
4434
+ onValueChange === null || onValueChange === void 0 || onValueChange(next);
4435
+ return;
4436
+ }
4437
+ onValueChange === null || onValueChange === void 0 || onValueChange(id);
4438
+ };
4439
+ return /* @__PURE__ */ jsxs("fieldset", {
4440
+ className: cn("min-w-0", className),
4441
+ "data-opt-id": "ZZG2R",
4442
+ "data-opt-slug": "key-pad-menu.KeyPadMenu",
4443
+ children: [/* @__PURE__ */ jsx("legend", {
4444
+ className: cn("text-text-primary mb-element text-sm font-medium", hideLabel && "sr-only"),
4445
+ children: label
4446
+ }), /* @__PURE__ */ jsx("div", {
4447
+ className: "gap-element grid",
4448
+ style: { gridTemplateColumns: columns === "auto" ? "repeat(auto-fill, minmax(6.5rem, 1fr))" : `repeat(${columns}, minmax(0, 1fr))` },
4449
+ children: items.map((item) => {
4450
+ const isSelected = selected.has(item.id);
4451
+ return /* @__PURE__ */ jsxs("label", {
4452
+ className: cn("relative flex cursor-pointer flex-col items-center justify-center gap-1.5 rounded-lg text-center transition-colors", OPT_BORDER, sizeStyles[size], isSelected ? "border-accent bg-accent-subtle text-text-primary" : "text-text-secondary hover:border-border-hover hover:bg-bg-subtle", item.disabled && "cursor-not-allowed opacity-50", "has-[:focus-visible]:outline-ring has-[:focus-visible]:outline-2"),
4453
+ children: [
4454
+ /* @__PURE__ */ jsx("input", {
4455
+ type: selection === "single" ? "radio" : "checkbox",
4456
+ name: selection === "single" ? name : void 0,
4457
+ value: item.id,
4458
+ checked: isSelected,
4459
+ disabled: item.disabled,
4460
+ onChange: () => handleChange(item.id),
4461
+ className: cn("sr-only", OPT_FOCUS_VISIBLE)
4462
+ }),
4463
+ /* @__PURE__ */ jsx("span", {
4464
+ "data-keypad-icon": "",
4465
+ "aria-hidden": "true",
4466
+ className: cn("flex items-center justify-center", isSelected ? "text-accent" : "text-text-tertiary"),
4467
+ children: item.icon
4468
+ }),
4469
+ /* @__PURE__ */ jsx("span", {
4470
+ className: "font-medium",
4471
+ children: item.label
4472
+ }),
4473
+ item.hint && /* @__PURE__ */ jsx("span", {
4474
+ className: "text-text-tertiary line-clamp-2 text-[11px] leading-tight",
4475
+ children: item.hint
4476
+ }),
4477
+ item.badge && /* @__PURE__ */ jsx("span", {
4478
+ className: "bg-info-subtle text-info absolute top-1 right-1 rounded px-1 text-[10px] font-semibold",
4479
+ children: item.badge
4480
+ })
4481
+ ]
4482
+ }, item.id);
4483
+ })
4484
+ })]
4485
+ });
4486
+ }
4487
+ //#endregion
4488
+ export { Textarea as $, ScrollArea as A, DropdownSeparator as B, DrawerRoot as C, ResizablePanelGroup as D, ResizablePanel as E, Radio as F, formatOptShortDate as G, ConditionBuilder as H, RadioGroup as I, Meter$1 as J, formatOptTime as K, DropdownContent as L, createBlock as M, BlockLayout as N, Section as O, LoadingOverlay as P, AvatarGroup as Q, DropdownItem as R, DrawerPanel as S, Text as St, ResizableHandle as T, formatOptDateTime as U, DropdownTrigger as V, formatOptNumber as W, Progress as X, CircularProgress as Y, Avatar as Z, SHADER_PRESET_IDS as _, Toggle$1 as _t, THEME_PALETTES as a, Switch as at, VirtualList as b, Separator$2 as bt, Expression as c, CardDescription as ct, FacetGroup as d, CardTitle as dt, Skeleton as et, ShaderSurface as f, Breadcrumb as ft, SHADER_PRESETS as g, AccordionTrigger$1 as gt, SHADER_PARAM_KEYS as h, AccordionRoot$1 as ht, ColorPalettePicker as i, SkeletonText as it, PageHeader as j, Container as k, ExpressionGroup as l, CardFooter as lt, SHADER_PARAM_FALLBACK as m, AccordionItem$1 as mt, InlineEdit as n, SkeletonCard as nt, LiveRegion as o, Card as ot, SHADER_OVERLAY_PARAM_KEYS as p, AccordionContent$1 as pt, StepBuilder as q, ColorPaletteDisplay as r, SkeletonTable as rt, TextTruncate as s, CardContent as st, KeyPadMenu as t, SkeletonAvatar as tt, Facet as u, CardHeader as ut, getShaderFragment as v, ToggleGroup$1 as vt, Logo as w, DrawerClose as x, Heading as xt, getShaderPreset as y, OtpField as yt, DropdownMenu as z };
3653
4489
 
3654
- //# sourceMappingURL=shader-surface-BUPZ0P-n.js.map
4490
+ //# sourceMappingURL=key-pad-menu-BFd79JoS.js.map