react-conditional-ui 1.2.0 → 1.2.1

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 CHANGED
@@ -1,8 +1,11 @@
1
1
  // src/components/Input.tsx
2
2
  import { useCallback as useCallback3, useState as useState2, useRef, useLayoutEffect, forwardRef } from "react";
3
3
  import TextField from "@mui/material/TextField";
4
+ import ClickAwayListener from "@mui/material/ClickAwayListener";
4
5
  import InputAdornment from "@mui/material/InputAdornment";
5
6
  import IconButton from "@mui/material/IconButton";
7
+ import Stack from "@mui/material/Stack";
8
+ import AddIcon from "@mui/icons-material/Add";
6
9
  import KeyboardReturnIcon from "@mui/icons-material/KeyboardReturn";
7
10
  import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
8
11
  import Tooltip from "@mui/material/Tooltip";
@@ -159,74 +162,70 @@ import createDebug from "debug";
159
162
  var BASE = "react-conditional-ui";
160
163
  var createLogger = (namespace) => createDebug(`${BASE}:${namespace}`);
161
164
 
162
- // src/fuzzy/match-engine.ts
163
- var log = createLogger("match-engine");
164
- var valueLog = createLogger("value");
165
+ // src/fuzzy/operator-policy.ts
165
166
  var TYPE_ALLOWED_OPS = {
166
167
  number: /* @__PURE__ */ new Set(["eq", "ne", "gt", "lt", "gte", "lte"]),
167
168
  enum: /* @__PURE__ */ new Set(["eq", "ne"]),
168
169
  text: /* @__PURE__ */ new Set(["eq", "ne", "contains", "starts_with"])
169
170
  };
171
+ function allowedOperatorsForField(field, allOperators) {
172
+ if (field.operators) return field.operators;
173
+ if (field.type) {
174
+ const allowed = TYPE_ALLOWED_OPS[field.type];
175
+ return allOperators.filter((op) => allowed.has(op.value));
176
+ }
177
+ return allOperators;
178
+ }
179
+
180
+ // src/fuzzy/match-engine.ts
181
+ var log = createLogger("match-engine");
182
+ var valueLog = createLogger("value");
183
+ var FUSE_THRESHOLD = 0.4;
184
+ var OPERATOR_ALIAS_MIN_COVERAGE = 0.5;
170
185
  var MatchEngine = class _MatchEngine {
171
186
  constructor(fields, operators) {
172
187
  this.fields = fields;
173
188
  this.operators = operators;
174
189
  this.fieldFuse = new Fuse(fields, {
175
190
  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,
191
+ threshold: FUSE_THRESHOLD,
185
192
  includeScore: true
186
193
  });
194
+ this.opFuse = this.createOperatorAliasFuse(operators);
187
195
  this.perFieldOpFuse = /* @__PURE__ */ new Map();
188
196
  for (const field of fields) {
189
197
  const restricted = this.resolveOpsForField(field, operators);
190
198
  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
- );
199
+ this.perFieldOpFuse.set(field.value, this.createOperatorAliasFuse(restricted));
198
200
  }
199
201
  }
200
202
  }
201
203
  matchField(candidate) {
202
204
  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 };
205
+ const best = results[0];
206
+ if (best && _MatchEngine.isWithinThreshold(best.score)) {
207
+ log("field match: %s -> %s (score: %f)", candidate, best.item.value, best.score);
208
+ return { option: best.item, score: best.score ?? 1 };
211
209
  }
212
210
  return null;
213
211
  }
