@djangocfg/ui-tools 2.1.320 → 2.1.322

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/JsonSchemaForm-OSPUUUHM.cjs +13 -0
  2. package/dist/{JsonSchemaForm-IIYKSH6X.cjs.map → JsonSchemaForm-OSPUUUHM.cjs.map} +1 -1
  3. package/dist/JsonSchemaForm-TSLX2GRO.mjs +4 -0
  4. package/dist/{JsonSchemaForm-RN3XWSWX.mjs.map → JsonSchemaForm-TSLX2GRO.mjs.map} +1 -1
  5. package/dist/{chunk-L37FZYJU.cjs → chunk-4IW7GZFQ.cjs} +189 -74
  6. package/dist/chunk-4IW7GZFQ.cjs.map +1 -0
  7. package/dist/{chunk-JUGQNNDC.mjs → chunk-EXGXUK2N.mjs} +190 -76
  8. package/dist/chunk-EXGXUK2N.mjs.map +1 -0
  9. package/dist/index.cjs +28 -24
  10. package/dist/index.d.cts +240 -206
  11. package/dist/index.d.ts +240 -206
  12. package/dist/index.mjs +2 -2
  13. package/package.json +6 -6
  14. package/src/index.ts +15 -0
  15. package/src/tools/JsonForm/JsonForm.story.tsx +217 -1
  16. package/src/tools/JsonForm/JsonSchemaForm.tsx +15 -4
  17. package/src/tools/JsonForm/README.md +268 -0
  18. package/src/tools/JsonForm/index.ts +22 -1
  19. package/src/tools/JsonForm/templates/FieldTemplate.tsx +28 -5
  20. package/src/tools/JsonForm/templates/ObjectFieldTemplate.tsx +110 -3
  21. package/src/tools/JsonForm/types.ts +37 -5
  22. package/src/tools/JsonForm/utils.ts +25 -0
  23. package/src/tools/JsonForm/widgets/CheckboxWidget.tsx +6 -11
  24. package/src/tools/JsonForm/widgets/SelectWidget.tsx +20 -12
  25. package/src/tools/JsonForm/widgets/SliderWidget.tsx +9 -5
  26. package/src/tools/JsonForm/widgets/SwitchWidget.tsx +6 -10
  27. package/src/tools/JsonForm/widgets/_useWidgetEnv.ts +43 -0
  28. package/dist/JsonSchemaForm-IIYKSH6X.cjs +0 -13
  29. package/dist/JsonSchemaForm-RN3XWSWX.mjs +0 -4
  30. package/dist/chunk-JUGQNNDC.mjs.map +0 -1
  31. package/dist/chunk-L37FZYJU.cjs.map +0 -1
@@ -1,12 +1,14 @@
1
1
  "use client"
2
2
 
3
- import { ChevronDown } from 'lucide-react';
3
+ import { ChevronDown, ChevronRight } from 'lucide-react';
4
4
  import React, { useState } from 'react';
5
5
 
6
6
  import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@djangocfg/ui-core/components';
7
7
  import { cn } from '@djangocfg/ui-core/lib';
8
8
  import { ObjectFieldTemplateProps } from '@rjsf/utils';
9
9
 
