@reopt-ai/opt-ui 1.8.0 → 1.10.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-Bqfp6dgg.cjs → field-sidebar-B6Ndr8HE.cjs} +304 -289
  13. package/dist/{field-sidebar-BYD_UIjG.js → field-sidebar-BXjvV7Cz.js} +25 -10
  14. package/dist/{field-sidebar-BYD_UIjG.js.map → field-sidebar-BXjvV7Cz.js.map} +1 -1
  15. package/dist/{field-sidebar-5KYVlpSs.d.ts → field-sidebar-DWfJAJlx.d.ts} +32 -3
  16. package/dist/{field-sidebar-5KYVlpSs.d.ts.map → field-sidebar-DWfJAJlx.d.ts.map} +1 -1
  17. package/dist/{field-sidebar-BP0iWkoV.d.cts → field-sidebar-nwcE9LC4.d.cts} +32 -3
  18. package/dist/{field-sidebar-BP0iWkoV.d.cts.map → field-sidebar-nwcE9LC4.d.cts.map} +1 -1
  19. package/dist/id-registry.cjs +98 -2
  20. package/dist/id-registry.d.cts.map +1 -1
  21. package/dist/id-registry.d.ts.map +1 -1
  22. package/dist/id-registry.js +98 -2
  23. package/dist/id-registry.js.map +1 -1
  24. package/dist/id-registry.json +202 -4
  25. package/dist/index.cjs +664 -643
  26. package/dist/index.d.cts +5 -4
  27. package/dist/index.d.cts.map +1 -1
  28. package/dist/index.d.ts +5 -4
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +10 -4
  31. package/dist/index.js.map +1 -1
  32. package/dist/{shader-surface-BUPZ0P-n.js → key-pad-menu-BQunU20e.js} +727 -19
  33. package/dist/key-pad-menu-BQunU20e.js.map +1 -0
  34. package/dist/{shader-surface-BfTP9c1Q.d.ts → key-pad-menu-BsgJMjD4.d.ts} +415 -3
  35. package/dist/key-pad-menu-BsgJMjD4.d.ts.map +1 -0
  36. package/dist/{shader-surface-Fav5K9UC.cjs → key-pad-menu-D5mQcS5v.cjs} +969 -201
  37. package/dist/{shader-surface-DRrcLrWf.d.cts → key-pad-menu-DfZbeXhG.d.cts} +415 -3
  38. package/dist/key-pad-menu-DfZbeXhG.d.cts.map +1 -0
  39. package/dist/meta.cjs +641 -0
  40. package/dist/meta.d.cts.map +1 -1
  41. package/dist/meta.d.ts.map +1 -1
  42. package/dist/meta.js +641 -0
  43. package/dist/meta.js.map +1 -1
  44. package/dist/shells/index.cjs +1 -1
  45. package/dist/shells/index.d.cts +1 -1
  46. package/dist/shells/index.d.ts +1 -1
  47. package/dist/shells/index.js +1 -1
  48. package/dist/{types-D0FlcYnM.d.ts → text-truncate-BGr7bB_8.d.cts} +110 -2
  49. package/dist/text-truncate-BGr7bB_8.d.cts.map +1 -0
  50. package/dist/{types-D0FlcYnM.d.cts → text-truncate-BGr7bB_8.d.ts} +110 -2
  51. package/dist/text-truncate-BGr7bB_8.d.ts.map +1 -0
  52. package/dist/{pagination-DjfxBcjX.cjs → text-truncate-CxdzAnhS.cjs} +418 -8
  53. package/dist/{pagination-DWbvACuN.js → text-truncate-j6X3z7Pv.js} +392 -12
  54. package/dist/text-truncate-j6X3z7Pv.js.map +1 -0
  55. package/package.json +3 -3
  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 { Ft as SelectPopover$1, Gn as _objectSpread2, Hn as OPT_TEXT_SECONDARY, It as SelectRoot$1, Jn as cn, Kn as _objectWithoutProperties, Lt as SelectTrigger$1, Nt as SelectItem$1, On as OPT_FOCUS, Pn as OPT_SURFACE, Qt as useInputId, Un as OPT_TEXT_TERTIARY, Vn as OPT_TEXT_PRIMARY, bn as OPT_BORDER, cn as useControlledState, dn as getDescribedBy, dt as MenuRoot$1, en as Spinner, ft as MenuSeparator$1, in as koBreadcrumbLabels, jn as OPT_INPUT, kn as OPT_FOCUS_VISIBLE, ln as FieldMessages, mn as OPT_ANIMATE_ENTER_EXIT, pn as OPT_ANIMATE_DRAWER, pt as MenuTrigger$1, rn as Button, st as MenuItem$1, un as RequiredMark, ut as MenuPopover$1, zn as OPT_TEXTAREA_SIZE } from "./text-truncate-j6X3z7Pv.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,714 @@ 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/live-region.tsx
