@object-ui/react 0.5.0 → 3.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.
Files changed (144) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +32 -0
  3. package/dist/LazyPluginLoader.d.ts +44 -0
  4. package/dist/LazyPluginLoader.js +92 -3
  5. package/dist/SchemaRenderer.d.ts +20 -1
  6. package/dist/SchemaRenderer.js +76 -13
  7. package/dist/components/form/FieldFactory.js +44 -40
  8. package/dist/components/form/FormRenderer.js +45 -3
  9. package/dist/context/ActionContext.d.ts +76 -0
  10. package/dist/context/ActionContext.js +139 -0
  11. package/dist/context/DndContext.d.ts +135 -0
  12. package/dist/context/DndContext.js +120 -0
  13. package/dist/context/NotificationContext.d.ts +113 -0
  14. package/dist/context/NotificationContext.js +130 -0
  15. package/dist/context/ThemeContext.d.ts +94 -0
  16. package/dist/context/ThemeContext.js +215 -0
  17. package/dist/context/index.d.ts +4 -0
  18. package/dist/context/index.js +4 -0
  19. package/dist/hooks/index.d.ts +22 -0
  20. package/dist/hooks/index.js +22 -0
  21. package/dist/hooks/useActionRunner.d.ts +21 -7
  22. package/dist/hooks/useActionRunner.js +27 -7
  23. package/dist/hooks/useAnimation.d.ts +46 -0
  24. package/dist/hooks/useAnimation.js +59 -0
  25. package/dist/hooks/useClientNotifications.d.ts +53 -0
  26. package/dist/hooks/useClientNotifications.js +190 -0
  27. package/dist/hooks/useColumnSummary.d.ts +38 -0
  28. package/dist/hooks/useColumnSummary.js +54 -0
  29. package/dist/hooks/useCrudShortcuts.d.ts +39 -0
  30. package/dist/hooks/useCrudShortcuts.js +96 -0
  31. package/dist/hooks/useDensityMode.d.ts +48 -0
  32. package/dist/hooks/useDensityMode.js +60 -0
  33. package/dist/hooks/useDiscovery.d.ts +78 -0
  34. package/dist/hooks/useDiscovery.js +96 -0
  35. package/dist/hooks/useDynamicApp.d.ts +70 -0
  36. package/dist/hooks/useDynamicApp.js +92 -0
  37. package/dist/hooks/useETagCache.d.ts +75 -0
  38. package/dist/hooks/useETagCache.js +306 -0
  39. package/dist/hooks/useFocusManagement.d.ts +89 -0
  40. package/dist/hooks/useFocusManagement.js +188 -0
  41. package/dist/hooks/useFocusTrap.d.ts +41 -0
  42. package/dist/hooks/useFocusTrap.js +103 -0
  43. package/dist/hooks/useKeyboardShortcuts.d.ts +49 -0
  44. package/dist/hooks/useKeyboardShortcuts.js +86 -0
  45. package/dist/hooks/useNavigationOverlay.d.ts +78 -0
  46. package/dist/hooks/useNavigationOverlay.js +112 -0
  47. package/dist/hooks/useOffline.d.ts +96 -0
  48. package/dist/hooks/useOffline.js +185 -0
  49. package/dist/hooks/usePageTransition.d.ts +60 -0
  50. package/dist/hooks/usePageTransition.js +111 -0
  51. package/dist/hooks/usePageVariables.d.ts +67 -0
  52. package/dist/hooks/usePageVariables.js +109 -0
  53. package/dist/hooks/usePerformance.d.ts +87 -0
  54. package/dist/hooks/usePerformance.js +165 -0
  55. package/dist/hooks/usePerformanceBudget.d.ts +78 -0
  56. package/dist/hooks/usePerformanceBudget.js +209 -0
  57. package/dist/hooks/useReducedMotion.d.ts +28 -0
  58. package/dist/hooks/useReducedMotion.js +48 -0
  59. package/dist/hooks/useSchemaPersistence.d.ts +87 -0
  60. package/dist/hooks/useSchemaPersistence.js +173 -0
  61. package/dist/hooks/useViewData.d.ts +73 -0
  62. package/dist/hooks/useViewData.js +135 -0
  63. package/dist/hooks/useViewSharing.d.ts +57 -0
  64. package/dist/hooks/useViewSharing.js +74 -0
  65. package/dist/hooks/useViewTransition.d.ts +54 -0
  66. package/dist/hooks/useViewTransition.js +83 -0
  67. package/dist/index.d.ts +1 -0
  68. package/dist/index.js +2 -0
  69. package/dist/utils/i18n.d.ts +10 -0
  70. package/dist/utils/i18n.js +12 -0
  71. package/package.json +7 -5
  72. package/src/LazyPluginLoader.tsx +129 -7
  73. package/src/SchemaRenderer.tsx +122 -13
  74. package/src/__tests__/LazyPluginLoader.test.tsx +97 -1
  75. package/src/__tests__/SchemaRenderer.aria.test.tsx +118 -0
  76. package/src/__tests__/SchemaRenderer.errorBoundary.test.tsx +114 -0
  77. package/src/__tests__/SchemaRendererProvider.smoke.test.tsx +132 -0
  78. package/src/__tests__/ThemeProvider.test.tsx +451 -0
  79. package/src/__tests__/useNavigationOverlay.test.ts +416 -0
  80. package/src/__tests__/useOffline.test.ts +198 -0
  81. package/src/__tests__/usePageTransition.test.ts +214 -0
  82. package/src/__tests__/usePageVariables.test.tsx +334 -0
  83. package/src/__tests__/usePerformance.test.ts +137 -0
  84. package/src/__tests__/useViewData.test.tsx +422 -0
  85. package/src/__tests__/view-switching.test.tsx +456 -0
  86. package/src/components/form/FieldFactory.tsx +51 -46
  87. package/src/components/form/FormRenderer.test.tsx +145 -0
  88. package/src/components/form/FormRenderer.tsx +52 -3
  89. package/src/context/ActionContext.tsx +213 -0
  90. package/src/context/DndContext.tsx +260 -0
  91. package/src/context/NotificationContext.tsx +280 -0
  92. package/src/context/ThemeContext.tsx +325 -0
  93. package/src/context/__tests__/ActionContext.test.tsx +174 -0
  94. package/src/context/__tests__/DndContext.test.tsx +150 -0
  95. package/src/context/__tests__/NotificationContext.test.tsx +176 -0
  96. package/src/context/index.ts +4 -0
  97. package/src/hooks/__tests__/useActionRunner.test.ts +153 -0
  98. package/src/hooks/__tests__/useAnimation.test.ts +82 -0
  99. package/src/hooks/__tests__/useColumnSummary.test.ts +93 -0
  100. package/src/hooks/__tests__/useDensityMode.test.ts +81 -0
  101. package/src/hooks/__tests__/useDiscovery.test.tsx +149 -0
  102. package/src/hooks/__tests__/useDynamicApp.test.ts +161 -0
  103. package/src/hooks/__tests__/useETagCache.test.ts +153 -0
  104. package/src/hooks/__tests__/useExpression.test.ts +86 -0
  105. package/src/hooks/__tests__/useFocusTrap.test.ts +47 -0
  106. package/src/hooks/__tests__/useKeyboardShortcuts.test.ts +144 -0
  107. package/src/hooks/__tests__/usePerformanceBudget.test.ts +157 -0
  108. package/src/hooks/__tests__/useSchemaPersistence.test.ts +220 -0
  109. package/src/hooks/__tests__/useViewSharing.test.ts +121 -0
  110. package/src/hooks/__tests__/useViewTransition.test.ts +61 -0
  111. package/src/hooks/index.ts +22 -1
  112. package/src/hooks/useActionRunner.ts +49 -11
  113. package/src/hooks/useAnimation.ts +117 -0
  114. package/src/hooks/useClientNotifications.ts +251 -0
  115. package/src/hooks/useColumnSummary.ts +87 -0
  116. package/src/hooks/useCrudShortcuts.ts +126 -0
  117. package/src/hooks/useDensityMode.ts +102 -0
  118. package/src/hooks/useDiscovery.ts +141 -0
  119. package/src/hooks/useDynamicApp.ts +142 -0
  120. package/src/hooks/useETagCache.ts +404 -0
  121. package/src/hooks/useFocusManagement.ts +248 -0
  122. package/src/hooks/useFocusTrap.ts +138 -0
  123. package/src/hooks/useKeyboardShortcuts.ts +119 -0
  124. package/src/hooks/useNavigationOverlay.ts +180 -0
  125. package/src/hooks/useOffline.ts +301 -0
  126. package/src/hooks/usePageTransition.ts +187 -0
  127. package/src/hooks/usePageVariables.tsx +157 -0
  128. package/src/hooks/usePerformance.ts +246 -0
  129. package/src/hooks/usePerformanceBudget.ts +313 -0
  130. package/src/hooks/useReducedMotion.ts +53 -0
  131. package/src/hooks/useSchemaPersistence.ts +222 -0
  132. package/src/hooks/useViewData.ts +201 -0
  133. package/src/hooks/useViewSharing.ts +140 -0
  134. package/src/hooks/useViewTransition.ts +144 -0
  135. package/src/index.ts +21 -0
  136. package/src/utils/i18n.ts +10 -0
  137. package/tsconfig.json +1 -0
  138. package/tsconfig.tsbuildinfo +1 -1
  139. package/dist/__tests__/LazyPluginLoader.test.d.ts +0 -8
  140. package/dist/__tests__/LazyPluginLoader.test.js +0 -49
  141. package/dist/components/form/FormRenderer.test.d.ts +0 -8
  142. package/dist/components/form/FormRenderer.test.js +0 -534
  143. package/dist/index.test.d.ts +0 -8
  144. package/dist/index.test.js +0 -13
