hs-uix 2.1.0 → 2.2.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 (49) hide show
  1. package/README.md +3 -1
  2. package/common-components.d.ts +319 -68
  3. package/dist/calendar.js +397 -119
  4. package/dist/calendar.mjs +399 -119
  5. package/dist/common-components.js +3546 -88
  6. package/dist/common-components.mjs +3530 -84
  7. package/dist/datatable.js +108 -18
  8. package/dist/datatable.mjs +108 -18
  9. package/dist/experimental.js +2876 -0
  10. package/dist/experimental.mjs +2883 -0
  11. package/dist/feed.js +267 -38
  12. package/dist/feed.mjs +260 -37
  13. package/dist/filter.js +1379 -0
  14. package/dist/filter.mjs +1334 -0
  15. package/dist/form.js +222 -26
  16. package/dist/form.mjs +227 -27
  17. package/dist/index.js +3255 -353
  18. package/dist/index.mjs +3199 -344
  19. package/dist/kanban.js +282 -62
  20. package/dist/kanban.mjs +273 -61
  21. package/dist/safe.js +9207 -0
  22. package/dist/safe.mjs +9298 -0
  23. package/dist/utils.js +491 -75
  24. package/dist/utils.mjs +491 -75
  25. package/experimental.d.ts +1 -0
  26. package/filter.d.ts +1 -0
  27. package/index.d.ts +45 -3
  28. package/package.json +19 -1
  29. package/safe.d.ts +1 -0
  30. package/src/calendar/README.md +76 -5
  31. package/src/calendar/index.d.ts +108 -1
  32. package/src/common-components/README.md +140 -1
  33. package/src/datatable/README.md +0 -2
  34. package/src/experimental/README.md +126 -0
  35. package/src/experimental/index.d.ts +346 -0
  36. package/src/feed/README.md +69 -0
  37. package/src/feed/index.d.ts +103 -0
  38. package/src/filter/README.md +148 -0
  39. package/src/filter/index.d.ts +221 -0
  40. package/src/form/README.md +132 -4
  41. package/src/form/index.d.ts +82 -1
  42. package/src/kanban/README.md +119 -6
  43. package/src/kanban/index.d.ts +153 -2
  44. package/src/safe/README.md +108 -0
  45. package/src/safe/index.d.ts +158 -0
  46. package/src/utils/README.md +39 -0
  47. package/src/wizard/README.md +158 -0
  48. package/src/wizard/index.d.ts +138 -0
  49. package/utils.d.ts +17 -0
@@ -7,6 +7,7 @@ export type FeedFilterType = "select" | "multiselect" | "dateRange";
7
7
  export type FeedSortDirection = "asc" | "desc" | "ascending" | "descending";
8
8
  export type FeedStatusVariant = "default" | "info" | "success" | "warning" | "danger";
9
9
  export type FeedTagVariant = "default" | "info" | "success" | "warning" | "error";
10
+ export type FeedNewItemsBehavior = "immediate" | "pill";
10
11
 