3919
+ /**
3920
+ * LiveRegion — says out loud what a live view changed.
3921
+ *
3922
+ * Data that updates without a page change is invisible to a screen reader
3923
+ * unless something narrates it. This is that narrator: one polite region per
3924
+ * live surface, throttled so a fast stream produces a readable summary rather
3925
+ * than a torrent, and alternating between two nodes so a repeat of the same
3926
+ * words is still announced rather than swallowed as an unchanged region.
3927
+ *
3928
+ * @example
3929
+ * ```tsx
3930
+ * <LiveRegion
3931
+ * message={`${newRows} new events`}
3932
+ * nonce={batchId}
3933
+ * throttleMs={4000}
3934
+ * muted={paused}
3935
+ * />
3936
+ * ```
3937
+ */
3938
+ function LiveRegion({ message, nonce, politeness = "polite", throttleMs = 0, muted = false }) {
3939
+ const [slots, setSlots] = useState(["", ""]);
3940
+ const activeSlot = useRef(0);
3941
+ const pending = useRef(null);
3942
+ const timer = useRef(null);
3943
+ useEffect(() => {
3944
+ if (muted || message === "") return;
3945
+ const flush = () => {
3946
+ const next = pending.current;
3947
+ pending.current = null;
3948
+ timer.current = null;
3949
+ if (next === null) return;
3950
+ const slot = activeSlot.current;
3951
+ activeSlot.current = slot === 0 ? 1 : 0;
3952
+ setSlots(slot === 0 ? [next, ""] : ["", next]);
3953
+ };
3954
+ pending.current = message;
3955
+ if (throttleMs <= 0) {
3956
+ flush();
3957
+ return;
3958
+ }
3959
+ if (timer.current === null) timer.current = setTimeout(flush, throttleMs);
3960
+ return () => {
3961
+ if (timer.current !== null) {
3962
+ clearTimeout(timer.current);
3963
+ timer.current = null;
3964
+ }
3965
+ };
3966
+ }, [
3967
+ message,
3968
+ nonce,
3969
+ throttleMs,
3970
+ muted
3971
+ ]);
3972
+ useEffect(() => {
3973
+ if (!muted) return;
3974
+ pending.current = null;
3975
+ if (timer.current !== null) {
3976
+ clearTimeout(timer.current);
3977
+ timer.current = null;
3978
+ }
3979
+ setSlots(["", ""]);
3980
+ }, [muted]);
3981
+ return /* @__PURE__ */ jsxs("div", {
3982
+ className: "sr-only",
3983
+ "data-opt-id": "0CFGR",
3984
+ "data-opt-slug": "live-region.LiveRegion",
3985
+ children: [/* @__PURE__ */ jsx("div", {
3986
+ role: "status",
3987
+ "aria-live": politeness,
3988
+ "aria-atomic": "true",
3989
+ children: slots[0]
3990
+ }), /* @__PURE__ */ jsx("div", {
3991
+ role: "status",
3992
+ "aria-live": politeness,
3993
+ "aria-atomic": "true",
3994
+ children: slots[1]
3995
+ })]
3996
+ });
3997
+ }
3998
+ //#endregion
3999
+ //#region src/core/color-palette.tsx
4000
+ /** The theme's own chart ramps, so a picker can offer them without a literal. */
4001
+ const THEME_PALETTES = [
4002
+ {
4003
+ id: "categorical",
4004
+ label: "Categorical",
4005
+ kind: "categorical",
4006
+ colors: Array.from({ length: 10 }, (_, index) => `var(--opt-chart-${index + 1})`)
4007
+ },
4008
+ {
4009
+ id: "sequential",
4010
+ label: "Sequential",
4011
+ kind: "sequential",
4012
+ colors: Array.from({ length: 5 }, (_, index) => `var(--opt-chart-sequential-${index + 1})`)
4013
+ },
4014
+ {
4015
+ id: "diverging",
4016
+ label: "Diverging",
4017
+ kind: "diverging",
4018
+ colors: [
4019
+ "var(--opt-chart-diverging-negative)",
4020
+ "var(--opt-chart-diverging-neutral)",
4021
+ "var(--opt-chart-diverging-positive)"
4022
+ ]
4023
+ }
4024
+ ];
4025
+ const sizeStyles$2 = {
4026
+ xs: "h-1.5",
4027
+ sm: "h-2.5",
4028
+ md: "h-4",
4029
+ lg: "h-6"
4030
+ };
4031
+ /**
4032
+ * ColorPaletteDisplay — a palette, shown as the thing it will look like.
4033
+ *
4034
+ * A palette name tells a reader nothing; the colours do. Categorical palettes
4035
+ * draw as discrete blocks and ramps draw as a blend, so the picture itself
4036
+ * states which kind of data the palette is for.
4037
+ */
4038
+ function ColorPaletteDisplay({ palette, render, size = "sm", "aria-label": ariaLabel, className }) {
4039
+ const mode = render !== null && render !== void 0 ? render : palette.kind === "categorical" ? "stops" : "gradient";
4040
+ const colors = palette.colors.length > 0 ? palette.colors : ["transparent"];
4041
+ return /* @__PURE__ */ jsx("span", {
4042
+ className: cn("flex w-full overflow-hidden rounded-sm", sizeStyles$2[size], className),
4043
+ role: "img",
4044
+ "aria-label": ariaLabel !== null && ariaLabel !== void 0 ? ariaLabel : palette.label,
4045
+ style: mode === "gradient" ? { backgroundImage: `linear-gradient(to right, ${colors.join(", ")})` } : void 0,
4046
+ "data-opt-id": "E7T89",
4047
+ "data-opt-slug": "color-palette.ColorPaletteDisplay",
4048
+ children: mode === "stops" && colors.map((color, index) => /* @__PURE__ */ jsx("span", {
4049
+ className: "h-full flex-1",
4050
+ style: { backgroundColor: color }
4051
+ }, `${color}-${index}`))
4052
+ });
4053
+ }
4054
+ /**
4055
+ * ColorPalettePicker — choose the colours a visualisation is drawn in.
4056
+ *
4057
+ * Each option shows its own palette, so the choice is made by looking rather
4058
+ * than by remembering what "Cool" meant last time.
4059
+ *
4060
+ * @example
4061
+ * ```tsx
4062
+ * <ColorPalettePicker
4063
+ * palettes={THEME_PALETTES}
4064
+ * value={paletteId}
4065
+ * onValueChange={setPaletteId}
4066
+ * label="Series colours"
4067
+ * />
4068
+ * ```
4069
+ */
4070
+ function ColorPalettePicker({ palettes, value, onValueChange, label = "Colour palette", hideLabel = false, size = "sm", disabled = false, className }) {
4071
+ var _palettes$find, _ref;
4072
+ const labelId = useId();
4073
+ const selected = (_palettes$find = palettes.find((palette) => palette.id === value)) !== null && _palettes$find !== void 0 ? _palettes$find : palettes[0];
4074
+ return /* @__PURE__ */ jsxs("div", {
4075
+ className: cn("flex flex-col gap-element", className),
4076
+ "data-opt-id": "5R0PG",
4077
+ "data-opt-slug": "color-palette.ColorPalettePicker",
4078
+ children: [/* @__PURE__ */ jsx("span", {
4079
+ id: labelId,
4080
+ className: cn("text-text-primary text-sm font-medium", hideLabel && "sr-only"),
4081
+ children: label
4082
+ }), /* @__PURE__ */ jsxs(SelectRoot$1, {
4083
+ value: (_ref = value !== null && value !== void 0 ? value : selected === null || selected === void 0 ? void 0 : selected.id) !== null && _ref !== void 0 ? _ref : "",
4084
+ setValue: (next) => {
4085
+ if (typeof next === "string") onValueChange === null || onValueChange === void 0 || onValueChange(next);
4086
+ },
4087
+ children: [/* @__PURE__ */ jsx(SelectTrigger$1, {
4088
+ "aria-labelledby": labelId,
4089
+ disabled,
4090
+ className: cn("gap-group", OPT_FOCUS_VISIBLE),
4091
+ children: /* @__PURE__ */ jsxs("span", {
4092
+ className: "gap-group flex min-w-0 flex-1 items-center",
4093
+ children: [/* @__PURE__ */ jsx("span", {
4094
+ className: "text-text-primary shrink-0 truncate text-sm",
4095
+ children: selected === null || selected === void 0 ? void 0 : selected.label
4096
+ }), selected && /* @__PURE__ */ jsx(ColorPaletteDisplay, {
4097
+ palette: selected,
4098
+ size,
4099
+ className: cn("min-w-16 flex-1", "border border-border"),
4100
+ "aria-label": ""
4101
+ })]
4102
+ })
4103
+ }), /* @__PURE__ */ jsx(SelectPopover$1, { children: palettes.map((palette) => /* @__PURE__ */ jsx(SelectItem$1, {
4104
+ value: palette.id,
4105
+ children: /* @__PURE__ */ jsxs("span", {
4106
+ className: "gap-group flex w-full min-w-40 items-center",
4107
+ children: [/* @__PURE__ */ jsx("span", {
4108
+ className: "shrink-0 truncate text-sm",
4109
+ children: palette.label
4110
+ }), /* @__PURE__ */ jsx(ColorPaletteDisplay, {
4111
+ palette,
4112
+ size,
4113
+ className: "min-w-16 flex-1",
4114
+ "aria-label": ""
4115
+ })]
4116
+ })
4117
+ }, palette.id)) })]
4118
+ })]
4119
+ });
4120
+ }
4121
+ //#endregion
4122
+ //#region src/core/inline-edit.tsx
4123
+ const defaultLabels = {
4124
+ edit: (value) => `Edit ${value}`,
4125
+ save: "Save",
4126
+ cancel: "Cancel",
4127
+ invalid: "That value cannot be saved."
4128
+ };
4129
+ const sizeStyles$1 = {
4130
+ sm: "text-sm",
4131
+ md: "text-base",
4132
+ lg: "text-lg font-semibold",
4133
+ xl: "text-2xl font-semibold tracking-tight"
4134
+ };
4135
+ /**
4136
+ * InlineEdit — renames a thing where the thing is, instead of in a dialog.
4137
+ *
4138
+ * A dashboard title, a metric name, a saved segment: each is read far more
4139
+ * often than it is renamed, so the control has to disappear into the text and
4140
+ * reappear on intent. Read mode is a button carrying the text; edit mode is a
4141
+ * field sized to match it, so the words do not move when the mode does.
4142
+ *
4143
+ * Enter commits, Escape reverts, and a rejected save keeps the field open with
4144
+ * what was typed — losing an edit to a validation error is worse than the
4145
+ * error.
4146
+ *
4147
+ * @example
4148
+ * ```tsx
4149
+ * <InlineEdit as="h2" size="xl" value={dashboard.name} onSave={rename} />
4150
+ * ```
4151
+ */
4152
+ function InlineEdit({ value, onSave, as: Tag = "span", size = "md", placeholder, emptyText = "Untitled", disabled = false, maxLength, suffix, labels, className }) {
4153
+ const text = _objectSpread2(_objectSpread2({}, defaultLabels), labels);
4154
+ const [editing, setEditing] = useState(false);
4155
+ const [draft, setDraft] = useState(value);
4156
+ const [rejected, setRejected] = useState(false);
4157
+ const [saving, setSaving] = useState(false);
4158
+ const inputRef = useRef(null);
4159
+ useEffect(() => {
4160
+ if (!editing) setDraft(value);
4161
+ }, [value, editing]);
4162
+ useEffect(() => {
4163
+ if (editing) {
4164
+ var _inputRef$current, _inputRef$current2;
4165
+ (_inputRef$current = inputRef.current) === null || _inputRef$current === void 0 || _inputRef$current.focus();
4166
+ (_inputRef$current2 = inputRef.current) === null || _inputRef$current2 === void 0 || _inputRef$current2.select();
4167
+ }
4168
+ }, [editing]);
4169
+ const commit = async () => {
4170
+ if (saving) return;
4171
+ const next = draft.trim();
4172
+ if (next === value) {
4173
+ setEditing(false);
4174
+ setRejected(false);
4175
+ return;
4176
+ }
4177
+ setSaving(true);
4178
+ try {
4179
+ if (await onSave(next) === false) {
4180
+ setRejected(true);
4181
+ return;
4182
+ }
4183
+ setRejected(false);
4184
+ setEditing(false);
4185
+ } finally {
4186
+ setSaving(false);
4187
+ }
4188
+ };
4189
+ const cancel = () => {
4190
+ setDraft(value);
4191
+ setRejected(false);
4192
+ setEditing(false);
4193
+ };
4194
+ const handleKeyDown = (event) => {
4195
+ if (event.key === "Enter") {
4196
+ event.preventDefault();
4197
+ commit();
4198
+ } else if (event.key === "Escape") {
4199
+ event.preventDefault();
4200
+ cancel();
4201
+ }
4202
+ };
4203
+ if (editing) return /* @__PURE__ */ jsxs("span", {
4204
+ className: cn("inline-flex min-w-0 items-center gap-element", className),
4205
+ "data-opt-id": "RQ0H2",
4206
+ "data-opt-slug": "inline-edit.InlineEdit",
4207
+ children: [
4208
+ /* @__PURE__ */ jsx("input", {
4209
+ ref: inputRef,
4210
+ value: draft,
4211
+ onChange: (event) => {
4212
+ setDraft(event.target.value);
4213
+ setRejected(false);
4214
+ },
4215
+ onKeyDown: handleKeyDown,
4216
+ onBlur: () => void commit(),
4217
+ placeholder,
4218
+ maxLength,
4219
+ disabled: saving,
4220
+ "aria-invalid": rejected || void 0,
4221
+ "aria-errormessage": rejected ? "inline-edit-error" : void 0,
4222
+ className: cn(OPT_INPUT, sizeStyles$1[size], "min-w-0 flex-1")
4223
+ }),
4224
+ rejected && /* @__PURE__ */ jsx("span", {
4225
+ id: "inline-edit-error",
4226
+ role: "alert",
4227
+ className: "sr-only",
4228
+ children: text.invalid
4229
+ }),
4230
+ /* @__PURE__ */ jsx("button", {
4231
+ type: "button",
4232
+ "aria-label": text.save,
4233
+ disabled: saving,
4234
+ onMouseDown: (event) => event.preventDefault(),
4235
+ onClick: () => void commit(),
4236
+ className: cn("text-success hover:bg-success-subtle rounded-md p-1", OPT_FOCUS_VISIBLE),
4237
+ children: /* @__PURE__ */ jsx(Check, {
4238
+ className: "size-4",
4239
+ "aria-hidden": "true"
4240
+ })
4241
+ }),
4242
+ /* @__PURE__ */ jsx("button", {
4243
+ type: "button",
4244
+ "aria-label": text.cancel,
4245
+ onMouseDown: (event) => event.preventDefault(),
4246
+ onClick: cancel,
4247
+ className: cn("text-text-tertiary hover:bg-bg-subtle rounded-md p-1", OPT_FOCUS_VISIBLE),
4248
+ children: /* @__PURE__ */ jsx(X, {
4249
+ className: "size-4",
4250
+ "aria-hidden": "true"
4251
+ })
4252
+ })
4253
+ ]
4254
+ });
4255
+ return /* @__PURE__ */ jsxs("span", {
4256
+ className: cn("inline-flex min-w-0 items-center gap-element", className),
4257
+ "data-opt-id": "RQ0H2",
4258
+ "data-opt-slug": "inline-edit.InlineEdit",
4259
+ children: [/* @__PURE__ */ jsxs("button", {
4260
+ type: "button",
4261
+ disabled,
4262
+ onClick: () => setEditing(true),
4263
+ "aria-label": text.edit(value || emptyText),
4264
+ className: cn("group -mx-1.5 -my-0.5 inline-flex min-w-0 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),
4265
+ children: [/* @__PURE__ */ jsx(Tag, {
4266
+ className: cn("truncate", sizeStyles$1[size], value ? "text-text-primary" : "text-text-tertiary italic"),
4267
+ children: value || emptyText
4268
+ }), !disabled && /* @__PURE__ */ jsx(Pencil, {
4269
+ "aria-hidden": "true",
4270
+ className: cn("text-text-tertiary size-3.5 shrink-0 opacity-0 transition-opacity", "group-hover:opacity-100 group-focus-visible:opacity-100")
4271
+ })]
4272
+ }), suffix]
4273
+ });
4274
+ }
4275
+ //#endregion
4276
+ //#region src/core/key-pad-menu.tsx
4277
+ const sizeStyles = {
4278
+ sm: "min-h-20 p-2 text-xs [&_[data-keypad-icon]]:size-5",
4279
+ md: "min-h-24 p-3 text-sm [&_[data-keypad-icon]]:size-6"
4280
+ };
4281
+ /**
4282
+ * KeyPadMenu — a choice between things that are easier to recognise than to
4283
+ * name.
4284
+ *
4285
+ * A chart type, a layout, an import source: the picture is the label, and a
4286
+ * dropdown of words makes the reader translate twice. Tiles are real radios or
4287
+ * checkboxes underneath, so arrow keys, screen readers, and form submission
4288
+ * all behave as they would for any other group.
4289
+ *
4290
+ * @example
4291
+ * ```tsx
4292
+ * <KeyPadMenu
4293
+ * label="Chart type"
4294
+ * items={vizTypes}
4295
+ * value={vizType}
4296
+ * onValueChange={setVizType}
4297
+ * />
4298
+ * ```
4299
+ */
4300
+ function KeyPadMenu({ items, label, selection = "single", value, onValueChange, columns = "auto", size = "md", hideLabel = false, className }) {
4301
+ const name = useId();
4302
+ const selected = new Set(value === void 0 ? [] : Array.isArray(value) ? value : [value]);
4303
+ const handleChange = (id) => {
4304
+ if (selection === "multiple") {
4305
+ const next = selected.has(id) ? [...selected].filter((item) => item !== id) : [...selected, id];
4306
+ onValueChange === null || onValueChange === void 0 || onValueChange(next);
4307
+ return;
4308
+ }
4309
+ onValueChange === null || onValueChange === void 0 || onValueChange(id);
4310
+ };
4311
+ return /* @__PURE__ */ jsxs("fieldset", {
4312
+ className: cn("min-w-0", className),
4313
+ "data-opt-id": "ZZG2R",
4314
+ "data-opt-slug": "key-pad-menu.KeyPadMenu",
4315
+ children: [/* @__PURE__ */ jsx("legend", {
4316
+ className: cn("text-text-primary mb-element text-sm font-medium", hideLabel && "sr-only"),
4317
+ children: label
4318
+ }), /* @__PURE__ */ jsx("div", {
4319
+ className: "gap-element grid",
4320
+ style: { gridTemplateColumns: columns === "auto" ? "repeat(auto-fill, minmax(6.5rem, 1fr))" : `repeat(${columns}, minmax(0, 1fr))` },
4321
+ children: items.map((item) => {
4322
+ const isSelected = selected.has(item.id);
4323
+ return /* @__PURE__ */ jsxs("label", {
4324
+ 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"),
4325
+ children: [
4326
+ /* @__PURE__ */ jsx("input", {
4327
+ type: selection === "single" ? "radio" : "checkbox",
4328
+ name: selection === "single" ? name : void 0,
4329
+ value: item.id,
4330
+ checked: isSelected,
4331
+ disabled: item.disabled,
4332
+ onChange: () => handleChange(item.id),
4333
+ className: cn("sr-only", OPT_FOCUS_VISIBLE)
4334
+ }),
4335
+ /* @__PURE__ */ jsx("span", {
4336
+ "data-keypad-icon": "",
4337
+ "aria-hidden": "true",
4338
+ className: cn("flex items-center justify-center", isSelected ? "text-accent" : "text-text-tertiary"),
4339
+ children: item.icon
4340
+ }),
4341
+ /* @__PURE__ */ jsx("span", {
4342
+ className: "font-medium",
4343
+ children: item.label
4344
+ }),
4345
+ item.hint && /* @__PURE__ */ jsx("span", {
4346
+ className: "text-text-tertiary line-clamp-2 text-[11px] leading-tight",
4347
+ children: item.hint
4348
+ }),
4349
+ item.badge && /* @__PURE__ */ jsx("span", {
4350
+ className: "bg-info-subtle text-info absolute top-1 right-1 rounded px-1 text-[10px] font-semibold",
4351
+ children: item.badge
4352
+ })
4353
+ ]
4354
+ }, item.id);
4355
+ })
4356
+ })]
4357
+ });
4358
+ }
4359
+ //#endregion
4360
+ export { Skeleton as $, PageHeader as A, DropdownTrigger as B, Logo as C, Section as D, ResizablePanelGroup as E, RadioGroup as F, formatOptTime as G, formatOptDateTime as H, DropdownContent as I, CircularProgress as J, StepBuilder as K, DropdownItem as L, BlockLayout as M, LoadingOverlay as N, Container as O, Radio as P, Textarea as Q, DropdownMenu as R, DrawerRoot as S, ResizablePanel as T, formatOptNumber as U, ConditionBuilder as V, formatOptShortDate as W, Avatar as X, Progress as Y, AvatarGroup as Z, getShaderFragment as _, ToggleGroup$1 as _t, THEME_PALETTES as a, Card as at, DrawerClose as b, Heading as bt, ExpressionGroup as c, CardFooter as ct, ShaderSurface as d, Breadcrumb as dt, SkeletonAvatar as et, SHADER_OVERLAY_PARAM_KEYS as f, AccordionContent$1 as ft, SHADER_PRESET_IDS as g, Toggle$1 as gt, SHADER_PRESETS as h, AccordionTrigger$1 as ht, ColorPalettePicker as i, Switch as it, createBlock as j, ScrollArea as k, Facet as l, CardHeader as lt, SHADER_PARAM_KEYS as m, AccordionRoot$1 as mt, InlineEdit as n, SkeletonTable as nt, LiveRegion as o, CardContent as ot, SHADER_PARAM_FALLBACK as p, AccordionItem$1 as pt, Meter$1 as q, ColorPaletteDisplay as r, SkeletonText as rt, Expression as s, CardDescription as st, KeyPadMenu as t, SkeletonCard as tt, FacetGroup as u, CardTitle as ut, getShaderPreset as v, OtpField as vt, ResizableHandle as w, DrawerPanel as x, Text as xt, VirtualList as y, Separator$2 as yt, DropdownSeparator as z };
3653
4361
 
3654
- //# sourceMappingURL=shader-surface-BUPZ0P-n.js.map
4362
+ //# sourceMappingURL=key-pad-menu-BQunU20e.js.map