@@ -9,6 +9,7 @@
9
9
  import React from 'react';
10
10
  import { UseFormReturn } from 'react-hook-form';
11
11
  import type { FormField } from '@objectstack/spec/ui';
12
+ import { resolveI18nLabel } from '../../utils/i18n';
12
13
 
13
14
  /**
14
15
  * Extended form field with additional properties for complex widgets
@@ -58,6 +59,11 @@ export const FieldFactory: React.FC<FieldFactoryProps> = ({
58
59
  const widgetType = field.widget || 'text';
59
60
  const fieldName = field.field;
60
61
  const error = errors[fieldName];
62
+
63
+ // Resolve I18nLabel values to plain strings
64
+ const fieldLabel = resolveI18nLabel(field.label);
65
+ const fieldPlaceholder = resolveI18nLabel(field.placeholder);
66
+ const fieldHelpText = resolveI18nLabel(field.helpText);
61
67
 
62
68
  // Helper function to handle multiple select value conversion
63
69
  const handleMultipleSelectValue = (value: any) => {
@@ -68,9 +74,8 @@ export const FieldFactory: React.FC<FieldFactoryProps> = ({
68
74
  };
69
75
 
70
76
  // Handle conditional visibility
71
- // Note: visibleOn expression evaluation is not yet implemented
72
- // Fields are always visible unless explicitly hidden
73
- // Skip if explicitly hidden
77
+ // Fields are hidden when explicitly hidden or when visibleOn evaluates to false
78
+ // Note: dependsOn is handled at the FormSectionRenderer level
74
79
  if (field.hidden) {
75
80
  return null;
76
81
  }
@@ -78,15 +83,15 @@ export const FieldFactory: React.FC<FieldFactoryProps> = ({
78
83
  // Common field wrapper
79
84
  const renderField = (input: React.ReactNode) => (
80
85
  <div className="space-y-2">
81
- {field.label && (
86
+ {fieldLabel && (
82
87
  <label htmlFor={fieldName} className="block text-sm font-medium text-gray-700">
83
- {field.label}
88
+ {fieldLabel}
84
89
  {field.required && <span className="text-red-500 ml-1">*</span>}
85
90
  </label>
86
91
  )}
87
92
  {input}
88
- {field.helpText && (
89
- <p className="text-sm text-gray-500">{field.helpText}</p>
93
+ {fieldHelpText && (
94
+ <p className="text-sm text-gray-500">{fieldHelpText}</p>
90
95
  )}
91
96
  {error && (
92
97
  <p className="text-sm text-red-600">{error.message as string}</p>
@@ -106,11 +111,11 @@ export const FieldFactory: React.FC<FieldFactoryProps> = ({
106
111
  <input
107
112
  id={fieldName}
108
113
  type={widgetType === 'string' ? 'text' : widgetType}
109
- placeholder={field.placeholder}
114
+ placeholder={fieldPlaceholder}
110
115
  disabled={disabled}
111
116
  className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed"
112
117
  {...register(fieldName, {
113
- required: field.required ? `${field.label || fieldName} is required` : false,
118
+ required: field.required ? `${fieldLabel || fieldName} is required` : false,
114
119
  })}
115
120
  />
116
121
  );
@@ -122,12 +127,12 @@ export const FieldFactory: React.FC<FieldFactoryProps> = ({
122
127
  <input
123
128
  id={fieldName}
124
129
  type="number"
125
- placeholder={field.placeholder}
130
+ placeholder={fieldPlaceholder}
126
131
  disabled={disabled}
127
132
  step={widgetType === 'integer' ? '1' : 'any'}
128
133
  className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed"
129
134
  {...register(fieldName, {
130
- required: field.required ? `${field.label || fieldName} is required` : false,
135
+ required: field.required ? `${fieldLabel || fieldName} is required` : false,
131
136
  valueAsNumber: true,
132
137
  })}
133
138
  />
@@ -145,14 +150,14 @@ export const FieldFactory: React.FC<FieldFactoryProps> = ({
145
150
  {...register(fieldName)}
146
151
  />
147
152
  <div className="flex-1">
148
- {field.label && (
153
+ {fieldLabel && (
149
154
  <label htmlFor={fieldName} className="text-sm font-medium text-gray-700">
150
- {field.label}
155
+ {fieldLabel}
151
156
  {field.required && <span className="text-red-500 ml-1">*</span>}
152
157
  </label>
153
158
  )}
154
- {field.helpText && (
155
- <p className="text-sm text-gray-500 mt-1">{field.helpText}</p>
159
+ {fieldHelpText && (
160
+ <p className="text-sm text-gray-500 mt-1">{fieldHelpText}</p>
156
161
  )}
157
162
  {error && (
158
163
  <p className="text-sm text-red-600 mt-1">{error.message as string}</p>
@@ -165,12 +170,12 @@ export const FieldFactory: React.FC<FieldFactoryProps> = ({
165
170
  return renderField(
166
171
  <textarea
167
172
  id={fieldName}
168
- placeholder={field.placeholder}
173
+ placeholder={fieldPlaceholder}
169
174
  disabled={disabled}
170
175
  rows={4}
171
176
  className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed"
172
177
  {...register(fieldName, {
173
- required: field.required ? `${field.label || fieldName} is required` : false,
178
+ required: field.required ? `${fieldLabel || fieldName} is required` : false,
174
179
  })}
175
180
  />
176
181
  );
@@ -184,11 +189,11 @@ export const FieldFactory: React.FC<FieldFactoryProps> = ({
184
189
  multiple={extendedField.multiple}
185
190
  className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed"
186
191
  {...register(fieldName, {
187
- required: field.required ? `${field.label || fieldName} is required` : false,
192
+ required: field.required ? `${fieldLabel || fieldName} is required` : false,
188
193
  setValueAs: handleMultipleSelectValue,
189
194
  })}
190
195
  >
191
- {!extendedField.multiple && <option value="">{field.placeholder || 'Select an option'}</option>}
196
+ {!extendedField.multiple && <option value="">{fieldPlaceholder || 'Select an option'}</option>}
192
197
  {extendedField.options?.map((option) => (
193
198
  <option key={option.value} value={option.value} disabled={option.disabled}>
194
199
  {option.label}
@@ -202,11 +207,11 @@ export const FieldFactory: React.FC<FieldFactoryProps> = ({
202
207
  <input
203
208
  id={fieldName}
204
209
  type="date"
205
- placeholder={field.placeholder}
210
+ placeholder={fieldPlaceholder}
206
211
  disabled={disabled}
207
212
  className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed"
208
213
  {...register(fieldName, {
209
- required: field.required ? `${field.label || fieldName} is required` : false,
214
+ required: field.required ? `${fieldLabel || fieldName} is required` : false,
210
215
  })}
211
216
  />
212
217
  );
@@ -217,11 +222,11 @@ export const FieldFactory: React.FC<FieldFactoryProps> = ({
217
222
  <input
218
223
  id={fieldName}
219
224
  type="datetime-local"
220
- placeholder={field.placeholder}
225
+ placeholder={fieldPlaceholder}
221
226
  disabled={disabled}
222
227
  className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed"
223
228
  {...register(fieldName, {
224
- required: field.required ? `${field.label || fieldName} is required` : false,
229
+ required: field.required ? `${fieldLabel || fieldName} is required` : false,
225
230
  })}
226
231
  />
227
232
  );
@@ -231,11 +236,11 @@ export const FieldFactory: React.FC<FieldFactoryProps> = ({
231
236
  <input
232
237
  id={fieldName}
233
238
  type="time"
234
- placeholder={field.placeholder}
239
+ placeholder={fieldPlaceholder}
235
240
  disabled={disabled}
236
241
  className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed"
237
242
  {...register(fieldName, {
238
- required: field.required ? `${field.label || fieldName} is required` : false,
243
+ required: field.required ? `${fieldLabel || fieldName} is required` : false,
239
244
  })}
240
245
  />
241
246
  );
@@ -247,12 +252,12 @@ export const FieldFactory: React.FC<FieldFactoryProps> = ({
247
252
  <input
248
253
  id={fieldName}
249
254
  type="number"
250
- placeholder={field.placeholder}
255
+ placeholder={fieldPlaceholder}
251
256
  disabled={disabled}
252
257
  step="0.01"
253
258
  className="w-full pl-8 pr-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed"
254
259
  {...register(fieldName, {
255
- required: field.required ? `${field.label || fieldName} is required` : false,
260
+ required: field.required ? `${fieldLabel || fieldName} is required` : false,
256
261
  valueAsNumber: true,
257
262
  })}
258
263
  />
@@ -265,14 +270,14 @@ export const FieldFactory: React.FC<FieldFactoryProps> = ({
265
270
  <input
266
271
  id={fieldName}
267
272
  type="number"
268
- placeholder={field.placeholder}
273
+ placeholder={fieldPlaceholder}
269
274
  disabled={disabled}
270
275
  step="0.01"
271
276
  min="0"
272
277
  max="100"
273
278
  className="w-full pr-8 pl-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed"
274
279
  {...register(fieldName, {
275
- required: field.required ? `${field.label || fieldName} is required` : false,
280
+ required: field.required ? `${fieldLabel || fieldName} is required` : false,
276
281
  valueAsNumber: true,
277
282
  })}
278
283
  />
@@ -285,11 +290,11 @@ export const FieldFactory: React.FC<FieldFactoryProps> = ({
285
290
  <input
286
291
  id={fieldName}
287
292
  type="tel"
288
- placeholder={field.placeholder || '+1 (555) 000-0000'}
293
+ placeholder={fieldPlaceholder || '+1 (555) 000-0000'}
289
294
  disabled={disabled}
290
295
  className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed"
291
296
  {...register(fieldName, {
292
- required: field.required ? `${field.label || fieldName} is required` : false,
297
+ required: field.required ? `${fieldLabel || fieldName} is required` : false,
293
298
  })}
294
299
  />
295
300
  );
@@ -298,12 +303,12 @@ export const FieldFactory: React.FC<FieldFactoryProps> = ({
298
303
  return renderField(
299
304
  <textarea
300
305
  id={fieldName}
301
- placeholder={field.placeholder || 'Enter markdown text...'}
306
+ placeholder={fieldPlaceholder || 'Enter markdown text...'}
302
307
  disabled={disabled}
303
308
  rows={8}
304
309
  className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed font-mono"
305
310
  {...register(fieldName, {
306
- required: field.required ? `${field.label || fieldName} is required` : false,
311
+ required: field.required ? `${fieldLabel || fieldName} is required` : false,
307
312
  })}
308
313
  />
309
314
  );
@@ -312,12 +317,12 @@ export const FieldFactory: React.FC<FieldFactoryProps> = ({
312
317
  return renderField(
313
318
  <textarea
314
319
  id={fieldName}
315
- placeholder={field.placeholder || 'Enter HTML...'}
320
+ placeholder={fieldPlaceholder || 'Enter HTML...'}
316
321
  disabled={disabled}
317
322
  rows={8}
318
323
  className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed font-mono"
319
324
  {...register(fieldName, {
320
- required: field.required ? `${field.label || fieldName} is required` : false,
325
+ required: field.required ? `${fieldLabel || fieldName} is required` : false,
321
326
  })}
322
327
  />
323
328
  );
@@ -332,7 +337,7 @@ export const FieldFactory: React.FC<FieldFactoryProps> = ({
332
337
  accept={extendedField.accept?.join(',')}
333
338
  className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100"
334
339
  {...register(fieldName, {
335
- required: field.required ? `${field.label || fieldName} is required` : false,
340
+ required: field.required ? `${fieldLabel || fieldName} is required` : false,
336
341
  })}
337
342
  />
338
343
  );
@@ -347,7 +352,7 @@ export const FieldFactory: React.FC<FieldFactoryProps> = ({
347
352
  accept={extendedField.accept?.join(',') || 'image/*'}
348
353
  className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100"
349
354
  {...register(fieldName, {
350
- required: field.required ? `${field.label || fieldName} is required` : false,
355
+ required: field.required ? `${fieldLabel || fieldName} is required` : false,
351
356
  })}
352
357
  />
353
358
  );
@@ -391,11 +396,11 @@ export const FieldFactory: React.FC<FieldFactoryProps> = ({
391
396
  multiple={extendedField.multiple}
392
397
  className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed"
393
398
  {...register(fieldName, {
394
- required: field.required ? `${field.label || fieldName} is required` : false,
399
+ required: field.required ? `${fieldLabel || fieldName} is required` : false,
395
400
  setValueAs: handleMultipleSelectValue,
396
401
  })}
397
402
  >
398
- {!extendedField.multiple && <option value="">{field.placeholder || 'Select an option'}</option>}
403
+ {!extendedField.multiple && <option value="">{fieldPlaceholder || 'Select an option'}</option>}
399
404
  {extendedField.options?.map((option) => (
400
405
  <option key={option.value} value={option.value} disabled={option.disabled}>
401
406
  {option.label}
@@ -413,11 +418,11 @@ export const FieldFactory: React.FC<FieldFactoryProps> = ({
413
418
  multiple={extendedField.multiple}
414
419
  className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed"
415
420
  {...register(fieldName, {
416
- required: field.required ? `${field.label || fieldName} is required` : false,
421
+ required: field.required ? `${fieldLabel || fieldName} is required` : false,
417
422
  setValueAs: handleMultipleSelectValue,
418
423
  })}
419
424
  >
420
- {!extendedField.multiple && <option value="">{field.placeholder || 'Select user'}</option>}
425
+ {!extendedField.multiple && <option value="">{fieldPlaceholder || 'Select user'}</option>}
421
426
  {extendedField.options?.map((option) => (
422
427
  <option key={option.value} value={option.value} disabled={option.disabled}>
423
428
  {option.label}
@@ -444,12 +449,12 @@ export const FieldFactory: React.FC<FieldFactoryProps> = ({
444
449
  return renderField(
445
450
  <textarea
446
451
  id={fieldName}
447
- placeholder={field.placeholder || 'Enter JSON object...'}
452
+ placeholder={fieldPlaceholder || 'Enter JSON object...'}
448
453
  disabled={disabled}
449
454
  rows={6}
450
455
  className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed font-mono text-sm"
451
456
  {...register(fieldName, {
452
- required: field.required ? `${field.label || fieldName} is required` : false,
457
+ required: field.required ? `${fieldLabel || fieldName} is required` : false,
453
458
  validate: (value) => {
454
459
  if (!value) return true;
455
460
  try {
@@ -469,7 +474,7 @@ export const FieldFactory: React.FC<FieldFactoryProps> = ({
469
474
  <input
470
475
  id={fieldName}
471
476
  type="text"
472
- placeholder={field.placeholder || 'Vector data (read-only)'}
477
+ placeholder={fieldPlaceholder || 'Vector data (read-only)'}
473
478
  disabled={true}
474
479
  className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-gray-50 cursor-not-allowed text-gray-600"
475
480
  {...register(fieldName)}
@@ -490,11 +495,11 @@ export const FieldFactory: React.FC<FieldFactoryProps> = ({
490
495
  <input
491
496
  id={fieldName}
492
497
  type="text"
493
- placeholder={field.placeholder}
498
+ placeholder={fieldPlaceholder}
494
499
  disabled={disabled}
495
500
  className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed"
496
501
  {...register(fieldName, {
497
- required: field.required ? `${field.label || fieldName} is required` : false,
502
+ required: field.required ? `${fieldLabel || fieldName} is required` : false,
498
503
  })}
499
504
  />
500
505
  );
@@ -596,4 +596,149 @@ describe('FormRenderer', () => {
596
596
  expect(textarea.tagName).toBe('TEXTAREA');
597
597
  });
598
598
  });
599
+
600
+ // ==========================================================================
601
+ // FormField.dependsOn — conditional field visibility based on parent value
602
+ // ==========================================================================
603
+
604
+ describe('FormField.dependsOn', () => {
605
+ it('should hide fields with dependsOn when parent has no value', () => {
606
+ const schema: FormView = {
607
+ type: 'simple',
608
+ sections: [
609
+ {
610
+ label: 'Cascading Fields',
611
+ collapsible: false,
612
+ collapsed: false,
613
+ columns: 1,
614
+ fields: [
615
+ {
616
+ field: 'country',
617
+ label: 'Country',
618
+ widget: 'text',
619
+ },
620
+ {
621
+ field: 'state',
622
+ label: 'State',
623
+ widget: 'text',
624
+ dependsOn: 'country',
625
+ },
626
+ ],
627
+ },
628
+ ],
629
+ };
630
+
631
+ render(<FormRenderer schema={schema} />);
632
+
633
+ // Parent field is always visible
634
+ expect(screen.getByLabelText('Country')).toBeInTheDocument();
635
+ // Dependent field is hidden when parent has no value
636
+ expect(screen.queryByLabelText('State')).not.toBeInTheDocument();
637
+ });
638
+
639
+ it('should show fields with dependsOn when parent has a value', () => {
640
+ const schema: FormView = {
641
+ type: 'simple',
642
+ sections: [
643
+ {
644
+ label: 'Cascading Fields',
645
+ collapsible: false,
646
+ collapsed: false,
647
+ columns: 1,
648
+ fields: [
649
+ {
650
+ field: 'country',
651
+ label: 'Country',
652
+ widget: 'text',
653
+ },
654
+ {
655
+ field: 'state',
656
+ label: 'State',
657
+ widget: 'text',
658
+ dependsOn: 'country',
659
+ },
660
+ ],
661
+ },
662
+ ],
663
+ };
664
+
665
+ // When country has an initial value, state should be visible
666
+ render(<FormRenderer schema={schema} data={{ country: 'USA' }} />);
667
+
668
+ expect(screen.getByLabelText('Country')).toBeInTheDocument();
669
+ expect(screen.getByLabelText('State')).toBeInTheDocument();
670
+ });
671
+ });
672
+
673
+ // ==========================================================================
674
+ // FormField.visibleOn — expression-based visibility
675
+ // ==========================================================================
676
+
677
+ describe('FormField.visibleOn', () => {
678
+ it('should hide fields when visibleOn evaluates to false', () => {
679
+ const schema: FormView = {
680
+ type: 'simple',
681
+ sections: [
682
+ {
683
+ label: 'Conditional Fields',
684
+ collapsible: false,
685
+ collapsed: false,
686
+ columns: 1,
687
+ fields: [
688
+ {
689
+ field: 'status',
690
+ label: 'Status',
691
+ widget: 'text',
692
+ },
693
+ {
694
+ field: 'price',
695
+ label: 'Price',
696
+ widget: 'currency',
697
+ visibleOn: "${data.status === 'active'}",
698
+ },
699
+ ],
700
+ },
701
+ ],
702
+ };
703
+
704
+ // Status is 'draft', so price should be hidden
705
+ render(<FormRenderer schema={schema} data={{ status: 'draft' }} />);
706
+
707
+ expect(screen.getByLabelText('Status')).toBeInTheDocument();
708
+ expect(screen.queryByLabelText('Price')).not.toBeInTheDocument();
709
+ });
710
+
711
+ it('should show fields when visibleOn evaluates to true', () => {
712
+ const schema: FormView = {
713
+ type: 'simple',
714
+ sections: [
715
+ {
716
+ label: 'Conditional Fields',
717
+ collapsible: false,
718
+ collapsed: false,
719
+ columns: 1,
720
+ fields: [
721
+ {
722
+ field: 'status',
723
+ label: 'Status',
724
+ widget: 'text',
725
+ },
726
+ {
727
+ field: 'price',
728
+ label: 'Price',
729
+ widget: 'currency',
730
+ visibleOn: "${data.status === 'active'}",
731
+ },
732
+ ],
733
+ },
734
+ ],
735
+ };
736
+
737
+ // Status is 'active', so price should be visible
738
+ render(<FormRenderer schema={schema} data={{ status: 'active' }} />);
739
+
740
+ expect(screen.getByLabelText('Status')).toBeInTheDocument();
741
+ expect(screen.getByLabelText('Price')).toBeInTheDocument();
742
+ });
743
+ });
599
744
  });
@@ -10,6 +10,8 @@ import React from 'react';
10
10
  import { useForm, FormProvider, UseFormReturn } from 'react-hook-form';
11
11
  import type { FormView, FormSection } from '@objectstack/spec/ui';
12
12
  import { FieldFactory } from './FieldFactory';
13
+ import { ExpressionEvaluator } from '@object-ui/core';
14
+ import { resolveI18nLabel } from '../../utils/i18n';
13
15
 
14
16
  export interface FormRendererProps {
15
17
  /**
@@ -137,6 +139,39 @@ const COL_SPAN = {
137
139
  4: 'col-span-4',
138
140
  } as const;
139
141
 
142
+ /**
143
+ * Evaluate whether a field should be visible based on visibleOn expression
144
+ * and dependsOn parent field value.
145
+ */
146
+ function evaluateFieldVisibility(
147
+ fieldConfig: any,
148
+ formValues: Record<string, any>,
149
+ ): boolean {
150
+ // If explicitly hidden, skip
151
+ if (fieldConfig.hidden) {
152
+ return false;
153
+ }
154
+
155
+ // Evaluate visibleOn expression
156
+ if (fieldConfig.visibleOn) {
157
+ try {
158
+ const evaluator = new ExpressionEvaluator({ data: formValues, form: formValues });
159
+ return evaluator.evaluateCondition(fieldConfig.visibleOn);
160
+ } catch {
161
+ // On error, default to visible
162
+ return true;
163
+ }
164
+ }
165
+
166
+ // Evaluate dependsOn: field is hidden if parent field has no value
167
+ if (fieldConfig.dependsOn) {
168
+ const parentValue = formValues[fieldConfig.dependsOn];
169
+ return parentValue !== undefined && parentValue !== null && parentValue !== '';
170
+ }
171
+
172
+ return true;
173
+ }
174
+
140
175
  /**
141
176
  * Renders a form section with grid layout
142
177
  */
