@camstack/ui-library 1.2.341 → 1.2.342

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.2.342] - 2026-10-28
11
+
12
+ ### Changed
13
+
14
+ - Option lists can group options under headings, and a dropdown can be opened and closed from outside
15
+
10
16
  ## [1.2.341] - 2026-10-28
11
17
 
12
18
  ### Changed
@@ -13,6 +13,12 @@ export type OptionSelectOption = {
13
13
  iconId?: string;
14
14
  disabled?: boolean;
15
15
  badge?: string;
16
+ /**
17
+ * Heading this option is listed under. Options of one group are expected
18
+ * together (the list keeps its order); a heading is drawn where the group
19
+ * changes.
20
+ */
21
+ group?: string;
16
22
  };
17
23
  /** Number of options at/above which search is enabled by default (unless creatable). */
18
24
  export declare const OPTION_SELECT_SEARCH_MIN_OPTIONS = 8;
package/dist/index.cjs CHANGED
@@ -12818,8 +12818,14 @@ function useDropdownContext() {
12818
12818
  if (!ctx) throw new Error("Dropdown compound components must be used within <Dropdown>");
12819
12819
  return ctx;
12820
12820
  }
12821
- function Dropdown({ children }) {
12822
- const [open, setOpen] = (0, react$1.useState)(false);
12821
+ function Dropdown({ children, open: controlledOpen, onOpenChange }) {
12822
+ const [innerOpen, setInnerOpen] = (0, react$1.useState)(false);
12823
+ const controlled = controlledOpen !== void 0 && onOpenChange !== void 0;
12824
+ const open = controlled ? controlledOpen : innerOpen;
12825
+ const setOpen = (0, react$1.useCallback)((next) => {
12826
+ if (controlled) onOpenChange(next);
12827
+ else setInnerOpen(next);
12828
+ }, [controlled, onOpenChange]);
12823
12829
  const triggerId = (0, react$1.useId)();
12824
12830
  const contentId = (0, react$1.useId)();
12825
12831
  const triggerRef = (0, react$1.useRef)(null);
@@ -14010,7 +14016,13 @@ function OptionSelect(props) {
14010
14016
  const checked = mode === "single" ? props.value === opt.value : props.value.includes(opt.value);
14011
14017
  const blockedByMax = mode === "multi" && !checked && atMax === true;
14012
14018
  const optionDisabled = opt.disabled === true || blockedByMax;
14013
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
14019
+ const previous = dropdownRows[i - 1];
14020
+ const previousGroup = previous !== void 0 && previous.kind === "option" ? previous.opt.group : void 0;
14021
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react$1.Fragment, { children: [opt.group !== void 0 && opt.group !== previousGroup ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
14022
+ role: "presentation",
14023
+ className: "px-2.5 pb-0.5 pt-1.5 text-[10px] font-medium uppercase tracking-wide text-foreground-subtle",
14024
+ children: opt.group
14025
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
14014
14026
  type: "button",
14015
14027
  role: "option",
14016
14028
  "aria-selected": checked,
@@ -14038,7 +14050,7 @@ function OptionSelect(props) {
14038
14050
  children: opt.badge
14039
14051
  }) : null
14040
14052
  ]
14041
- }, opt.value);
14053
+ })] }, opt.value);
14042
14054
  }) });
14043
14055
  const footerAddNewIndex = dropdownRows.length;
14044
14056
  const fieldShowsChoice = mode === "single" && props.singleDisplay === "field" && selectedOptions.length > 0;
package/dist/index.js CHANGED
@@ -12794,8 +12794,14 @@ function useDropdownContext() {
12794
12794
  if (!ctx) throw new Error("Dropdown compound components must be used within <Dropdown>");
12795
12795
  return ctx;
12796
12796
  }
12797
- function Dropdown({ children }) {
12798
- const [open, setOpen] = useState(false);
12797
+ function Dropdown({ children, open: controlledOpen, onOpenChange }) {
12798
+ const [innerOpen, setInnerOpen] = useState(false);
12799
+ const controlled = controlledOpen !== void 0 && onOpenChange !== void 0;
12800
+ const open = controlled ? controlledOpen : innerOpen;
12801
+ const setOpen = useCallback((next) => {
12802
+ if (controlled) onOpenChange(next);
12803
+ else setInnerOpen(next);
12804
+ }, [controlled, onOpenChange]);
12799
12805
  const triggerId = useId();
12800
12806
  const contentId = useId();
12801
12807
  const triggerRef = useRef(null);
@@ -13986,7 +13992,13 @@ function OptionSelect(props) {
13986
13992
  const checked = mode === "single" ? props.value === opt.value : props.value.includes(opt.value);
13987
13993
  const blockedByMax = mode === "multi" && !checked && atMax === true;
13988
13994
  const optionDisabled = opt.disabled === true || blockedByMax;
13989
- return /* @__PURE__ */ jsxs("button", {
13995
+ const previous = dropdownRows[i - 1];
13996
+ const previousGroup = previous !== void 0 && previous.kind === "option" ? previous.opt.group : void 0;
13997
+ return /* @__PURE__ */ jsxs(Fragment, { children: [opt.group !== void 0 && opt.group !== previousGroup ? /* @__PURE__ */ jsx("div", {
13998
+ role: "presentation",
13999
+ className: "px-2.5 pb-0.5 pt-1.5 text-[10px] font-medium uppercase tracking-wide text-foreground-subtle",
14000
+ children: opt.group
14001
+ }) : null, /* @__PURE__ */ jsxs("button", {
13990
14002
  type: "button",
13991
14003
  role: "option",
13992
14004
  "aria-selected": checked,
@@ -14014,7 +14026,7 @@ function OptionSelect(props) {
14014
14026
  children: opt.badge
14015
14027
  }) : null
14016
14028
  ]
14017
- }, opt.value);
14029
+ })] }, opt.value);
14018
14030
  }) });
14019
14031
  const footerAddNewIndex = dropdownRows.length;
14020
14032
  const fieldShowsChoice = mode === "single" && props.singleDisplay === "field" && selectedOptions.length > 0;
@@ -1,8 +1,16 @@
1
1
  import { type JSX, type ButtonHTMLAttributes, type HTMLAttributes, type ReactNode } from "react";
2
2
  import { type LucideIcon } from "lucide-react";
3
- export declare function Dropdown({ children }: {
4
- children: ReactNode;
5
- }): JSX.Element | null;
3
+ export interface DropdownProps {
4
+ readonly children: ReactNode;
5
+ /**
6
+ * Controlled open state. Give both `open` and `onOpenChange` to drive the menu
7
+ * from outside (a "{" typed in a field opens the variable list); give neither
8
+ * and the dropdown keeps its own state.
9
+ */
10
+ readonly open?: boolean;
11
+ readonly onOpenChange?: (open: boolean) => void;
12
+ }
13
+ export declare function Dropdown({ children, open: controlledOpen, onOpenChange }: DropdownProps): JSX.Element | null;
6
14
  export declare function DropdownTrigger({ children, ...props }: ButtonHTMLAttributes<HTMLButtonElement>): JSX.Element | null;
7
15
  interface DropdownContentProps extends HTMLAttributes<HTMLDivElement> {
8
16
  /** Horizontal alignment relative to the trigger. Default: 'start' (left-aligned). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/ui-library",
3
- "version": "1.2.341",
3
+ "version": "1.2.342",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/camstack/server.git",