10
+ import type { JsonFormDensity, UiGroup } from '../types';
11
+
10
12
  /**
11
13
  * Object field template for JSON Schema Form
12
14
  *
@@ -34,12 +36,16 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
34
36
  required,
35
37
  uiSchema,
36
38
  } = props;
39
+ const formContext = (props as { formContext?: { density?: JsonFormDensity } }).formContext;
37
40
 
38
41
  // UI options
39
42
  const isCollapsible = uiSchema?.['ui:collapsible'] === true;
40
43
  const defaultCollapsed = uiSchema?.['ui:collapsed'] === true;
41
44
  const gridCols = uiSchema?.['ui:grid'] as number | undefined;
42
45
  const className = uiSchema?.['ui:className'] as string | undefined;
46
+ const uiGroups = uiSchema?.['ui:groups'] as readonly UiGroup[] | undefined;
47
+ const density = (formContext?.density as JsonFormDensity | undefined) ?? 'comfortable';
48
+ const compact = density === 'compact';
43
49
 
44
50
  // Collapsible state
45
51
  const [isOpen, setIsOpen] = useState(!defaultCollapsed);
@@ -50,10 +56,18 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
50
56
  // Grid class based on columns
51
57
  const gridClass = gridCols
52
58
  ? `grid gap-4 grid-cols-${gridCols}`
53
- : 'space-y-4';
59
+ : compact
60
+ ? 'space-y-2'
61
+ : 'space-y-4';
62
+
63
+ // When `ui:groups` is specified, render listed fields inside collapsible
64
+ // sub-sections; remaining (ungrouped) fields render flat above the groups.
65
+ const groupedContent = uiGroups
66
+ ? renderUiGroups({ properties, uiGroups, gridClass, className, compact })
67
+ : null;
54
68
 
55
69
  // Content wrapper
56
- const content = (
70
+ const content = groupedContent ?? (
57
71
  <div className={cn(gridClass, className)}>
58
72
  {properties.map((element) => (
59
73
  <div key={element.name} className="property-wrapper">
@@ -114,3 +128,96 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
114
128
  </div>
115
129
  );
116
130
  }
131
+
132
+ interface RenderUiGroupsOptions {
133
+ properties: ObjectFieldTemplateProps['properties'];
134
+ uiGroups: readonly UiGroup[];
135
+ gridClass: string;
136
+ className?: string;
137
+ compact: boolean;
138
+ }
139
+
140
+ function renderUiGroups({
141
+ properties,
142
+ uiGroups,
143
+ gridClass,
144
+ className,
145
+ compact,
146
+ }: RenderUiGroupsOptions) {
147
+ const propsByName = new Map(properties.map((p) => [p.name, p]));
148
+ const groupedFieldNames = new Set(uiGroups.flatMap((g) => g.fields));
149
+ // Fields not listed in any group render flat above the groups.
150
+ const ungrouped = properties.filter((p) => !groupedFieldNames.has(p.name));
151
+
152
+ return (
153
+ <div className={cn(compact ? 'space-y-2' : 'space-y-3', className)}>
154
+ {ungrouped.length > 0 ? (
155
+ <div className={gridClass}>
156
+ {ungrouped.map((element) => (
157
+ <div key={element.name} className="property-wrapper">
158
+ {element.content}
159
+ </div>
160
+ ))}
161
+ </div>
162
+ ) : null}
163
+
164
+ {uiGroups.map((group) => {
165
+ const groupProps = group.fields
166
+ .map((name) => propsByName.get(name))
167
+ .filter((p): p is ObjectFieldTemplateProps['properties'][number] => Boolean(p));
168
+ if (groupProps.length === 0) return null;
169
+ return (
170
+ <UiGroupSection
171
+ key={group.title}
172
+ group={group}
173
+ gridClass={gridClass}
174
+ compact={compact}
175
+ >
176
+ {groupProps.map((element) => (
177
+ <div key={element.name} className="property-wrapper">
178
+ {element.content}
179
+ </div>
180
+ ))}
181
+ </UiGroupSection>
182
+ );
183
+ })}
184
+ </div>
185
+ );
186
+ }
187
+
188
+ interface UiGroupSectionProps {
189
+ group: UiGroup;
190
+ gridClass: string;
191
+ compact: boolean;
192
+ children: React.ReactNode;
193
+ }
194
+
195
+ function UiGroupSection({ group, gridClass, compact, children }: UiGroupSectionProps) {
196
+ const [open, setOpen] = useState(group.defaultOpen ?? true);
197
+ return (
198
+ <Collapsible open={open} onOpenChange={setOpen}>
199
+ <CollapsibleTrigger
200
+ className={cn(
201
+ 'flex w-full items-center gap-1.5 py-1 transition-colors hover:text-foreground',
202
+ compact
203
+ ? 'text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/70'
204
+ : 'text-xs font-semibold uppercase tracking-wide text-muted-foreground',
205
+ )}
206
+ >
207
+ <ChevronRight
208
+ className={cn(
209
+ 'h-3 w-3 shrink-0 transition-transform duration-200',
210
+ open && 'rotate-90',
211
+ )}
212
+ aria-hidden
213
+ />
214
+ <span>{group.title}</span>
215
+ </CollapsibleTrigger>
216
+ <CollapsibleContent className="overflow-hidden data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down">
217
+ <div className={cn('border-l border-border/40 pl-3 pb-2 pt-1', gridClass)}>
218
+ {children}
219
+ </div>
220
+ </CollapsibleContent>
221
+ </Collapsible>
222
+ );
223
+ }
@@ -1,15 +1,44 @@
1
1
  import type { RJSFSchema, UiSchema } from '@rjsf/utils';
2
2
  import type { IChangeEvent, FormProps } from '@rjsf/core';
3
+ import type {
4
+ CustomJsonSchema7,
5
+ CustomJsonUiSchema7,
6
+ CustomJsonUiDisabledWhenRule,
7
+ CustomJsonUiGroup,
8
+ } from '@djangocfg/ui-core/lib';
9
+
10
+ /** Visual density for form controls. */
11
+ export type JsonFormDensity = 'comfortable' | 'compact';
12
+
13
+ /**
14
+ * Aliases that point to the portable schema types in `@djangocfg/ui-core/lib`.
15
+ * Kept under their old short names for back-compat — new code can import the
16
+ * `CustomJson*` originals directly from ui-core.
17
+ */
18
+ export type DisabledWhenRule = CustomJsonUiDisabledWhenRule;
19
+ export type UiGroup = CustomJsonUiGroup;
20
+
21
+ /** What we put into RJSF's `formContext` so widgets/templates can react to global form state. */
22
+ export interface JsonFormContext {
23
+ density: JsonFormDensity;
24
+ /** Latest form data — used by `evaluateDisabledWhen`. */
25
+ formData: unknown;
26
+ }
3
27
 