@@ -150,6 +185,20 @@ const FormSectionRenderer: React.FC<FormSectionRendererProps> = ({
150
185
  // Determine grid columns based on section.columns
151
186
  const gridCols = GRID_COLS[section.columns || 1];
152
187
 
188
+ // Collect whether there are any conditional fields
189
+ const hasConditionalFields = React.useMemo(() => {
190
+ return section.fields.some((field) => {
191
+ if (typeof field === 'string') return false;
192
+ return !!field.dependsOn || !!field.visibleOn;
193
+ });
194
+ }, [section.fields]);
195
+
196
+ // Watch form values for conditional field evaluation
197
+ // Only watch when there are fields with dependsOn or visibleOn to avoid
198
+ // unnecessary re-renders in forms without conditional fields.
199
+ const allFormValues = methods.watch();
200
+ const formValues = hasConditionalFields ? allFormValues : undefined;
201
+
153
202
  const handleToggleCollapse = () => {
154
203
  if (section.collapsible) {
155
204
  setIsCollapsed(!isCollapsed);
@@ -165,7 +214,7 @@ const FormSectionRenderer: React.FC<FormSectionRendererProps> = ({
165
214
  }`}
166
215
  onClick={handleToggleCollapse}
167
216
  >
168
- <h3 className="text-lg font-semibold">{section.label}</h3>
217
+ <h3 className="text-lg font-semibold">{resolveI18nLabel(section.label)}</h3>
169
218
  {section.collapsible && (
170
219
  <span className="text-sm text-gray-500">
171
220
  {isCollapsed ? '▶' : '▼'}
@@ -181,8 +230,8 @@ const FormSectionRenderer: React.FC<FormSectionRendererProps> = ({
181
230
  const fieldName = typeof field === 'string' ? field : field.field;
182
231
  const fieldConfig = typeof field === 'string' ? { field: fieldName } : field;
183
232
 
184
- // Skip hidden fields
185
- if (fieldConfig.hidden) {
233
+ // Evaluate visibility: static hidden, visibleOn expression, dependsOn parent
234
+ if (!evaluateFieldVisibility(fieldConfig, formValues || {})) {
186
235
  return null;
187
236
  }
188
237