@l3mpire/ui 2.21.1 → 2.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/USAGE.md CHANGED
@@ -963,6 +963,16 @@ import {
963
963
 
964
964
  **Advanced filter shortcut:** the PropertySelector footer shows an "Advanced filter · N rule(s)" item. Clicking it closes the property selector and opens the advanced popover directly in its initial empty state — where the user picks the first property inline via a "Where | [Select property ▾]" draft row. When no advanced filters exist yet, clicking this shortcut makes the AdvancedChip appear with the popover already open; closing without adding a filter removes the chip automatically.
965
965
 
966
+ **Pinned groups (hoist a group to root):** mark a property group as "primary" so its properties render flat at the top of the `PropertySelector` popover — no category click needed. Set `groupPinned: true` on any property and the entire group is hoisted; remaining groups stay nested as categories below. Search works across everything (pinned + nested). The popover has a max-height and the "Advanced filter" footer stays sticky at the bottom when the list scrolls. The same flattening is applied in `AdvancedRow`'s property-swap popover and in `InteractiveFilterChip`'s swap popover.
967
+
968
+ ```tsx
969
+ { id: "status", label: "Status", type: "enum", icon: faCircleOutline, group: "task", groupLabel: "Task", groupPinned: true, ... },
970
+ { id: "priority", label: "Priority", type: "enum", icon: faFlagOutline, group: "task", groupLabel: "Task", groupPinned: true, ... },
971
+ { id: "due_date", label: "Due date", type: "date", icon: faCalendarOutline, group: "task", groupLabel: "Task", groupPinned: true, ... },
972
+ // Other groups stay nested — click "Contact" to see its properties.
973
+ { id: "contact_name", label: "Contact name", type: "text", icon: faUserOutline, group: "contact", groupLabel: "Contact", ... },
974
+ ```
975
+
966
976
  **Dynamic options ("Me", "Unassigned", …):** enum/tags/relation properties accept a `dynamicOptions` array. Each entry is `{ value, label, description?, icon? }` and is rendered at the top of the SingleSelect / MultiSelect dropdown with a divider separating it from the regular options. The `value` is a sentinel string stored on `FilterCondition.value` — the DS only renders, the consuming app resolves it at query time (e.g. `"__me__"` → `currentUser.id`). This keeps session/business logic out of the DS while still getting a consistent visual treatment.
967
977
 
968
978
  ```tsx
package/dist/index.d.mts CHANGED
@@ -733,6 +733,13 @@ interface PropertyDefinition {
733
733
  icon: _l3mpire_icons.IconDefinition;
734
734
  group: string;
735
735
  groupLabel: string;
736
+ /**
737
+ * When true, this property's entire group is hoisted to the root of the
738
+ * PropertySelector popover (no category click needed). All properties in the
739
+ * same group share this behavior — setting it on any one property pins the
740
+ * whole group. Useful for "primary" groups that hold the most-used filters.
741
+ */
742
+ groupPinned?: boolean;
736
743
  options?: string[];
737
744
  /**
738
745
  * Dynamic/smart options rendered at the top of the value selector with a
package/dist/index.d.ts CHANGED
@@ -733,6 +733,13 @@ interface PropertyDefinition {
733
733
  icon: _l3mpire_icons.IconDefinition;
734
734
  group: string;
735
735
  groupLabel: string;
736
+ /**
737
+ * When true, this property's entire group is hoisted to the root of the
738
+ * PropertySelector popover (no category click needed). All properties in the
739
+ * same group share this behavior — setting it on any one property pins the
740
+ * whole group. Useful for "primary" groups that hold the most-used filters.
741
+ */
742
+ groupPinned?: boolean;
736
743
  options?: string[];
737
744
  /**
738
745
  * Dynamic/smart options rendered at the top of the value selector with a
package/dist/index.js CHANGED
@@ -6932,7 +6932,7 @@ var React44 = __toESM(require("react"));
6932
6932
  var PopoverPrimitive6 = __toESM(require("@radix-ui/react-popover"));
6933
6933
  var import_icons29 = require("@l3mpire/icons");
6934
6934
  var import_jsx_runtime50 = require("react/jsx-runtime");
6935
- var AdvancedFilterFooter = ({ onClick, count }) => /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(import_jsx_runtime50.Fragment, { children: [
6935
+ var AdvancedFilterFooter = ({ onClick, count }) => /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("div", { className: "shrink-0 flex flex-col", children: [
6936
6936
  /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("div", { className: "h-px bg-dropdown-border mx-xs" }),
6937
6937
  /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(
6938
6938
  "button",
@@ -6983,9 +6983,21 @@ var PropertySelector = ({
6983
6983
  setSearch("");
6984
6984
  }
6985
6985
  }, [open]);
6986
+ const pinnedGroupIds = React44.useMemo(() => {
6987
+ const set = /* @__PURE__ */ new Set();
6988
+ for (const p of properties) {
6989
+ if (p.groupPinned) set.add(p.group);
6990
+ }
6991
+ return set;
6992
+ }, [properties]);
6993
+ const pinnedProperties = React44.useMemo(
6994
+ () => properties.filter((p) => pinnedGroupIds.has(p.group)),
6995
+ [properties, pinnedGroupIds]
6996
+ );
6986
6997
  const groups = React44.useMemo(() => {
6987
6998
  const map = /* @__PURE__ */ new Map();
6988
6999
  for (const prop of properties) {
7000
+ if (pinnedGroupIds.has(prop.group)) continue;
6989
7001
  const existing = map.get(prop.group);
6990
7002
  if (existing) {
6991
7003
  existing.count++;
@@ -6999,12 +7011,14 @@ var PropertySelector = ({
6999
7011
  }
7000
7012
  }
7001
7013
  return Array.from(map.values());
7002
- }, [properties]);
7003
- const globalSearchResults = React44.useMemo(() => {
7004
- if (!search || activeGroup) return [];
7014
+ }, [properties, pinnedGroupIds]);
7015
+ const filteredPinnedProperties = React44.useMemo(() => {
7016
+ if (!search || activeGroup) return pinnedProperties;
7005
7017
  const lower = search.toLowerCase();
7006
- return properties.filter((p) => p.label.toLowerCase().includes(lower));
7007
- }, [properties, search, activeGroup]);
7018
+ return pinnedProperties.filter(
7019
+ (p) => p.label.toLowerCase().includes(lower)
7020
+ );
7021
+ }, [pinnedProperties, search, activeGroup]);
7008
7022
  const filteredGroups = React44.useMemo(() => {
7009
7023
  if (!search || activeGroup) return groups;
7010
7024
  const lower = search.toLowerCase();
@@ -7022,7 +7036,13 @@ var PropertySelector = ({
7022
7036
  return groupProps.filter((p) => p.label.toLowerCase().includes(lower));
7023
7037
  }, [properties, activeGroup, search]);
7024
7038
  const activeGroupInfo = groups.find((g) => g.group === activeGroup);
7025
- const showGlobalResults = search.length > 0 && !activeGroup && globalSearchResults.length > 0;
7039
+ const nonPinnedSearchResults = React44.useMemo(() => {
7040
+ if (!search || activeGroup) return [];
7041
+ const lower = search.toLowerCase();
7042
+ return properties.filter(
7043
+ (p) => !pinnedGroupIds.has(p.group) && p.label.toLowerCase().includes(lower)
7044
+ );
7045
+ }, [properties, search, activeGroup, pinnedGroupIds]);
7026
7046
  return /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(PopoverPrimitive6.Root, { open, onOpenChange, children: [
7027
7047
  /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(PopoverPrimitive6.Trigger, { asChild: true, children }),
7028
7048
  /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(PopoverPrimitive6.Portal, { children: /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(
@@ -7032,18 +7052,18 @@ var PropertySelector = ({
7032
7052
  align: "start",
7033
7053
  onCloseAutoFocus: (e) => e.preventDefault(),
7034
7054
  className: cn(
7035
- "z-50 flex flex-col gap-xs overflow-clip p-xs",
7055
+ "z-50 flex flex-col gap-xs overflow-hidden p-xs",
7036
7056
  "bg-dropdown-bg border border-dropdown-border rounded-md shadow-lg",
7037
7057
  "data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
7038
7058
  "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
7039
7059
  "data-[side=bottom]:slide-in-from-top-2",
7040
- "min-w-[230px]"
7060
+ "min-w-[230px] max-h-[360px]"
7041
7061
  ),
7042
7062
  children: [
7043
7063
  activeGroup === null ? (
7044
- /* ── Level 1: Search + Categories ───────────────────────── */
7045
- /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("div", { className: "flex flex-col gap-xs", children: [
7046
- /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("div", { className: "flex items-center gap-base px-md py-base border border-input rounded-md", children: [
7064
+ /* ── Level 1: Search + (pinned props) + Categories ──────── */
7065
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("div", { className: "flex flex-col gap-xs flex-1 min-h-0", children: [
7066
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("div", { className: "shrink-0 flex items-center gap-base px-md py-base border border-input rounded-md", children: [
7047
7067
  /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
7048
7068
  import_icons29.Icon,
7049
7069
  {
@@ -7064,9 +7084,8 @@ var PropertySelector = ({
7064
7084
  }
7065
7085
  )
7066
7086
  ] }),
7067
- showGlobalResults ? (
7068
- /* ── Global search results (flat property list) ─────── */
7069
- /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("div", { className: "flex flex-col max-h-[300px] overflow-y-auto", children: globalSearchResults.map((prop) => /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(
7087
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("div", { className: "flex flex-col flex-1 min-h-0 overflow-y-auto", children: [
7088
+ filteredPinnedProperties.map((prop) => /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(
7070
7089
  "button",
7071
7090
  {
7072
7091
  type: "button",
@@ -7084,15 +7103,39 @@ var PropertySelector = ({
7084
7103
  className: "shrink-0 text-dropdown-item-icon"
7085
7104
  }
7086
7105
  ),
7087
- /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("span", { className: "flex-1 text-sm font-regular leading-sm text-dropdown-item-text text-left truncate", children: prop.label }),
7088
- /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("span", { className: "text-xs font-regular leading-xs text-muted-foreground", children: prop.groupLabel })
7106
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("span", { className: "flex-1 text-sm font-regular leading-sm text-dropdown-item-text text-left truncate", children: prop.label })
7089
7107
  ]
7090
7108
  },
7091
7109
  prop.id
7092
- )) })
7093
- ) : (
7094
- /* ── Group list ─────────────────────────────────────── */
7095
- /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("div", { className: "flex flex-col", children: [
7110
+ )),
7111
+ search ? (
7112
+ /* ── Flat matches across non-pinned groups ────────── */
7113
+ nonPinnedSearchResults.map((prop) => /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(
7114
+ "button",
7115
+ {
7116
+ type: "button",
7117
+ onClick: () => {
7118
+ onSelect(prop);
7119
+ onOpenChange?.(false);
7120
+ },
7121
+ className: "flex items-center gap-base p-base rounded-base cursor-pointer transition-colors hover:bg-dropdown-item-hover",
7122
+ children: [
7123
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
7124
+ import_icons29.Icon,
7125
+ {
7126
+ icon: prop.icon,
7127
+ size: "sm",
7128
+ className: "shrink-0 text-dropdown-item-icon"
7129
+ }
7130
+ ),
7131
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("span", { className: "flex-1 text-sm font-regular leading-sm text-dropdown-item-text text-left truncate", children: prop.label }),
7132
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("span", { className: "text-xs font-regular leading-xs text-muted-foreground", children: prop.groupLabel })
7133
+ ]
7134
+ },
7135
+ prop.id
7136
+ ))
7137
+ ) : (
7138
+ /* ── Category list ────────────────────────────────── */
7096
7139
  filteredGroups.map((g) => /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(
7097
7140
  "button",
7098
7141
  {
@@ -7124,14 +7167,14 @@ var PropertySelector = ({
7124
7167
  ]
7125
7168
  },
7126
7169
  g.group
7127
- )),
7128
- filteredGroups.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("span", { className: "p-base text-sm text-muted-foreground", children: "No results" })
7129
- ] })
7130
- )
7170
+ ))
7171
+ ),
7172
+ filteredPinnedProperties.length === 0 && (search ? nonPinnedSearchResults.length === 0 : filteredGroups.length === 0) && /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("span", { className: "p-base text-sm text-muted-foreground", children: "No results" })
7173
+ ] })
7131
7174
  ] })
7132
7175
  ) : (
7133
7176
  /* ── Level 2: Properties ─────────────────────────────────── */
7134
- /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("div", { className: "flex flex-col gap-xs", children: [
7177
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("div", { className: "flex flex-col gap-xs flex-1 min-h-0", children: [
7135
7178
  /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(
7136
7179
  "button",
7137
7180
  {
@@ -7140,7 +7183,7 @@ var PropertySelector = ({
7140
7183
  setActiveGroup(null);
7141
7184
  setSearch("");
7142
7185
  },
7143
- className: "flex items-center gap-base p-base rounded-base cursor-pointer transition-colors hover:bg-dropdown-item-hover",
7186
+ className: "shrink-0 flex items-center gap-base p-base rounded-base cursor-pointer transition-colors hover:bg-dropdown-item-hover",
7144
7187
  children: [
7145
7188
  /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
7146
7189
  import_icons29.Icon,
@@ -7154,7 +7197,7 @@ var PropertySelector = ({
7154
7197
  ]
7155
7198
  }
7156
7199
  ),
7157
- /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("div", { className: "flex items-center gap-base px-md py-base border border-input rounded-md", children: [
7200
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("div", { className: "shrink-0 flex items-center gap-base px-md py-base border border-input rounded-md", children: [
7158
7201
  /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
7159
7202
  import_icons29.Icon,
7160
7203
  {
@@ -7175,7 +7218,7 @@ var PropertySelector = ({
7175
7218
  }
7176
7219
  )
7177
7220
  ] }),
7178
- /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("div", { className: "flex flex-col max-h-[300px] overflow-y-auto", children: [
7221
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("div", { className: "flex flex-col flex-1 min-h-0 overflow-y-auto", children: [
7179
7222
  filteredProperties.map((prop) => /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(
7180
7223
  "button",
7181
7224
  {
@@ -7830,6 +7873,19 @@ var AdvancedRow = ({
7830
7873
  const [operatorOpen, setOperatorOpen] = React49.useState(false);
7831
7874
  const [propertyOpen, setPropertyOpen] = React49.useState(false);
7832
7875
  const [valueOpen, setValueOpen] = React49.useState(false);
7876
+ const { pinnedProperties, unpinnedProperties } = React49.useMemo(() => {
7877
+ const pinnedGroups = /* @__PURE__ */ new Set();
7878
+ for (const p of properties) {
7879
+ if (p.groupPinned) pinnedGroups.add(p.group);
7880
+ }
7881
+ const pinned = [];
7882
+ const unpinned = [];
7883
+ for (const p of properties) {
7884
+ if (pinnedGroups.has(p.group)) pinned.push(p);
7885
+ else unpinned.push(p);
7886
+ }
7887
+ return { pinnedProperties: pinned, unpinnedProperties: unpinned };
7888
+ }, [properties]);
7833
7889
  const handleOperatorSelect = (op) => {
7834
7890
  if (isNoValueOperator(op)) {
7835
7891
  onUpdate({ ...condition, operator: op, value: null });
@@ -7866,7 +7922,7 @@ var AdvancedRow = ({
7866
7922
  /* @__PURE__ */ (0, import_jsx_runtime56.jsx)("span", { className: "text-sm font-regular leading-sm text-foreground whitespace-nowrap truncate", children: propertyDef.label }),
7867
7923
  /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(import_icons34.Icon, { icon: import_icons34.faChevronDownOutline, size: "xs", className: "shrink-0 text-foreground" })
7868
7924
  ] }) }),
7869
- /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(PopoverPrimitive11.Portal, { children: /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(
7925
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(PopoverPrimitive11.Portal, { children: /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)(
7870
7926
  PopoverPrimitive11.Content,
7871
7927
  {
7872
7928
  sideOffset: 4,
@@ -7878,26 +7934,49 @@ var AdvancedRow = ({
7878
7934
  "data-[state=closed]:animate-out data-[state=closed]:fade-out-0",
7879
7935
  "min-w-[200px]"
7880
7936
  ),
7881
- children: properties.map((p) => /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)(
7882
- "button",
7883
- {
7884
- type: "button",
7885
- onClick: () => {
7886
- onPropertyChange(p);
7887
- setPropertyOpen(false);
7937
+ children: [
7938
+ pinnedProperties.map((p) => /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)(
7939
+ "button",
7940
+ {
7941
+ type: "button",
7942
+ onClick: () => {
7943
+ onPropertyChange(p);
7944
+ setPropertyOpen(false);
7945
+ },
7946
+ className: cn(
7947
+ "flex items-center gap-base p-base rounded-base cursor-pointer transition-colors text-left",
7948
+ "hover:bg-dropdown-item-hover",
7949
+ p.id === condition.propertyId && "bg-dropdown-item-hover"
7950
+ ),
7951
+ children: [
7952
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(import_icons34.Icon, { icon: p.icon, size: "sm", className: "shrink-0 text-dropdown-item-icon" }),
7953
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)("span", { className: "text-sm font-regular leading-sm text-dropdown-item-text truncate", children: p.label })
7954
+ ]
7888
7955
  },
7889
- className: cn(
7890
- "flex items-center gap-base p-base rounded-base cursor-pointer transition-colors text-left",
7891
- "hover:bg-dropdown-item-hover",
7892
- p.id === condition.propertyId && "bg-dropdown-item-hover"
7893
- ),
7894
- children: [
7895
- /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(import_icons34.Icon, { icon: p.icon, size: "sm", className: "shrink-0 text-dropdown-item-icon" }),
7896
- /* @__PURE__ */ (0, import_jsx_runtime56.jsx)("span", { className: "text-sm font-regular leading-sm text-dropdown-item-text truncate", children: p.label })
7897
- ]
7898
- },
7899
- p.id
7900
- ))
7956
+ p.id
7957
+ )),
7958
+ unpinnedProperties.map((p) => /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)(
7959
+ "button",
7960
+ {
7961
+ type: "button",
7962
+ onClick: () => {
7963
+ onPropertyChange(p);
7964
+ setPropertyOpen(false);
7965
+ },
7966
+ className: cn(
7967
+ "flex items-center gap-base p-base rounded-base cursor-pointer transition-colors text-left",
7968
+ "hover:bg-dropdown-item-hover",
7969
+ p.id === condition.propertyId && "bg-dropdown-item-hover"
7970
+ ),
7971
+ children: [
7972
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(import_icons34.Icon, { icon: p.icon, size: "sm", className: "shrink-0 text-dropdown-item-icon" }),
7973
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)("span", { className: "text-sm font-regular leading-sm text-dropdown-item-text truncate", children: p.label }),
7974
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)("span", { className: "ml-auto text-xs font-regular leading-xs text-muted-foreground", children: p.groupLabel })
7975
+ ]
7976
+ },
7977
+ p.id
7978
+ ))
7979
+ ]
7901
7980
  }
7902
7981
  ) })
7903
7982
  ] }),