4
28
  /**
5
29
  * JSON Schema Form props interface
6
30
  */
7
- export interface JsonSchemaFormProps<T = any> extends Partial<FormProps<T>> {
8
- /** JSON Schema that defines the form structure */
9
- schema: RJSFSchema;
31
+ export interface JsonSchemaFormProps<T = any>
32
+ extends Omit<Partial<FormProps<T>>, 'schema' | 'uiSchema'> {
33
+ /**
34
+ * JSON Schema that defines the form structure. Accepts either RJSF's
35
+ * `RJSFSchema` directly or our portable `CustomJsonSchema7` from
36
+ * `@djangocfg/ui-core/lib` — both are cast to RJSF internally.
37
+ */
38
+ schema: RJSFSchema | CustomJsonSchema7;
10
39
 
11
- /** UI Schema for customizing the form appearance */
12
- uiSchema?: UiSchema;
40
+ /** UI Schema for customizing the form appearance. Same dual-shape acceptance as `schema`. */
41
+ uiSchema?: UiSchema | CustomJsonUiSchema7;
13
42
 
14
43
  /** Initial form data */
15
44
  formData?: T;
@@ -43,6 +72,9 @@ export interface JsonSchemaFormProps<T = any> extends Partial<FormProps<T>> {
43
72
 
44
73
  /** Submit button text */
45
74
  submitButtonText?: string;
75
+
76
+ /** Visual density preset. `'compact'` shrinks rows and hides description text (moves it to label `title=` tooltip). */
77
+ density?: JsonFormDensity;
46
78
  }
47
79
 
48
80
  /**
@@ -212,3 +212,28 @@ export function validateRequiredFields(
212
212
  function getNestedValue(obj: any, path: string): any {
213
213
  return path.split('.').reduce((current, key) => current?.[key], obj);
214
214
  }
215
+
216
+ import type { DisabledWhenRule } from './types';
217
+
218
+ /**
219
+ * Evaluates a `ui:disabledWhen` rule against form data. Returns `true` when the
220
+ * field should be disabled. If `rule` is undefined, returns `false`.
221
+ *
222
+ * Supported rule shapes:
223
+ * { path, eq } | { path, notEq } | { path, in } | { path, notIn }
224
+ * | { path, truthy: true } | { path, falsy: true }
225
+ */
226
+ export function evaluateDisabledWhen(
227
+ rule: DisabledWhenRule | undefined,
228
+ formData: unknown,
229
+ ): boolean {
230
+ if (!rule) return false;
231
+ const value = getNestedValue(formData, rule.path);
232
+ if ('eq' in rule) return value === rule.eq;
233
+ if ('notEq' in rule) return value !== rule.notEq;
234
+ if ('in' in rule) return rule.in.includes(value);
235
+ if ('notIn' in rule) return !rule.notIn.includes(value);
236
+ if ('truthy' in rule) return Boolean(value);
237
+ if ('falsy' in rule) return !value;
238
+ return false;
239
+ }
@@ -3,21 +3,15 @@ import React from 'react';
3
3
  import { Checkbox } from '@djangocfg/ui-core/components';
4
4
  import { WidgetProps } from '@rjsf/utils';
5
5
 
6
+ import { useWidgetEnv } from './_useWidgetEnv';
7
+
6
8
  /**
7
9
  * Checkbox widget for JSON Schema Form
8
10
  * Handles boolean fields
9
11
  */
10
12
  export function CheckboxWidget(props: WidgetProps) {
11
- const {
12
- id,
13
- value,
14
- disabled,
15
- readonly,
16
- autofocus,
17
- onChange,
18
- onBlur,
19
- onFocus,
20
- } = props;
13
+ const { id, value, autofocus, onChange, onBlur, onFocus } = props;
14
+ const { disabled, tooltipText } = useWidgetEnv(props);
21
15
 
22
16
  const handleChange = (checked: boolean) => {
23
17
  onChange(checked);
@@ -27,11 +21,12 @@ export function CheckboxWidget(props: WidgetProps) {
27
21
  <Checkbox
28
22
  id={id}
29
23
  checked={value || false}
30
- disabled={disabled || readonly}
24
+ disabled={disabled}
31
25
  autoFocus={autofocus}
32
26
  onCheckedChange={handleChange}
33
27
  onBlur={() => onBlur(id, value)}
34
28
  onFocus={() => onFocus(id, value)}
29
+ title={tooltipText}
35
30
  />
36
31
  );
37
32
  }
@@ -7,6 +7,8 @@ import {
7
7
  } from '@djangocfg/ui-core/components';
8
8
  import { WidgetProps } from '@rjsf/utils';
9
9
 
10
+ import { useWidgetEnv } from './_useWidgetEnv';
11
+
10
12
  /**
11
13
  * Select dropdown widget for JSON Schema Form
12
14
  * Handles enum fields
@@ -17,8 +19,6 @@ export function SelectWidget(props: WidgetProps) {
17
19
  options,
18
20
  value,
19
21
  required,
20
- disabled,
21
- readonly,
22
22
  autofocus,
23
23
  onChange,
24
24
  onBlur,
@@ -26,6 +26,7 @@ export function SelectWidget(props: WidgetProps) {
26
26
  placeholder,
27
27
  rawErrors,
28
28
  } = props;
29
+ const { disabled, compact, tooltipText } = useWidgetEnv(props);
29
30
 
30
31
  // Safely extract and validate enum options
31
32
  const enumOptions = useMemo(() => {
@@ -38,7 +39,9 @@ export function SelectWidget(props: WidgetProps) {
38
39
  return rawErrors && rawErrors.length > 0;
39
40
  }, [rawErrors]);
40
41
 
41
- // Ensure value is always a string
42
+ // Empty-string enum values are handled by the ui-core <Select> wrapper
43
+ // (which substitutes a Radix-safe sentinel internally), so we can pass
44
+ // values through verbatim here.
42
45
  const safeValue = useMemo(() => {
43
46
  if (value === null || value === undefined) return '';
44
47
  return String(value);
@@ -60,8 +63,9 @@ export function SelectWidget(props: WidgetProps) {
60
63
  onChange={(e) => onChange(e.target.value)}
61
64
  onBlur={(e) => onBlur(id, e.target.value)}
62
65
  onFocus={(e) => onFocus(id, e.target.value)}
63
- disabled={disabled || readonly}
64
- readOnly={readonly}
66
+ disabled={disabled}
67
+ readOnly={disabled}
68
+ title={tooltipText}
65
69
  className={`flex h-10 w-full rounded-md border ${
66
70
  hasError ? 'border-destructive' : 'border-input'
67
71
  } bg-transparent px-3 py-2 text-base shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm`}
@@ -74,23 +78,27 @@ export function SelectWidget(props: WidgetProps) {
74
78
  <Select
75
79
  value={safeValue}
76
80
  onValueChange={handleChange}
77
- disabled={disabled || readonly}
81
+ disabled={disabled}
78
82
  required={required}
79
83
  >
80
84
  <SelectTrigger
81
85
  id={id}
82
- className={hasError ? 'border-destructive' : ''}
86
+ className={`${hasError ? 'border-destructive' : ''} ${compact ? 'h-7 text-xs' : ''}`.trim()}
83
87
  autoFocus={autofocus}
84
88
  onFocus={() => onFocus(id, value)}
89
+ title={tooltipText}
85
90
  >
86
91
  <SelectValue placeholder={placeholder || 'Select an option'} />
87
92
  </SelectTrigger>
88
93
  <SelectContent>
89
- {enumOptions.map((option: any) => (
90
- <SelectItem key={String(option.value)} value={String(option.value)}>
91
- {option.label || String(option.value)}
92
- </SelectItem>
93
- ))}
94
+ {enumOptions.map((option: any) => {
95
+ const optValue = String(option.value);
96
+ return (
97
+ <SelectItem key={optValue || '__empty__'} value={optValue}>
98
+ {option.label || optValue}
99
+ </SelectItem>
100
+ );
101
+ })}
94
102
  </SelectContent>
95
103
  </Select>
96
104
  );
@@ -6,6 +6,8 @@ import { Input, Slider } from '@djangocfg/ui-core/components';
6
6
  import { cn } from '@djangocfg/ui-core/lib';
7
7
  import { WidgetProps } from '@rjsf/utils';
8
8
 
9
+ import { useWidgetEnv } from './_useWidgetEnv';
10
+
9
11
  /**
10
12
  * Slider widget for JSON Schema Form
11
13
  *
@@ -33,14 +35,13 @@ import { WidgetProps } from '@rjsf/utils';
33
35
  export function SliderWidget(props: WidgetProps) {
34
36
  const {
35
37
  id,
36
- disabled,
37
- readonly,
38
38
  value,
39
39
  onChange,
40
40
  schema,
41
41
  options,
42
42
  rawErrors,
43
43
  } = props;
44
+ const { disabled, tooltipText } = useWidgetEnv(props);
44
45
 
45
46
  // Extract configuration
46
47
  const config = useMemo(() => {
@@ -112,11 +113,14 @@ export function SliderWidget(props: WidgetProps) {
112
113
  }, [numericValue, config.unit]);
113
114
 
114
115
  return (
115
- <div className={cn('flex items-center gap-3', hasError && 'text-destructive')}>
116
+ <div
117
+ className={cn('flex items-center gap-3', hasError && 'text-destructive')}
118
+ title={tooltipText}
119
+ >
116
120
  {/* Slider */}
117
121
  <Slider
118
122
  id={id}
119
- disabled={disabled || readonly}
123
+ disabled={disabled}
120
124
  value={[numericValue]}
121
125
  onValueChange={handleSliderChange}
122
126
  min={config.min}
@@ -132,7 +136,7 @@ export function SliderWidget(props: WidgetProps) {
132
136
  value={displayValue}
133
137
  onChange={handleInputChange}
134
138
  disabled={disabled}
135
- readOnly={readonly}
139
+ readOnly={disabled}
136
140
  className={cn(
137
141
  'w-20 text-center font-mono text-sm',
138
142
  hasError && 'border-destructive'
@@ -3,20 +3,15 @@ import React from 'react';
3
3
  import { Switch } from '@djangocfg/ui-core/components';
4
4
  import { WidgetProps } from '@rjsf/utils';
5
5
 
6
+ import { useWidgetEnv } from './_useWidgetEnv';
7
+
6
8
  /**
7
9
  * Switch toggle widget for JSON Schema Form
8
10
  * Alternative boolean input
9
11
  */
10
12
  export function SwitchWidget(props: WidgetProps) {
11
- const {
12
- id,
13
- value,
14
- disabled,
15
- readonly,
16
- onChange,
17
- onBlur,
18
- onFocus,
19
- } = props;
13
+ const { id, value, onChange, onBlur, onFocus } = props;
14
+ const { disabled, tooltipText } = useWidgetEnv(props);
20
15
 
21
16
  const handleChange = (checked: boolean) => {
22
17
  onChange(checked);
@@ -26,10 +21,11 @@ export function SwitchWidget(props: WidgetProps) {
26
21
  <Switch
27
22
  id={id}
28
23
  checked={value || false}
29
- disabled={disabled || readonly}
24
+ disabled={disabled}
30
25
  onCheckedChange={handleChange}
31
26
  onBlur={() => onBlur(id, value)}
32
27
  onFocus={() => onFocus(id, value)}
28
+ title={tooltipText}
33
29
  />
34
30
  );
35
31
  }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Internal helper that resolves widget-level environment from RJSF props:
3
+ * - density (from formContext, default 'comfortable')
4
+ * - effective `disabled` flag (combines props.disabled with `ui:disabledWhen`)
5
+ *
6
+ * Keeps each widget terse — no need to repeat the formContext + disabledWhen
7
+ * boilerplate.
8
+ */
9
+
10
+ import type { WidgetProps } from '@rjsf/utils';
11
+
12
+ import { evaluateDisabledWhen } from '../utils';
13
+ import type { DisabledWhenRule, JsonFormDensity } from '../types';
14
+
15
+ export interface WidgetEnv {
16
+ density: JsonFormDensity;
17
+ compact: boolean;
18
+ disabled: boolean;
19
+ /** Title-attribute text — in compact mode the description moves into a tooltip on the label/control. */
20
+ tooltipText?: string;
21
+ }
22
+
23
+ export function useWidgetEnv(props: WidgetProps): WidgetEnv {
24
+ const { disabled, readonly, formContext, uiSchema, schema } = props;
25
+ const density: JsonFormDensity =
26
+ (formContext?.density as JsonFormDensity | undefined) ?? 'comfortable';
27
+ const compact = density === 'compact';
28
+
29
+ const disabledWhen = uiSchema?.['ui:disabledWhen'] as DisabledWhenRule | undefined;
30
+ const disabledByRule = evaluateDisabledWhen(disabledWhen, formContext?.formData);
31
+
32
+ const tooltipText = compact
33
+ ? (uiSchema?.['ui:description'] as string | undefined)
34
+ ?? (schema?.description as string | undefined)
35
+ : undefined;
36
+
37
+ return {
38
+ density,
39
+ compact,
40
+ disabled: Boolean(disabled || readonly || disabledByRule),
41
+ tooltipText,
42
+ };
43
+ }
@@ -1,13 +0,0 @@
1
- 'use strict';
2
-
3
- var chunkL37FZYJU_cjs = require('./chunk-L37FZYJU.cjs');
4
- require('./chunk-WGEGR3DF.cjs');
5
-
6
-
7
-
8
- Object.defineProperty(exports, "JsonSchemaForm", {
9
- enumerable: true,
10
- get: function () { return chunkL37FZYJU_cjs.JsonSchemaForm; }
11
- });
12
- //# sourceMappingURL=JsonSchemaForm-IIYKSH6X.cjs.map
13
- //# sourceMappingURL=JsonSchemaForm-IIYKSH6X.cjs.map
@@ -1,4 +0,0 @@
1
- export { JsonSchemaForm } from './chunk-JUGQNNDC.mjs';
2
- import './chunk-CGILA3WO.mjs';
3
- //# sourceMappingURL=JsonSchemaForm-RN3XWSWX.mjs.map
4
- //# sourceMappingURL=JsonSchemaForm-RN3XWSWX.mjs.map