214
212
  matchOperator(candidate, field) {
215
213
  const fuse = (field && this.perFieldOpFuse.get(field.value)) ?? this.opFuse;
216
214
  const results = fuse.search(candidate);
217
- if (results.length > 0 && (results[0].score ?? 1) <= 0.4) {
218
- const alias = results[0].item.alias;
215
+ const best = results[0];
216
+ if (best && _MatchEngine.isWithinThreshold(best.score)) {
217
+ const alias = best.item.alias;
219
218
  const candidateWords = candidate.split(/\s+/).length;
220
219
  const aliasWords = alias.split(/\s+/).length;
221
220
  if (candidateWords !== aliasWords) return null;
222
- if (candidate.length < alias.length * 0.5) return null;
221
+ if (candidate.length < alias.length * OPERATOR_ALIAS_MIN_COVERAGE) return null;
223
222
  log(
224
223
  "operator match: %s -> %s (score: %f)",
225
224
  candidate,
226
- results[0].item.operator.value,
227
- results[0].score
225
+ best.item.operator.value,
226
+ best.score
228
227
  );
229
- return { option: results[0].item.operator, score: results[0].score ?? 1 };
228
+ return { option: best.item.operator, score: best.score ?? 1 };
230
229
  }
231
230
  return null;
232
231
  }
@@ -261,18 +260,14 @@ var MatchEngine = class _MatchEngine {
261
260
  if (raw.length === 0) return Value.invalid(raw, "Value not recognized");
262
261
  const fuse = new Fuse(knownValues, {
263
262
  keys: ["label", "value"],
264
- threshold: 0.4,
263
+ threshold: FUSE_THRESHOLD,
265
264
  includeScore: true
266
265
  });
267
266
  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);
267
+ const best = results[0];
268
+ if (best && _MatchEngine.isWithinThreshold(best.score)) {
269
+ valueLog("fuzzy match: raw=%s -> %s (score: %f)", raw, best.item.value, best.score);
270
+ return _MatchEngine.applyValidator(raw, best.item, validateValue);
276
271
  }
277
272
  valueLog("no match: raw=%s", raw);
278
273
  return Value.invalid(raw, "Value not recognized");
@@ -285,51 +280,22 @@ var MatchEngine = class _MatchEngine {
285
280
  return Value.valid(raw, matched);
286
281
  }
287
282
  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;
283
+ return allowedOperatorsForField(field, allOps);
294
284
  }
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;
285
+ createOperatorAliasFuse(operators) {
286
+ const aliases = operators.flatMap(
287
+ (op) => op.aliases.map((alias) => ({ alias: alias.toLowerCase(), operator: op }))
288
+ );
289
+ return new Fuse(aliases, {
290
+ keys: ["alias"],
291
+ threshold: FUSE_THRESHOLD,
292
+ includeScore: true
293
+ });
316
294
  }
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
- }
295
+ static isWithinThreshold(score) {
296
+ return (score ?? 1) <= FUSE_THRESHOLD;
330
297
  }
331
- return score;
332
- }
298
+ };
333
299
 
334
300
  // src/fuzzy/word-utils.ts
