react-conditional-ui 1.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.
package/dist/index.js ADDED
@@ -0,0 +1,1802 @@
1
+ // src/components/Input.tsx
2
+ import { useCallback as useCallback3, useState as useState2, useRef, useLayoutEffect, forwardRef } from "react";
3
+ import TextField from "@mui/material/TextField";
4
+ import InputAdornment from "@mui/material/InputAdornment";
5
+ import IconButton from "@mui/material/IconButton";
6
+ import KeyboardReturnIcon from "@mui/icons-material/KeyboardReturn";
7
+ import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
8
+ import Tooltip from "@mui/material/Tooltip";
9
+
10
+ // src/hooks/useConditionalInput.ts
11
+ import { useState, useCallback as useCallback2 } from "react";
12
+
13
+ // src/condition-structure/Field.ts
14
+ var Field = class _Field {
15
+ constructor(raw, value, label) {
16
+ this.raw = raw;
17
+ this.value = value;
18
+ this.label = label;
19
+ this.isValid = value !== "";
20
+ }
21
+ static invalid(raw) {
22
+ return new _Field(raw, "", raw);
23
+ }
24
+ };
25
+
26
+ // src/condition-structure/Operator.ts
27
+ var Operator = class _Operator {
28
+ constructor(raw, value, label) {
29
+ this.raw = raw;
30
+ this.value = value;
31
+ this.label = label;
32
+ this.isValid = value !== "";
33
+ }
34
+ static invalid(raw) {
35
+ return new _Operator(raw, "", raw);
36
+ }
37
+ };
38
+
39
+ // src/condition-structure/Value.ts
40
+ var Value = class _Value {
41
+ constructor(raw, opts = { isValid: raw.length > 0 }) {
42
+ this.raw = raw;
43
+ this.isValid = opts.isValid;
44
+ this.errorMessage = opts.errorMessage ?? null;
45
+ this.matchedOption = opts.matchedOption ?? null;
46
+ this.value = this.matchedOption?.value ?? raw;
47
+ this.label = this.matchedOption?.label ?? raw;
48
+ }
49
+ static valid(raw, matchedOption) {
50
+ return new _Value(raw, { isValid: true, matchedOption });
51
+ }
52
+ static invalid(raw, errorMessage) {
53
+ return new _Value(raw, { isValid: false, errorMessage });
54
+ }
55
+ static empty() {
56
+ return new _Value("", { isValid: false, errorMessage: "Missing value" });
57
+ }
58
+ };
59
+
60
+ // src/fuzzy/operators.ts
61
+ var DEFAULT_OPERATORS = [
62
+ {
63
+ label: "equals",
64
+ value: "eq",
65
+ aliases: ["equals", "equal", "equal to", "is equal to", "eq", "=", "==", "is"]
66
+ },
67
+ {
68
+ label: "not equals",
69
+ value: "ne",
70
+ aliases: [
71
+ "not equals",
72
+ "not equal",
73
+ "not equal to",
74
+ "is not equal to",
75
+ "ne",
76
+ "!=",
77
+ "is not",
78
+ "isn't",
79
+ "isnt",
80
+ "doesn't equal",
81
+ "does not equal"
82
+ ]
83
+ },
84
+ {
85
+ label: "greater than",
86
+ value: "gt",
87
+ aliases: [
88
+ "greater than",
89
+ "gt",
90
+ ">",
91
+ "more than",
92
+ "bigger than",
93
+ "above",
94
+ "exceeds",
95
+ "higher than"
96
+ ]
97
+ },
98
+ {
99
+ label: "greater than or equal to",
100
+ value: "gte",
101
+ aliases: [
102
+ "greater than or equal to",
103
+ "greater than or equal",
104
+ "gte",
105
+ ">=",
106
+ "at least",
107
+ "no less than",
108
+ "not less than",
109
+ "not smaller than",
110
+ "not below",
111
+ "not under"
112
+ ]
113
+ },
114
+ {
115
+ label: "less than",
116
+ value: "lt",
117
+ aliases: [
118
+ "less than",
119
+ "lt",
120
+ "<",
121
+ "fewer than",
122
+ "smaller than",
123
+ "below",
124
+ "under",
125
+ "lower than"
126
+ ]
127
+ },
128
+ {
129
+ label: "less than or equal to",
130
+ value: "lte",
131
+ aliases: [
132
+ "less than or equal to",
133
+ "less than or equal",
134
+ "lte",
135
+ "<=",
136
+ "at most",
137
+ "no more than",
138
+ "not greater than",
139
+ "not bigger than",
140
+ "not above",
141
+ "not more than"
142
+ ]
143
+ },
144
+ {
145
+ label: "contains",
146
+ value: "contains",
147
+ aliases: ["contains", "includes", "has", "not missing"]
148
+ }
149
+ ];
150
+
151
+ // src/hooks/useConditionDataProvider.ts
152
+ import { useMemo, useCallback } from "react";
153
+
154
+ // src/fuzzy/match-engine.ts
155
+ import Fuse from "fuse.js";
156
+
157
+ // src/logger.ts
158
+ import createDebug from "debug";
159
+ var BASE = "react-conditional-ui";
160
+ var createLogger = (namespace) => createDebug(`${BASE}:${namespace}`);
161
+
162
+ // src/fuzzy/match-engine.ts
163
+ var log = createLogger("match-engine");
164
+ var valueLog = createLogger("value");
165
+ var TYPE_ALLOWED_OPS = {
166
+ number: /* @__PURE__ */ new Set(["eq", "ne", "gt", "lt", "gte", "lte"]),
167
+ enum: /* @__PURE__ */ new Set(["eq", "ne"]),
168
+ text: /* @__PURE__ */ new Set(["eq", "ne", "contains", "starts_with"])
169
+ };
170
+ var MatchEngine = class _MatchEngine {
171
+ constructor(fields, operators) {
172
+ this.fields = fields;
173
+ this.operators = operators;
174
+ this.fieldFuse = new Fuse(fields, {
175
+ keys: ["label"],
176
+ threshold: 0.4,
177
+ includeScore: true
178
+ });
179
+ const flatAliases = operators.flatMap(
180
+ (op) => op.aliases.map((alias) => ({ alias: alias.toLowerCase(), operator: op }))
181
+ );
182
+ this.opFuse = new Fuse(flatAliases, {
183
+ keys: ["alias"],
184
+ threshold: 0.4,
185
+ includeScore: true
186
+ });
187
+ this.perFieldOpFuse = /* @__PURE__ */ new Map();
188
+ for (const field of fields) {
189
+ const restricted = this.resolveOpsForField(field, operators);
190
+ if (restricted !== operators) {
191
+ const aliases = restricted.flatMap(
192
+ (op) => op.aliases.map((alias) => ({ alias: alias.toLowerCase(), operator: op }))
193
+ );
194
+ this.perFieldOpFuse.set(
195
+ field.value,
196
+ new Fuse(aliases, { keys: ["alias"], threshold: 0.4, includeScore: true })
197
+ );
198
+ }
199
+ }
200
+ }
201
+ matchField(candidate) {
202
+ const results = this.fieldFuse.search(candidate);
203
+ if (results.length > 0 && (results[0].score ?? 1) <= 0.4) {
204
+ log(
205
+ "field match: %s -> %s (score: %f)",
206
+ candidate,
207
+ results[0].item.value,
208
+ results[0].score
209
+ );
210
+ return { option: results[0].item, score: results[0].score ?? 1 };
211
+ }
212
+ return null;
213
+ }
214
+ matchOperator(candidate, field) {
215
+ const fuse = (field && this.perFieldOpFuse.get(field.value)) ?? this.opFuse;
216
+ const results = fuse.search(candidate);
217
+ if (results.length > 0 && (results[0].score ?? 1) <= 0.4) {
218
+ const alias = results[0].item.alias;
219
+ const candidateWords = candidate.split(/\s+/).length;
220
+ const aliasWords = alias.split(/\s+/).length;
221
+ if (candidateWords !== aliasWords) return null;
222
+ if (candidate.length < alias.length * 0.5) return null;
223
+ log(
224
+ "operator match: %s -> %s (score: %f)",
225
+ candidate,
226
+ results[0].item.operator.value,
227
+ results[0].score
228
+ );
229
+ return { option: results[0].item.operator, score: results[0].score ?? 1 };
230
+ }
231
+ return null;
232
+ }
233
+ static matchValue(raw, fieldConfig) {
234
+ const knownValues = fieldConfig?.fieldValues;
235
+ const validateValue = fieldConfig?.validateValue;
236
+ const fieldType = fieldConfig?.type;
237
+ if (knownValues && knownValues.length > 0) {
238
+ return _MatchEngine.matchKnownValue(raw, knownValues, validateValue);
239
+ }
240
+ if (raw.length === 0) return Value.empty();
241
+ if (validateValue) {
242
+ const result = validateValue(raw);
243
+ if (result !== true) return Value.invalid(raw, result);
244
+ return Value.valid(raw);
245
+ }
246
+ if (fieldType === "number" && !isFinite(Number(raw))) {
247
+ valueLog("numeric validation failed: raw=%s", raw);
248
+ return Value.invalid(raw, "Expected a number");
249
+ }
250
+ return Value.valid(raw);
251
+ }
252
+ static matchKnownValue(raw, knownValues, validateValue) {
253
+ const lower = raw.toLowerCase();
254
+ const exact = knownValues.find(
255
+ (v) => v.value.toLowerCase() === lower || v.label.toLowerCase() === lower
256
+ );
257
+ if (exact) {
258
+ valueLog("exact match: raw=%s -> %s", raw, exact.value);
259
+ return _MatchEngine.applyValidator(raw, exact, validateValue);
260
+ }
261
+ if (raw.length === 0) return Value.invalid(raw, "Value not recognized");
262
+ const fuse = new Fuse(knownValues, {
263
+ keys: ["label", "value"],
264
+ threshold: 0.4,
265
+ includeScore: true
266
+ });
267
+ const results = fuse.search(raw);
268
+ if (results.length > 0 && (results[0].score ?? 1) <= 0.4) {
269
+ valueLog(
270
+ "fuzzy match: raw=%s -> %s (score: %f)",
271
+ raw,
272
+ results[0].item.value,
273
+ results[0].score
274
+ );
275
+ return _MatchEngine.applyValidator(raw, results[0].item, validateValue);
276
+ }
277
+ valueLog("no match: raw=%s", raw);
278
+ return Value.invalid(raw, "Value not recognized");
279
+ }
280
+ static applyValidator(raw, matched, validateValue) {
281
+ if (validateValue) {
282
+ const result = validateValue(raw);
283
+ if (result !== true) return Value.invalid(raw, result);
284
+ }
285
+ return Value.valid(raw, matched);
286
+ }
287
+ resolveOpsForField(field, allOps) {
288
+ if (field.operators) return field.operators;
289
+ if (field.type) {
290
+ const allowed = TYPE_ALLOWED_OPS[field.type];
291
+ return allOps.filter((op) => allowed.has(op.value));
292
+ }
293
+ return allOps;
294
+ }
295
+ };
296
+
297
+ // src/fuzzy/score.ts
298
+ var AMBIGUOUS_OP_WORDS = /* @__PURE__ */ new Set(["is", "has"]);
299
+ var AMBIGUITY_PENALTY = 0.35;
300
+ var LINKING_VERB_PENALTY = 0.35;
301
+ var LENGTH_BONUS_PER_WORD = 0.01;
302
+ var GAP_PENALTY_PER_WORD = 0.3;
303
+ var UNPARSED_PENALTY = 10;
304
+ var FREETEXT_WORD_COST = 0.1;
305
+ function adjustOperatorScore(baseScore, words, start, end) {
306
+ let score = baseScore;
307
+ const firstWord = words[start];
308
+ if (end - start === 1 && AMBIGUOUS_OP_WORDS.has(firstWord)) {
309
+ score += AMBIGUITY_PENALTY;
310
+ } else if (end - start > 1 && AMBIGUOUS_OP_WORDS.has(firstWord)) {
311
+ score += LINKING_VERB_PENALTY;
312
+ }
313
+ score -= (end - start) * LENGTH_BONUS_PER_WORD;
314
+ if (start > 0) {
315
+ score += start * GAP_PENALTY_PER_WORD;
316
+ }
317
+ return score;
318
+ }
319
+ function scoreConditions(conditions, segmentCount) {
320
+ const unparsed = segmentCount - conditions.length;
321
+ let score = unparsed * UNPARSED_PENALTY;
322
+ for (const c of conditions) {
323
+ score += c.score;
324
+ if (!c.field.isValid) score += UNPARSED_PENALTY;
325
+ if (!c.operator.isValid) score += UNPARSED_PENALTY;
326
+ if (!c.value.isValid) score += UNPARSED_PENALTY;
327
+ if (c.value.isValid && c.value.matchedOption === null) {
328
+ score += c.value.raw.split(/\s+/).length * FREETEXT_WORD_COST;
329
+ }
330
+ }
331
+ return score;
332
+ }
333
+
334
+ // src/fuzzy/word-utils.ts
335
+ var NOISE_WORDS = /* @__PURE__ */ new Set([
336
+ "when",
337
+ "if",
338
+ "the",
339
+ "where",
340
+ "show",
341
+ "that",
342
+ "then",
343
+ "a",
344
+ "an",
345
+ "for",
346
+ "while",
347
+ "can",
348
+ "expect",
349
+ "check",
350
+ "verify",
351
+ "ensure",
352
+ "find",
353
+ "get",
354
+ "list",
355
+ "filter",
356
+ "only",
357
+ "all",
358
+ "with",
359
+ "having",
360
+ "whose",
361
+ "which",
362
+ "given",
363
+ "assuming",
364
+ "suppose",
365
+ "i",
366
+ "want",
367
+ "need",
368
+ "like",
369
+ "please",
370
+ "me",
371
+ "my"
372
+ ]);
373
+ function powerSet(items) {
374
+ const result = [];
375
+ const total = 1 << items.length;
376
+ for (let mask = 1; mask < total; mask++) {
377
+ const subset = [];
378
+ for (let i = 0; i < items.length; i++) {
379
+ if (mask & 1 << i) subset.push(items[i]);
380
+ }
381
+ result.push(subset);
382
+ }
383
+ return result;
384
+ }
385
+ function splitAtIndices(words, indices) {
386
+ const segments = [];
387
+ let start = 0;
388
+ for (const idx of indices) {
389
+ segments.push(words.slice(start, idx).join(" "));
390
+ start = idx + 1;
391
+ }
392
+ segments.push(words.slice(start).join(" "));
393
+ return segments;
394
+ }
395
+ function stripLeadingNoise(words) {
396
+ let i = 0;
397
+ while (i < words.length && NOISE_WORDS.has(words[i])) i++;
398
+ const result = words.slice(i);
399
+ return result;
400
+ }
401
+
402
+ // src/conditions/parser.ts
403
+ var log2 = createLogger("parser");
404
+ var TYPE_ALLOWED_OPS2 = {
405
+ number: /* @__PURE__ */ new Set(["eq", "ne", "gt", "lt", "gte", "lte"]),
406
+ enum: /* @__PURE__ */ new Set(["eq", "ne"]),
407
+ text: /* @__PURE__ */ new Set(["eq", "ne", "contains", "starts_with"])
408
+ };
409
+ var ConditionParser = class {
410
+ constructor(engine) {
411
+ this.fields = engine.fields;
412
+ this.operators = engine.operators;
413
+ this.engine = engine;
414
+ }
415
+ parse(text) {
416
+ const input = text.trim().toLowerCase();
417
+ if (!input) return null;
418
+ const allWords = input.split(/\s+/);
419
+ const words = stripLeadingNoise(allWords);
420
+ if (words.length === 0) return null;
421
+ const fieldResult = this.identifyField(words);
422
+ if (!fieldResult) return null;
423
+ if (fieldResult.remaining.length === 0) {
424
+ return {
425
+ field: fieldResult.field,
426
+ operator: Operator.invalid(""),
427
+ value: Value.empty(),
428
+ score: fieldResult.fieldScore
429
+ };
430
+ }
431
+ const { operator, value, operatorScore } = this.resolveOperator(
432
+ fieldResult.remaining,
433
+ fieldResult.fieldOption
434
+ );
435
+ const result = {
436
+ field: fieldResult.field,
437
+ operator,
438
+ value,
439
+ score: fieldResult.fieldScore + operatorScore
440
+ };
441
+ log2(
442
+ "result: field=%s(%s) op=%s(%s) value=%s (valid: %o)",
443
+ result.field.value,
444
+ result.field.label,
445
+ result.operator.value,
446
+ result.operator.label,
447
+ result.value.value,
448
+ {
449
+ field: result.field.isValid,
450
+ op: result.operator.isValid,
451
+ value: result.value.isValid
452
+ }
453
+ );
454
+ return result;
455
+ }
456
+ identifyField(words) {
457
+ let best = null;
458
+ for (let i = words.length; i >= 1; i--) {
459
+ const candidate = words.slice(0, i).join(" ");
460
+ const match = this.engine.matchField(candidate);
461
+ if (match && (!best || match.score < best.match.score)) {
462
+ best = { candidate, match, wordCount: i };
463
+ }
464
+ }
465
+ if (!best) return null;
466
+ return {
467
+ field: new Field(best.candidate, best.match.option.value, best.match.option.label),
468
+ fieldOption: best.match.option,
469
+ fieldScore: best.match.score,
470
+ remaining: words.slice(best.wordCount)
471
+ };
472
+ }
473
+ getOperatorCandidates(words, fieldOption) {
474
+ const candidates = [];
475
+ for (let start = 0; start < words.length; start++) {
476
+ for (let end = start + 1; end <= words.length; end++) {
477
+ const opRaw = words.slice(start, end).join(" ");
478
+ const opMatch = this.engine.matchOperator(opRaw, fieldOption);
479
+ if (!opMatch) continue;
480
+ const adjustedScore = adjustOperatorScore(opMatch.score, words, start, end);
481
+ candidates.push({
482
+ match: opMatch,
483
+ raw: opRaw,
484
+ endIdx: end,
485
+ adjustedScore
486
+ });
487
+ }
488
+ }
489
+ candidates.sort((a, b) => a.adjustedScore - b.adjustedScore);
490
+ if (candidates.length > 0) {
491
+ log2(
492
+ "operator candidates: %o",
493
+ candidates.map(
494
+ (c) => `${c.raw} -> ${c.match.option.value} (${c.adjustedScore.toFixed(3)})`
495
+ )
496
+ );
497
+ }
498
+ return candidates;
499
+ }
500
+ resolveOperator(words, fieldOption) {
501
+ const candidates = this.getOperatorCandidates(words, fieldOption);
502
+ if (candidates.length === 0) {
503
+ return {
504
+ operator: Operator.invalid(words.join(" ")),
505
+ value: Value.empty(),
506
+ operatorScore: 1
507
+ };
508
+ }
509
+ for (const candidate of candidates) {
510
+ const valueRaw2 = words.slice(candidate.endIdx).join(" ");
511
+ const value = MatchEngine.matchValue(valueRaw2, fieldOption);
512
+ if (value.isValid) {
513
+ const op = candidate.match.option;
514
+ return {
515
+ operator: new Operator(candidate.raw, op.value, op.label),
516
+ value,
517
+ operatorScore: candidate.adjustedScore
518
+ };
519
+ }
520
+ log2(
521
+ "operator '%s' (%s) rejected \u2014 value '%s' invalid, trying next",
522
+ candidate.raw,
523
+ candidate.match.option.value,
524
+ valueRaw2
525
+ );
526
+ }
527
+ const best = candidates[0];
528
+ const bestOp = best.match.option;
529
+ const valueRaw = words.slice(best.endIdx).join(" ");
530
+ return {
531
+ operator: new Operator(best.raw, bestOp.value, bestOp.label),
532
+ value: MatchEngine.matchValue(valueRaw, fieldOption),
533
+ operatorScore: best.adjustedScore
534
+ };
535
+ }
536
+ resolveOpsForField(field, allOps) {
537
+ if (field.operators) return field.operators;
538
+ if (field.type) {
539
+ const allowed = TYPE_ALLOWED_OPS2[field.type];
540
+ return allOps.filter((op) => allowed.has(op.value));
541
+ }
542
+ return allOps;
543
+ }
544
+ allowedOpsForField(field) {
545
+ return this.resolveOpsForField(field, this.operators);
546
+ }
547
+ prefixMatch(partial, candidates) {
548
+ const matches = this.prefixMatches(partial, candidates);
549
+ return matches.length > 0 ? matches[0] : null;
550
+ }
551
+ prefixMatches(partial, candidates, limit = 6) {
552
+ const lower = partial.toLowerCase();
553
+ const results = [];
554
+ for (const candidate of candidates) {
555
+ const cl = candidate.toLowerCase();
556
+ if (cl.startsWith(lower) && cl !== lower) {
557
+ results.push({ completion: cl.slice(lower.length), display: candidate });
558
+ if (results.length >= limit) break;
559
+ }
560
+ }
561
+ return results;
562
+ }
563
+ };
564
+
565
+ // src/consts.ts
566
+ var AND_CONJUNCTION = "and";
567
+ var OR_CONJUNCTION = "or";
568
+
569
+ // src/fuzzy/providers/suggestions.ts
570
+ var SuggestionsProvider = class {
571
+ constructor(parser, segmentResolver) {
572
+ this.parser = parser;
573
+ this.segmentResolver = segmentResolver;
574
+ }
575
+ getCompletions(text, limit = 6) {
576
+ const { last, before } = this.splitForSuggestion(text);
577
+ const trailingSpace = /\s$/.test(text) ? " " : "";
578
+ const direct = this.completionsForSegment(last + trailingSpace, limit);
579
+ if (direct.length > 0) return direct;
580
+ if (!before) return [];
581
+ const { conditions } = this.segmentResolver.resolve(before);
582
+ const previous = conditions[conditions.length - 1];
583
+ if (!previous) return [];
584
+ const inherited = `${previous.field.raw} ${previous.operator.raw} ${last}${trailingSpace}`;
585
+ return this.completionsForSegment(inherited, limit);
586
+ }
587
+ getSuggestion(text) {
588
+ if (!text.trim()) return null;
589
+ const results = this.getCompletions(text, 1);
590
+ if (results.length === 0) return null;
591
+ const r = results[0];
592
+ if (r.completion === r.display && /\s$/.test(text)) return null;
593
+ return r;
594
+ }
595
+ completionsForSegment(text, limit) {
596
+ const input = text.trimStart().toLowerCase();
597
+ if (!input) {
598
+ return this.parser.fields.slice(0, limit).map((f) => ({ completion: f.label, display: f.label }));
599
+ }
600
+ const endsWithSpace = /\s$/.test(input);
601
+ const words = input.split(/\s+/).filter(Boolean);
602
+ if (words.length === 0) return [];
603
+ const fieldResult = this.parser.identifyField(words);
604
+ if (!fieldResult) {
605
+ if (endsWithSpace) return [];
606
+ return this.parser.prefixMatches(
607
+ words.join(" "),
608
+ this.parser.fields.map((f) => f.label),
609
+ limit
610
+ );
611
+ }
612
+ const fieldOption = fieldResult.fieldOption;
613
+ if (fieldResult.remaining.length === 0) {
614
+ if (endsWithSpace) {
615
+ const ops = this.parser.allowedOpsForField(fieldOption);
616
+ return ops.slice(0, limit).map((op) => ({ completion: op.label, display: op.label }));
617
+ }
618
+ return this.parser.prefixMatches(
619
+ words.join(" "),
620
+ this.parser.fields.map((f) => f.label),
621
+ limit
622
+ );
623
+ }
624
+ const candidates = this.parser.getOperatorCandidates(fieldResult.remaining, fieldOption);
625
+ const bestOp = candidates[0];
626
+ if (!bestOp || bestOp.endIdx > fieldResult.remaining.length) {
627
+ if (endsWithSpace) return [];
628
+ const ops = this.parser.allowedOpsForField(fieldOption);
629
+ const aliases = ops.flatMap((op) => op.aliases);
630
+ const opPartial = fieldResult.remaining.join(" ");
631
+ return this.parser.prefixMatches(opPartial, aliases, limit);
632
+ }
633
+ const valueRaw = fieldResult.remaining.slice(bestOp.endIdx).join(" ");
634
+ if (!valueRaw) {
635
+ if (endsWithSpace) {
636
+ const fieldValues2 = fieldOption.fieldValues;
637
+ if (!fieldValues2?.length) return [];
638
+ return fieldValues2.slice(0, limit).map((v) => ({ completion: v.label, display: v.label }));
639
+ }
640
+ const ops = this.parser.allowedOpsForField(fieldOption);
641
+ const aliases = ops.flatMap((op) => op.aliases);
642
+ const opPartial = fieldResult.remaining.join(" ");
643
+ return this.parser.prefixMatches(opPartial, aliases, limit);
644
+ }
645
+ if (endsWithSpace) return [];
646
+ const fieldValues = fieldOption.fieldValues;
647
+ if (!fieldValues?.length) return [];
648
+ return this.parser.prefixMatches(
649
+ valueRaw,
650
+ fieldValues.map((v) => v.label),
651
+ limit
652
+ );
653
+ }
654
+ splitForSuggestion(text) {
655
+ const words = text.split(/\s+/);
656
+ for (let i = words.length - 1; i > 0; i--) {
657
+ const lower = words[i].toLowerCase();
658
+ if (lower === AND_CONJUNCTION || lower === OR_CONJUNCTION) {
659
+ const last = words.slice(i + 1).join(" ");
660
+ if (!last) break;
661
+ return {
662
+ before: words.slice(0, i).join(" "),
663
+ last
664
+ };
665
+ }
666
+ }
667
+ return { last: text, before: null };
668
+ }
669
+ };
670
+
671
+ // src/fuzzy/providers/diagnostics.ts
672
+ var DiagnosticsProvider = class {
673
+ constructor(parser, segmentResolver) {
674
+ this.parser = parser;
675
+ this.segmentResolver = segmentResolver;
676
+ }
677
+ diagnose(text) {
678
+ const input = text.trim();
679
+ if (!input) return [{ start: 0, end: text.length || 1, message: "Empty condition" }];
680
+ const { segments, conditions } = this.segmentResolver.resolve(input);
681
+ const diagnostics = [];
682
+ if (conditions.length === 0) {
683
+ return [
684
+ { start: 0, end: input.length, message: "Could not understand this condition" }
685
+ ];
686
+ }
687
+ for (let i = 0; i < segments.length; i++) {
688
+ const seg = segments[i];
689
+ const offset = input.toLowerCase().indexOf(seg.toLowerCase());
690
+ const condition = conditions[i];
691
+ if (!condition) {
692
+ diagnostics.push({
693
+ start: offset,
694
+ end: offset + seg.length,
695
+ message: "Could not understand this condition"
696
+ });
697
+ continue;
698
+ }
699
+ if (!condition.operator.isValid) {
700
+ const fieldEnd = offset + condition.field.raw.length;
701
+ const fieldConfig = this.parser.fields.find(
702
+ (f) => f.value === condition.field.value
703
+ );
704
+ const hasRestriction = fieldConfig?.operators || fieldConfig?.type;
705
+ diagnostics.push({
706
+ start: fieldEnd,
707
+ end: offset + seg.length,
708
+ message: hasRestriction ? `Operator not supported for ${condition.field.label}` : "Unknown operator"
709
+ });
710
+ }
711
+ if (condition.operator.isValid && !condition.value.isValid) {
712
+ if (!condition.value.raw) {
713
+ diagnostics.push({
714
+ start: offset + seg.length,
715
+ end: offset + seg.length + 1,
716
+ message: "Missing value"
717
+ });
718
+ } else {
719
+ const valStart = offset + seg.toLowerCase().lastIndexOf(condition.value.raw.toLowerCase());
720
+ diagnostics.push({
721
+ start: valStart,
722
+ end: valStart + condition.value.raw.length,
723
+ message: condition.value.errorMessage ?? `Value not recognized for ${condition.field.label}`
724
+ });
725
+ }
726
+ }
727
+ }
728
+ return diagnostics;
729
+ }
730
+ };
731
+
732
+ // src/fuzzy/segments.ts
733
+ import { log as log3 } from "debug";
734
+
735
+ // src/id.ts
736
+ var counter = 0;
737
+ var generateId = () => {
738
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
739
+ return crypto.randomUUID();
740
+ }
741
+ return `id-${Date.now()}-${++counter}`;
742
+ };
743
+
744
+ // src/fuzzy/segments.ts
745
+ var SegmentResolver = class {
746
+ constructor(parser) {
747
+ this.parser = parser;
748
+ }
749
+ resolve(text) {
750
+ const originalWords = text.split(/\s+/);
751
+ const lowerWords = originalWords.map((w) => w.toLowerCase());
752
+ const conjIndices = [];
753
+ for (let i = 0; i < lowerWords.length; i++) {
754
+ if (lowerWords[i] === AND_CONJUNCTION || lowerWords[i] === OR_CONJUNCTION) {
755
+ conjIndices.push({ index: i, conjunction: lowerWords[i] });
756
+ }
757
+ }
758
+ if (conjIndices.length === 0) {
759
+ const single = this.parser.parse(text);
760
+ return {
761
+ segments: [text],
762
+ conditions: single ? [single] : [],
763
+ connector: AND_CONJUNCTION
764
+ };
765
+ }
766
+ const noSplit = this.parser.parse(text);
767
+ let best = {
768
+ segments: [text],
769
+ conditions: noSplit ? [noSplit] : [],
770
+ connector: AND_CONJUNCTION
771
+ };
772
+ let bestScore = scoreConditions(best.conditions, 1);
773
+ for (const subset of powerSet(conjIndices)) {
774
+ const connector = subset[0].conjunction;
775
+ if (subset.some((s) => s.conjunction !== connector)) continue;
776
+ const splitIndices = subset.map((s) => s.index);
777
+ const segments = splitAtIndices(originalWords, splitIndices);
778
+ if (segments.some((s) => !s)) continue;
779
+ const conditions = this.parseSegments(segments);
780
+ const score = scoreConditions(conditions, segments.length);
781
+ if (score < bestScore) {
782
+ bestScore = score;
783
+ best = { segments, conditions, connector };
784
+ }
785
+ }
786
+ if (best.conditions.length === 1 && best.conditions[0].value.isValid && best.conditions[0].value.matchedOption === null) {
787
+ const valueWords = best.conditions[0].value.raw.split(/\s+/);
788
+ const conjInValue = valueWords.find(
789
+ (w) => w.toLowerCase() === AND_CONJUNCTION || w.toLowerCase() === OR_CONJUNCTION
790
+ );
791
+ if (conjInValue) {
792
+ const connector = conjInValue.toLowerCase();
793
+ const matching = conjIndices.filter((c) => c.conjunction === connector);
794
+ for (const subset of powerSet(matching).reverse()) {
795
+ const splitIndices = subset.map((s) => s.index);
796
+ const segments = splitAtIndices(originalWords, splitIndices);
797
+ if (segments.some((s) => !s)) continue;
798
+ const conditions = this.parseSegments(segments);
799
+ if (conditions.length === segments.length && conditions.every((c) => c.field.isValid && c.operator.isValid)) {
800
+ return { segments, conditions, connector };
801
+ }
802
+ }
803
+ }
804
+ }
805
+ return best;
806
+ }
807
+ parseConditions(text) {
808
+ const input = text.trim();
809
+ if (!input) return null;
810
+ const { conditions, connector } = this.resolve(input);
811
+ if (conditions.length === 0) return null;
812
+ const entries = conditions.map((condition) => ({
813
+ id: generateId(),
814
+ condition,
815
+ connector
816
+ }));
817
+ return { id: generateId(), entries };
818
+ }
819
+ parseSegments(segments) {
820
+ const conditions = [];
821
+ for (const segment of segments) {
822
+ const previous = conditions[conditions.length - 1] ?? null;
823
+ const result = this.getInherited(segment, previous) ?? this.parser.parse(segment);
824
+ if (result) conditions.push(result);
825
+ }
826
+ return conditions;
827
+ }
828
+ getInherited(segment, previous) {
829
+ if (!previous) return null;
830
+ const direct = this.parser.parse(segment);
831
+ if (direct && direct.operator.isValid) return direct;
832
+ const inheritField = `${previous.field.raw} ${segment}`;
833
+ const fieldOnly = this.parser.parse(inheritField);
834
+ if (fieldOnly && fieldOnly.operator.isValid) {
835
+ log3("inheriting field for segment: %s -> %s", segment, inheritField);
836
+ return fieldOnly;
837
+ }
838
+ const inheritAll = `${previous.field.raw} ${previous.operator.raw} ${segment}`;
839
+ const full = this.parser.parse(inheritAll);
840
+ if (full) {
841
+ log3("inheriting field+op for segment: %s -> %s", segment, inheritAll);
842
+ return full;
843
+ }
844
+ return direct;
845
+ }
846
+ };
847
+
848
+ // src/condition-data-provider.ts
849
+ var ConditionDataProvider = class {
850
+ constructor(fields, operators) {
851
+ const engine = new MatchEngine(fields, operators);
852
+ const parser = new ConditionParser(engine);
853
+ this.segments = new SegmentResolver(parser);
854
+ this.suggestions = new SuggestionsProvider(parser, this.segments);
855
+ this.diagnostics = new DiagnosticsProvider(parser, this.segments);
856
+ }
857
+ parseComplexCondition(text) {
858
+ return this.segments.parseConditions(text);
859
+ }
860
+ getCompletions(text, limit = 6) {
861
+ return this.suggestions.getCompletions(text, limit);
862
+ }
863
+ getSuggestion(text) {
864
+ return this.suggestions.getSuggestion(text);
865
+ }
866
+ diagnose(text) {
867
+ return this.diagnostics.diagnose(text);
868
+ }
869
+ };
870
+
871
+ // src/hooks/useConditionDataProvider.ts
872
+ function useConditionDataProvider({
873
+ fields,
874
+ operators = DEFAULT_OPERATORS
875
+ }) {
876
+ const provider = useMemo(
877
+ () => new ConditionDataProvider(fields, operators),
878
+ [fields, operators]
879
+ );
880
+ const parseComplexCondition = useCallback(
881
+ (text) => provider.parseComplexCondition(text),
882
+ [provider]
883
+ );
884
+ const getCompletions = useCallback(
885
+ (text, limit) => provider.getCompletions(text, limit),
886
+ [provider]
887
+ );
888
+ const getSuggestion = useCallback((text) => provider.getSuggestion(text), [provider]);
889
+ const diagnose = useCallback((text) => provider.diagnose(text), [provider]);
890
+ return { provider, parseComplexCondition, getCompletions, getSuggestion, diagnose };
891
+ }
892
+
893
+ // src/hooks/useConditionalInput.ts
894
+ function useConditionalInput({
895
+ fields,
896
+ operators = DEFAULT_OPERATORS,
897
+ value,
898
+ onChange,
899
+ onSubmit
900
+ }) {
901
+ const [internal, setInternal] = useState("");
902
+ const [diagnostics, setDiagnostics] = useState([]);
903
+ const text = value ?? internal;
904
+ const { parseComplexCondition, getSuggestion, getCompletions, diagnose } = useConditionDataProvider({
905
+ fields,
906
+ operators
907
+ });
908
+ const handleChange = useCallback2(
909
+ (next) => {
910
+ if (value === void 0) setInternal(next);
911
+ onChange?.(next);
912
+ setDiagnostics([]);
913
+ },
914
+ [value, onChange]
915
+ );
916
+ const handleSubmit = useCallback2(() => {
917
+ const group = parseComplexCondition(text);
918
+ if (group) {
919
+ const hasInvalid = group.entries.some(
920
+ (e) => !e.condition.field.isValid || !e.condition.operator.isValid || !e.condition.value.isValid
921
+ );
922
+ if (!hasInvalid) {
923
+ onSubmit?.(group);
924
+ setDiagnostics([]);
925
+ if (value === void 0) setInternal("");
926
+ onChange?.("");
927
+ return;
928
+ }
929
+ }
930
+ setDiagnostics(diagnose(text));
931
+ }, [text, parseComplexCondition, diagnose, value, onChange, onSubmit]);
932
+ return { text, diagnostics, handleChange, handleSubmit, getSuggestion, getCompletions };
933
+ }
934
+
935
+ // src/components/Input.tsx
936
+ import { jsx, jsxs } from "react/jsx-runtime";
937
+ function Input(props) {
938
+ if (isStandalone(props)) {
939
+ return /* @__PURE__ */ jsx(StandaloneInput, { ...props });
940
+ }
941
+ return /* @__PURE__ */ jsx(
942
+ InputView,
943
+ {
944
+ value: props.value,
945
+ onChange: props.onChange,
946
+ onSubmit: props.onSubmit,
947
+ getSuggestion: props.getSuggestion,
948
+ getCompletions: props.getCompletions,
949
+ diagnostics: props.diagnostics,
950
+ placeholder: props.placeholder,
951
+ className: props.className,
952
+ style: props.style
953
+ }
954
+ );
955
+ }
956
+ function isStandalone(props) {
957
+ return props.fields !== void 0;
958
+ }
959
+ function buildOverlaySegments(value, diagnostics) {
960
+ const sorted = [...diagnostics].sort((a, b) => a.start - b.start);
961
+ const segments = [];
962
+ let cursor = 0;
963
+ for (const d of sorted) {
964
+ if (d.start > cursor) segments.push({ text: value.slice(cursor, d.start) });
965
+ segments.push({ text: value.slice(d.start, d.end), diagnostic: d });
966
+ cursor = d.end;
967
+ }
968
+ if (cursor < value.length) segments.push({ text: value.slice(cursor) });
969
+ return segments;
970
+ }
971
+ var GhostInput = forwardRef(function GhostInput2({ ghost, diagnosticSegments, inputValue, onCursorOffset, className, ...inputProps }, ref) {
972
+ const innerRef = useRef(null);
973
+ const mirrorRef = useRef(null);
974
+ const ghostRef = useRef(null);
975
+ const squigglyRef = useRef(null);
976
+ useLayoutEffect(() => {
977
+ if (!mirrorRef.current || !innerRef.current) return;
978
+ const padding = parseFloat(getComputedStyle(innerRef.current).paddingLeft) || 0;
979
+ const left = mirrorRef.current.offsetWidth + padding;
980
+ if (ghostRef.current) ghostRef.current.style.left = `${left}px`;
981
+ if (squigglyRef.current) {
982
+ squigglyRef.current.style.paddingLeft = `${padding}px`;
983
+ squigglyRef.current.style.paddingRight = `${padding}px`;
984
+ }
985
+ onCursorOffset?.(left);
986
+ });
987
+ const setRefs = useCallback3(
988
+ (el) => {
989
+ innerRef.current = el;
990
+ if (typeof ref === "function") ref(el);
991
+ else if (ref) ref.current = el;
992
+ },
993
+ [ref]
994
+ );
995
+ const showSquiggly = diagnosticSegments && diagnosticSegments.some((s) => s.diagnostic);
996
+ return /* @__PURE__ */ jsxs("div", { className: "rcui-input-inner", children: [
997
+ /* @__PURE__ */ jsx("input", { ref: setRefs, className, ...inputProps }),
998
+ /* @__PURE__ */ jsx("span", { ref: mirrorRef, className: "rcui-mirror", "aria-hidden": "true", children: inputValue }),
999
+ ghost && /* @__PURE__ */ jsx("span", { ref: ghostRef, className: "rcui-ghost", "aria-hidden": "true", children: ghost }),
1000
+ showSquiggly && /* @__PURE__ */ jsx("span", { ref: squigglyRef, className: "rcui-squiggly-layer", "aria-hidden": "true", children: diagnosticSegments.map(
1001
+ (seg, i) => seg.diagnostic ? /* @__PURE__ */ jsx("span", { className: "rcui-squiggly", title: seg.diagnostic.message, children: seg.text }, i) : /* @__PURE__ */ jsx("span", { children: seg.text }, i)
1002
+ ) })
1003
+ ] });
1004
+ });
1005
+ function StandaloneInput({
1006
+ fields,
1007
+ operators,
1008
+ value: controlledValue,
1009
+ onChange: controlledOnChange,
1010
+ onSubmit: onGroupParsed,
1011
+ placeholder,
1012
+ className,
1013
+ style
1014
+ }) {
1015
+ const { text, diagnostics, handleChange, handleSubmit, getSuggestion, getCompletions } = useConditionalInput({
1016
+ fields,
1017
+ operators,
1018
+ value: controlledValue,
1019
+ onChange: controlledOnChange,
1020
+ onSubmit: onGroupParsed
1021
+ });
1022
+ return /* @__PURE__ */ jsx(
1023
+ InputView,
1024
+ {
1025
+ value: text,
1026
+ onChange: handleChange,
1027
+ onSubmit: handleSubmit,
1028
+ getSuggestion,
1029
+ getCompletions,
1030
+ diagnostics,
1031
+ placeholder,
1032
+ className,
1033
+ style
1034
+ }
1035
+ );
1036
+ }
1037
+ function InputView({
1038
+ value,
1039
+ onChange,
1040
+ onSubmit,
1041
+ placeholder = "e.g. age greater than 18, ctrl+space for suggestions",
1042
+ getSuggestion,
1043
+ getCompletions,
1044
+ diagnostics = [],
1045
+ className,
1046
+ style
1047
+ }) {
1048
+ const [completions, setCompletions] = useState2([]);
1049
+ const [activeIndex, setActiveIndex] = useState2(-1);
1050
+ const [cursorLeft, setCursorLeft] = useState2(0);
1051
+ const listRef = useRef(null);
1052
+ const ghost = getSuggestion && value ? getSuggestion(value)?.completion ?? null : null;
1053
+ function closeCompletions() {
1054
+ setCompletions([]);
1055
+ setActiveIndex(-1);
1056
+ }
1057
+ function openCompletions() {
1058
+ if (!getCompletions) return;
1059
+ const items = getCompletions(value);
1060
+ setCompletions(items);
1061
+ setActiveIndex(items.length > 0 ? 0 : -1);
1062
+ }
1063
+ function acceptCompletion(item) {
1064
+ onChange(value + item.completion);
1065
+ closeCompletions();
1066
+ }
1067
+ function handleChange(e) {
1068
+ onChange(e.target.value);
1069
+ closeCompletions();
1070
+ }
1071
+ function acceptGhost() {
1072
+ if (ghost) onChange(value + ghost);
1073
+ }
1074
+ function handleKeyDown(e) {
1075
+ if (e.key === " " && e.ctrlKey) {
1076
+ e.preventDefault();
1077
+ openCompletions();
1078
+ return;
1079
+ }
1080
+ if (completions.length > 0) {
1081
+ if (e.key === "ArrowDown") {
1082
+ e.preventDefault();
1083
+ setActiveIndex((i) => (i + 1) % completions.length);
1084
+ return;
1085
+ }
1086
+ if (e.key === "ArrowUp") {
1087
+ e.preventDefault();
1088
+ setActiveIndex((i) => (i - 1 + completions.length) % completions.length);
1089
+ return;
1090
+ }
1091
+ if (e.key === "Enter") {
1092
+ e.preventDefault();
1093
+ if (activeIndex >= 0) {
1094
+ acceptCompletion(completions[activeIndex]);
1095
+ } else {
1096
+ closeCompletions();
1097
+ onSubmit();
1098
+ }
1099
+ return;
1100
+ }
1101
+ if (e.key === "Escape") {
1102
+ e.preventDefault();
1103
+ closeCompletions();
1104
+ return;
1105
+ }
1106
+ if (e.key === "Tab") {
1107
+ e.preventDefault();
1108
+ if (activeIndex >= 0) {
1109
+ acceptCompletion(completions[activeIndex]);
1110
+ }
1111
+ return;
1112
+ }
1113
+ }
1114
+ if (e.key === "Enter") {
1115
+ e.preventDefault();
1116
+ onSubmit();
1117
+ return;
1118
+ }
1119
+ if (e.key === "Tab" && ghost) {
1120
+ e.preventDefault();
1121
+ acceptGhost();
1122
+ }
1123
+ }
1124
+ const hasErrors = diagnostics.length > 0;
1125
+ const errorSummary = diagnostics.map((d) => d.message).join("; ");
1126
+ const diagnosticSegments = hasErrors ? buildOverlaySegments(value, diagnostics) : null;
1127
+ const ghostInputProps = {
1128
+ ghost,
1129
+ diagnosticSegments,
1130
+ inputValue: value,
1131
+ onCursorOffset: setCursorLeft
1132
+ };
1133
+ return /* @__PURE__ */ jsxs("div", { className: ["rcui-input-wrapper", className].filter(Boolean).join(" "), style, children: [
1134
+ /* @__PURE__ */ jsx(
1135
+ TextField,
1136
+ {
1137
+ fullWidth: true,
1138
+ variant: "outlined",
1139
+ size: "small",
1140
+ placeholder,
1141
+ value,
1142
+ onChange: handleChange,
1143
+ onKeyDown: handleKeyDown,
1144
+ onBlur: () => setTimeout(closeCompletions, 150),
1145
+ className: "rcui-input",
1146
+ slotProps: {
1147
+ input: {
1148
+ inputComponent: GhostInput,
1149
+ inputProps: ghostInputProps,
1150
+ endAdornment: /* @__PURE__ */ jsx(InputAdornment, { position: "end", children: hasErrors ? /* @__PURE__ */ jsx(Tooltip, { title: errorSummary, arrow: true, children: /* @__PURE__ */ jsx(IconButton, { size: "small", onClick: onSubmit, edge: "end", children: /* @__PURE__ */ jsx(
1151
+ ErrorOutlineIcon,
1152
+ {
1153
+ fontSize: "small",
1154
+ className: "rcui-adornment-error"
1155
+ }
1156
+ ) }) }) : /* @__PURE__ */ jsx(IconButton, { size: "small", onClick: onSubmit, edge: "end", children: /* @__PURE__ */ jsx(
1157
+ KeyboardReturnIcon,
1158
+ {
1159
+ fontSize: "small",
1160
+ className: "rcui-adornment-enter"
1161
+ }
1162
+ ) }) })
1163
+ }
1164
+ }
1165
+ }
1166
+ ),
1167
+ completions.length > 0 && /* @__PURE__ */ jsx(
1168
+ "ul",
1169
+ {
1170
+ ref: listRef,
1171
+ className: "rcui-completions",
1172
+ role: "listbox",
1173
+ style: { left: cursorLeft },
1174
+ children: completions.map((item, i) => /* @__PURE__ */ jsx(
1175
+ "li",
1176
+ {
1177
+ id: `rcui-completion-${i}`,
1178
+ role: "option",
1179
+ "aria-selected": i === activeIndex,
1180
+ className: `rcui-completion-item${i === activeIndex ? " rcui-completion-item--active" : ""}`,
1181
+ onMouseDown: (e) => {
1182
+ e.preventDefault();
1183
+ acceptCompletion(item);
1184
+ },
1185
+ onMouseEnter: () => setActiveIndex(i),
1186
+ children: item.display
1187
+ },
1188
+ item.display
1189
+ ))
1190
+ }
1191
+ )
1192
+ ] });
1193
+ }
1194
+
1195
+ // src/components/Output.tsx
1196
+ import Chip3 from "@mui/material/Chip";
1197
+ import Paper from "@mui/material/Paper";
1198
+ import Typography from "@mui/material/Typography";
1199
+ import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
1200
+ import { useDroppable } from "@dnd-kit/core";
1201
+
1202
+ // src/components/OutputRow.tsx
1203
+ import { useState as useState3 } from "react";
1204
+ import { useSortable } from "@dnd-kit/sortable";
1205
+ import { CSS } from "@dnd-kit/utilities";
1206
+ import Chip from "@mui/material/Chip";
1207
+ import Popover from "@mui/material/Popover";
1208
+ import List from "@mui/material/List";
1209
+ import ListItemButton from "@mui/material/ListItemButton";
1210
+ import ListItemText from "@mui/material/ListItemText";
1211
+ import TextField2 from "@mui/material/TextField";
1212
+ import IconButton2 from "@mui/material/IconButton";
1213
+ import CloseIcon from "@mui/icons-material/Close";
1214
+ import DragIndicatorIcon from "@mui/icons-material/DragIndicator";
1215
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1216
+ function OutputRow({
1217
+ id,
1218
+ condition,
1219
+ fields,
1220
+ operators,
1221
+ onUpdate,
1222
+ onRemove
1223
+ }) {
1224
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging, isOver } = useSortable({ id });
1225
+ const style = {
1226
+ transform: CSS.Transform.toString(transform),
1227
+ transition,
1228
+ opacity: isDragging ? 0.3 : 1
1229
+ };
1230
+ const [popover, setPopover] = useState3(null);
1231
+ const [valueEdit, setValueEdit] = useState3("");
1232
+ const openPopover = (target, anchor) => {
1233
+ setPopover({ target, anchor });
1234
+ if (target === "value") setValueEdit(condition.value.raw ?? "");
1235
+ };
1236
+ const closePopover = () => setPopover(null);
1237
+ const editable = !!onUpdate;
1238
+ const resolveFieldValues = (f) => f.fieldValues;
1239
+ const resolveOperators = (f) => f.operators ?? operators;
1240
+ const selectField = (f) => {
1241
+ onUpdate?.({
1242
+ ...condition,
1243
+ field: new Field(f.label, f.value, f.label),
1244
+ value: MatchEngine.matchValue(condition.value.raw, f)
1245
+ });
1246
+ closePopover();
1247
+ };
1248
+ const selectOperator = (op) => {
1249
+ onUpdate?.({
1250
+ ...condition,
1251
+ operator: new Operator(op.label, op.value, op.label)
1252
+ });
1253
+ closePopover();
1254
+ };
1255
+ const submitValue = (raw) => {
1256
+ const fieldConfig = fields.find((f) => f.value === condition.field.value);
1257
+ onUpdate?.({
1258
+ ...condition,
1259
+ value: MatchEngine.matchValue(raw, fieldConfig)
1260
+ });
1261
+ closePopover();
1262
+ };
1263
+ const currentField = fields.find((f) => f.value === condition.field.value);
1264
+ const fieldValues = currentField ? resolveFieldValues(currentField) : void 0;
1265
+ const activeOperators = currentField ? resolveOperators(currentField) : operators;
1266
+ const rowClass = ["rcui-row", isOver && !isDragging ? "rcui-row--over" : ""].filter(Boolean).join(" ");
1267
+ return /* @__PURE__ */ jsxs2("div", { ref: setNodeRef, style, className: rowClass, children: [
1268
+ /* @__PURE__ */ jsx2(
1269
+ "div",
1270
+ {
1271
+ ...attributes,
1272
+ ...listeners,
1273
+ className: "rcui-drag-handle",
1274
+ "aria-label": "drag handle",
1275
+ children: /* @__PURE__ */ jsx2(DragIndicatorIcon, { fontSize: "small" })
1276
+ }
1277
+ ),
1278
+ /* @__PURE__ */ jsx2(
1279
+ Chip,
1280
+ {
1281
+ label: condition.field.label,
1282
+ color: condition.field.isValid ? "primary" : "error",
1283
+ variant: "filled",
1284
+ onClick: editable ? (e) => openPopover("field", e.currentTarget) : void 0,
1285
+ className: condition.field.isValid ? "rcui-chip-field" : "rcui-chip-field--error"
1286
+ }
1287
+ ),
1288
+ /* @__PURE__ */ jsx2(
1289
+ Chip,
1290
+ {
1291
+ label: condition.operator.label,
1292
+ color: condition.operator.isValid ? "secondary" : "error",
1293
+ variant: "filled",
1294
+ onClick: editable ? (e) => openPopover("operator", e.currentTarget) : void 0,
1295
+ className: condition.operator.isValid ? "rcui-chip-operator" : "rcui-chip-operator--error"
1296
+ }
1297
+ ),
1298
+ /* @__PURE__ */ jsx2(
1299
+ Chip,
1300
+ {
1301
+ label: condition.value.label || "\u2026",
1302
+ variant: "filled",
1303
+ onClick: editable ? (e) => openPopover("value", e.currentTarget) : void 0,
1304
+ className: "rcui-chip-value"
1305
+ }
1306
+ ),
1307
+ onRemove && /* @__PURE__ */ jsx2(IconButton2, { size: "small", onClick: onRemove, "aria-label": "remove condition", children: /* @__PURE__ */ jsx2(CloseIcon, { fontSize: "small" }) }),
1308
+ editable && /* @__PURE__ */ jsxs2(
1309
+ Popover,
1310
+ {
1311
+ open: !!popover,
1312
+ anchorEl: popover?.anchor,
1313
+ onClose: closePopover,
1314
+ anchorOrigin: { vertical: "bottom", horizontal: "left" },
1315
+ children: [
1316
+ popover?.target === "field" && /* @__PURE__ */ jsx2(List, { dense: true, children: fields.map((f) => /* @__PURE__ */ jsx2(
1317
+ ListItemButton,
1318
+ {
1319
+ selected: f.value === condition.field.value,
1320
+ onClick: () => selectField(f),
1321
+ children: /* @__PURE__ */ jsx2(ListItemText, { primary: f.label })
1322
+ },
1323
+ f.value
1324
+ )) }),
1325
+ popover?.target === "operator" && /* @__PURE__ */ jsx2(List, { dense: true, children: activeOperators.map((op) => /* @__PURE__ */ jsx2(
1326
+ ListItemButton,
1327
+ {
1328
+ selected: op.value === condition.operator.value,
1329
+ onClick: () => selectOperator(op),
1330
+ children: /* @__PURE__ */ jsx2(ListItemText, { primary: op.label })
1331
+ },
1332
+ op.value
1333
+ )) }),
1334
+ popover?.target === "value" && (fieldValues ? /* @__PURE__ */ jsx2(List, { dense: true, children: fieldValues.map((v) => /* @__PURE__ */ jsx2(
1335
+ ListItemButton,
1336
+ {
1337
+ selected: v.value === condition.value.value,
1338
+ onClick: () => submitValue(v.value),
1339
+ children: /* @__PURE__ */ jsx2(ListItemText, { primary: v.label })
1340
+ },
1341
+ v.value
1342
+ )) }) : /* @__PURE__ */ jsx2("div", { className: "rcui-popover-value-edit", children: /* @__PURE__ */ jsx2(
1343
+ TextField2,
1344
+ {
1345
+ size: "small",
1346
+ autoFocus: true,
1347
+ value: valueEdit,
1348
+ onChange: (e) => setValueEdit(e.target.value),
1349
+ onKeyDown: (e) => {
1350
+ if (e.key === "Enter") submitValue(valueEdit);
1351
+ },
1352
+ placeholder: "Enter value"
1353
+ }
1354
+ ) }))
1355
+ ]
1356
+ }
1357
+ )
1358
+ ] });
1359
+ }
1360
+
1361
+ // src/components/ConnectorChip.tsx
1362
+ import Chip2 from "@mui/material/Chip";
1363
+ import { jsx as jsx3 } from "react/jsx-runtime";
1364
+ function ConnectorChip({ connector, onToggle }) {
1365
+ const chipClass = ["rcui-connector-chip", onToggle ? "rcui-connector-chip--clickable" : ""].filter(Boolean).join(" ");
1366
+ return /* @__PURE__ */ jsx3("div", { className: "rcui-connector", children: /* @__PURE__ */ jsx3(
1367
+ Chip2,
1368
+ {
1369
+ label: connector.toUpperCase(),
1370
+ size: "small",
1371
+ variant: "filled",
1372
+ onClick: onToggle,
1373
+ className: chipClass
1374
+ }
1375
+ ) });
1376
+ }
1377
+
1378
+ // src/components/OutputDndContext.tsx
1379
+ import { useCallback as useCallback4, useState as useState4 } from "react";
1380
+ import {
1381
+ DndContext,
1382
+ DragOverlay,
1383
+ pointerWithin,
1384
+ PointerSensor,
1385
+ KeyboardSensor,
1386
+ useSensor,
1387
+ useSensors
1388
+ } from "@dnd-kit/core";
1389
+ import { sortableKeyboardCoordinates } from "@dnd-kit/sortable";
1390
+
1391
+ // src/components/output-drag-end.ts
1392
+ var UNGROUP_ZONE_ID = "ungrouped-drop-zone";
1393
+ var GROUP_DROPPABLE_PREFIX = "group:";
1394
+ function findGroupByEntryId(groups, entryId) {
1395
+ for (const group of groups) {
1396
+ if (group.entries.some((e) => e.id === entryId)) return group;
1397
+ }
1398
+ return null;
1399
+ }
1400
+ function applyOutputDragEnd(groups, activeId, overId, mutations) {
1401
+ if (!overId || activeId === overId) return;
1402
+ const sourceGroup = findGroupByEntryId(groups, activeId);
1403
+ if (!sourceGroup) return;
1404
+ if (overId === UNGROUP_ZONE_ID) {
1405
+ mutations.ungroupEntry(sourceGroup.id, activeId);
1406
+ return;
1407
+ }
1408
+ if (overId.startsWith(GROUP_DROPPABLE_PREFIX)) {
1409
+ const targetGroupId = overId.slice(GROUP_DROPPABLE_PREFIX.length);
1410
+ if (targetGroupId !== sourceGroup.id) {
1411
+ mutations.moveBetweenGroups(sourceGroup.id, targetGroupId, activeId, null);
1412
+ }
1413
+ return;
1414
+ }
1415
+ const targetGroup = findGroupByEntryId(groups, overId);
1416
+ if (targetGroup && sourceGroup.id === targetGroup.id) {
1417
+ mutations.reorderWithinGroup(sourceGroup.id, activeId, overId);
1418
+ } else if (targetGroup) {
1419
+ mutations.moveBetweenGroups(sourceGroup.id, targetGroup.id, activeId, overId);
1420
+ } else {
1421
+ mutations.ungroupEntry(sourceGroup.id, activeId);
1422
+ }
1423
+ }
1424
+
1425
+ // src/components/OutputDndContext.tsx
1426
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1427
+ function OutputDndContext({ groups, mutations, children, renderOverlay }) {
1428
+ const [activeEntry, setActiveEntry] = useState4(null);
1429
+ const sensors = useSensors(
1430
+ useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
1431
+ useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
1432
+ );
1433
+ const handleDragStart = useCallback4(
1434
+ (event) => {
1435
+ const entryId = String(event.active.id);
1436
+ for (const group of groups) {
1437
+ const entry = group.entries.find((e) => e.id === entryId);
1438
+ if (entry) {
1439
+ setActiveEntry(entry);
1440
+ return;
1441
+ }
1442
+ }
1443
+ },
1444
+ [groups]
1445
+ );
1446
+ const handleDragEnd = useCallback4(
1447
+ (event) => {
1448
+ setActiveEntry(null);
1449
+ const over = event.over;
1450
+ applyOutputDragEnd(
1451
+ groups,
1452
+ String(event.active.id),
1453
+ over ? String(over.id) : null,
1454
+ mutations
1455
+ );
1456
+ },
1457
+ [groups, mutations]
1458
+ );
1459
+ return /* @__PURE__ */ jsxs3(
1460
+ DndContext,
1461
+ {
1462
+ sensors,
1463
+ collisionDetection: pointerWithin,
1464
+ onDragStart: handleDragStart,
1465
+ onDragEnd: handleDragEnd,
1466
+ children: [
1467
+ children,
1468
+ /* @__PURE__ */ jsx4(DragOverlay, { dropAnimation: null, children: activeEntry && renderOverlay ? renderOverlay(activeEntry) : null })
1469
+ ]
1470
+ }
1471
+ );
1472
+ }
1473
+
1474
+ // src/hooks/useConditionalOutput.ts
1475
+ import { useState as useState5, useCallback as useCallback5, useRef as useRef2, useEffect } from "react";
1476
+ function useConditionalOutput({
1477
+ groups: controlledGroups,
1478
+ onGroupsChange
1479
+ } = {}) {
1480
+ const [internalGroups, setInternalGroups] = useState5([]);
1481
+ const isControlled = controlledGroups !== void 0;
1482
+ const groups = isControlled ? controlledGroups : internalGroups;
1483
+ const onGroupsChangeRef = useRef2(onGroupsChange);
1484
+ useEffect(() => {
1485
+ onGroupsChangeRef.current = onGroupsChange;
1486
+ }, [onGroupsChange]);
1487
+ const didMount = useRef2(false);
1488
+ useEffect(() => {
1489
+ if (!didMount.current) {
1490
+ didMount.current = true;
1491
+ onGroupsChangeRef.current?.(groups);
1492
+ }
1493
+ }, []);
1494
+ const commit = useCallback5(
1495
+ (next) => {
1496
+ if (!isControlled) setInternalGroups(next);
1497
+ onGroupsChangeRef.current?.(next);
1498
+ },
1499
+ [isControlled]
1500
+ );
1501
+ const addGroup = useCallback5(
1502
+ (group) => {
1503
+ commit([...groups, group]);
1504
+ },
1505
+ [groups, commit]
1506
+ );
1507
+ const removeEntry = useCallback5(
1508
+ (groupId, entryId) => {
1509
+ commit(
1510
+ groups.map(
1511
+ (g) => g.id === groupId ? { ...g, entries: g.entries.filter((e) => e.id !== entryId) } : g
1512
+ ).filter((g) => g.entries.length > 0)
1513
+ );
1514
+ },
1515
+ [groups, commit]
1516
+ );
1517
+ const toggleConnector = useCallback5(
1518
+ (groupId, entryId) => {
1519
+ commit(
1520
+ groups.map((g) => {
1521
+ if (g.id !== groupId) return g;
1522
+ return {
1523
+ ...g,
1524
+ entries: g.entries.map(
1525
+ (e) => e.id === entryId ? {
1526
+ ...e,
1527
+ connector: e.connector === "and" ? "or" : "and"
1528
+ } : e
1529
+ )
1530
+ };
1531
+ })
1532
+ );
1533
+ },
1534
+ [groups, commit]
1535
+ );
1536
+ const updateCondition = useCallback5(
1537
+ (groupId, entryId, condition) => {
1538
+ commit(
1539
+ groups.map((g) => {
1540
+ if (g.id !== groupId) return g;
1541
+ return {
1542
+ ...g,
1543
+ entries: g.entries.map((e) => e.id === entryId ? { ...e, condition } : e)
1544
+ };
1545
+ })
1546
+ );
1547
+ },
1548
+ [groups, commit]
1549
+ );
1550
+ const updateGroupConfig = useCallback5(
1551
+ (groupId, config) => {
1552
+ commit(
1553
+ groups.map(
1554
+ (g) => g.id === groupId ? { ...g, config: { ...g.config, ...config } } : g
1555
+ )
1556
+ );
1557
+ },
1558
+ [groups, commit]
1559
+ );
1560
+ const reorderWithinGroup = useCallback5(
1561
+ (groupId, activeId, overId) => {
1562
+ commit(
1563
+ groups.map((g) => {
1564
+ if (g.id !== groupId) return g;
1565
+ const oldIdx = g.entries.findIndex((e) => e.id === activeId);
1566
+ const newIdx = g.entries.findIndex((e) => e.id === overId);
1567
+ if (oldIdx === -1 || newIdx === -1) return g;
1568
+ const entries = [...g.entries];
1569
+ const [moved] = entries.splice(oldIdx, 1);
1570
+ entries.splice(newIdx, 0, moved);
1571
+ return { ...g, entries };
1572
+ })
1573
+ );
1574
+ },
1575
+ [groups, commit]
1576
+ );
1577
+ const moveBetweenGroups = useCallback5(
1578
+ (sourceGroupId, targetGroupId, entryId, overEntryId) => {
1579
+ const sourceGroup = groups.find((g) => g.id === sourceGroupId);
1580
+ const targetGroup = groups.find((g) => g.id === targetGroupId);
1581
+ if (!sourceGroup || !targetGroup) return;
1582
+ const entry = sourceGroup.entries.find((e) => e.id === entryId);
1583
+ if (!entry) return;
1584
+ const targetConnector = targetGroup.entries.length > 0 ? targetGroup.entries[0].connector : "and";
1585
+ const movedEntry = { ...entry, connector: targetConnector };
1586
+ let newGroups = groups.map((g) => {
1587
+ if (g.id === sourceGroupId) {
1588
+ return { ...g, entries: g.entries.filter((e) => e.id !== entryId) };
1589
+ }
1590
+ if (g.id === targetGroupId) {
1591
+ const entries = [...g.entries];
1592
+ if (overEntryId) {
1593
+ const idx = entries.findIndex((e) => e.id === overEntryId);
1594
+ entries.splice(idx + 1, 0, movedEntry);
1595
+ } else {
1596
+ entries.push(movedEntry);
1597
+ }
1598
+ return { ...g, entries };
1599
+ }
1600
+ return g;
1601
+ });
1602
+ commit(newGroups.filter((g) => g.entries.length > 0));
1603
+ },
1604
+ [groups, commit]
1605
+ );
1606
+ const ungroupEntry = useCallback5(
1607
+ (groupId, entryId) => {
1608
+ const group = groups.find((g) => g.id === groupId);
1609
+ if (!group) return;
1610
+ const entry = group.entries.find((e) => e.id === entryId);
1611
+ if (!entry) return;
1612
+ const newGroup = {
1613
+ id: generateId(),
1614
+ entries: [{ ...entry, connector: "and" }]
1615
+ };
1616
+ let newGroups = groups.map(
1617
+ (g) => g.id === groupId ? { ...g, entries: g.entries.filter((e) => e.id !== entryId) } : g
1618
+ ).filter((g) => g.entries.length > 0);
1619
+ newGroups.push(newGroup);
1620
+ commit(newGroups);
1621
+ },
1622
+ [groups, commit]
1623
+ );
1624
+ const setGroups = useCallback5((next) => commit(next), [commit]);
1625
+ const mutations = {
1626
+ addGroup,
1627
+ removeEntry,
1628
+ toggleConnector,
1629
+ updateCondition,
1630
+ updateGroupConfig,
1631
+ reorderWithinGroup,
1632
+ moveBetweenGroups,
1633
+ ungroupEntry,
1634
+ setGroups
1635
+ };
1636
+ return { groups, mutations };
1637
+ }
1638
+
1639
+ // src/components/Output.tsx
1640
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1641
+ var DEFAULT_GROUP_CONFIG = {
1642
+ editable: true,
1643
+ removable: true,
1644
+ variant: "outlined"
1645
+ };
1646
+ function Output({
1647
+ fields,
1648
+ operators,
1649
+ groups: controlledGroups,
1650
+ onGroupsChange,
1651
+ defaultGroupConfig,
1652
+ className,
1653
+ style
1654
+ }) {
1655
+ const { groups, mutations } = useConditionalOutput({
1656
+ groups: controlledGroups,
1657
+ onGroupsChange
1658
+ });
1659
+ const readOnly = controlledGroups !== void 0 && !onGroupsChange;
1660
+ const effectiveDefault = readOnly ? { editable: false, removable: false, ...defaultGroupConfig } : defaultGroupConfig;
1661
+ const rootClass = ["rcui-output", className].filter(Boolean).join(" ");
1662
+ const renderOverlay = (entry) => /* @__PURE__ */ jsxs4("div", { className: "rcui-overlay", children: [
1663
+ /* @__PURE__ */ jsx5(Chip3, { label: entry.condition.field.label, size: "small", className: "rcui-chip-field" }),
1664
+ /* @__PURE__ */ jsx5(
1665
+ Chip3,
1666
+ {
1667
+ label: entry.condition.operator.label,
1668
+ size: "small",
1669
+ className: "rcui-chip-operator"
1670
+ }
1671
+ ),
1672
+ /* @__PURE__ */ jsx5(
1673
+ Chip3,
1674
+ {
1675
+ label: entry.condition.value.label || "\u2026",
1676
+ size: "small",
1677
+ className: "rcui-chip-value"
1678
+ }
1679
+ )
1680
+ ] });
1681
+ return /* @__PURE__ */ jsx5(OutputDndContext, { groups, mutations, renderOverlay, children: /* @__PURE__ */ jsx5(DropZone, { className: rootClass, style, children: groups.length === 0 ? /* @__PURE__ */ jsx5(Typography, { variant: "body2", color: "text.secondary", children: "Parsed condition will appear here\u2026" }) : groups.map((group) => /* @__PURE__ */ jsx5(
1682
+ GroupCard,
1683
+ {
1684
+ group,
1685
+ fields,
1686
+ operators,
1687
+ config: resolveConfig(group.config, effectiveDefault),
1688
+ mutations
1689
+ },
1690
+ group.id
1691
+ )) }) });
1692
+ }
1693
+ function resolveConfig(groupConfig, defaultConfig) {
1694
+ return { ...DEFAULT_GROUP_CONFIG, ...defaultConfig, ...groupConfig };
1695
+ }
1696
+ function GroupCard({
1697
+ group,
1698
+ fields,
1699
+ operators,
1700
+ config,
1701
+ mutations
1702
+ }) {
1703
+ const allIds = group.entries.map((e) => e.id);
1704
+ const isMulti = group.entries.length > 1;
1705
+ const { setNodeRef, isOver } = useDroppable({ id: `group:${group.id}` });
1706
+ const entries = /* @__PURE__ */ jsxs4(SortableContext, { items: allIds, strategy: verticalListSortingStrategy, children: [
1707
+ config.label && /* @__PURE__ */ jsx5(Typography, { variant: "caption", color: "text.secondary", className: "rcui-group-label", children: config.label }),
1708
+ /* @__PURE__ */ jsx5("div", { style: { display: "flex", flexDirection: "column", gap: 4 }, children: group.entries.map((entry, idx) => /* @__PURE__ */ jsxs4("div", { children: [
1709
+ idx > 0 && /* @__PURE__ */ jsx5(
1710
+ ConnectorChip,
1711
+ {
1712
+ connector: entry.connector,
1713
+ onToggle: config.editable ? () => mutations.toggleConnector(group.id, entry.id) : void 0
1714
+ }
1715
+ ),
1716
+ /* @__PURE__ */ jsx5(
1717
+ OutputRow,
1718
+ {
1719
+ id: entry.id,
1720
+ condition: entry.condition,
1721
+ fields,
1722
+ operators,
1723
+ onUpdate: config.editable ? (c) => mutations.updateCondition(group.id, entry.id, c) : void 0,
1724
+ onRemove: config.removable ? () => mutations.removeEntry(group.id, entry.id) : void 0
1725
+ }
1726
+ )
1727
+ ] }, entry.id)) })
1728
+ ] });
1729
+ if (!isMulti) {
1730
+ const singleClass = ["rcui-droppable-single", isOver ? "rcui-droppable-single--over" : ""].filter(Boolean).join(" ");
1731
+ return /* @__PURE__ */ jsx5("div", { ref: setNodeRef, className: singleClass, children: entries });
1732
+ }
1733
+ const paperClass = ["rcui-group-paper", isOver ? "rcui-group-paper--over" : ""].filter(Boolean).join(" ");
1734
+ return /* @__PURE__ */ jsx5(
1735
+ Paper,
1736
+ {
1737
+ ref: setNodeRef,
1738
+ variant: config.variant,
1739
+ className: paperClass,
1740
+ children: entries
1741
+ }
1742
+ );
1743
+ }
1744
+ function DropZone({
1745
+ children,
1746
+ className,
1747
+ style
1748
+ }) {
1749
+ const { setNodeRef, isOver } = useDroppable({ id: UNGROUP_ZONE_ID });
1750
+ const zoneClass = ["rcui-drop-zone", isOver ? "rcui-drop-zone--over" : "", className].filter(Boolean).join(" ");
1751
+ return /* @__PURE__ */ jsx5("div", { ref: setNodeRef, className: zoneClass, style, children });
1752
+ }
1753
+
1754
+ // src/components/ConditionalUI.tsx
1755
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1756
+ function ConditionalUI({
1757
+ fields,
1758
+ operators = DEFAULT_OPERATORS,
1759
+ value,
1760
+ onChange,
1761
+ onConditionsChange,
1762
+ className,
1763
+ style
1764
+ }) {
1765
+ const { groups, mutations } = useConditionalOutput({
1766
+ onGroupsChange: onConditionsChange
1767
+ });
1768
+ const rootClass = ["rcui-root", className].filter(Boolean).join(" ");
1769
+ return /* @__PURE__ */ jsxs5("div", { className: rootClass, style, children: [
1770
+ /* @__PURE__ */ jsx6(
1771
+ Input,
1772
+ {
1773
+ fields,
1774
+ operators,
1775
+ value,
1776
+ onChange,
1777
+ onSubmit: mutations.addGroup
1778
+ }
1779
+ ),
1780
+ /* @__PURE__ */ jsx6(
1781
+ Output,
1782
+ {
1783
+ groups,
1784
+ fields,
1785
+ operators,
1786
+ onGroupsChange: mutations.setGroups
1787
+ }
1788
+ )
1789
+ ] });
1790
+ }
1791
+ export {
1792
+ ConditionDataProvider,
1793
+ ConditionDataProvider as ConditionParser,
1794
+ ConditionalUI,
1795
+ DEFAULT_OPERATORS,
1796
+ Input,
1797
+ Output,
1798
+ useConditionDataProvider,
1799
+ useConditionalInput,
1800
+ useConditionalOutput
1801
+ };
1802
+ //# sourceMappingURL=index.js.map