@showwhat/configurator 1.0.1 → 2.0.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/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # @showwhat/configurator
2
2
 
3
- A reusable React component library for visually editing showwhat feature flag definitions. Provides a complete rule-builder UI bring your own store and persistence.
3
+ A reusable React component library for visually editing showwhat definitions like Swagger UI for your flag and config rules.
4
+
5
+ Provides a complete rule-builder UI while letting your app own storage, workflow, and persistence.
4
6
 
5
7
  ## Installation
6
8
 
package/dist/index.d.ts CHANGED
@@ -84,6 +84,7 @@ type ValueInputProps = {
84
84
  type DateTimeInputProps = {
85
85
  value: string;
86
86
  onChange: (value: string) => void;
87
+ disabled?: boolean;
87
88
  };
88
89
  type ValidationIssueDisplay = {
89
90
  path: (string | number)[];
@@ -219,7 +220,7 @@ declare function TabsContent({ className, ...props }: React__default.ComponentPr
219
220
 
220
221
  declare function ValueInput({ value, onChange, placeholder }: ValueInputProps): react_jsx_runtime.JSX.Element;
221
222
 
222
- declare function DateTimeInput({ value, onChange }: DateTimeInputProps): react_jsx_runtime.JSX.Element;
223
+ declare function DateTimeInput({ value, onChange, disabled }: DateTimeInputProps): react_jsx_runtime.JSX.Element;
223
224
 
224
225
  declare function ValidationMessage({ errors }: ValidationMessageProps): react_jsx_runtime.JSX.Element | null;
225
226
 
package/dist/index.js CHANGED
@@ -309,7 +309,7 @@ function Badge({
309
309
 
310
310
  // src/components/condition-builder/TagInput.tsx
311
311
  import { jsx as jsx7, jsxs as jsxs3 } from "react/jsx-runtime";
312
- function TagInput({ value, onChange, placeholder }) {
312
+ function TagInput({ value, onChange, placeholder, disabled }) {
313
313
  const values = Array.isArray(value) ? value.filter(Boolean) : value ? [value] : [];
314
314
  const [text, setText] = useState("");
315
315
  const emit = useCallback(
@@ -358,32 +358,39 @@ function TagInput({ value, onChange, placeholder }) {
358
358
  },
359
359
  [addValues]
360
360
  );
361
- return /* @__PURE__ */ jsxs3("div", { className: "border-input focus-within:border-ring focus-within:ring-ring/50 flex min-h-9 flex-1 flex-wrap items-center gap-1 rounded-md border px-2 py-1 focus-within:ring-[3px]", children: [
362
- values.map((v, i) => /* @__PURE__ */ jsxs3(Badge, { variant: "outline", className: "bg-muted gap-1 font-mono text-xs", children: [
363
- v,
364
- /* @__PURE__ */ jsx7(
365
- "button",
366
- {
367
- type: "button",
368
- className: "text-muted-foreground hover:text-foreground ml-0.5 cursor-pointer leading-none",
369
- onClick: () => removeValue(i),
370
- "aria-label": `Remove ${v}`,
371
- children: "\xD7"
372
- }
373
- )
374
- ] }, `${v}-${i}`)),
375
- /* @__PURE__ */ jsx7(
376
- "input",
377
- {
378
- className: "min-w-[80px] flex-1 bg-transparent py-0.5 font-mono text-sm outline-none placeholder:text-muted-foreground",
379
- value: text,
380
- placeholder: values.length === 0 ? placeholder ?? "type and press Enter" : "",
381
- onChange: (e) => setText(e.target.value),
382
- onKeyDown: handleKeyDown,
383
- onPaste: handlePaste
384
- }
385
- )
386
- ] });
361
+ return /* @__PURE__ */ jsxs3(
362
+ "div",
363
+ {
364
+ className: `border-input focus-within:border-ring focus-within:ring-ring/50 flex min-h-9 flex-1 flex-wrap items-center gap-1 rounded-md border px-2 py-1 focus-within:ring-[3px]${disabled ? " opacity-50" : ""}`,
365
+ children: [
366
+ values.map((v, i) => /* @__PURE__ */ jsxs3(Badge, { variant: "outline", className: "bg-muted gap-1 font-mono text-xs", children: [
367
+ v,
368
+ !disabled && /* @__PURE__ */ jsx7(
369
+ "button",
370
+ {
371
+ type: "button",
372
+ className: "text-muted-foreground hover:text-foreground ml-0.5 cursor-pointer leading-none",
373
+ onClick: () => removeValue(i),
374
+ "aria-label": `Remove ${v}`,
375
+ children: "\xD7"
376
+ }
377
+ )
378
+ ] }, `${v}-${i}`)),
379
+ /* @__PURE__ */ jsx7(
380
+ "input",
381
+ {
382
+ className: "min-w-[80px] flex-1 bg-transparent py-0.5 font-mono text-sm outline-none placeholder:text-muted-foreground",
383
+ value: text,
384
+ placeholder: values.length === 0 ? placeholder ?? "type and press Enter" : "",
385
+ onChange: (e) => setText(e.target.value),
386
+ onKeyDown: handleKeyDown,
387
+ onPaste: handlePaste,
388
+ disabled
389
+ }
390
+ )
391
+ ]
392
+ }
393
+ );
387
394
  }
388
395
 
389
396
  // src/components/condition-builder/condition-builders.ts
@@ -482,7 +489,7 @@ import { useCallback as useCallback4, useMemo as useMemo2 } from "react";
482
489
  // src/components/condition-builder/NumberTagInput.tsx
483
490
  import { useCallback as useCallback3, useState as useState2 } from "react";
484
491
  import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
485
- function NumberTagInput({ value, onChange, placeholder }) {
492
+ function NumberTagInput({ value, onChange, placeholder, disabled }) {
486
493
  const values = Array.isArray(value) ? value : [value];
487
494
  const [text, setText] = useState2("");
488
495
  const emit = useCallback3(
@@ -531,33 +538,40 @@ function NumberTagInput({ value, onChange, placeholder }) {
531
538
  },
532
539
  [addValues]
533
540
  );
534
- return /* @__PURE__ */ jsxs5("div", { className: "border-input focus-within:border-ring focus-within:ring-ring/50 flex min-h-9 flex-1 flex-wrap items-center gap-1 rounded-md border px-2 py-1 focus-within:ring-[3px]", children: [
535
- values.map((v, i) => /* @__PURE__ */ jsxs5(Badge, { variant: "outline", className: "bg-muted gap-1 font-mono text-xs", children: [
536
- v,
537
- /* @__PURE__ */ jsx9(
538
- "button",
539
- {
540
- type: "button",
541
- className: "text-muted-foreground hover:text-foreground ml-0.5 cursor-pointer leading-none",
542
- onClick: () => removeValue(i),
543
- "aria-label": `Remove ${v}`,
544
- children: "\xD7"
545
- }
546
- )
547
- ] }, `${v}-${i}`)),
548
- /* @__PURE__ */ jsx9(
549
- "input",
550
- {
551
- className: "min-w-[80px] flex-1 bg-transparent py-0.5 font-mono text-sm outline-none placeholder:text-muted-foreground",
552
- type: "number",
553
- value: text,
554
- placeholder: values.length === 0 ? placeholder ?? "type and press Enter" : "",
555
- onChange: (e) => setText(e.target.value),
556
- onKeyDown: handleKeyDown,
557
- onPaste: handlePaste
558
- }
559
- )
560
- ] });
541
+ return /* @__PURE__ */ jsxs5(
542
+ "div",
543
+ {
544
+ className: `border-input focus-within:border-ring focus-within:ring-ring/50 flex min-h-9 flex-1 flex-wrap items-center gap-1 rounded-md border px-2 py-1 focus-within:ring-[3px]${disabled ? " opacity-50" : ""}`,
545
+ children: [
546
+ values.map((v, i) => /* @__PURE__ */ jsxs5(Badge, { variant: "outline", className: "bg-muted gap-1 font-mono text-xs", children: [
547
+ v,
548
+ !disabled && /* @__PURE__ */ jsx9(
549
+ "button",
550
+ {
551
+ type: "button",
552
+ className: "text-muted-foreground hover:text-foreground ml-0.5 cursor-pointer leading-none",
553
+ onClick: () => removeValue(i),
554
+ "aria-label": `Remove ${v}`,
555
+ children: "\xD7"
556
+ }
557
+ )
558
+ ] }, `${v}-${i}`)),
559
+ /* @__PURE__ */ jsx9(
560
+ "input",
561
+ {
562
+ className: "min-w-[80px] flex-1 bg-transparent py-0.5 font-mono text-sm outline-none placeholder:text-muted-foreground",
563
+ type: "number",
564
+ value: text,
565
+ placeholder: values.length === 0 ? placeholder ?? "type and press Enter" : "",
566
+ onChange: (e) => setText(e.target.value),
567
+ onKeyDown: handleKeyDown,
568
+ onPaste: handlePaste,
569
+ disabled
570
+ }
571
+ )
572
+ ]
573
+ }
574
+ );
561
575
  }
562
576
 
563
577
  // src/components/condition-builder/NumberConditionEditor.tsx
@@ -656,7 +670,7 @@ function fromLocalDatetime(local) {
656
670
  if (Number.isNaN(d.getTime())) return local;
657
671
  return d.toISOString();
658
672
  }
659
- function DateTimeInput({ value, onChange }) {
673
+ function DateTimeInput({ value, onChange, disabled }) {
660
674
  const [rawValue, setRawValue] = useState3(value);
661
675
  const [showRaw, setShowRaw] = useState3(false);
662
676
  const prevValueRef = useRef(value);
@@ -675,7 +689,8 @@ function DateTimeInput({ value, onChange }) {
675
689
  onChange: (e) => {
676
690
  setRawValue(e.target.value);
677
691
  onChange(e.target.value);
678
- }
692
+ },
693
+ disabled
679
694
  }
680
695
  ),
681
696
  /* @__PURE__ */ jsx11(
@@ -697,7 +712,8 @@ function DateTimeInput({ value, onChange }) {
697
712
  className: "h-8 flex-1 text-xs",
698
713
  type: "datetime-local",
699
714
  value: toLocalDatetime(value),
700
- onChange: (e) => onChange(fromLocalDatetime(e.target.value))
715
+ onChange: (e) => onChange(fromLocalDatetime(e.target.value)),
716
+ disabled
701
717
  }
702
718
  ),
703
719
  /* @__PURE__ */ jsx11(
@@ -2968,17 +2984,24 @@ function createPresetConditionMeta(presets) {
2968
2984
  type: name,
2969
2985
  label: capitalize(name),
2970
2986
  description,
2971
- defaults: { ...baseDefaults, ...preset.defaults, type: name }
2987
+ defaults: { ...baseDefaults, ...preset.overrides, type: name }
2972
2988
  };
2973
2989
  });
2974
2990
  }
2975
- function createPresetEditor(presetName, builtinType, presetKey) {
2991
+ function createPresetEditor(presetName, builtinType, presetKey, overrides = {}) {
2992
+ const lockedFields = new Set(Object.keys(overrides));
2976
2993
  function PresetConditionEditor({ condition, onChange }) {
2977
2994
  const rec = useMemo10(() => condition, [condition]);
2978
2995
  const update = useCallback11(
2979
2996
  (field, value) => {
2980
2997
  onChange(
2981
- buildCustomCondition({ ...rec, [field]: value, key: presetKey, type: presetName })
2998
+ buildCustomCondition({
2999
+ ...rec,
3000
+ [field]: value,
3001
+ ...overrides,
3002
+ key: presetKey,
3003
+ type: presetName
3004
+ })
2982
3005
  );
2983
3006
  },
2984
3007
  [rec, onChange]
@@ -3002,6 +3025,8 @@ function createPresetEditor(presetName, builtinType, presetKey) {
3002
3025
  })
3003
3026
  );
3004
3027
  };
3028
+ const opLocked = lockedFields.has("op");
3029
+ const valueLocked = lockedFields.has("value");
3005
3030
  return /* @__PURE__ */ jsxs31(ConditionRow, { children: [
3006
3031
  /* @__PURE__ */ jsx45(KeyInput, { value: presetKey, disabled: true }),
3007
3032
  /* @__PURE__ */ jsx45(
@@ -3009,7 +3034,8 @@ function createPresetEditor(presetName, builtinType, presetKey) {
3009
3034
  {
3010
3035
  value: String(rec.op ?? "eq"),
3011
3036
  onChange: handleOpChange,
3012
- options: OP_OPTIONS
3037
+ options: OP_OPTIONS,
3038
+ disabled: opLocked
3013
3039
  }
3014
3040
  ),
3015
3041
  isArray ? /* @__PURE__ */ jsx45(
@@ -3017,7 +3043,8 @@ function createPresetEditor(presetName, builtinType, presetKey) {
3017
3043
  {
3018
3044
  value: rec.value ?? "",
3019
3045
  onChange: (v) => update("value", v),
3020
- placeholder: `e.g. ${presetKey} value`
3046
+ placeholder: `e.g. ${presetKey} value`,
3047
+ disabled: valueLocked
3021
3048
  }
3022
3049
  ) : isRegex ? /* @__PURE__ */ jsx45(
3023
3050
  Input,
@@ -3025,7 +3052,8 @@ function createPresetEditor(presetName, builtinType, presetKey) {
3025
3052
  className: "h-8 font-mono text-sm",
3026
3053
  value: String(rec.value ?? ""),
3027
3054
  placeholder: "e.g. ^test.*$",
3028
- onChange: (e) => update("value", e.target.value)
3055
+ onChange: (e) => update("value", e.target.value),
3056
+ disabled: valueLocked
3029
3057
  }
3030
3058
  ) : /* @__PURE__ */ jsx45(
3031
3059
  Input,
@@ -3033,7 +3061,8 @@ function createPresetEditor(presetName, builtinType, presetKey) {
3033
3061
  className: "h-8 text-sm",
3034
3062
  value: String(rec.value ?? ""),
3035
3063
  placeholder: `e.g. ${presetKey} value`,
3036
- onChange: (e) => update("value", e.target.value)
3064
+ onChange: (e) => update("value", e.target.value),
3065
+ disabled: valueLocked
3037
3066
  }
3038
3067
  )
3039
3068
  ] });
@@ -3055,6 +3084,8 @@ function createPresetEditor(presetName, builtinType, presetKey) {
3055
3084
  })
3056
3085
  );
3057
3086
  };
3087
+ const numOpLocked = lockedFields.has("op");
3088
+ const numValueLocked = lockedFields.has("value");
3058
3089
  return /* @__PURE__ */ jsxs31(ConditionRow, { children: [
3059
3090
  /* @__PURE__ */ jsx45(KeyInput, { value: presetKey, disabled: true }),
3060
3091
  /* @__PURE__ */ jsx45(
@@ -3062,7 +3093,8 @@ function createPresetEditor(presetName, builtinType, presetKey) {
3062
3093
  {
3063
3094
  value: String(rec.op ?? "eq"),
3064
3095
  onChange: handleNumOpChange,
3065
- options: OP_OPTIONS2
3096
+ options: OP_OPTIONS2,
3097
+ disabled: numOpLocked
3066
3098
  }
3067
3099
  ),
3068
3100
  isNumArray ? /* @__PURE__ */ jsx45(
@@ -3070,7 +3102,8 @@ function createPresetEditor(presetName, builtinType, presetKey) {
3070
3102
  {
3071
3103
  value: rec.value ?? [],
3072
3104
  onChange: (v) => update("value", v),
3073
- placeholder: `e.g. ${presetKey} value`
3105
+ placeholder: `e.g. ${presetKey} value`,
3106
+ disabled: numValueLocked
3074
3107
  }
3075
3108
  ) : /* @__PURE__ */ jsx45(
3076
3109
  Input,
@@ -3079,12 +3112,14 @@ function createPresetEditor(presetName, builtinType, presetKey) {
3079
3112
  className: "h-8 font-mono text-sm",
3080
3113
  value: rec.value !== void 0 ? String(rec.value) : "",
3081
3114
  placeholder: "e.g. 100",
3082
- onChange: (e) => update("value", e.target.value === "" ? "" : Number(e.target.value))
3115
+ onChange: (e) => update("value", e.target.value === "" ? "" : Number(e.target.value)),
3116
+ disabled: numValueLocked
3083
3117
  }
3084
3118
  )
3085
3119
  ] });
3086
3120
  }
3087
- case "bool":
3121
+ case "bool": {
3122
+ const boolValueLocked = lockedFields.has("value");
3088
3123
  return /* @__PURE__ */ jsxs31(ConditionRow, { children: [
3089
3124
  /* @__PURE__ */ jsx45(KeyInput, { value: presetKey, disabled: true }),
3090
3125
  /* @__PURE__ */ jsx45(OperatorSelect, { value: "eq", options: OP_OPTIONS4, disabled: true }),
@@ -3093,6 +3128,7 @@ function createPresetEditor(presetName, builtinType, presetKey) {
3093
3128
  {
3094
3129
  value: String(rec.value ?? "true"),
3095
3130
  onValueChange: (v) => update("value", v === "true"),
3131
+ disabled: boolValueLocked,
3096
3132
  children: [
3097
3133
  /* @__PURE__ */ jsx45(SelectTrigger, { className: "h-8 text-sm", children: /* @__PURE__ */ jsx45(SelectValue, {}) }),
3098
3134
  /* @__PURE__ */ jsxs31(SelectContent, { children: [
@@ -3103,7 +3139,10 @@ function createPresetEditor(presetName, builtinType, presetKey) {
3103
3139
  }
3104
3140
  )
3105
3141
  ] });
3106
- case "datetime":
3142
+ }
3143
+ case "datetime": {
3144
+ const dtOpLocked = lockedFields.has("op");
3145
+ const dtValueLocked = lockedFields.has("value");
3107
3146
  return /* @__PURE__ */ jsxs31(ConditionRow, { children: [
3108
3147
  /* @__PURE__ */ jsx45(KeyInput, { value: presetKey, disabled: true }),
3109
3148
  /* @__PURE__ */ jsx45(
@@ -3111,11 +3150,20 @@ function createPresetEditor(presetName, builtinType, presetKey) {
3111
3150
  {
3112
3151
  value: String(rec.op ?? "eq"),
3113
3152
  onChange: (v) => update("op", v),
3114
- options: OP_OPTIONS3
3153
+ options: OP_OPTIONS3,
3154
+ disabled: dtOpLocked
3115
3155
  }
3116
3156
  ),
3117
- /* @__PURE__ */ jsx45(DateTimeInput, { value: String(rec.value ?? ""), onChange: (v) => update("value", v) })
3157
+ /* @__PURE__ */ jsx45(
3158
+ DateTimeInput,
3159
+ {
3160
+ value: String(rec.value ?? ""),
3161
+ onChange: (v) => update("value", v),
3162
+ disabled: dtValueLocked
3163
+ }
3164
+ )
3118
3165
  ] });
3166
+ }
3119
3167
  }
3120
3168
  return null;
3121
3169
  }
@@ -3127,7 +3175,10 @@ function createPresetUI(presets) {
3127
3175
  const editorOverrides = /* @__PURE__ */ new Map();
3128
3176
  for (const [name, preset] of Object.entries(presets)) {
3129
3177
  if (PRIMITIVE_TYPES.has(preset.type) && preset.key) {
3130
- editorOverrides.set(name, createPresetEditor(name, preset.type, preset.key));
3178
+ editorOverrides.set(
3179
+ name,
3180
+ createPresetEditor(name, preset.type, preset.key, preset.overrides)
3181
+ );
3131
3182
  }
3132
3183
  }
3133
3184
  return { extraConditionTypes, editorOverrides };
@@ -3192,7 +3243,7 @@ function useConfiguratorSelector(selector) {
3192
3243
  // src/configurator/PreviewPanel.tsx
3193
3244
  import { useCallback as useCallback14, useEffect, useMemo as useMemo11, useRef as useRef5, useState as useState11 } from "react";
3194
3245
  import { ChevronRight as ChevronRight2, Eye as Eye2, Loader2, Maximize2, Play } from "lucide-react";
3195
- import { resolve } from "showwhat";
3246
+ import { resolve, builtinEvaluators } from "showwhat";
3196
3247
  import { DefinitionInactiveError, DefinitionNotFoundError, VariationNotFoundError } from "showwhat";
3197
3248
 
3198
3249
  // src/configurator/selectors.ts
@@ -3397,38 +3448,49 @@ function PreviewPanel() {
3397
3448
  const result = await resolve({
3398
3449
  definitions: { [selectedKey]: definitions[selectedKey] },
3399
3450
  context,
3400
- options: fallback ? { fallback } : void 0
3451
+ options: {
3452
+ evaluators: builtinEvaluators,
3453
+ ...fallback ? { fallback } : void 0
3454
+ }
3401
3455
  });
3402
3456
  if (controller.signal.aborted) return;
3403
3457
  const resolution = result[selectedKey];
3404
- setPreviewResult({
3405
- status: "success",
3406
- value: resolution.value,
3407
- meta: resolution.meta
3408
- });
3409
- } catch (err) {
3410
- if (controller.signal.aborted) return;
3411
- if (err instanceof DefinitionInactiveError) {
3412
- setPreviewResult({
3413
- status: "inactive",
3414
- message: `"${selectedKey}" is inactive`
3415
- });
3416
- } else if (err instanceof VariationNotFoundError) {
3458
+ if (resolution.success) {
3417
3459
  setPreviewResult({
3418
- status: "no-match",
3419
- message: "No variation matched the given context"
3420
- });
3421
- } else if (err instanceof DefinitionNotFoundError) {
3422
- setPreviewResult({
3423
- status: "error",
3424
- message: `Definition "${selectedKey}" not found`
3460
+ status: "success",
3461
+ value: resolution.value,
3462
+ meta: resolution.meta
3425
3463
  });
3426
3464
  } else {
3427
- setPreviewResult({
3428
- status: "error",
3429
- message: err instanceof Error ? err.message : "Unknown error"
3430
- });
3465
+ const err = resolution.error;
3466
+ if (err instanceof DefinitionInactiveError) {
3467
+ setPreviewResult({
3468
+ status: "inactive",
3469
+ message: `"${selectedKey}" is inactive`
3470
+ });
3471
+ } else if (err instanceof VariationNotFoundError) {
3472
+ setPreviewResult({
3473
+ status: "no-match",
3474
+ message: "No variation matched the given context"
3475
+ });
3476
+ } else if (err instanceof DefinitionNotFoundError) {
3477
+ setPreviewResult({
3478
+ status: "error",
3479
+ message: `Definition "${selectedKey}" not found`
3480
+ });
3481
+ } else {
3482
+ setPreviewResult({
3483
+ status: "error",
3484
+ message: err.message
3485
+ });
3486
+ }
3431
3487
  }
3488
+ } catch (err) {
3489
+ if (controller.signal.aborted) return;
3490
+ setPreviewResult({
3491
+ status: "error",
3492
+ message: err instanceof Error ? err.message : "Unknown error"
3493
+ });
3432
3494
  } finally {
3433
3495
  if (!controller.signal.aborted) {
3434
3496
  setIsResolving(false);