335
301
  var NOISE_WORDS = /* @__PURE__ */ new Set([
@@ -399,18 +365,113 @@ function stripLeadingNoise(words) {
399
365
  return result;
400
366
  }
401
367
 
368
+ // src/fuzzy/score.ts
369
+ var AMBIGUOUS_OP_WORDS = /* @__PURE__ */ new Set(["is", "has"]);
370
+ var AMBIGUITY_PENALTY = 0.35;
371
+ var LINKING_VERB_PENALTY = 0.35;
372
+ var LENGTH_BONUS_PER_WORD = 0.01;
373
+ var GAP_PENALTY_PER_WORD = 0.3;
374
+ var UNPARSED_PENALTY = 10;
375
+ var FREETEXT_WORD_COST = 0.1;
376
+ function adjustOperatorScore(baseScore, words, start, end) {
377
+ let score = baseScore;
378
+ const firstWord = words[start];
379
+ if (end - start === 1 && AMBIGUOUS_OP_WORDS.has(firstWord)) {
380
+ score += AMBIGUITY_PENALTY;
381
+ } else if (end - start > 1 && AMBIGUOUS_OP_WORDS.has(firstWord)) {
382
+ score += LINKING_VERB_PENALTY;
383
+ }
384
+ score -= (end - start) * LENGTH_BONUS_PER_WORD;
385
+ if (start > 0) {
386
+ score += start * GAP_PENALTY_PER_WORD;
387
+ }
388
+ return score;
389
+ }
390
+ function scoreConditions(conditions, segmentCount) {
391
+ const unparsed = segmentCount - conditions.length;
392
+ let score = unparsed * UNPARSED_PENALTY;
393
+ for (const c of conditions) {
394
+ score += c.score;
395
+ if (!c.field.isValid) score += UNPARSED_PENALTY;
396
+ if (!c.operator.isValid) score += UNPARSED_PENALTY;
397
+ if (!c.value.isValid) score += UNPARSED_PENALTY;
398
+ if (c.value.isValid && c.value.matchedOption === null) {
399
+ score += c.value.raw.split(/\s+/).length * FREETEXT_WORD_COST;
400
+ }
401
+ }
402
+ return score;
403
+ }
404
+
405
+ // src/conditions/query-helper.ts
406
+ var ConditionQueryHelper = class {
407
+ constructor(engine, fields, operators) {
408
+ this.engine = engine;
409
+ this.fields = fields;
410
+ this.operators = operators;
411
+ }
412
+ getFields() {
413
+ return this.fields;
414
+ }
415
+ findFieldByValue(value) {
416
+ return this.fields.find((field) => field.value === value);
417
+ }
418
+ identifyField(words) {
419
+ let best = null;
420
+ for (let i = words.length; i >= 1; i--) {
421
+ const candidate = words.slice(0, i).join(" ");
422
+ const match = this.engine.matchField(candidate);
423
+ if (match && (!best || match.score < best.match.score)) {
424
+ best = { candidate, match, wordCount: i };
425
+ }
426
+ }
427
+ if (!best) return null;
428
+ return {
429
+ raw: best.candidate,
430
+ fieldOption: best.match.option,
431
+ fieldScore: best.match.score,
432
+ remaining: words.slice(best.wordCount)
433
+ };
434
+ }
435
+ getOperatorCandidates(words, fieldOption) {
436
+ const candidates = [];
437
+ for (let start = 0; start < words.length; start++) {
438
+ for (let end = start + 1; end <= words.length; end++) {
439
+ const opRaw = words.slice(start, end).join(" ");
440
+ const opMatch = this.engine.matchOperator(opRaw, fieldOption);
441
+ if (!opMatch) continue;
442
+ candidates.push({
443
+ match: opMatch,
444
+ raw: opRaw,
445
+ endIdx: end,
446
+ adjustedScore: adjustOperatorScore(opMatch.score, words, start, end)
447
+ });
448
+ }
449
+ }
450
+ candidates.sort((a, b) => a.adjustedScore - b.adjustedScore);
451
+ return candidates;
452
+ }
453
+ allowedOpsForField(field) {
454
+ return allowedOperatorsForField(field, this.operators);
455
+ }
456
+ prefixMatches(partial, candidates, limit = 6) {
457
+ const lower = partial.toLowerCase();
458
+ const results = [];
459
+ for (const candidate of candidates) {
460
+ const cl = candidate.toLowerCase();
461
+ if (cl.startsWith(lower) && cl !== lower) {
462
+ results.push({ completion: cl.slice(lower.length), display: candidate });
463
+ if (results.length >= limit) break;
464
+ }
465
+ }
466
+ return results;
467
+ }
468
+ };
469
+
402
470
  // src/conditions/parser.ts
403
471
  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
472
  var ConditionParser = class {
410
- constructor(engine) {
411
- this.fields = engine.fields;
412
- this.operators = engine.operators;
413
- this.engine = engine;
473
+ constructor(engine, queryHelper) {
474
+ this.query = queryHelper ?? new ConditionQueryHelper(engine, engine.fields, engine.operators);
414
475
  }
415
476
  parse(text) {
416
477
  const input = text.trim().toLowerCase();
@@ -418,11 +479,15 @@ var ConditionParser = class {
418
479
  const allWords = input.split(/\s+/);
419
480
  const words = stripLeadingNoise(allWords);
420
481
  if (words.length === 0) return null;
421
- const fieldResult = this.identifyField(words);
482
+ const fieldResult = this.query.identifyField(words);
422
483
  if (!fieldResult) return null;
423
484
  if (fieldResult.remaining.length === 0) {
424
485
  return {
425
- field: fieldResult.field,
486
+ field: new Field(
487
+ fieldResult.raw,
488
+ fieldResult.fieldOption.value,
489
+ fieldResult.fieldOption.label
490
+ ),
426
491
  operator: Operator.invalid(""),
427
492
  value: Value.empty(),
428
493
  score: fieldResult.fieldScore
@@ -433,7 +498,11 @@ var ConditionParser = class {
433
498
  fieldResult.fieldOption
434
499
  );
435
500
  const result = {
436
- field: fieldResult.field,
501
+ field: new Field(
502
+ fieldResult.raw,
503
+ fieldResult.fieldOption.value,
504
+ fieldResult.fieldOption.label
505
+ ),
437
506
  operator,
438
507
  value,
439
508
  score: fieldResult.fieldScore + operatorScore
@@ -453,40 +522,8 @@ var ConditionParser = class {
453
522
  );
454
523
  return result;
455
524
  }
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);
525
+ resolveOperator(words, fieldOption) {
526
+ const candidates = this.query.getOperatorCandidates(words, fieldOption);
490
527
  if (candidates.length > 0) {
491
528
  log2(
492
529
  "operator candidates: %o",
@@ -495,10 +532,6 @@ var ConditionParser = class {
495
532
  )
496
533
  );
497
534
  }
498
- return candidates;
499
- }
500
- resolveOperator(words, fieldOption) {
501
- const candidates = this.getOperatorCandidates(words, fieldOption);
502
535
  if (candidates.length === 0) {
503
536
  return {
504
537
  operator: Operator.invalid(words.join(" ")),
@@ -533,33 +566,6 @@ var ConditionParser = class {
533
566
  operatorScore: best.adjustedScore
534
567
  };
535
568
  }
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
569
  };
564
570
 
565
571
  // src/consts.ts
@@ -568,8 +574,8 @@ var OR_CONJUNCTION = "or";
568
574
 
569
575
  // src/fuzzy/providers/suggestions.ts
570
576
  var SuggestionsProvider = class {
571
- constructor(parser, segmentResolver) {
572
- this.parser = parser;
577
+ constructor(query, segmentResolver) {
578
+ this.query = query;
573
579
  this.segmentResolver = segmentResolver;
574
580
  }
575
581
  getCompletions(text, limit = 6) {
@@ -595,40 +601,40 @@ var SuggestionsProvider = class {
595
601
  completionsForSegment(text, limit) {
596
602
  const input = text.trimStart().toLowerCase();
597
603
  if (!input) {
598
- return this.parser.fields.slice(0, limit).map((f) => ({ completion: f.label, display: f.label }));
604
+ return this.query.getFields().slice(0, limit).map((f) => ({ completion: f.label, display: f.label }));
599
605
  }
600
606
  const endsWithSpace = /\s$/.test(input);
601
607
  const words = input.split(/\s+/).filter(Boolean);
602
608
  if (words.length === 0) return [];
603
- const fieldResult = this.parser.identifyField(words);
609
+ const fieldResult = this.query.identifyField(words);
604
610
  if (!fieldResult) {
605
611
  if (endsWithSpace) return [];
606
- return this.parser.prefixMatches(
612
+ return this.query.prefixMatches(
607
613
  words.join(" "),
608
- this.parser.fields.map((f) => f.label),
614
+ this.query.getFields().map((f) => f.label),
609
615
  limit
610
616
  );
611
617
  }
612
618
  const fieldOption = fieldResult.fieldOption;
613
619
  if (fieldResult.remaining.length === 0) {
614
620
  if (endsWithSpace) {
615
- const ops = this.parser.allowedOpsForField(fieldOption);
621
+ const ops = this.query.allowedOpsForField(fieldOption);
616
622
  return ops.slice(0, limit).map((op) => ({ completion: op.label, display: op.label }));
617
623
  }
618
- return this.parser.prefixMatches(
624
+ return this.query.prefixMatches(
619
625
  words.join(" "),
620
- this.parser.fields.map((f) => f.label),
626
+ this.query.getFields().map((f) => f.label),
621
627
  limit
622
628
  );
623
629
  }
624
- const candidates = this.parser.getOperatorCandidates(fieldResult.remaining, fieldOption);
630
+ const candidates = this.query.getOperatorCandidates(fieldResult.remaining, fieldOption);
625
631
  const bestOp = candidates[0];
626
632
  if (!bestOp || bestOp.endIdx > fieldResult.remaining.length) {
627
633
  if (endsWithSpace) return [];
628
- const ops = this.parser.allowedOpsForField(fieldOption);
634
+ const ops = this.query.allowedOpsForField(fieldOption);
629
635
  const aliases = ops.flatMap((op) => op.aliases);
630
636
  const opPartial = fieldResult.remaining.join(" ");
631
- return this.parser.prefixMatches(opPartial, aliases, limit);
637
+ return this.query.prefixMatches(opPartial, aliases, limit);
632
638
  }
633
639
  const valueRaw = fieldResult.remaining.slice(bestOp.endIdx).join(" ");
634
640
  if (!valueRaw) {
@@ -637,15 +643,15 @@ var SuggestionsProvider = class {
637
643
  if (!fieldValues2?.length) return [];
638
644
  return fieldValues2.slice(0, limit).map((v) => ({ completion: v.label, display: v.label }));
639
645
  }
640
- const ops = this.parser.allowedOpsForField(fieldOption);
646
+ const ops = this.query.allowedOpsForField(fieldOption);
641
647
  const aliases = ops.flatMap((op) => op.aliases);
642
648
  const opPartial = fieldResult.remaining.join(" ");
643
- return this.parser.prefixMatches(opPartial, aliases, limit);
649
+ return this.query.prefixMatches(opPartial, aliases, limit);
644
650
  }
645
651
  if (endsWithSpace) return [];
646
652
  const fieldValues = fieldOption.fieldValues;
647
653
  if (!fieldValues?.length) return [];
648
- return this.parser.prefixMatches(
654
+ return this.query.prefixMatches(
649
655
  valueRaw,
650
656
  fieldValues.map((v) => v.label),
651
657
  limit
@@ -670,8 +676,8 @@ var SuggestionsProvider = class {
670
676
 
671
677
  // src/fuzzy/providers/diagnostics.ts
672
678
  var DiagnosticsProvider = class {
673
- constructor(parser, segmentResolver) {
674
- this.parser = parser;
679
+ constructor(query, segmentResolver) {
680
+ this.query = query;
675
681
  this.segmentResolver = segmentResolver;
676
682
  }
677
683
  diagnose(text) {
@@ -698,9 +704,7 @@ var DiagnosticsProvider = class {
698
704
  }
699
705
  if (!condition.operator.isValid) {
700
706
  const fieldEnd = offset + condition.field.raw.length;
701
- const fieldConfig = this.parser.fields.find(
702
- (f) => f.value === condition.field.value
703
- );
707
+ const fieldConfig = this.query.findFieldByValue(condition.field.value);
704
708
  const hasRestriction = fieldConfig?.operators || fieldConfig?.type;
705
709
  diagnostics.push({
706
710
  start: fieldEnd,
@@ -749,60 +753,26 @@ var SegmentResolver = class {
749
753
  resolve(text) {
750
754
  const originalWords = text.split(/\s+/);
751
755
  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
- }
756
+ const conjIndices = this.findConjunctionIndices(lowerWords);
757
+ const fallback = this.createSingleCandidate(text);
758
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
- };
759
+ return fallback;
765
760
  }
766
- const noSplit = this.parser.parse(text);
767
- let best = {
768
- segments: [text],
769
- conditions: noSplit ? [noSplit] : [],
770
- connector: AND_CONJUNCTION
771
- };
761
+ let best = fallback;
772
762
  let bestScore = scoreConditions(best.conditions, 1);
773
763
  for (const subset of powerSet(conjIndices)) {
774
764
  const connector = subset[0].conjunction;
775
765
  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);
766
+ const next = this.buildSplitCandidate(originalWords, subset, connector);
767
+ if (!next) continue;
768
+ const { segments, conditions } = next;
780
769
  const score = scoreConditions(conditions, segments.length);
781
770
  if (score < bestScore) {
782
771
  bestScore = score;
783
772
  best = { segments, conditions, connector };
784
773
  }
785
774
  }
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;
775
+ return this.trySplitFromValueConjunction(best, originalWords, conjIndices) ?? best;
806
776
  }
807
777
  parseConditions(text) {
808
778
  const input = text.trim();
@@ -825,6 +795,54 @@ var SegmentResolver = class {
825
795
  }
826
796
  return conditions;
827
797
  }
798
+ createSingleCandidate(text) {
799
+ const parsed = this.parser.parse(text);
800
+ return {
801
+ segments: [text],
802
+ conditions: parsed ? [parsed] : [],
803
+ connector: AND_CONJUNCTION
804
+ };
805
+ }
806
+ findConjunctionIndices(words) {
807
+ const indices = [];
808
+ for (let i = 0; i < words.length; i++) {
809
+ if (words[i] === AND_CONJUNCTION || words[i] === OR_CONJUNCTION) {
810
+ indices.push({ index: i, conjunction: words[i] });
811
+ }
812
+ }
813
+ return indices;
814
+ }
815
+ buildSplitCandidate(originalWords, splitPoints, connector) {
816
+ const splitIndices = splitPoints.map((s) => s.index);
817
+ const segments = splitAtIndices(originalWords, splitIndices);
818
+ if (segments.some((s) => !s)) return null;
819
+ return {
820
+ segments,
821
+ conditions: this.parseSegments(segments),
822
+ connector
823
+ };
824
+ }
825
+ trySplitFromValueConjunction(best, originalWords, conjIndices) {
826
+ const onlyCondition = best.conditions[0];
827
+ if (best.conditions.length !== 1 || !onlyCondition?.value.isValid || onlyCondition.value.matchedOption !== null) {
828
+ return null;
829
+ }
830
+ const valueWords = onlyCondition.value.raw.split(/\s+/);
831
+ const conjInValue = valueWords.find(
832
+ (w) => w.toLowerCase() === AND_CONJUNCTION || w.toLowerCase() === OR_CONJUNCTION
833
+ );
834
+ if (!conjInValue) return null;
835
+ const connector = conjInValue.toLowerCase();
836
+ const matching = conjIndices.filter((c) => c.conjunction === connector);
837
+ for (const subset of powerSet(matching).reverse()) {
838
+ const candidate = this.buildSplitCandidate(originalWords, subset, connector);
839
+ if (!candidate) continue;
840
+ if (candidate.conditions.length === candidate.segments.length && candidate.conditions.every((c) => c.field.isValid && c.operator.isValid)) {
841
+ return candidate;
842
+ }
843
+ }
844
+ return null;
845
+ }
828
846
  getInherited(segment, previous) {
829
847
  if (!previous) return null;
830
848
  const direct = this.parser.parse(segment);
@@ -849,10 +867,11 @@ var SegmentResolver = class {
849
867
  var ConditionDataProvider = class {
850
868
  constructor(fields, operators) {
851
869
  const engine = new MatchEngine(fields, operators);
852
- const parser = new ConditionParser(engine);
870
+ const query = new ConditionQueryHelper(engine, fields, operators);
871
+ const parser = new ConditionParser(engine, query);
853
872
  this.segments = new SegmentResolver(parser);
854
- this.suggestions = new SuggestionsProvider(parser, this.segments);
855
- this.diagnostics = new DiagnosticsProvider(parser, this.segments);
873
+ this.suggestions = new SuggestionsProvider(query, this.segments);
874
+ this.diagnostics = new DiagnosticsProvider(query, this.segments);
856
875
  }
857
876
  parseComplexCondition(text) {
858
877
  return this.segments.parseConditions(text);
@@ -935,9 +954,12 @@ function useConditionalInput({
935
954
  // src/components/Input.tsx
936
955
  import { jsx, jsxs } from "react/jsx-runtime";
937
956
  function Input(props) {
938
- if (isStandalone(props)) {
939
- return /* @__PURE__ */ jsx(StandaloneInput, { ...props });
957
+ if (isManaged(props)) {
958
+ return /* @__PURE__ */ jsx(ManagedInput, { ...props });
940
959
  }
960
+ return /* @__PURE__ */ jsx(ControlledInput, { ...props });
961
+ }
962
+ function ControlledInput(props) {
941
963
  return /* @__PURE__ */ jsx(
942
964
  InputView,
943
965
  {
@@ -953,7 +975,7 @@ function Input(props) {
953
975
  }
954
976
  );
955
977
  }
956
- function isStandalone(props) {
978
+ function isManaged(props) {
957
979
  return props.fields !== void 0;
958
980
  }
959
981
  function buildOverlaySegments(value, diagnostics) {
@@ -1002,7 +1024,7 @@ var GhostInput = forwardRef(function GhostInput2({ ghost, diagnosticSegments, in
1002
1024
  ) })
1003
1025
  ] });
1004
1026
  });
1005
- function StandaloneInput({
1027
+ function ManagedInput({
1006
1028
  fields,
1007
1029
  operators,
1008
1030
  value: controlledValue,
@@ -1038,7 +1060,7 @@ function InputView({
1038
1060
  value,
1039
1061
  onChange,
1040
1062
  onSubmit,
1041
- placeholder = "e.g. age greater than 18, ctrl+space for suggestions",
1063
+ placeholder = "e.g. age greater than 18 \u2014 Control(Option)+Space for suggestions",
1042
1064
  getSuggestion,
1043
1065
  getCompletions,
1044
1066
  diagnostics = [],
@@ -1049,11 +1071,17 @@ function InputView({
1049
1071
  const [activeIndex, setActiveIndex] = useState2(-1);
1050
1072
  const [cursorLeft, setCursorLeft] = useState2(0);
1051
1073
  const listRef = useRef(null);
1074
+ const inputRef = useRef(null);
1052
1075
  const ghost = getSuggestion && value ? getSuggestion(value)?.completion ?? null : null;
1053
1076
  function closeCompletions() {
1054
1077
  setCompletions([]);
1055
1078
  setActiveIndex(-1);
1056
1079
  }
1080
+ function handleCompletionsClickAway() {
1081
+ if (completions.length === 0) return;
1082
+ closeCompletions();
1083
+ queueMicrotask(() => inputRef.current?.focus());
1084
+ }
1057
1085
  function openCompletions() {
1058
1086
  if (!getCompletions) return;
1059
1087
  const items = getCompletions(value);
@@ -1072,7 +1100,9 @@ function InputView({
1072
1100
  if (ghost) onChange(value + ghost);
1073
1101
  }
1074
1102
  function handleKeyDown(e) {
1075
- if (e.key === " " && e.ctrlKey) {
1103
+ const isSpace = e.key === " " || e.code === "Space";
1104
+ const suggestChord = isSpace && !e.shiftKey && !e.metaKey && (e.ctrlKey || e.altKey);
1105
+ if (suggestChord) {
1076
1106
  e.preventDefault();
1077
1107
  openCompletions();
1078
1108
  return;
@@ -1130,66 +1160,106 @@ function InputView({
1130
1160
  inputValue: value,
1131
1161
  onCursorOffset: setCursorLeft
1132
1162
  };
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"
1163
+ return /* @__PURE__ */ jsx(ClickAwayListener, { onClickAway: handleCompletionsClickAway, children: /* @__PURE__ */ jsxs(
1164
+ "div",
1165
+ {
1166
+ className: ["rcui-input-wrapper", className].filter(Boolean).join(" "),
1167
+ style,
1168
+ children: [
1169
+ /* @__PURE__ */ jsx(
1170
+ TextField,
1171
+ {
1172
+ fullWidth: true,
1173
+ variant: "outlined",
1174
+ size: "small",
1175
+ placeholder,
1176
+ value,
1177
+ onChange: handleChange,
1178
+ onKeyDown: handleKeyDown,
1179
+ className: "rcui-input",
1180
+ slotProps: {
1181
+ input: {
1182
+ inputRef,
1183
+ inputComponent: GhostInput,
1184
+ inputProps: ghostInputProps,
1185
+ endAdornment: /* @__PURE__ */ jsx(InputAdornment, { position: "end", children: /* @__PURE__ */ jsxs(Stack, { direction: "row", alignItems: "center", spacing: 0, children: [
1186
+ getCompletions ? /* @__PURE__ */ jsx(Tooltip, { title: "Show suggestions", arrow: true, children: /* @__PURE__ */ jsx(
1187
+ IconButton,
1188
+ {
1189
+ size: "small",
1190
+ "aria-label": "Show suggestions",
1191
+ className: "rcui-adornment-suggestions",
1192
+ onMouseDown: (e) => {
1193
+ e.preventDefault();
1194
+ openCompletions();
1195
+ },
1196
+ children: /* @__PURE__ */ jsx(AddIcon, { fontSize: "small" })
1197
+ }
1198
+ ) }) : null,
1199
+ hasErrors ? /* @__PURE__ */ jsx(Tooltip, { title: errorSummary, arrow: true, children: /* @__PURE__ */ jsx(
1200
+ IconButton,
1201
+ {
1202
+ size: "small",
1203
+ onMouseDown: (e) => e.preventDefault(),
1204
+ onClick: onSubmit,
1205
+ edge: "end",
1206
+ children: /* @__PURE__ */ jsx(
1207
+ ErrorOutlineIcon,
1208
+ {
1209
+ fontSize: "small",
1210
+ className: "rcui-adornment-error"
1211
+ }
1212
+ )
1213
+ }
1214
+ ) }) : /* @__PURE__ */ jsx(
1215
+ IconButton,
1216
+ {
1217
+ size: "small",
1218
+ onMouseDown: (e) => e.preventDefault(),
1219
+ onClick: onSubmit,
1220
+ edge: "end",
1221
+ children: /* @__PURE__ */ jsx(
1222
+ KeyboardReturnIcon,
1223
+ {
1224
+ fontSize: "small",
1225
+ className: "rcui-adornment-enter"
1226
+ }
1227
+ )
1228
+ }
1229
+ )
1230
+ ] }) })
1161
1231
  }
1162
- ) }) })
1232
+ }
1163
1233
  }
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",
1234
+ ),
1235
+ completions.length > 0 && /* @__PURE__ */ jsx(
1236
+ "ul",
1176
1237
  {
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
- ] });
1238
+ ref: listRef,
1239
+ className: "rcui-completions",
1240
+ role: "listbox",
1241
+ style: { left: cursorLeft },
1242
+ children: completions.map((item, i) => /* @__PURE__ */ jsx(
1243
+ "li",
1244
+ {
1245
+ id: `rcui-completion-${i}`,
1246
+ role: "option",
1247
+ "aria-selected": i === activeIndex,
1248
+ className: `rcui-completion-item${i === activeIndex ? " rcui-completion-item--active" : ""}`,
1249
+ onMouseDown: (e) => {
1250
+ e.preventDefault();
1251
+ acceptCompletion(item);
1252
+ },
1253
+ onMouseEnter: () => setActiveIndex(i),
1254
+ children: item.display
1255
+ },
1256
+ item.display
1257
+ ))
1258
+ }
1259
+ )
1260
+ ]
1261
+ }
1262
+ ) });
1193
1263
  }
1194
1264
 
1195
1265
  // src/components/Output.tsx
@@ -1388,7 +1458,7 @@ import {
1388
1458
  } from "@dnd-kit/core";
1389
1459
  import { sortableKeyboardCoordinates } from "@dnd-kit/sortable";
1390
1460
 
1391
- // src/components/output-drag-end.ts
1461
+ // src/dnd/output-drag-end.ts
1392
1462
  var UNGROUP_ZONE_ID = "ungrouped-drop-zone";
1393
1463
  var GROUP_DROPPABLE_PREFIX = "group:";
1394
1464
  function findGroupByEntryId(groups, entryId) {
@@ -1643,20 +1713,91 @@ var DEFAULT_GROUP_CONFIG = {
1643
1713
  removable: true,
1644
1714
  variant: "outlined"
1645
1715
  };
1646
- function Output({
1716
+ function Output(props) {
1717
+ if (isManagedOutput(props)) return /* @__PURE__ */ jsx5(ManagedOutput, { ...props });
1718
+ if (isReadOnlyOutput(props)) return /* @__PURE__ */ jsx5(ReadOnlyOutput, { ...props });
1719
+ return /* @__PURE__ */ jsx5(ControlledOutput, { ...props });
1720
+ }
1721
+ function ManagedOutput({
1647
1722
  fields,
1648
1723
  operators,
1649
- groups: controlledGroups,
1650
1724
  onGroupsChange,
1651
1725
  defaultGroupConfig,
1652
1726
  className,
1653
1727
  style
1654
1728
  }) {
1655
- const { groups, mutations } = useConditionalOutput({
1656
- groups: controlledGroups,
1657
- onGroupsChange
1658
- });
1659
- const readOnly = controlledGroups !== void 0 && !onGroupsChange;
1729
+ const { groups, mutations } = useConditionalOutput({ onGroupsChange });
1730
+ return /* @__PURE__ */ jsx5(
1731
+ OutputView,
1732
+ {
1733
+ groups,
1734
+ mutations,
1735
+ readOnly: false,
1736
+ fields,
1737
+ operators,
1738
+ defaultGroupConfig,
1739
+ className,
1740
+ style
1741
+ }
1742
+ );
1743
+ }
1744
+ function ControlledOutput({
1745
+ fields,
1746
+ operators,
1747
+ groups,
1748
+ onGroupsChange,
1749
+ defaultGroupConfig,
1750
+ className,
1751
+ style
1752
+ }) {
1753
+ const { mutations } = useConditionalOutput({ groups, onGroupsChange });
1754
+ return /* @__PURE__ */ jsx5(
1755
+ OutputView,
1756
+ {
1757
+ groups,
1758
+ mutations,
1759
+ readOnly: false,
1760
+ fields,
1761
+ operators,
1762
+ defaultGroupConfig,
1763
+ className,
1764
+ style
1765
+ }
1766
+ );
1767
+ }
1768
+ function ReadOnlyOutput({
1769
+ fields,
1770
+ operators,
1771
+ groups,
1772
+ defaultGroupConfig,
1773
+ className,
1774
+ style
1775
+ }) {
1776
+ const { mutations } = useConditionalOutput({ groups });
1777
+ return /* @__PURE__ */ jsx5(
1778
+ OutputView,
1779
+ {
1780
+ groups,
1781
+ mutations,
1782
+ readOnly: true,
1783
+ fields,
1784
+ operators,
1785
+ defaultGroupConfig,
1786
+ className,
1787
+ style
1788
+ }
1789
+ );
1790
+ }
1791
+ function OutputView({
1792
+ groups,
1793
+ mutations,
1794
+ readOnly,
1795
+ fields,
1796
+ operators,
1797
+ defaultGroupConfig,
1798
+ className,
1799
+ style
1800
+ }) {
1660
1801
  const effectiveDefault = readOnly ? { editable: false, removable: false, ...defaultGroupConfig } : defaultGroupConfig;
1661
1802
  const rootClass = ["rcui-output", className].filter(Boolean).join(" ");
1662
1803
  const renderOverlay = (entry) => /* @__PURE__ */ jsxs4("div", { className: "rcui-overlay", children: [
@@ -1690,6 +1831,12 @@ function Output({
1690
1831
  group.id
1691
1832
  )) }) });
1692
1833
  }
1834
+ function isManagedOutput(props) {
1835
+ return props.groups === void 0;
1836
+ }
1837
+ function isReadOnlyOutput(props) {
1838
+ return props.groups !== void 0 && props.onGroupsChange === void 0;
1839
+ }
1693
1840
  function resolveConfig(groupConfig, defaultConfig) {
1694
1841
  return { ...DEFAULT_GROUP_CONFIG, ...defaultConfig, ...groupConfig };
1695
1842
  }
@@ -1759,6 +1906,8 @@ function ConditionalUI({
1759
1906
  value,
1760
1907
  onChange,
1761
1908
  onConditionsChange,
1909
+ InputComponent = ManagedInput,
1910
+ OutputComponent = ControlledOutput,
1762
1911
  className,
1763
1912
  style
1764
1913
  }) {
@@ -1768,7 +1917,7 @@ function ConditionalUI({
1768
1917
  const rootClass = ["rcui-root", className].filter(Boolean).join(" ");
1769
1918
  return /* @__PURE__ */ jsxs5("div", { className: rootClass, style, children: [
1770
1919
  /* @__PURE__ */ jsx6(
1771
- Input,
1920
+ InputComponent,
1772
1921
  {
1773
1922
  fields,
1774
1923
  operators,
@@ -1778,7 +1927,7 @@ function ConditionalUI({
1778
1927
  }
1779
1928
  ),
1780
1929
  /* @__PURE__ */ jsx6(
1781
- Output,
1930
+ OutputComponent,
1782
1931
  {
1783
1932
  groups,
1784
1933
  fields,
@@ -1792,9 +1941,14 @@ export {
1792
1941
  ConditionDataProvider,
1793
1942
  ConditionDataProvider as ConditionParser,
1794
1943
  ConditionalUI,
1944
+ ControlledInput,
1945
+ ControlledOutput,
1795
1946
  DEFAULT_OPERATORS,
1796
1947
  Input,
1948
+ ManagedInput,
1949
+ ManagedOutput,
1797
1950
  Output,
1951
+ ReadOnlyOutput,
1798
1952
  useConditionDataProvider,
1799
1953
  useConditionalInput,
1800
1954
  useConditionalOutput