11
12
  export interface FeedOption<T = string | number | boolean> {
12
13
  label: string;
@@ -74,6 +75,8 @@ export interface FeedItem {
74
75
  avatarSize?: string | number;
75
76
  icon?: ReactNode | string;
76
77
  iconName?: string;
78
+ /** Icon color: native enum (`alert`/`warning`/`success`/`inherit`) or any CSS color (renders via the SVG fallback). */
79
+ iconColor?: string;
77
80
  href?: string | { url: string; external?: boolean };
78
81
  meta?: ReactNode | ReactNode[];
79
82
  metadata?: ReactNode | ReactNode[];
@@ -141,6 +144,10 @@ export interface FeedLabels {
141
144
  errorTitle?: string;
142
145
  errorMessage?: string;
143
146
  itemCount?: (shown: number, total: number, label: string) => string;
147
+ /** "Show N new items" pill label (string or count-aware function). */
148
+ newItems?: string | ((count: number) => string);
149
+ /** Text inside the info Tag marking recently arrived items. */
150
+ newItemTag?: string;
144
151
  }
145
152
 
146
153
  export interface FeedTabOption<T = string | number | boolean> {
@@ -193,6 +200,80 @@ export interface FeedRecordLabel {
193
200
  plural?: string;
194
201
  }
195
202
 
203
+ export interface FeedTypePreset {
204
+ /** Native HubSpot icon name (verified against the native whitelist for the built-in presets). */
205
+ icon?: string;
206
+ /** Icon color: native enum (`alert`/`warning`/`success`/`inherit`) or any CSS color. */
207
+ color?: string;
208
+ /** Display label used as `typeLabel` when the item has none. */
209
+ label?: ReactNode;
210
+ /** StatusTag variant applied when the item has no status variant of its own. */
211
+ statusVariant?: FeedStatusVariant;
212
+ }
213
+
214
+ export type FeedTypePresets = Record<string, FeedTypePreset>;
215
+
216
+ /**
217
+ * Built-in presets for HubSpot's standard activity types (`call`, `email`,
218
+ * `incoming_email`, `forwarded_email`, `meeting`, `note`, `task`, `sms`,
219
+ * `whatsapp`, `linkedin_message`, `postal_mail`, `conversation`).
220
+ */
221
+ export declare const DEFAULT_FEED_TYPE_PRESETS: FeedTypePresets;
222
+
223
+ /** Resolve the preset for a type value (exact, lowercase, then snake_case lookup). */
224
+ export declare function lookupTypePreset(
225
+ type: unknown,
226
+ presets?: FeedTypePresets | null
227
+ ): FeedTypePreset | null;
228
+
229
+ /** Merge a type preset UNDER an item (item-level values win). Returns the same reference when nothing changes. */
230
+ export declare function applyTypePreset<Row = FeedItem>(
231
+ item: Row,
232
+ typePresets?: FeedTypePresets | null
233
+ ): Row;
234
+
235
+ export interface FeedPartitionOptions<Row = FeedItem> {
236
+ /** Ids already rendered — updates to these never buffer. */
237
+ knownIds?: Set<string | number> | null;
238
+ /** Id accessor; defaults to `item.id ?? item.key ?? index`. */
239
+ getId?: (item: Row, index: number) => string | number | undefined;
240
+ }
241
+
242
+ export interface FeedPartitionResult<Row = FeedItem> {
243
+ visible: Row[];
244
+ buffered: Row[];
245
+ visibleIds: Array<string | number | undefined>;
246
+ bufferedIds: Array<string | number | undefined>;
247
+ /** New monotonic watermark over VISIBLE items only (epoch ms), or null. */
248
+ newestTs: number | null;
249
+ }
250
+
251
+ /** Pure live-buffer kernel: split items into visible vs newer-than-watermark buffered. */
252
+ export declare function partitionNewItems<Row = FeedItem>(
253
+ prevNewestTs: number | null | undefined,
254
+ items: Row[] | null | undefined,
255
+ getTs: (item: Row) => unknown,
256
+ options?: FeedPartitionOptions<Row>
257
+ ): FeedPartitionResult<Row>;
258
+
259
+ export interface FeedFlushResult<Row = FeedItem> {
260
+ /** Buffered items first, then the previously visible items. */
261
+ items: Row[];
262
+ flushed: Row[];
263
+ /** New watermark across ALL merged items (epoch ms), or null. */
264
+ newestTs: number | null;
265
+ }
266
+
267
+ /** Pure flush: merge the buffer into the visible list and compute the new watermark. */
268
+ export declare function flushBuffer<Row = FeedItem>(
269
+ visible: Row[] | null | undefined,
270
+ buffered: Row[] | null | undefined,
271
+ getTs: (item: Row) => unknown
272
+ ): FeedFlushResult<Row>;
273
+
274
+ /** Coerce Date | epoch number | parseable string | { year, month, date } to epoch ms (or null). */
275
+ export declare function toTimestampMs(value: unknown): number | null;
276
+
196
277
  export interface FeedProps<Row = FeedItem> {
197
278
  items?: Row[];
198
279
  fields?: FeedField<Row>[];
@@ -274,6 +355,28 @@ export interface FeedProps<Row = FeedItem> {
274
355
  collapsedIds?: (string | number)[];
275
356
  onCollapsedIdsChange?: (next: (string | number)[]) => void;
276
357
  showCollapseToggle?: boolean;
358
+ /**
359
+ * Real-time append behavior for items that arrive NEWER than the
360
+ * previously-newest visible timestamp. "immediate" (default) renders them
361
+ * right away; "pill" holds them in a buffer behind a centered
362
+ * "Show N new items" Button until clicked. Updates to already-visible items
363
+ * (matched by key) are never buffered.
364
+ */
365
+ newItemsBehavior?: FeedNewItemsBehavior;
366
+ /** Called with the released items when the "Show N new items" pill is clicked. */
367
+ onNewItemsFlush?: (items: Row[]) => void;
368
+ /**
369
+ * Window in ms during which flushed/freshly-prepended items carry an info
370
+ * Tag "New" marker. `false` (default) disables the marker. The initial load
371
+ * is never marked.
372
+ */
373
+ highlightNew?: number | false;
374
+ /**
375
+ * Per-type display defaults merged UNDER item values (item wins):
376
+ * `{ [type]: { icon, color, label, statusVariant } }`. Pass `true` to use
377
+ * DEFAULT_FEED_TYPE_PRESETS. Lookup by `item.type` is case-insensitive.
378
+ */
379
+ typePresets?: FeedTypePresets | boolean | null;
277
380
  }
278
381
 
279
382
  export declare function Feed<Row = FeedItem>(props: FeedProps<Row>): ReactNode;
@@ -0,0 +1,148 @@
1
+ # FilterBuilder (hs-uix/filter)
2
+
3
+ The HubSpot list/workflow segment-builder pattern as one component: nested AND/OR groups of property → operator → value rows. If your extension needs "show me deals where amount > 10k AND (stage is X OR stage is Y)", this is the component — stop hand-rolling three Selects in a Flex row with an ad-hoc state shape. The tree it emits converts directly to HubSpot CRM search `filterGroups` via `toCrmSearchFilterGroups`.
4
+
5
+ Renders entirely with native components: `Select` / `MultiSelect` / `Input` / `NumberInput` / `DateInput` rows inside `Flex`, nested groups in `Tile`. Every action is a `Button` carrying HubSpot's segment-builder iconography — add (`+`) for "Add filter" / "Add filter group", a remove (`x`) icon button on each condition row, and copy / trash icon buttons on each group header for clone / delete.
6
+
7
+ ## Quick Start
8
+
9
+ ```jsx
10
+ import { useState } from "react";
11
+ import { FilterBuilder, toCrmSearchFilterGroups, validateTree } from "hs-uix/filter";
12
+
13
+ const PROPERTIES = [
14
+ { name: "dealname", label: "Deal name", type: "string" },
15
+ { name: "amount", label: "Amount", type: "number" },
16
+ { name: "closedate", label: "Close date", type: "date" },
17
+ {
18
+ name: "dealstage",
19
+ label: "Stage",
20
+ type: "enum",
21
+ options: [
22
+ { label: "Appointment", value: "appointmentscheduled" },
23
+ { label: "Qualified", value: "qualifiedtobuy" },
24
+ ],
25
+ },
26
+ { name: "hs_is_closed", label: "Is closed", type: "bool" },
27
+ ];
28
+
29
+ const SegmentEditor = () => {
30
+ const [tree, setTree] = useState();
31
+
32
+ const runSearch = () => {
33
+ if (!tree || !validateTree(tree, PROPERTIES).valid) return;
34
+ const { filterGroups } = toCrmSearchFilterGroups(tree);
35
+ // → hubspot.fetch CRM search body: { filterGroups, properties: [...], ... }
36
+ };
37
+
38
+ return <FilterBuilder properties={PROPERTIES} defaultValue={tree} onChange={setTree} />;
39
+ };
40
+ ```
41
+
42
+ ## The filter tree (public contract)
43
+
44
+ ```js
45
+ {
46
+ type: "group",
47
+ operator: "AND" | "OR",
48
+ filters: [
49
+ { type: "condition", property: "amount", operator: "BETWEEN", value: 1000, highValue: 5000 },
50
+ { type: "group", operator: "OR", filters: [ /* nested */ ] },
51
+ ],
52
+ }
53
+ ```
54
+
55
+ - The root is always a group. An empty root means "no filters".
56
+ - Conditions carry `value` (scalar, or array for `IN` / `NOT_IN`) and `highValue` (only `BETWEEN`). `HAS_PROPERTY` / `NOT_HAS_PROPERTY` carry no value.
57
+ - Paths into the tree are index arrays: `[]` is the root, `[1, 0]` is the root's second child's first child.
58
+
59
+ ## Operators per property type (CRM search names)
60
+
61
+ | Type | Operators | Value editor |
62
+ |---|---|---|
63
+ | `string` | `EQ`, `NEQ`, `CONTAINS_TOKEN`, `NOT_CONTAINS_TOKEN`, `HAS_PROPERTY`, `NOT_HAS_PROPERTY` | `Input` |
64
+ | `number` | `EQ`, `NEQ`, `GT`, `GTE`, `LT`, `LTE`, `BETWEEN`, `HAS_PROPERTY`, `NOT_HAS_PROPERTY` | `NumberInput` (× 2 for `BETWEEN`) |
65
+ | `date` / `datetime` | same as `number`, labeled "is after" / "is before" | `DateInput` (× 2 for `BETWEEN`) |
66
+ | `enum` | `IN`, `NOT_IN`, `HAS_PROPERTY`, `NOT_HAS_PROPERTY` | `MultiSelect` over the property's `options` |
67
+ | `bool` | `EQ` | `Select` (True / False → `"true"` / `"false"`) |
68
+
69
+ `HAS_PROPERTY` / `NOT_HAS_PROPERTY` render no value editor. The native `DateInput` is date-only, so `datetime` properties get day precision in the UI.
70
+
71
+ ## Features
72
+
73
+ - Controlled (`value` + `onChange`) or uncontrolled (`defaultValue`) tree state
74
+ - Nested groups to `maxDepth` (default 2 — root plus one level, matching HubSpot's builder)
75
+ - Per-group AND/OR toggle rendered between rows; changing any separator updates the whole group
76
+ - Nested groups get a numbered heading ("Group 1", "Group 2", …) with clone (copy icon) and delete (trash icon) buttons, matching HubSpot's builder
77
+ - Property changes keep the operator when still valid for the new type, otherwise reset it; values are always cleared
78
+ - Operator changes keep values whose shape still fits (scalar→scalar, `IN`↔`NOT_IN`)
79
+ - Removing a group's last row prunes the now-empty group (root always survives)
80
+ - `readOnly` mode renders the tree without add/remove/edit affordances
81
+ - Every behavioral rule lives in pure, exported, unit-tested helpers (`filterTree.js`)
82
+
83
+ ## `toCrmSearchFilterGroups(tree, options?)`
84
+
85
+ Converts the tree to the HubSpot CRM search shape — `{ filterGroups: [{ filters: [...] }] }`, where filters in a group are ANDed and the groups are ORed.
86
+
87
+ ```js
88
+ toCrmSearchFilterGroups({
89
+ type: "group", operator: "AND", filters: [
90
+ { type: "condition", property: "amount", operator: "GT", value: 1000 },
91
+ { type: "group", operator: "OR", filters: [
92
+ { type: "condition", property: "dealstage", operator: "IN", value: ["a"] },
93
+ { type: "condition", property: "hs_is_closed", operator: "EQ", value: "false" },
94
+ ] },
95
+ ],
96
+ });
97
+ // → { filterGroups: [
98
+ // { filters: [{ propertyName: "amount", operator: "GT", value: 1000 }, { propertyName: "dealstage", operator: "IN", values: ["a"] }] },
99
+ // { filters: [{ propertyName: "amount", operator: "GT", value: 1000 }, { propertyName: "hs_is_closed", operator: "EQ", value: "false" }] },
100
+ // ] }
101
+ ```
102
+
103
+ **Flattening rules.** The tree is expanded to disjunctive normal form, so nesting at ANY depth converts — `A AND (B OR C)` distributes to `[A,B] | [A,C]`, ORs nested under ORs flatten, and `(A OR B) AND (C OR D)` becomes the 4-group cartesian product. The cost is combinatorial: every OR nested under an AND **multiplies** filterGroups and duplicates the sibling conditions into each one.
104
+
105
+ **Limits.** CRM search rejects more than 5 filterGroups, 6 filters per group, or 18 filters total. When the expansion exceeds a limit, this **throws** with a descriptive message instead of sending a request that will 400. Tune via `maxGroups` / `maxFiltersPerGroup` / `maxTotalFilters`, or pass `enforceLimits: false`.
106
+
107
+ **Value coercion** (default on, disable with `coerceValues: false`): `DateInput` value objects (`{ year, month, date }`) become epoch-ms numbers (local midnight, matching the `hs-uix/utils` dateRange helpers); booleans become `"true"` / `"false"` strings. Pre-convert and pass numbers/strings yourself if you need different semantics (e.g. UTC midnight).
108
+
109
+ Empty nested groups throw — run `validateTree` first and gate your search button on `valid`.
110
+
111
+ ## Props
112
+
113
+ ### `<FilterBuilder />`
114
+
115
+ | Prop | Type | Default | Notes |
116
+ |---|---|---|---|
117
+ | `properties` | `FilterProperty[]` | — | Required. `{ name, label?, type?, options? }`; `type` defaults to `"string"`; `enum` needs `options`. |
118
+ | `value` | `FilterGroupNode` | — | Controlled tree. |
119
+ | `defaultValue` | `FilterGroupNode` | empty AND group | Initial tree for uncontrolled mode. |
120
+ | `onChange` | `(tree) => void` | — | Fired with the full new tree after every edit. |
121
+ | `maxDepth` | `number` | `2` | Max group nesting; root counts as 1. `1` disables "Add filter group". |
122
+ | `labels` | `FilterBuilderLabels` | built-in copy | Overrides for `addFilter`, `addGroup`, `remove`, `removeGroup`, `cloneGroup`, `group`, `and`, `or`, `property`, `operator`, `value`, `values`, `between`, `empty`, `true`, `false`. `remove` / `removeGroup` / `cloneGroup` are screen-reader text on the icon buttons; `group` prefixes group headings. |
123
+ | `operatorLabels` | `Record<operator, string>` | — | Per-operator label overrides for the operator dropdowns. |
124
+ | `readOnly` | `boolean` | `false` | Render without edit affordances. |
125
+ | `namePrefix` | `string` | `"filter-builder"` | Prefix for native input `name`s; set when rendering two builders on one surface. |
126
+ | `...rest` | — | — | Spread onto the root `Flex`. |
127
+
128
+ ### Pure helpers (all exported from `hs-uix/filter`)
129
+
130
+ | Export | Signature | Notes |
131
+ |---|---|---|
132
+ | `FILTER_OPERATORS` | `Record<type, operator[]>` | The operator sets per property type. |
133
+ | `getOperatorOptions` | `(type, labelOverrides?) => { label, value }[]` | Select-ready operator options; date types get before/after phrasing. |
134
+ | `operatorExpectsValue` / `operatorExpectsHighValue` / `operatorExpectsValues` | `(operator) => boolean` | Value-arity rules. |
135
+ | `createCondition` | `(property?, operator?, value?, highValue?) => node` | `value`/`highValue` keys only present when provided. |
136
+ | `createGroup` | `(operator?, filters?) => node` | Defaults to an empty AND group. |
137
+ | `isGroupNode` / `isConditionNode` | `(node) => boolean` | Type guards. |
138
+ | `getNodeAtPath` | `(tree, path) => node \| undefined` | Lenient read. |
139
+ | `addFilter` | `(tree, groupPath, node) => tree` | Immutable append; throws if the path isn't a group. |
140
+ | `updateFilter` | `(tree, path, patch \| fn) => tree` | Object patch merges; function patch replaces the node. |
141
+ | `removeFilter` | `(tree, path, { pruneEmptyGroups? }) => tree` | Throws on the root path; prune cascades upward but never removes the root. |
142
+ | `duplicateFilter` | `(tree, path) => tree` | Deep-clones the node at `path` (condition or group) and inserts the copy right after it. Throws on the root path. |
143
+ | `countConditions` | `(node) => number` | Conditions only; groups don't count. |
144
+ | `changeConditionProperty` | `(condition, propertyDef) => condition` | Keeps a still-valid operator, clears values. |
145
+ | `changeConditionOperator` | `(condition, operator) => condition` | Keeps shape-compatible values. |
146
+ | `validateTree` | `(tree, properties?) => { valid, errors: [{ path, message }] }` | Empty root is valid; empty nested groups are not. |
147
+ | `conditionToCrmFilter` | `(condition, { coerceValues? }) => crmFilter` | One condition → `{ propertyName, operator, value/values/highValue }`. |
148
+ | `toCrmSearchFilterGroups` | `(tree, options?) => { filterGroups }` | DNF expansion + limit enforcement (see above). |
@@ -0,0 +1,221 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ export type FilterPropertyType = "string" | "number" | "date" | "datetime" | "enum" | "bool";
4
+
5
+ export type FilterOperator =
6
+ | "EQ"
7
+ | "NEQ"
8
+ | "CONTAINS_TOKEN"
9
+ | "NOT_CONTAINS_TOKEN"
10
+ | "GT"
11
+ | "GTE"
12
+ | "LT"
13
+ | "LTE"
14
+ | "BETWEEN"
15
+ | "IN"
16
+ | "NOT_IN"
17
+ | "HAS_PROPERTY"
18
+ | "NOT_HAS_PROPERTY";
19
+
20
+ export type FilterGroupOperator = "AND" | "OR";
21
+
22
+ /** Path into nested `filters` arrays. `[]` is the root group, `[1, 0]` is the root's second child's first child. */
23
+ export type FilterTreePath = number[];
24
+
25
+ export interface FilterPropertyOption {
26
+ label: string;
27
+ value: string | number | boolean;
28
+ }
29
+
30
+ export interface FilterProperty {
31
+ name: string;
32
+ label?: string;
33
+ /** Defaults to "string". Enum properties need `options`. */
34
+ type?: FilterPropertyType;
35
+ options?: FilterPropertyOption[];
36
+ }
37
+
38
+ export interface FilterConditionNode {
39
+ type: "condition";
40
+ property: string;
41
+ /** "" while the user has not picked an operator yet. */
42
+ operator: FilterOperator | "";
43
+ /** Scalar for most operators; array for IN / NOT_IN; absent for HAS_PROPERTY / NOT_HAS_PROPERTY. */
44
+ value?: unknown;
45
+ /** Upper bound, BETWEEN only. */
46
+ highValue?: unknown;
47
+ }
48
+
49
+ export interface FilterGroupNode {
50
+ type: "group";
51
+ operator: FilterGroupOperator;
52
+ filters: FilterNode[];
53
+ }
54
+
55
+ export type FilterNode = FilterConditionNode | FilterGroupNode;
56
+
57
+ export interface FilterBuilderLabels {
58
+ addFilter?: string;
59
+ addGroup?: string;
60
+ /** Screen-reader text on the condition row's remove (x) icon button. */
61
+ remove?: string;
62
+ /** Screen-reader text on the group header's delete (trash) icon button. */
63
+ removeGroup?: string;
64
+ /** Screen-reader text on the group header's clone (copy) icon button. */
65
+ cloneGroup?: string;
66
+ /** Group heading prefix — rendered as "Group 1", "Group 2", … */
67
+ group?: string;
68
+ and?: string;
69
+ or?: string;
70
+ property?: string;
71
+ operator?: string;
72
+ value?: string;
73
+ values?: string;
74
+ between?: string;
75
+ empty?: string;
76
+ true?: string;
77
+ false?: string;
78
+ }
79
+
80
+ export interface FilterValidationError {
81
+ path: FilterTreePath;
82
+ message: string;
83
+ }
84
+
85
+ export interface FilterValidationResult {
86
+ valid: boolean;
87
+ errors: FilterValidationError[];
88
+ }
89
+
90
+ export interface FilterCrmSearchFilter {
91
+ propertyName: string;
92
+ operator: FilterOperator;
93
+ value?: string | number | boolean;
94
+ highValue?: string | number | boolean;
95
+ values?: Array<string | number | boolean>;
96
+ }
97
+
98
+ export interface FilterCrmSearchFilterGroups {
99
+ filterGroups: Array<{ filters: FilterCrmSearchFilter[] }>;
100
+ }
101
+
102
+ export interface FilterToCrmSearchOptions {
103
+ /** Max filterGroups after DNF expansion (default 5, the CRM search limit). */
104
+ maxGroups?: number;
105
+ /** Max filters per filterGroup (default 6). */
106
+ maxFiltersPerGroup?: number;
107
+ /** Max filters across all groups (default 18). */
108
+ maxTotalFilters?: number;
109
+ /** Set false to skip limit checks (default true). */
110
+ enforceLimits?: boolean;
111
+ /** Set false to pass values through without DateInput→epoch-ms / boolean→string coercion (default true). */
112
+ coerceValues?: boolean;
113
+ }
114
+
115
+ export interface FilterConditionToCrmOptions {
116
+ coerceValues?: boolean;
117
+ }
118
+
119
+ export interface FilterRemoveOptions {
120
+ /** Also remove ancestor groups left empty by the removal (root is never pruned). */
121
+ pruneEmptyGroups?: boolean;
122
+ }
123
+
124
+ export interface FilterBuilderProps {
125
+ properties: FilterProperty[];
126
+ /** Controlled tree value. */
127
+ value?: FilterGroupNode | null;
128
+ /** Initial tree for uncontrolled mode. */
129
+ defaultValue?: FilterGroupNode | null;
130
+ onChange?: (tree: FilterGroupNode) => void;
131
+ /** Max group nesting depth; root counts as 1 (default 2). */
132
+ maxDepth?: number;
133
+ labels?: FilterBuilderLabels;
134
+ /** Per-operator label overrides for the operator dropdowns. */
135
+ operatorLabels?: Partial<Record<FilterOperator, string>>;
136
+ readOnly?: boolean;
137
+ /** Prefix for native input `name`s; set when rendering two builders on one surface. */
138
+ namePrefix?: string;
139
+ /** Remaining props are spread onto the root Flex. */
140
+ [key: string]: unknown;
141
+ }
142
+
143
+ export declare const FILTER_OPERATORS: Record<FilterPropertyType, FilterOperator[]>;
144
+
145
+ export declare function getOperatorOptions(
146
+ type?: FilterPropertyType | string,
147
+ labelOverrides?: Partial<Record<FilterOperator, string>>
148
+ ): Array<{ label: string; value: FilterOperator }>;
149
+
150
+ export declare function operatorExpectsValue(operator?: string): boolean;
151
+ export declare function operatorExpectsHighValue(operator?: string): boolean;
152
+ export declare function operatorExpectsValues(operator?: string): boolean;
153
+
154
+ export declare function isGroupNode(node: unknown): node is FilterGroupNode;
155
+ export declare function isConditionNode(node: unknown): node is FilterConditionNode;
156
+
157
+ export declare function createCondition(
158
+ property?: string,
159
+ operator?: FilterOperator | "",
160
+ value?: unknown,
161
+ highValue?: unknown
162
+ ): FilterConditionNode;
163
+
164
+ export declare function createGroup(
165
+ operator?: FilterGroupOperator,
166
+ filters?: FilterNode[]
167
+ ): FilterGroupNode;
168
+
169
+ export declare function getNodeAtPath(tree: FilterNode, path: FilterTreePath): FilterNode | undefined;
170
+
171
+ export declare function addFilter(
172
+ tree: FilterGroupNode,
173
+ path: FilterTreePath,
174
+ node: FilterNode
175
+ ): FilterGroupNode;
176
+
177
+ export declare function updateFilter(
178
+ tree: FilterGroupNode,
179
+ path: FilterTreePath,
180
+ patch: Partial<FilterConditionNode> | Partial<FilterGroupNode> | ((node: FilterNode) => FilterNode)
181
+ ): FilterGroupNode;
182
+
183
+ export declare function removeFilter(
184
+ tree: FilterGroupNode,
185
+ path: FilterTreePath,
186
+ options?: FilterRemoveOptions
187
+ ): FilterGroupNode;
188
+
189
+ export declare function duplicateFilter(
190
+ tree: FilterGroupNode,
191
+ path: FilterTreePath
192
+ ): FilterGroupNode;
193
+
194
+ export declare function countConditions(node: FilterNode): number;
195
+
196
+ export declare function changeConditionProperty(
197
+ condition: FilterConditionNode,
198
+ property: FilterProperty | string
199
+ ): FilterConditionNode;
200
+
201
+ export declare function changeConditionOperator(
202
+ condition: FilterConditionNode,
203
+ operator: FilterOperator | ""
204
+ ): FilterConditionNode;
205
+
206
+ export declare function validateTree(
207
+ tree: FilterNode,
208
+ properties?: FilterProperty[]
209
+ ): FilterValidationResult;
210
+
211
+ export declare function conditionToCrmFilter(
212
+ condition: FilterConditionNode,
213
+ options?: FilterConditionToCrmOptions
214
+ ): FilterCrmSearchFilter;
215
+
216
+ export declare function toCrmSearchFilterGroups(
217
+ tree: FilterGroupNode,
218
+ options?: FilterToCrmSearchOptions
219
+ ): FilterCrmSearchFilterGroups;
220
+
221
+ export declare function FilterBuilder(props: FilterBuilderProps): ReactNode;