@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,10 +1,12 @@
1
1
  import { defineStory, useSelect, useBoolean } from '@djangocfg/playground';
2
+ import { useState } from 'react';
2
3
  import { JsonSchemaForm } from './index';
3
4
 
4
5
  export default defineStory({
5
6
  title: 'Tools/Json Schema Form',
6
7
  component: JsonSchemaForm,
7
- description: 'Dynamic form generator from JSON Schema using react-jsonschema-form.',
8
+ description:
9
+ 'Dynamic form generator from JSON Schema using react-jsonschema-form. Supports density (compact), conditional disable via `ui:disabledWhen`, and collapsible sub-sections via `ui:groups`.',
8
10
  });
9
11
 
10
12
  const SCHEMAS = {
@@ -77,6 +79,13 @@ export const Interactive = () => {
77
79
  description: 'Select form schema',
78
80
  });
79
81
 
82
+ const [density] = useSelect('density', {
83
+ options: ['comfortable', 'compact'] as const,
84
+ defaultValue: 'comfortable',
85
+ label: 'Density',
86
+ description: '`compact` shrinks rows and moves description into label tooltip.',
87
+ });
88
+
80
89
  const [liveValidate] = useBoolean('liveValidate', {
81
90
  defaultValue: false,
82
91
  label: 'Live Validate',
@@ -91,6 +100,7 @@ export const Interactive = () => {
91
100
  schema={config.schema}
92
101
  uiSchema={config.uiSchema}
93
102
  liveValidate={liveValidate}
103
+ density={density}
94
104
  onSubmit={(data) => console.log('Submitted:', data.formData)}
95
105
  />
96
106
  </div>
@@ -132,3 +142,209 @@ export const WithDefaultValues = () => (
132
142
  />
133
143
  </div>
134
144
  );
145
+
146
+ /**
147
+ * Compact density — shrinks rows, moves description into a tooltip on the
148
+ * label. Pair with `ui:groups` for dense sidebar / playground configurators.
149
+ */
150
+ export const Compact = () => (
151
+ <div className="max-w-md">
152
+ <JsonSchemaForm
153
+ schema={SCHEMAS.simple.schema}
154
+ density="compact"
155
+ showSubmitButton={false}
156
+ onSubmit={(data) => console.log('Submitted:', data.formData)}
157
+ />
158
+ </div>
159
+ );
160
+
161
+ /**
162
+ * `ui:disabledWhen` greys out a field based on another field's value.
163
+ * Here `phone` is disabled until `subscribe` is checked, and `email` is
164
+ * disabled when `firstName` is empty.
165
+ */
166
+ export const ConditionalDisable = () => {
167
+ const [data, setData] = useState<Record<string, unknown>>({
168
+ firstName: '',
169
+ subscribe: false,
170
+ });
171
+ return (
172
+ <div className="max-w-lg">
173
+ <JsonSchemaForm
174
+ schema={SCHEMAS.contact.schema}
175
+ uiSchema={{
176
+ ...SCHEMAS.contact.uiSchema,
177
+ email: {
178
+ 'ui:disabledWhen': { path: 'firstName', falsy: true },
179
+ },
180
+ phone: {
181
+ 'ui:disabledWhen': { path: 'subscribe', falsy: true },
182
+ },
183
+ subscribe: { 'ui:widget': 'switch' },
184
+ }}
185
+ formData={data}
186
+ onChange={(e) => setData(e.formData ?? {})}
187
+ showSubmitButton={false}
188
+ />
189
+ <pre className="mt-3 text-[11px] text-muted-foreground">
190
+ {JSON.stringify(data, null, 2)}
191
+ </pre>
192
+ </div>
193
+ );
194
+ };
195
+
196
+ /**
197
+ * `ui:groups` splits a flat object into collapsible sub-sections without
198
+ * restructuring the schema. Fields not listed in any group render flat above
199
+ * the groups.
200
+ */
201
+ export const CollapsibleGroups = () => (
202
+ <div className="max-w-lg">
203
+ <JsonSchemaForm
204
+ schema={{
205
+ type: 'object',
206
+ properties: {
207
+ identity: { type: 'string', title: 'Display name' },
208
+ firstName: { type: 'string', title: 'First name' },
209
+ lastName: { type: 'string', title: 'Last name' },
210
+ email: { type: 'string', title: 'Email', format: 'email' },
211
+ phone: { type: 'string', title: 'Phone' },
212
+ newsletter: { type: 'boolean', title: 'Newsletter' },
213
+ marketing: { type: 'boolean', title: 'Marketing emails' },
214
+ tracking: { type: 'boolean', title: 'Usage analytics' },
215
+ },
216
+ }}
217
+ uiSchema={{
218
+ 'ui:groups': [
219
+ {
220
+ title: 'Personal',
221
+ fields: ['firstName', 'lastName'],
222
+ defaultOpen: true,
223
+ },
224
+ { title: 'Contact', fields: ['email', 'phone'], defaultOpen: false },
225
+ {
226
+ title: 'Preferences',
227
+ fields: ['newsletter', 'marketing', 'tracking'],
228
+ defaultOpen: false,
229
+ },
230
+ ],
231
+ newsletter: { 'ui:widget': 'switch' },
232
+ marketing: { 'ui:widget': 'switch' },
233
+ tracking: { 'ui:widget': 'switch' },
234
+ }}
235
+ density="compact"
236
+ showSubmitButton={false}
237
+ onChange={(e) => console.log('changed:', e.formData)}
238
+ />
239
+ </div>
240
+ );
241
+
242
+ /**
243
+ * Empty-string enum values are now allowed — Radix `<Select.Item value="" />`
244
+ * is reserved, so the ui-core `Select` wrapper substitutes a sentinel
245
+ * internally. From the schema's perspective `''` is just another option.
246
+ */
247
+ export const EmptyStringSelect = () => {
248
+ const [data, setData] = useState<Record<string, unknown>>({ plan: '' });
249
+ return (
250
+ <div className="max-w-md">
251
+ <JsonSchemaForm
252
+ schema={{
253
+ type: 'object',
254
+ properties: {
255
+ plan: {
256
+ type: 'string',
257
+ title: 'Plan',
258
+ enum: ['', 'Free', 'Pro', 'Max', 'Enterprise'],
259
+ description: 'An empty string is a valid enum value here.',
260
+ },
261
+ },
262
+ }}
263
+ uiSchema={{
264
+ plan: {
265
+ 'ui:enumNames': ['— none —', 'Free', 'Pro', 'Max', 'Enterprise'],
266
+ },
267
+ }}
268
+ formData={data}
269
+ onChange={(e) => setData(e.formData ?? {})}
270
+ showSubmitButton={false}
271
+ />
272
+ <pre className="mt-3 text-[11px] text-muted-foreground">
273
+ {JSON.stringify(data, null, 2)}
274
+ </pre>
275
+ </div>
276
+ );
277
+ };
278
+
279
+ /**
280
+ * Live demo of all extensions wired together — compact density, collapsible
281
+ * groups, conditional disable, slider with unit. Mirrors what the layouts
282
+ * playground sidebar uses.
283
+ */
284
+ export const PlaygroundStyle = () => {
285
+ const [data, setData] = useState<Record<string, unknown>>({
286
+ shell: { variant: 'boxed', inset: 12, radius: '2xl', border: true },
287
+ sidebar: {
288
+ density: 'default',
289
+ activeIndicator: 'background',
290
+ collapsibleGroups: false,
291
+ },
292
+ });
293
+ return (
294
+ <div className="max-w-md">
295
+ <JsonSchemaForm
296
+ schema={{
297
+ type: 'object',
298
+ properties: {
299
+ shell: {
300
+ type: 'object',
301
+ title: 'Shell',
302
+ properties: {
303
+ variant: { type: 'string', title: 'Variant', enum: ['full-bleed', 'boxed'] },
304
+ inset: { type: 'integer', title: 'Inset', minimum: 0, maximum: 32 },
305
+ radius: { type: 'string', title: 'Radius', enum: ['sm', 'md', 'lg', 'xl', '2xl', '3xl'] },
306
+ border: { type: 'boolean', title: 'Border' },
307
+ },
308
+ },
309
+ sidebar: {
310
+ type: 'object',
311
+ title: 'Sidebar',
312
+ properties: {
313
+ density: { type: 'string', title: 'Density', enum: ['sparse', 'default', 'dense'] },
314
+ activeIndicator: {
315
+ type: 'string',
316
+ title: 'Active indicator',
317
+ enum: ['background', 'rail', 'both'],
318
+ },
319
+ collapsibleGroups: { type: 'boolean', title: 'Collapsible groups' },
320
+ },
321
+ },
322
+ },
323
+ }}
324
+ uiSchema={{
325
+ shell: {
326
+ 'ui:collapsible': true,
327
+ inset: {
328
+ 'ui:widget': 'slider',
329
+ 'ui:options': { unit: 'px', step: 2, showInput: false },
330
+ 'ui:disabledWhen': { path: 'shell.variant', notEq: 'boxed' },
331
+ },
332
+ radius: { 'ui:disabledWhen': { path: 'shell.variant', notEq: 'boxed' } },
333
+ border: {
334
+ 'ui:widget': 'switch',
335
+ 'ui:disabledWhen': { path: 'shell.variant', notEq: 'boxed' },
336
+ },
337
+ },
338
+ sidebar: {
339
+ 'ui:collapsible': true,
340
+ collapsibleGroups: { 'ui:widget': 'switch' },
341
+ },
342
+ }}
343
+ formData={data}
344
+ onChange={(e) => setData(e.formData ?? {})}
345
+ density="compact"
346
+ showSubmitButton={false}
347
+ />
348
+ </div>
349
+ );
350
+ };
@@ -7,14 +7,14 @@ import React, { useCallback, useMemo } from 'react';
7
7
 
8
8
  import { Alert, AlertDescription, Button } from '@djangocfg/ui-core/components';
9
9
  import Form from '@rjsf/core';
10
- import { RegistryWidgetsType } from '@rjsf/utils';
10
+ import { RegistryWidgetsType, UiSchema } from '@rjsf/utils';
11
11
  import validator from '@rjsf/validator-ajv8';
12
12
 
13
13
  import {
14
14
  ArrayFieldItemTemplate, ArrayFieldTemplate, BaseInputTemplate, ErrorListTemplate, FieldTemplate,
15
15
  ObjectFieldTemplate
16
16
  } from './templates';
17
- import { JsonSchemaFormProps } from './types';
17
+ import { JsonFormContext, JsonSchemaFormProps } from './types';
18
18
  import { normalizeFormData, validateSchema } from './utils';
19
19
  import {
20
20
  CheckboxWidget, ColorWidget, NumberWidget, SelectWidget, SliderWidget, SwitchWidget, TextWidget
@@ -60,6 +60,8 @@ export function JsonSchemaForm<T = any>(props: JsonSchemaFormProps<T>) {
60
60
  className,
61
61
  showSubmitButton = true,
62
62
  submitButtonText = 'Submit',
63
+ density = 'comfortable',
64
+ formContext: callerFormContext,
63
65
  ...restProps
64
66
  } = props;
65
67
 
@@ -167,15 +169,24 @@ export function JsonSchemaForm<T = any>(props: JsonSchemaFormProps<T>) {
167
169
  );
168
170
  }
169
171
 
172
+ // formContext threads the latest formData + density into widgets/templates so
173
+ // they can react to global form state (conditional disable, density CSS).
174
+ const formContext = useMemo<JsonFormContext & Record<string, unknown>>(() => ({
175
+ ...(callerFormContext as object | undefined),
176
+ density,
177
+ formData: normalizedFormData,
178
+ }), [callerFormContext, density, normalizedFormData]);
179
+
170
180
  return (
171
- <div className={className}>
181
+ <div className={className} data-jsonform-density={density}>
172
182
  <Form
173
183
  schema={validatedSchema}
174
- uiSchema={uiSchema}
184
+ uiSchema={uiSchema as UiSchema | undefined}
175
185
  formData={normalizedFormData}
176
186
  validator={validator}
177
187
  widgets={widgets}
178
188
  templates={templates}
189
+ formContext={formContext}
179
190
  onSubmit={handleSubmit}
180
191
  onChange={handleChange}
181
192
  onError={handleError}
@@ -0,0 +1,268 @@
1
+ # JsonForm
2
+
3
+ Schema-driven forms on top of [react-jsonschema-form](https://github.com/rjsf-team/react-jsonschema-form) (RJSF) with `@djangocfg/ui` widgets, AJV8 validation, and a few custom extensions for compact playground-style UIs.
4
+
5
+ ```tsx
6
+ import { JsonSchemaForm } from '@djangocfg/ui-tools/json-form';
7
+ // or, for code-splitting:
8
+ import { LazyJsonSchemaForm } from '@djangocfg/ui-tools';
9
+
10
+ const schema = {
11
+ type: 'object',
12
+ properties: {
13
+ name: { type: 'string', title: 'Name' },
14
+ age: { type: 'integer', title: 'Age', minimum: 0 },
15
+ },
16
+ };
17
+
18
+ <JsonSchemaForm schema={schema} onSubmit={(e) => console.log(e.formData)} />
19
+ ```
20
+
21
+ ---
22
+
23
+ ## Components
24
+
25
+ | Export | Use |
26
+ |---|---|
27
+ | `JsonSchemaForm` | Direct (synchronous) form. |
28
+ | `LazyJsonSchemaForm` | Same API, lazy-loaded with a `<CardLoadingFallback>` (RJSF is ~300KB). |
29
+
30
+ Both accept the same `JsonSchemaFormProps`:
31
+
32
+ | Prop | Type | Notes |
33
+ |---|---|---|
34
+ | `schema` | `RJSFSchema` | Required. JSON Schema 7. |
35
+ | `uiSchema` | `UiSchema` | Optional. Widget overrides + custom extensions (see below). |
36
+ | `formData` | `T` | Initial / controlled data. |
37
+ | `onChange` | `(e) => void` | Live changes. |
38
+ | `onSubmit` | `(e) => void` | Submit (enter or button). |
39
+ | `onError` | `(errors) => void` | Validation errors. |
40
+ | `density` | `'comfortable' \| 'compact'` | Default `'comfortable'`. See **Density** below. |
41
+ | `liveValidate` | `boolean` | Default `false`. |
42
+ | `disabled` / `readonly` | `boolean` | Form-wide. |
43
+ | `showSubmitButton` | `boolean` | Default `true`. |
44
+ | `submitButtonText` | `string` | Default `'Submit'`. |
45
+ | `showErrorList` | `false \| 'top' \| 'bottom'` | Default `'top'`. |
46
+ | `className` | `string` | Wrapper class. |
47
+
48
+ ---
49
+
50
+ ## Built-in widgets
51
+
52
+ Mapped via `widgets={{ ... }}` and registered under both PascalCase and lowercase aliases — use whichever matches your `uiSchema`.
53
+
54
+ | `ui:widget` | Widget | Built on |
55
+ |---|---|---|
56
+ | `text` | `TextWidget` | `<Input />` |
57
+ | `number` | `NumberWidget` | `<Input type="number" />` |
58
+ | `checkbox` | `CheckboxWidget` | `<Checkbox />` |
59
+ | `switch` | `SwitchWidget` | `<Switch />` |
60
+ | `select` | `SelectWidget` | `<Select />` (auto-falls back to `<input type="text">` when no enum) |
61
+ | `slider` / `range` | `SliderWidget` | `<Slider />` (+ optional inline numeric input, supports unit suffix) |
62
+ | `color` | `ColorWidget` | Native colour picker. |
63
+
64
+ ### Slider options
65
+
66
+ ```ts
67
+ 'ui:widget': 'slider',
68
+ 'ui:options': {
69
+ unit: 'px', // appended to the displayed value: "12px"
70
+ step: 2,
71
+ showInput: false, // hide the editable text input next to the slider
72
+ }
73
+ ```
74
+
75
+ ---
76
+
77
+ ## Templates
78
+
79
+ Custom RJSF templates live in `./templates/`:
80
+
81
+ - `FieldTemplate` — label + body + errors. Honors `density`.
82
+ - `ObjectFieldTemplate` — object body. Supports `ui:collapsible`, `ui:grid`, `ui:className`, and **`ui:groups`** (see below).
83
+ - `ArrayFieldTemplate`, `ArrayFieldItemTemplate` — array editing.
84
+ - `ErrorListTemplate` — top/bottom error block.
85
+ - `BaseInputTemplate` — fallback input wrapper.
86
+
87
+ ---
88
+
89
+ ## Custom extensions
90
+
91
+ These are extra `uiSchema` keys this fork understands on top of stock RJSF.
92
+
93
+ ### `density`
94
+
95
+ Form-wide compact mode. Drives label sizing, row spacing, and tooltip placement.
96
+
97
+ ```tsx
98
+ <JsonSchemaForm density="compact" ... />
99
+ ```
100
+
101
+ In compact mode:
102
+ - Labels: 10px uppercase muted, no description paragraph
103
+ - Description (`schema.description` or `ui:description`) becomes a `title=` tooltip on the label
104
+ - Tighter row spacing (`space-y-1` vs `space-y-2`)
105
+ - Slimmer select trigger (`h-7`) and inputs
106
+
107
+ Use this for sidebars, settings drawers, dense playground configurators.
108
+
109
+ ### `ui:disabledWhen`
110
+
111
+ Declarative conditional disable, evaluated against the current `formData`. Avoids the JSON Schema `dependencies` boilerplate when you just want to grey a field out.
112
+
113
+ ```ts
114
+ const uiSchema = {
115
+ shell: {
116
+ inset: {
117
+ 'ui:widget': 'slider',
118
+ 'ui:disabledWhen': { path: 'shell.variant', notEq: 'boxed' },
119
+ },
120
+ },
121
+ };
122
+ ```
123
+
124
+ Supported rule shapes:
125
+
126
+ | Shape | Disabled when |
127
+ |---|---|
128
+ | `{ path, eq: value }` | field at `path` equals `value` |
129
+ | `{ path, notEq: value }` | field at `path` does not equal `value` |
130
+ | `{ path, in: [...] }` | field is in array |
131
+ | `{ path, notIn: [...] }` | field is not in array |
132
+ | `{ path, truthy: true }` | field is truthy |
133
+ | `{ path, falsy: true }` | field is falsy / empty / `false` / `0` |
134
+
135
+ `path` is dot-separated against `formData` root (e.g. `'shell.variant'`, `'navbar.controls.showThemeSwitcher'`).
136
+
137
+ The runtime `evaluateDisabledWhen(rule, formData)` is exported for tests / reuse.
138
+
139
+ ### `ui:groups` (in `ObjectFieldTemplate`)
140
+
141
+ Splits a flat object into collapsible sub-sections without restructuring the schema.
142
+
143
+ ```ts
144
+ sidebar: {
145
+ 'ui:groups': [
146
+ { title: 'Density', fields: ['density'], defaultOpen: true },
147
+ { title: 'Slots', fields: ['showMenuStart', 'showMenuEnd'], defaultOpen: false },
148
+ { title: 'Visual', fields: ['activeIndicator', 'groupLabelStyle'] },
149
+ ],
150
+ }
151
+ ```
152
+
153
+ Fields not listed in any group render flat above the groups. Each group is a `<Collapsible>` with a chevron-right header.
154
+
155
+ ### `ui:collapsible` / `ui:collapsed` (existing)
156
+
157
+ Wraps the entire object in a single collapsible card. Independent of `ui:groups` — use one or the other on a given object.
158
+
159
+ ### `ui:grid` (existing)
160
+
161
+ `'ui:grid': 2` switches the object body to a 2-column grid (`gap-4`). Default is vertical stack.
162
+
163
+ ---
164
+
165
+ ## Example — playground sidebar
166
+
167
+ The `apps/demo` layouts playground uses the stack end-to-end:
168
+
169
+ ```ts
170
+ // schema
171
+ const privateSchema: RJSFSchema = {
172
+ type: 'object',
173
+ properties: {
174
+ shell: { type: 'object', properties: { variant: { type: 'string', enum: ['full-bleed', 'boxed'] }, inset: { type: 'integer', minimum: 0, maximum: 32 } } },
175
+ sidebar: { type: 'object', properties: { /* ... */ } },
176
+ },
177
+ };
178
+
179
+ // uiSchema
180
+ const privateUiSchema: UiSchema = {
181
+ shell: {
182
+ 'ui:collapsible': true,
183
+ inset: {
184
+ 'ui:widget': 'slider',
185
+ 'ui:options': { unit: 'px', step: 2, showInput: false },
186
+ 'ui:disabledWhen': { path: 'shell.variant', notEq: 'boxed' },
187
+ },
188
+ },
189
+ sidebar: {
190
+ 'ui:collapsible': true,
191
+ 'ui:groups': [
192
+ { title: 'Density', fields: ['density'], defaultOpen: true },
193
+ { title: 'Visual', fields: ['activeIndicator', 'groupLabelStyle'] },
194
+ ],
195
+ },
196
+ };
197
+
198
+ // component
199
+ <LazyJsonSchemaForm
200
+ schema={privateSchema}
201
+ uiSchema={privateUiSchema}
202
+ formData={config}
203
+ onChange={(e) => setConfig(e.formData)}
204
+ density="compact"
205
+ showSubmitButton={false}
206
+ liveValidate={false}
207
+ />
208
+ ```
209
+
210
+ See `apps/demo/.../layouts/sidebar/{private,public}/Sidebar.tsx` for the full integration.
211
+
212
+ ---
213
+
214
+ ## Internals
215
+
216
+ | File | Role |
217
+ |---|---|
218
+ | `JsonSchemaForm.tsx` | Top-level component. Validates schema, normalises `formData`, threads `formContext = { density, formData }` to widgets/templates. |
219
+ | `widgets/_useWidgetEnv.ts` | Internal hook reading density + `ui:disabledWhen` from `formContext`. Used by `Switch`, `Select`, `Slider`, `Checkbox`. |
220
+ | `templates/ObjectFieldTemplate.tsx` | Implements `ui:collapsible`, `ui:grid`, `ui:groups`. |
221
+ | `templates/FieldTemplate.tsx` | Density-aware label / description / errors layout. |
222
+ | `utils.ts` | `validateSchema`, `normalizeFormData`, `evaluateDisabledWhen`, plus default-merging helpers. |
223
+
224
+ ---
225
+
226
+ ## Re-exported types
227
+
228
+ For consumers that don't have `@rjsf/utils` as a direct dependency:
229
+
230
+ ```ts
231
+ import type {
232
+ RJSFSchema,
233
+ UiSchema,
234
+ JsonSchemaFormProps,
235
+ JsonFormDensity,
236
+ DisabledWhenRule,
237
+ UiGroup,
238
+ JsonFormContext,
239
+ } from '@djangocfg/ui-tools';
240
+ ```
241
+
242
+ ### Portable schema types (`CustomJson*`)
243
+
244
+ For packages that **ship a configurator schema** without taking a runtime
245
+ dependency on RJSF — typically library packages like `@djangocfg/layouts/configurator`
246
+ — use the portable types from `@djangocfg/ui-core/lib`:
247
+
248
+ ```ts
249
+ import type {
250
+ CustomJsonSchema7,
251
+ CustomJsonSchema7Type,
252
+ CustomJsonUiSchema7,
253
+ CustomJsonUiGroup,
254
+ CustomJsonUiDisabledWhenRule,
255
+ } from '@djangocfg/ui-core/lib';
256
+
257
+ // also re-exported from `@djangocfg/ui-tools` for convenience.
258
+ ```
259
+
260
+ These are our own Draft 7-shaped subset (not a copy of RJSF's types) and
261
+ `<JsonSchemaForm>` accepts them directly via union with `RJSFSchema`. Library
262
+ packages typing their schemas with `CustomJsonSchema7` stay decoupled from
263
+ the form framework — if RJSF is ever swapped out, these schemas don't need
264
+ to change.
265
+
266
+ The aliases `DisabledWhenRule` and `UiGroup` exported from `@djangocfg/ui-tools`
267
+ point at `CustomJsonUiDisabledWhenRule` / `CustomJsonUiGroup` respectively
268
+ (kept for back-compat with existing consumers).
@@ -34,7 +34,28 @@
34
34
  */
35
35
 
36
36
  export { JsonSchemaForm } from './JsonSchemaForm';
37
- export type { JsonSchemaFormProps } from './types';
37
+ export type {
38
+ JsonSchemaFormProps,
39
+ JsonFormDensity,
40
+ DisabledWhenRule,
41
+ UiGroup,
42
+ JsonFormContext,
43
+ } from './types';
44
+ export { evaluateDisabledWhen } from './utils';
45
+
46
+ // Re-export RJSF schema types so consumers don't need to install `@rjsf/utils`
47
+ // directly. Keeps ui-tools the single dependency for schema-driven forms.
48
+ export type { RJSFSchema, UiSchema } from '@rjsf/utils';
49
+
50
+ // Re-export the portable schema types from ui-core for convenience: most
51
+ // configurator authors only need these (no RJSF coupling).
52
+ export type {
53
+ CustomJsonSchema7,
54
+ CustomJsonSchema7Type,
55
+ CustomJsonUiSchema7,
56
+ CustomJsonUiGroup,
57
+ CustomJsonUiDisabledWhenRule,
58
+ } from '@djangocfg/ui-core/lib';
38
59
 
39
60
  // Export widgets for custom usage
40
61
  export * from './widgets';
@@ -6,6 +6,8 @@ import { Label } from '@djangocfg/ui-core/components';
6
6
  import { cn } from '@djangocfg/ui-core/lib';
7
7
  import { FieldTemplateProps } from '@rjsf/utils';
8
8
 
9
+ import type { JsonFormDensity } from '../types';
10
+
9
11
  /**
10
12
  * Field template for JSON Schema Form
11
13
  * Controls the layout and styling of individual form fields
@@ -25,36 +27,57 @@ export function FieldTemplate(props: FieldTemplateProps) {
25
27
  hidden,
26
28
  rawErrors,
27
29
  } = props;
30
+ const formContext = (props as { formContext?: { density?: JsonFormDensity } }).formContext;
28
31
 
29
32
  if (hidden) {
30
33
  return <div className="hidden">{children}</div>;
31
34
  }
32
35
 
33
36
  const hasError = rawErrors && rawErrors.length > 0;
37
+ const density = (formContext?.density as JsonFormDensity | undefined) ?? 'comfortable';
38
+ const compact = density === 'compact';
39
+
40
+ // In compact mode the description is suppressed in the body and forwarded to
41
+ // the label `title=` tooltip — keeps rows dense without losing context.
42
+ const descriptionText =
43
+ typeof description === 'string'
44
+ ? description
45
+ : undefined;
46
+ const labelTitle = compact ? descriptionText : undefined;
47
+ const showDescription = !compact && Boolean(description);
34
48
 
35
49
  return (
36
50
  <div
37
- className={cn('space-y-2', classNames)}
51
+ className={cn(compact ? 'space-y-1' : 'space-y-2', classNames)}
38
52
  style={style}
39
53
  >
40
54
  {displayLabel && label && (
41
- <Label htmlFor={id} className={cn(hasError && 'text-destructive')}>
55
+ <Label
56
+ htmlFor={id}
57
+ className={cn(
58
+ hasError && 'text-destructive',
59
+ compact && 'text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/70',
60
+ )}
61
+ title={labelTitle}
62
+ >
42
63
  {label}
43
64
  {required && <span className="text-destructive ml-1">*</span>}
44
65
  </Label>
45
66
  )}
46
67
 
47
- {description && (
68
+ {showDescription && (
48
69
  <div className="text-sm text-muted-foreground">{description}</div>
49
70
  )}
50
71
 
51
72
  <div>{children}</div>
52
73
 
53
74
  {errors && (
54
- <div className="text-sm text-destructive">{errors}</div>
75
+ <div className={compact ? 'text-xs text-destructive' : 'text-sm text-destructive'}>
76
+ {errors}
77
+ </div>
55
78
  )}
56
79
 
57
- {help && (
80
+ {help && !compact && (
58
81
  <div className="text-sm text-muted-foreground">{help}</div>
59
82
  )}
60
83
  </div>