@qoretechnologies/reqraft 0.10.25 → 0.10.26

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 (310) hide show
  1. package/.claude/CLAUDE.md +52 -23
  2. package/design/COMPACT_ENGINE_REDESIGN.md +156 -0
  3. package/design/FORM_ENGINE_COMPACT_UX_PLAN.md +353 -0
  4. package/dist/components/FocusedEditing/index.d.ts +9 -1
  5. package/dist/components/FocusedEditing/index.d.ts.map +1 -1
  6. package/dist/components/FocusedEditing/index.js +2 -2
  7. package/dist/components/FocusedEditing/index.js.map +1 -1
  8. package/dist/components/composer/Composer.d.ts +108 -0
  9. package/dist/components/composer/Composer.d.ts.map +1 -0
  10. package/dist/components/composer/Composer.js +238 -0
  11. package/dist/components/composer/Composer.js.map +1 -0
  12. package/dist/components/composer/helpers.d.ts +21 -0
  13. package/dist/components/composer/helpers.d.ts.map +1 -0
  14. package/dist/components/composer/helpers.js +75 -0
  15. package/dist/components/composer/helpers.js.map +1 -0
  16. package/dist/components/composer/index.d.ts +3 -0
  17. package/dist/components/composer/index.d.ts.map +1 -0
  18. package/dist/components/composer/index.js +19 -0
  19. package/dist/components/composer/index.js.map +1 -0
  20. package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
  21. package/dist/components/form/engine/CompactRow.js +307 -110
  22. package/dist/components/form/engine/CompactRow.js.map +1 -1
  23. package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
  24. package/dist/components/form/engine/CompactToolbar.js +123 -106
  25. package/dist/components/form/engine/CompactToolbar.js.map +1 -1
  26. package/dist/components/form/engine/FormEngine.d.ts +107 -11
  27. package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
  28. package/dist/components/form/engine/FormEngine.js +618 -179
  29. package/dist/components/form/engine/FormEngine.js.map +1 -1
  30. package/dist/components/form/engine/compactRowContext.d.ts +9 -0
  31. package/dist/components/form/engine/compactRowContext.d.ts.map +1 -1
  32. package/dist/components/form/engine/compactRowContext.js.map +1 -1
  33. package/dist/components/form/engine/compactRowStyles.d.ts +7 -3
  34. package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
  35. package/dist/components/form/engine/compactRowStyles.js +88 -51
  36. package/dist/components/form/engine/compactRowStyles.js.map +1 -1
  37. package/dist/components/form/engine/compactToolbarContext.d.ts +1 -0
  38. package/dist/components/form/engine/compactToolbarContext.d.ts.map +1 -1
  39. package/dist/components/form/engine/compactToolbarContext.js.map +1 -1
  40. package/dist/components/form/engine/optionActions.d.ts +28 -0
  41. package/dist/components/form/engine/optionActions.d.ts.map +1 -0
  42. package/dist/components/form/engine/optionActions.js +18 -0
  43. package/dist/components/form/engine/optionActions.js.map +1 -0
  44. package/dist/components/form/engine/readFirst.d.ts +63 -2
  45. package/dist/components/form/engine/readFirst.d.ts.map +1 -1
  46. package/dist/components/form/engine/readFirst.js +121 -6
  47. package/dist/components/form/engine/readFirst.js.map +1 -1
  48. package/dist/components/form/engine/rendererTypes.d.ts +35 -0
  49. package/dist/components/form/engine/rendererTypes.d.ts.map +1 -0
  50. package/dist/components/form/engine/rendererTypes.js +63 -0
  51. package/dist/components/form/engine/rendererTypes.js.map +1 -0
  52. package/dist/components/form/engine/variants/VariantCalmTable.d.ts +6 -0
  53. package/dist/components/form/engine/variants/VariantCalmTable.d.ts.map +1 -0
  54. package/dist/components/form/engine/variants/VariantCalmTable.js +94 -0
  55. package/dist/components/form/engine/variants/VariantCalmTable.js.map +1 -0
  56. package/dist/components/form/engine/variants/VariantCards.d.ts +6 -0
  57. package/dist/components/form/engine/variants/VariantCards.d.ts.map +1 -0
  58. package/dist/components/form/engine/variants/VariantCards.js +80 -0
  59. package/dist/components/form/engine/variants/VariantCards.js.map +1 -0
  60. package/dist/components/form/engine/variants/VariantFocus.d.ts +7 -0
  61. package/dist/components/form/engine/variants/VariantFocus.d.ts.map +1 -0
  62. package/dist/components/form/engine/variants/VariantFocus.js +138 -0
  63. package/dist/components/form/engine/variants/VariantFocus.js.map +1 -0
  64. package/dist/components/form/engine/variants/VariantMinimal.d.ts +6 -0
  65. package/dist/components/form/engine/variants/VariantMinimal.d.ts.map +1 -0
  66. package/dist/components/form/engine/variants/VariantMinimal.js +73 -0
  67. package/dist/components/form/engine/variants/VariantMinimal.js.map +1 -0
  68. package/dist/components/form/engine/variants/focusDemo.d.ts +13 -0
  69. package/dist/components/form/engine/variants/focusDemo.d.ts.map +1 -0
  70. package/dist/components/form/engine/variants/focusDemo.js +139 -0
  71. package/dist/components/form/engine/variants/focusDemo.js.map +1 -0
  72. package/dist/components/form/engine/variants/variantModel.d.ts +70 -0
  73. package/dist/components/form/engine/variants/variantModel.d.ts.map +1 -0
  74. package/dist/components/form/engine/variants/variantModel.js +133 -0
  75. package/dist/components/form/engine/variants/variantModel.js.map +1 -0
  76. package/dist/components/form/engine/variants/variantParts.d.ts +79 -0
  77. package/dist/components/form/engine/variants/variantParts.d.ts.map +1 -0
  78. package/dist/components/form/engine/variants/variantParts.js +191 -0
  79. package/dist/components/form/engine/variants/variantParts.js.map +1 -0
  80. package/dist/components/form/expressions/builder/index.d.ts.map +1 -1
  81. package/dist/components/form/expressions/builder/index.js +13 -2
  82. package/dist/components/form/expressions/builder/index.js.map +1 -1
  83. package/dist/components/form/expressions/useExpressions.d.ts.map +1 -1
  84. package/dist/components/form/expressions/useExpressions.js +4 -1
  85. package/dist/components/form/expressions/useExpressions.js.map +1 -1
  86. package/dist/components/form/fields/Field.d.ts.map +1 -1
  87. package/dist/components/form/fields/Field.js +5 -3
  88. package/dist/components/form/fields/Field.js.map +1 -1
  89. package/dist/components/form/fields/allowed-values/AllowedValues.d.ts +2 -1
  90. package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
  91. package/dist/components/form/fields/allowed-values/AllowedValues.js +8 -26
  92. package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
  93. package/dist/components/form/fields/auto/AutoFormField.d.ts +3 -0
  94. package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
  95. package/dist/components/form/fields/auto/AutoFormField.js +42 -8
  96. package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
  97. package/dist/components/form/fields/binary/Binary.d.ts +15 -0
  98. package/dist/components/form/fields/binary/Binary.d.ts.map +1 -0
  99. package/dist/components/form/fields/binary/Binary.js +74 -0
  100. package/dist/components/form/fields/binary/Binary.js.map +1 -0
  101. package/dist/components/form/fields/boolean/Boolean.js +1 -1
  102. package/dist/components/form/fields/boolean/Boolean.js.map +1 -1
  103. package/dist/components/form/fields/multi-select/MultiSelectFormField.d.ts.map +1 -1
  104. package/dist/components/form/fields/multi-select/MultiSelectFormField.js +5 -4
  105. package/dist/components/form/fields/multi-select/MultiSelectFormField.js.map +1 -1
  106. package/dist/components/form/fields/select/Select.d.ts.map +1 -1
  107. package/dist/components/form/fields/select/Select.js +29 -12
  108. package/dist/components/form/fields/select/Select.js.map +1 -1
  109. package/dist/components/form/fields/select/SelectCollection.d.ts +8 -2
  110. package/dist/components/form/fields/select/SelectCollection.d.ts.map +1 -1
  111. package/dist/components/form/fields/select/SelectCollection.js +52 -16
  112. package/dist/components/form/fields/select/SelectCollection.js.map +1 -1
  113. package/dist/components/form/index.d.ts +2 -0
  114. package/dist/components/form/index.d.ts.map +1 -1
  115. package/dist/components/form/index.js +2 -0
  116. package/dist/components/form/index.js.map +1 -1
  117. package/dist/components/imageLightbox/ImageLightbox.d.ts +21 -0
  118. package/dist/components/imageLightbox/ImageLightbox.d.ts.map +1 -0
  119. package/dist/components/imageLightbox/ImageLightbox.js +169 -0
  120. package/dist/components/imageLightbox/ImageLightbox.js.map +1 -0
  121. package/dist/components/imageLightbox/index.d.ts +2 -0
  122. package/dist/components/imageLightbox/index.d.ts.map +1 -0
  123. package/dist/components/imageLightbox/index.js +18 -0
  124. package/dist/components/imageLightbox/index.js.map +1 -0
  125. package/dist/components/searchFilterBar/SearchFilterBar.d.ts +33 -0
  126. package/dist/components/searchFilterBar/SearchFilterBar.d.ts.map +1 -0
  127. package/dist/components/searchFilterBar/SearchFilterBar.js +76 -0
  128. package/dist/components/searchFilterBar/SearchFilterBar.js.map +1 -0
  129. package/dist/components/searchFilterBar/index.d.ts +2 -0
  130. package/dist/components/searchFilterBar/index.d.ts.map +1 -0
  131. package/dist/components/searchFilterBar/index.js +18 -0
  132. package/dist/components/searchFilterBar/index.js.map +1 -0
  133. package/dist/components/supportTicket/AttachmentChips.d.ts +10 -0
  134. package/dist/components/supportTicket/AttachmentChips.d.ts.map +1 -0
  135. package/dist/components/supportTicket/AttachmentChips.js +122 -0
  136. package/dist/components/supportTicket/AttachmentChips.js.map +1 -0
  137. package/dist/components/supportTicket/InterfaceReferenceTags.d.ts +43 -0
  138. package/dist/components/supportTicket/InterfaceReferenceTags.d.ts.map +1 -0
  139. package/dist/components/supportTicket/InterfaceReferenceTags.js +31 -0
  140. package/dist/components/supportTicket/InterfaceReferenceTags.js.map +1 -0
  141. package/dist/components/supportTicket/ReferencePicker.d.ts +87 -0
  142. package/dist/components/supportTicket/ReferencePicker.d.ts.map +1 -0
  143. package/dist/components/supportTicket/ReferencePicker.js +295 -0
  144. package/dist/components/supportTicket/ReferencePicker.js.map +1 -0
  145. package/dist/components/supportTicket/TicketHeader.d.ts +86 -0
  146. package/dist/components/supportTicket/TicketHeader.d.ts.map +1 -0
  147. package/dist/components/supportTicket/TicketHeader.js +149 -0
  148. package/dist/components/supportTicket/TicketHeader.js.map +1 -0
  149. package/dist/components/supportTicket/TicketMetaTags.d.ts +32 -0
  150. package/dist/components/supportTicket/TicketMetaTags.d.ts.map +1 -0
  151. package/dist/components/supportTicket/TicketMetaTags.js +21 -0
  152. package/dist/components/supportTicket/TicketMetaTags.js.map +1 -0
  153. package/dist/components/supportTicket/TicketReplyBox.d.ts +63 -0
  154. package/dist/components/supportTicket/TicketReplyBox.d.ts.map +1 -0
  155. package/dist/components/supportTicket/TicketReplyBox.js +115 -0
  156. package/dist/components/supportTicket/TicketReplyBox.js.map +1 -0
  157. package/dist/components/supportTicket/attachmentAccess.d.ts +37 -0
  158. package/dist/components/supportTicket/attachmentAccess.d.ts.map +1 -0
  159. package/dist/components/supportTicket/attachmentAccess.js +132 -0
  160. package/dist/components/supportTicket/attachmentAccess.js.map +1 -0
  161. package/dist/components/supportTicket/fileToAttachment.d.ts +9 -0
  162. package/dist/components/supportTicket/fileToAttachment.d.ts.map +1 -0
  163. package/dist/components/supportTicket/fileToAttachment.js +28 -0
  164. package/dist/components/supportTicket/fileToAttachment.js.map +1 -0
  165. package/dist/components/supportTicket/index.d.ts +11 -0
  166. package/dist/components/supportTicket/index.d.ts.map +1 -0
  167. package/dist/components/supportTicket/index.js +27 -0
  168. package/dist/components/supportTicket/index.js.map +1 -0
  169. package/dist/components/supportTicket/meta.d.ts +71 -0
  170. package/dist/components/supportTicket/meta.d.ts.map +1 -0
  171. package/dist/components/supportTicket/meta.js +115 -0
  172. package/dist/components/supportTicket/meta.js.map +1 -0
  173. package/dist/components/supportTicket/ticketModel.d.ts +102 -0
  174. package/dist/components/supportTicket/ticketModel.d.ts.map +1 -0
  175. package/dist/components/supportTicket/ticketModel.js +3 -0
  176. package/dist/components/supportTicket/ticketModel.js.map +1 -0
  177. package/dist/components/ticketReferences/TicketReferences.d.ts +52 -0
  178. package/dist/components/ticketReferences/TicketReferences.d.ts.map +1 -0
  179. package/dist/components/ticketReferences/TicketReferences.js +492 -0
  180. package/dist/components/ticketReferences/TicketReferences.js.map +1 -0
  181. package/dist/components/ticketReferences/index.d.ts +3 -0
  182. package/dist/components/ticketReferences/index.d.ts.map +1 -0
  183. package/dist/components/ticketReferences/index.js +6 -0
  184. package/dist/components/ticketReferences/index.js.map +1 -0
  185. package/dist/components/ticketThread/TicketThread.d.ts +57 -0
  186. package/dist/components/ticketThread/TicketThread.d.ts.map +1 -0
  187. package/dist/components/ticketThread/TicketThread.js +299 -0
  188. package/dist/components/ticketThread/TicketThread.js.map +1 -0
  189. package/dist/components/ticketThread/index.d.ts +3 -0
  190. package/dist/components/ticketThread/index.d.ts.map +1 -0
  191. package/dist/components/ticketThread/index.js +7 -0
  192. package/dist/components/ticketThread/index.js.map +1 -0
  193. package/dist/helpers/common.d.ts +5 -0
  194. package/dist/helpers/common.d.ts.map +1 -1
  195. package/dist/helpers/common.js +20 -1
  196. package/dist/helpers/common.js.map +1 -1
  197. package/dist/helpers/scrollbar.d.ts +2 -0
  198. package/dist/helpers/scrollbar.d.ts.map +1 -0
  199. package/dist/helpers/scrollbar.js +25 -0
  200. package/dist/helpers/scrollbar.js.map +1 -0
  201. package/dist/index.d.ts +8 -1
  202. package/dist/index.d.ts.map +1 -1
  203. package/dist/index.js +10 -1
  204. package/dist/index.js.map +1 -1
  205. package/dist/providers/StorageProvider.d.ts +13 -0
  206. package/dist/providers/StorageProvider.d.ts.map +1 -1
  207. package/dist/providers/StorageProvider.js +29 -5
  208. package/dist/providers/StorageProvider.js.map +1 -1
  209. package/package.json +6 -5
  210. package/src/components/FocusedEditing/index.tsx +17 -2
  211. package/src/components/composer/Composer.stories.tsx +238 -0
  212. package/src/components/composer/Composer.tsx +545 -0
  213. package/src/components/composer/helpers.ts +74 -0
  214. package/src/components/composer/index.ts +2 -0
  215. package/src/components/dpqlEditor/DpqlEditor.stories.tsx +74 -2
  216. package/src/components/form/engine/CompactRow.tsx +561 -283
  217. package/src/components/form/engine/CompactToolbar.tsx +216 -182
  218. package/src/components/form/engine/FormEngine.stories.tsx +1736 -162
  219. package/src/components/form/engine/FormEngine.tsx +886 -170
  220. package/src/components/form/engine/FormEngineRemote.stories.tsx +377 -0
  221. package/src/components/form/engine/_structuredData/StructuredDataView.stories.tsx +8 -0
  222. package/src/components/form/engine/compactRowContext.ts +14 -0
  223. package/src/components/form/engine/compactRowStyles.ts +273 -146
  224. package/src/components/form/engine/compactToolbarContext.ts +1 -0
  225. package/src/components/form/engine/optionActions.ts +40 -0
  226. package/src/components/form/engine/readFirst.ts +169 -10
  227. package/src/components/form/engine/rendererTypes.ts +55 -0
  228. package/src/components/form/engine/variants/FormEngineVariants.stories.tsx +159 -0
  229. package/src/components/form/engine/variants/VariantCalmTable.tsx +242 -0
  230. package/src/components/form/engine/variants/VariantCards.tsx +212 -0
  231. package/src/components/form/engine/variants/VariantFocus.tsx +382 -0
  232. package/src/components/form/engine/variants/VariantMinimal.tsx +170 -0
  233. package/src/components/form/engine/variants/focusDemo.ts +145 -0
  234. package/src/components/form/engine/variants/variantModel.ts +216 -0
  235. package/src/components/form/engine/variants/variantParts.tsx +313 -0
  236. package/src/components/form/expressions/ExpressionField.stories.tsx +84 -2
  237. package/src/components/form/expressions/builder/ExpressionBuilder.stories.tsx +213 -19
  238. package/src/components/form/expressions/builder/index.tsx +12 -1
  239. package/src/components/form/expressions/useExpressions.ts +4 -1
  240. package/src/components/form/fields/Field.stories.tsx +40 -0
  241. package/src/components/form/fields/Field.tsx +10 -1
  242. package/src/components/form/fields/allowed-values/AllowedValues.tsx +13 -36
  243. package/src/components/form/fields/array/ArrayAutoField.stories.tsx +64 -0
  244. package/src/components/form/fields/auto/AutoFormField.stories.tsx +146 -2
  245. package/src/components/form/fields/auto/AutoFormField.tsx +42 -1
  246. package/src/components/form/fields/binary/Binary.stories.tsx +122 -0
  247. package/src/components/form/fields/binary/Binary.tsx +81 -0
  248. package/src/components/form/fields/boolean/Boolean.stories.tsx +58 -0
  249. package/src/components/form/fields/boolean/Boolean.tsx +1 -1
  250. package/src/components/form/fields/byte-size/ByteSize.stories.tsx +24 -0
  251. package/src/components/form/fields/color/Color.stories.tsx +30 -0
  252. package/src/components/form/fields/cron/Cron.stories.tsx +8 -0
  253. package/src/components/form/fields/date/Date.stories.tsx +22 -0
  254. package/src/components/form/fields/file/File.stories.tsx +48 -0
  255. package/src/components/form/fields/long-string/LongString.stories.tsx +24 -0
  256. package/src/components/form/fields/markdown/Markdown.stories.tsx +8 -0
  257. package/src/components/form/fields/multi-select/MultiSelectFormField.tsx +14 -10
  258. package/src/components/form/fields/number/Number.stories.tsx +31 -0
  259. package/src/components/form/fields/object/Object.stories.tsx +88 -0
  260. package/src/components/form/fields/radio-group/RadioGroup.stories.tsx +24 -0
  261. package/src/components/form/fields/rich-text/RichText.stories.tsx +23 -0
  262. package/src/components/form/fields/schema-definition/SchemaDefinitionField.stories.tsx +72 -0
  263. package/src/components/form/fields/select/Select.stories.tsx +108 -0
  264. package/src/components/form/fields/select/Select.tsx +38 -12
  265. package/src/components/form/fields/select/SelectCollection.tsx +78 -17
  266. package/src/components/form/fields/string/String.stories.tsx +16 -0
  267. package/src/components/form/fields/template/TemplateField.stories.tsx +160 -0
  268. package/src/components/form/fields/url/Url.stories.tsx +24 -0
  269. package/src/components/form/index.tsx +2 -0
  270. package/src/components/imageLightbox/ImageLightbox.stories.tsx +238 -0
  271. package/src/components/imageLightbox/ImageLightbox.tsx +236 -0
  272. package/src/components/imageLightbox/index.ts +1 -0
  273. package/src/components/log/Log.stories.tsx +96 -0
  274. package/src/components/menu/Menu.stories.tsx +54 -0
  275. package/src/components/qonsoleSmartInput/QonsoleSmartInput.stories.tsx +83 -37
  276. package/src/components/searchFilterBar/SearchFilterBar.tsx +131 -0
  277. package/src/components/searchFilterBar/index.ts +1 -0
  278. package/src/components/smartEditor/SmartEditor.stories.tsx +32 -0
  279. package/src/components/supportTicket/AttachmentChips.stories.tsx +171 -0
  280. package/src/components/supportTicket/AttachmentChips.tsx +147 -0
  281. package/src/components/supportTicket/InterfaceReferenceTags.stories.tsx +171 -0
  282. package/src/components/supportTicket/InterfaceReferenceTags.tsx +96 -0
  283. package/src/components/supportTicket/ReferencePicker.stories.tsx +519 -0
  284. package/src/components/supportTicket/ReferencePicker.tsx +563 -0
  285. package/src/components/supportTicket/TicketConversation.stories.tsx +121 -0
  286. package/src/components/supportTicket/TicketHeader.stories.tsx +183 -0
  287. package/src/components/supportTicket/TicketHeader.tsx +247 -0
  288. package/src/components/supportTicket/TicketMetaTags.stories.tsx +103 -0
  289. package/src/components/supportTicket/TicketMetaTags.tsx +85 -0
  290. package/src/components/supportTicket/TicketReplyBox.stories.tsx +351 -0
  291. package/src/components/supportTicket/TicketReplyBox.tsx +146 -0
  292. package/src/components/supportTicket/attachmentAccess.ts +97 -0
  293. package/src/components/supportTicket/fileToAttachment.ts +24 -0
  294. package/src/components/supportTicket/index.ts +10 -0
  295. package/src/components/supportTicket/meta.ts +185 -0
  296. package/src/components/supportTicket/ticketModel.ts +131 -0
  297. package/src/components/ticketReferences/TicketReferences.stories.tsx +315 -0
  298. package/src/components/ticketReferences/TicketReferences.tsx +886 -0
  299. package/src/components/ticketReferences/index.ts +2 -0
  300. package/src/components/ticketThread/TicketThread.stories.tsx +828 -0
  301. package/src/components/ticketThread/TicketThread.tsx +704 -0
  302. package/src/components/ticketThread/index.ts +6 -0
  303. package/src/helpers/common.ts +19 -0
  304. package/src/helpers/scrollbar.ts +32 -0
  305. package/src/hooks/useFetch/useFetch.stories.tsx +30 -0
  306. package/src/hooks/useStorage/useStorage.stories.tsx +82 -0
  307. package/src/hooks/useWebSocket/useWebsocket.stories.tsx +127 -1
  308. package/src/index.tsx +8 -1
  309. package/src/providers/StorageProvider.tsx +30 -4
  310. package/src/stores/currentUser/currentUser.stories.tsx +49 -0
@@ -1,4 +1,9 @@
1
- import { IQorusFormField, TQorusFormFieldSchema } from '@qoretechnologies/ts-toolkit';
1
+ import {
2
+ IQorusFormField,
3
+ TQorusFormFieldSchema,
4
+ TQorusType,
5
+ } from '@qoretechnologies/ts-toolkit';
6
+ import { isRendererOnlyUiType } from './rendererTypes';
2
7
  import { isUiEncodedValue } from './_structuredData/structuredData';
3
8
  import { renderExpressionToText } from '../expressions/renderExpressionToText';
4
9
  import { IExpressionValue } from '../expressions/types';
@@ -17,12 +22,33 @@ const pluralize = (count: number, noun: string): string =>
17
22
  // full server allow-list, incl. keys like `sensitive`/`required`).
18
23
  const isTypedEnvelope = (raw: unknown): raw is IQorusFormField => isUiEncodedValue(raw);
19
24
 
20
- /** Summarise a list value: join item names, or fall back to an "N items" count. */
25
+ /** Reduce ONE list item to a short, human label never an object. Unwraps a
26
+ * typed `{type, value}` envelope, prefers a `name`/`display_name`, and returns
27
+ * undefined for a bare object (no name) so the caller falls back to a count
28
+ * instead of printing "[object Object]". */
29
+ const summarizeListItem = (item: unknown): string | number | undefined => {
30
+ if (item === null || item === undefined) return undefined;
31
+ if (typeof item !== 'object') return item as string | number;
32
+ const obj = item as Record<string, unknown>;
33
+ // A named object (or allowed-value) → its label.
34
+ if (typeof obj.name === 'string' || typeof obj.name === 'number') return obj.name;
35
+ if (typeof obj.display_name === 'string') return obj.display_name;
36
+ // A typed envelope ({type, value}) or a {value} wrapper → look inside once.
37
+ if ('value' in obj) {
38
+ const inner = obj.value;
39
+ if (inner === null || inner === undefined) return undefined;
40
+ if (typeof inner !== 'object') return inner as string | number;
41
+ const innerName = (inner as Record<string, unknown>).name;
42
+ return typeof innerName === 'string' || typeof innerName === 'number' ? innerName : undefined;
43
+ }
44
+ return undefined;
45
+ };
46
+
47
+ /** Summarise a list value: join item labels, or fall back to an "N items" count
48
+ * (e.g. a list of anonymous hashes). Never prints raw objects. */
21
49
  const formatList = (items: unknown[]): string => {
22
50
  const parts = items
23
- .map((item) =>
24
- item && typeof item === 'object' ? ((item as any).name ?? (item as any).value ?? '') : item
25
- )
51
+ .map(summarizeListItem)
26
52
  .filter((part) => part !== '' && part !== undefined && part !== null);
27
53
 
28
54
  return parts.length ? parts.join(', ') : pluralize(items.length, 'item');
@@ -61,6 +87,48 @@ export const isOptionValueEmpty = (value: unknown): boolean =>
61
87
  value === '' ||
62
88
  (Array.isArray(value) && value.length === 0);
63
89
 
90
+ export const isFixedCompactAllowedValueOption = (
91
+ schema: TQorusFormFieldSchema | undefined
92
+ ): boolean => {
93
+ if (!schema) {
94
+ return false;
95
+ }
96
+
97
+ const selectableSchema = schema as TQorusFormFieldSchema & { items?: unknown[] };
98
+ const options = selectableSchema.allowed_values?.length
99
+ ? selectableSchema.allowed_values
100
+ : selectableSchema.items;
101
+
102
+ return !!(
103
+ options?.length &&
104
+ !schema.allowed_values_creatable &&
105
+ !schema.multiselect &&
106
+ !schema.arg_schema
107
+ );
108
+ };
109
+
110
+ export const shouldAutoCollapseCompactAllowedValueOption = (
111
+ schema: TQorusFormFieldSchema | undefined,
112
+ value: unknown
113
+ ): boolean => !isOptionValueEmpty(value) && isFixedCompactAllowedValueOption(schema);
114
+
115
+ /** Boolean fields have three meaningful states: unset, Yes, and No. Once the
116
+ * user chooses either boolean value in compact mode, the choice is complete
117
+ * and the inline editor can close without a separate confirmation. */
118
+ export const isCompactBooleanOption = (
119
+ schema: TQorusFormFieldSchema | undefined
120
+ ): boolean => {
121
+ const type = (schema?.ui_type || schema?.type)?.toLowerCase();
122
+ return type === 'bool' || type === 'boolean';
123
+ };
124
+
125
+ export const shouldAutoCollapseCompactOption = (
126
+ schema: TQorusFormFieldSchema | undefined,
127
+ value: unknown
128
+ ): boolean =>
129
+ !isOptionValueEmpty(value) &&
130
+ (isCompactBooleanOption(schema) || isFixedCompactAllowedValueOption(schema));
131
+
64
132
  /** Prefer the matching allowed_values entry's display_name (fallback `name`)
65
133
  * over the raw stored value. */
66
134
  const findAllowedOption = (value: unknown, schema?: TQorusFormFieldSchema): any | undefined => {
@@ -202,15 +270,29 @@ export const getFileSize = (value: unknown): number | undefined => {
202
270
  return typeof size === 'number' && Number.isFinite(size) && size >= 0 ? size : undefined;
203
271
  };
204
272
 
205
- /** Effective UI type for display: the stored `type` wins (any/auto picks),
206
- * then schema `ui_type`, then `type`. */
273
+ /**
274
+ * Effective UI type for DISPLAY — which preview/summary a value renders as.
275
+ *
276
+ * A renderer-only `ui_type` wins outright: the field's stored `type` holds the
277
+ * STORAGE type for those (a `code-editor` stores a string), so consulting the
278
+ * stored type here would render the bespoke editor's value as a plain string and
279
+ * silently drop its preview. Every other case keeps the stored type first, so an
280
+ * `any`/`auto` field still displays as whatever concrete type the user picked.
281
+ */
207
282
  export const getValueType = (
208
283
  option?: IQorusFormField,
209
284
  schema?: TQorusFormFieldSchema
210
- ): string | undefined =>
211
- (option?.type ||
212
- (schema as { ui_type?: string; type?: string } | undefined)?.ui_type ||
285
+ ): string | undefined => {
286
+ const uiType = (schema as { ui_type?: string } | undefined)?.ui_type;
287
+
288
+ if (isRendererOnlyUiType(uiType as TQorusType)) {
289
+ return uiType;
290
+ }
291
+
292
+ return (option?.type ||
293
+ uiType ||
213
294
  (schema as { type?: string } | undefined)?.type) as string | undefined;
295
+ };
214
296
 
215
297
  /** Human-readable byte count (e.g. `1.2 KB`). */
216
298
  export const formatBytes = (bytes: number): string => {
@@ -435,6 +517,41 @@ export interface IReadFirstCompletion {
435
517
  pct: number;
436
518
  }
437
519
 
520
+ /** Read-first row status, shared by the status DOT (CompactRow) and the
521
+ * status BOX bucketing (FormEngine) so the two can never disagree:
522
+ * invalid — a value fails validation, or a danger message is present (red)
523
+ * todo — empty & required (or has a warning message), needs a value (amber)
524
+ * set — has a valid value (green)
525
+ * optional — empty & not required, or covered by a one-of sibling (calm) */
526
+ export type TReadFirstStatus = 'invalid' | 'todo' | 'set' | 'optional';
527
+
528
+ export const getReadFirstStatus = (s: {
529
+ empty: boolean;
530
+ required: boolean;
531
+ /** Empty member of a one-of required group already satisfied by a sibling. */
532
+ covered: boolean;
533
+ /** Non-empty value fails validation, or a danger message is attached. */
534
+ invalid: boolean;
535
+ /** A warning message is attached (surfaces an empty field for attention). */
536
+ warned: boolean;
537
+ }): TReadFirstStatus => {
538
+ if (s.invalid) return 'invalid';
539
+ if (!s.empty) return 'set';
540
+ // A one-of member covered by a satisfied sibling reads as "set" (green) — the
541
+ // requirement is met; the row just shows a "Covered by …" note.
542
+ if (s.covered) return 'set';
543
+ if (s.required || s.warned) return 'todo';
544
+ return 'optional';
545
+ };
546
+
547
+ /** Coarse bucket for the three status boxes (Needs attention / Set / Optional). */
548
+ export const getReadFirstBucket = (
549
+ status: TReadFirstStatus
550
+ ): 'attention' | 'set' | 'optional' =>
551
+ status === 'set' ? 'set'
552
+ : status === 'optional' ? 'optional'
553
+ : 'attention';
554
+
438
555
  /** Count how many of the shown options have a value set, for the progress meter. */
439
556
  export const getReadFirstCompletion = (
440
557
  shownOptions: Record<string, IQorusFormField | undefined> = {}
@@ -446,3 +563,45 @@ export const getReadFirstCompletion = (
446
563
 
447
564
  return { total, set, pct };
448
565
  };
566
+
567
+ /** What the "first field to fix" selector needs to know about one field.
568
+ * Kept deliberately minimal — both verdicts are supplied by the caller — so the
569
+ * selector is decoupled from the engine's value/schema shapes and unit-testable
570
+ * in isolation. */
571
+ export interface IFirstAttentionFieldMeta {
572
+ /** Whether the field can actually receive focus — the caller folds in
573
+ * disabled / readonly / read-only-form / unmet-dependency gating here. */
574
+ focusable: boolean;
575
+ /** Whether the form considers this field to "need attention". The caller
576
+ * supplies this from the SAME `getOptionBucket` the read-first status boxes
577
+ * use, so it covers every attention case — empty-required, an unsatisfied
578
+ * one-of group, AND a filled-but-invalid value — and the autofocus target can
579
+ * never drift from the visible needs-attention set. */
580
+ needsAttention: boolean;
581
+ }
582
+
583
+ /**
584
+ * Pick the first field the user must fix: the first focusable field — in the
585
+ * supplied (already ordered) name list — that the form buckets as "needs
586
+ * attention".
587
+ *
588
+ * Pure and side-effect free. It deliberately does NOT re-derive emptiness /
589
+ * required / validity itself; the caller passes `needsAttention` straight from
590
+ * the engine's own `getOptionBucket`, so there is a single source of truth and
591
+ * the "focus the first field to fix" affordance can never disagree with the
592
+ * per-row status the user sees (including filled-but-invalid rows). Returns
593
+ * `undefined` when nothing needs attention.
594
+ */
595
+ export const getFirstAttentionOptionName = (
596
+ orderedNames: string[],
597
+ getFieldMeta: (name: string) => IFirstAttentionFieldMeta | undefined
598
+ ): string | undefined => {
599
+ for (const name of orderedNames) {
600
+ const meta = getFieldMeta(name);
601
+ if (meta?.focusable && meta.needsAttention) {
602
+ return name;
603
+ }
604
+ }
605
+
606
+ return undefined;
607
+ };
@@ -0,0 +1,55 @@
1
+ import { TQorusType } from '@qoretechnologies/ts-toolkit';
2
+
3
+ /**
4
+ * A schema entry carries two type-ish keys with different jobs:
5
+ *
6
+ * - `type` names how the value is STORED and validated (`string`, `hash`, `list`…).
7
+ * - `ui_type` names which EDITOR renders it.
8
+ *
9
+ * For most fields `ui_type` is a storage-compatible refinement (`long-string`,
10
+ * `enum`, `url`) and it is safe to let it win for both jobs. But some `ui_type`s
11
+ * name a bespoke editor whose value is still stored as the plainer `type` — a
12
+ * `cron` editor stores a string, `processor-mappings` stores a hash. Writing
13
+ * those renderer names into the field's `type` breaks validation and round-trips
14
+ * the value through the wrong branch, which is what `RENDERER_ONLY_UI_TYPES`
15
+ * exists to prevent.
16
+ *
17
+ * There is deliberately no derivation here: a renderer-only `ui_type` is not
18
+ * distinguishable from a storage-compatible one by inspecting the value or the
19
+ * `TQorusType` union (`code-editor` and `tool-catalog` are members of it, and
20
+ * the validator has cases for `cron` and `schema-definition`). It is a fact
21
+ * about which editor a name selects, so it has to be declared.
22
+ */
23
+ export const BUILT_IN_RENDERER_ONLY_UI_TYPES: readonly string[] = [
24
+ 'active-windows',
25
+ 'alert-threshold',
26
+ 'code-editor',
27
+ 'cron',
28
+ 'dpql',
29
+ 'processor-mappings',
30
+ 'schema-definition',
31
+ 'test-cases',
32
+ 'test-value-contract',
33
+ 'tool-catalog',
34
+ ];
35
+
36
+ /**
37
+ * Build the renderer-only predicate for one FormEngine instance.
38
+ *
39
+ * Consumers inject their own editors through `componentOverrides`, so the set of
40
+ * renderer-only names is open-ended — the IDE ships editors reqraft has never
41
+ * heard of. `extraTypes` (the `rendererOnlyUiTypes` prop) lets a consumer declare
42
+ * its own without waiting on a reqraft release, which is what previously made
43
+ * every new IDE editor a silent value-corruption bug until this list caught up.
44
+ */
45
+ export const createRendererOnlyUiTypeCheck = (
46
+ extraTypes?: readonly string[]
47
+ ): ((type?: TQorusType | TQorusType[]) => boolean) => {
48
+ const known = new Set<string>([...BUILT_IN_RENDERER_ONLY_UI_TYPES, ...(extraTypes ?? [])]);
49
+
50
+ return (type?: TQorusType | TQorusType[]): boolean =>
51
+ typeof type === 'string' && known.has(type);
52
+ };
53
+
54
+ /** The default predicate — built-ins only, for module-scope callers with no props. */
55
+ export const isRendererOnlyUiType = createRendererOnlyUiTypeCheck();
@@ -0,0 +1,159 @@
1
+ import { ReqoreP, ReqorePanel } from '@qoretechnologies/reqore';
2
+ import { Meta, StoryObj } from '@storybook/react-vite';
3
+ import { IQorusFormField } from '@qoretechnologies/ts-toolkit';
4
+ import { basicFormValue, getBasicFormOptions } from '../__fixtures__/basicFormOptions';
5
+ import {
6
+ focusDemoGroupLabels,
7
+ focusDemoInvalid,
8
+ focusDemoValue,
9
+ getFocusDemoOptions,
10
+ } from './focusDemo';
11
+ import { VariantCalmTable } from './VariantCalmTable';
12
+ import { VariantCards } from './VariantCards';
13
+ import { VariantFocus } from './VariantFocus';
14
+ import { VariantMinimal } from './VariantMinimal';
15
+
16
+ /**
17
+ * VARIANTS PLAYGROUND for the compact FormEngine read-first view.
18
+ * All four render the SAME schema + values as `Form/Engine/FormEngine › Basic`
19
+ * (the reference screenshot), so the comparison is purely about layout / chrome
20
+ * / UX. Resize the Storybook viewport (or the canvas) to ~390px to check phone.
21
+ * See design/FORM_ENGINE_COMPACT_UX_PLAN.md for the rationale behind each.
22
+ */
23
+ const options = getBasicFormOptions();
24
+
25
+ // Merge declared default_values into the values map so "set via default" fields
26
+ // (number=42, bool=true, the file) render as set — matching the real engine.
27
+ const values: Record<string, IQorusFormField> = (() => {
28
+ const merged: Record<string, IQorusFormField> = { ...(basicFormValue as any) };
29
+ Object.entries(options).forEach(([name, schema]: [string, any]) => {
30
+ if (!merged[name] && schema?.default_value) merged[name] = schema.default_value;
31
+ });
32
+ return merged;
33
+ })();
34
+
35
+ // Fill the available width (the IDE form panel context); no artificial cap.
36
+ // Focus uses the richer demo schema (named groups, a one-of required group,
37
+ // complex nested fields) to show full engine parity.
38
+ const focusOptions = getFocusDemoOptions();
39
+ const focusValues: Record<string, IQorusFormField> = (() => {
40
+ const merged: Record<string, IQorusFormField> = { ...focusDemoValue };
41
+ Object.entries(focusOptions).forEach(([name, schema]: [string, any]) => {
42
+ if (!merged[name] && schema?.default_value) merged[name] = schema.default_value;
43
+ });
44
+ return merged;
45
+ })();
46
+ const focusConfig = { groupLabels: focusDemoGroupLabels, invalidReasons: focusDemoInvalid };
47
+
48
+ const Frame = ({ children }: { children: React.ReactNode }) => (
49
+ <div style={{ width: '100%', padding: '16px 32px', boxSizing: 'border-box' }}>{children}</div>
50
+ );
51
+
52
+ const meta = {
53
+ title: 'Form/Engine/FormEngine Variants',
54
+ parameters: { layout: 'fullscreen', chromatic: { viewports: [1440, 390] } },
55
+ } satisfies Meta;
56
+
57
+ export default meta;
58
+ type Story = StoryObj<typeof meta>;
59
+
60
+ export const V1CalmTable: Story = {
61
+ name: '1 · Calm Table',
62
+ parameters: {
63
+ docs: {
64
+ description: {
65
+ story:
66
+ 'Renders the "Calm Table" compact-form variant over the shared basic schema — one of four visual candidates for the read-first FormEngine view.',
67
+ },
68
+ },
69
+ },
70
+ render: () => (
71
+ <Frame>
72
+ <VariantCalmTable options={options} values={values} />
73
+ </Frame>
74
+ ),
75
+ };
76
+
77
+ export const V2Cards: Story = {
78
+ name: '2 · Cards',
79
+ parameters: {
80
+ docs: {
81
+ description: {
82
+ story:
83
+ 'Renders the "Cards" compact-form variant over the shared basic schema — each field group is a card so the presentation reads as chunked panels.',
84
+ },
85
+ },
86
+ },
87
+ render: () => (
88
+ <Frame>
89
+ <VariantCards options={options} values={values} />
90
+ </Frame>
91
+ ),
92
+ };
93
+
94
+ export const V3Focus: Story = {
95
+ name: '3 · Focus',
96
+ parameters: {
97
+ docs: {
98
+ description: {
99
+ story:
100
+ 'Renders the "Focus" compact-form variant over the richer focus-demo schema (named groups, required-group choices) so the variant is compared under full engine parity.',
101
+ },
102
+ },
103
+ },
104
+ render: () => (
105
+ <Frame>
106
+ <VariantFocus options={focusOptions} values={focusValues} config={focusConfig} />
107
+ </Frame>
108
+ ),
109
+ };
110
+
111
+ export const V4Minimal: Story = {
112
+ name: '4 · Minimal',
113
+ parameters: {
114
+ docs: {
115
+ description: {
116
+ story:
117
+ 'Renders the "Minimal" compact-form variant over the shared basic schema — the sparest of the four candidates, only labels and values.',
118
+ },
119
+ },
120
+ },
121
+ render: () => (
122
+ <Frame>
123
+ <VariantMinimal options={options} values={values} />
124
+ </Frame>
125
+ ),
126
+ };
127
+
128
+ /** All four stacked, labelled — for a quick scroll-through comparison. */
129
+ export const CompareAll: Story = {
130
+ name: '★ Compare all',
131
+ parameters: {
132
+ docs: {
133
+ description: {
134
+ story:
135
+ 'Renders all four compact-form variants stacked in a single scroll — the Calm Table, Cards, Focus and Minimal layouts sit under labelled panels so they can be compared side by side.',
136
+ },
137
+ },
138
+ },
139
+ render: () => {
140
+ const items: [string, React.ReactNode][] = [
141
+ ['1 · Calm Table', <VariantCalmTable key='v1' options={options} values={values} />],
142
+ ['2 · Cards', <VariantCards key='v2' options={options} values={values} />],
143
+ ['3 · Focus', <VariantFocus key='v3' options={focusOptions} values={focusValues} config={focusConfig} />],
144
+ ['4 · Minimal', <VariantMinimal key='v4' options={options} values={values} />],
145
+ ];
146
+ return (
147
+ <div style={{ maxWidth: 1100, margin: '0 auto', padding: 16, display: 'flex', flexFlow: 'column', gap: 28 }}>
148
+ {items.map(([label, node]) => (
149
+ <ReqorePanel key={label} label={label} flat minimal collapsible>
150
+ <div style={{ padding: 8 }}>{node}</div>
151
+ </ReqorePanel>
152
+ ))}
153
+ <ReqoreP size='small' effect={{ opacity: 0.5 }}>
154
+ Same data as “FormEngine › Basic”. Resize the viewport to ~390px to test phone layout.
155
+ </ReqoreP>
156
+ </div>
157
+ );
158
+ },
159
+ };
@@ -0,0 +1,242 @@
1
+ /**
2
+ * VARIANT 1 — "Calm Table"
3
+ *
4
+ * Direction: treat it like a clean data table. One status channel (segmented
5
+ * meter), one mark per row (a status dot), whitespace instead of borders/boxes.
6
+ * Errors are quiet inline text, not full-width red boxes. WHOLE-ROW hover.
7
+ * ONE unified description control (tap to reveal short+long inline). Click a row
8
+ * to edit it inline. Responsive: collapses to stacked label/value on phone.
9
+ */
10
+ import { ReqoreIcon, ReqoreP } from '@qoretechnologies/reqore';
11
+ import { IQorusFormField, IQorusFormSchema } from '@qoretechnologies/ts-toolkit';
12
+ import styled from 'styled-components';
13
+ import {
14
+ InlineEdit,
15
+ StatusDot,
16
+ TVariantForm,
17
+ ValueView,
18
+ VariantToolbar,
19
+ useDisclosure,
20
+ useVariantColors,
21
+ useVariantForm,
22
+ } from './variantParts';
23
+
24
+ const Wrap = styled.div<{ $line: string; $hover: string; $muted: string; $faint: string }>`
25
+ display: flex;
26
+ flex-flow: column;
27
+ gap: 22px;
28
+ font-size: 13px;
29
+
30
+ .vct-row {
31
+ display: grid;
32
+ grid-template-columns: minmax(180px, 320px) minmax(0, 1fr) auto;
33
+ align-items: center;
34
+ gap: 18px;
35
+ min-height: 40px;
36
+ padding: 6px 10px;
37
+ border-radius: 8px;
38
+ cursor: pointer;
39
+ transition: background 0.12s ease;
40
+ }
41
+ /* WHOLE-row hover (fixes: hover used to only tint the label cell) */
42
+ .vct-row:hover,
43
+ .vct-row:focus-within {
44
+ background: ${({ $hover }) => $hover};
45
+ }
46
+ .vct-label {
47
+ font-weight: 600;
48
+ display: flex;
49
+ align-items: center;
50
+ gap: 6px;
51
+ min-width: 0;
52
+ }
53
+ .vct-actions {
54
+ display: inline-flex;
55
+ align-items: center;
56
+ gap: 10px;
57
+ opacity: 0.85;
58
+ }
59
+ .vct-desc {
60
+ grid-column: 1 / -1;
61
+ padding: 2px 2px 6px 0;
62
+ color: ${({ $muted }) => $muted};
63
+ }
64
+ .vct-reason {
65
+ font-size: 12px;
66
+ margin-left: 8px;
67
+ }
68
+ .vct-info {
69
+ background: none;
70
+ border: none;
71
+ color: ${({ $faint }) => $faint};
72
+ cursor: pointer;
73
+ padding: 4px;
74
+ display: inline-flex;
75
+ border-radius: 6px;
76
+ }
77
+ .vct-info:hover {
78
+ color: ${({ $muted }) => $muted};
79
+ }
80
+
81
+ @media (max-width: 620px) {
82
+ .vct-row {
83
+ grid-template-columns: 1fr auto;
84
+ grid-template-areas: 'label actions' 'value value';
85
+ row-gap: 2px;
86
+ }
87
+ .vct-label {
88
+ grid-area: label;
89
+ }
90
+ .vct-value {
91
+ grid-area: value;
92
+ }
93
+ .vct-actions {
94
+ grid-area: actions;
95
+ }
96
+ }
97
+ `;
98
+
99
+ const Meter = ({ form }: { form: TVariantForm }) => {
100
+ const c = useVariantColors();
101
+ const s = form.summary;
102
+ const filtering = form.filter === 'attention';
103
+ return (
104
+ <div style={{ display: 'flex', flexFlow: 'column', gap: 8 }}>
105
+ <div style={{ display: 'flex', alignItems: 'center', gap: 12, fontSize: 12, color: c.muted }}>
106
+ <ReqoreP
107
+ size='small'
108
+ effect={{
109
+ uppercase: true,
110
+ spaced: 1,
111
+ weight: 'bold',
112
+ color: (s.attention ? c.warning : c.success) as never,
113
+ }}
114
+ >
115
+ {s.attention ? 'Draft' : 'Ready'}
116
+ </ReqoreP>
117
+ <span>
118
+ {s.set} of {s.total} set
119
+ </span>
120
+ {s.attention ?
121
+ <button
122
+ type='button'
123
+ onClick={form.toggleAttention}
124
+ style={{
125
+ marginLeft: 'auto',
126
+ background: 'none',
127
+ border: 'none',
128
+ cursor: 'pointer',
129
+ color: c.warning,
130
+ fontSize: 12,
131
+ textDecoration: 'underline',
132
+ textUnderlineOffset: 3,
133
+ }}
134
+ >
135
+ {filtering ? '← show all fields' : `${s.attention} need attention →`}
136
+ </button>
137
+ : <span style={{ color: c.success, marginLeft: 'auto' }}>All clear</span>}
138
+ </div>
139
+ <div style={{ display: 'flex', height: 6, borderRadius: 3, overflow: 'hidden', background: c.line }}>
140
+ <div style={{ width: `${(s.set / s.total) * 100}%`, background: c.success }} />
141
+ <div style={{ width: `${(s.attention / s.total) * 100}%`, background: c.warning }} />
142
+ </div>
143
+ </div>
144
+ );
145
+ };
146
+
147
+ export const VariantCalmTable = ({
148
+ options,
149
+ values,
150
+ }: {
151
+ options: IQorusFormSchema;
152
+ values: Record<string, IQorusFormField>;
153
+ }) => {
154
+ const c = useVariantColors();
155
+ const form = useVariantForm(options, values);
156
+ const disc = useDisclosure();
157
+
158
+ return (
159
+ <Wrap $line={c.line} $hover={c.hover} $muted={c.muted} $faint={c.faint}>
160
+ <Meter form={form} />
161
+ <VariantToolbar form={form} />
162
+ {form.visibleGroups.map((g) => (
163
+ <div key={g.name} style={{ display: 'flex', flexFlow: 'column', gap: 2 }}>
164
+ <div
165
+ style={{
166
+ display: 'flex',
167
+ alignItems: 'center',
168
+ gap: 10,
169
+ padding: '0 10px 6px',
170
+ borderBottom: `1px solid ${c.line}`,
171
+ marginBottom: 4,
172
+ }}
173
+ >
174
+ <ReqoreP effect={{ weight: 'bold' }}>{g.label}</ReqoreP>
175
+ <span style={{ color: c.faint, fontSize: 12 }}>{g.rows.length}</span>
176
+ </div>
177
+ {g.rows.map((r) => {
178
+ const hasDesc = !!(r.shortDesc || r.longDesc);
179
+ const open = disc.isOpen(r.name);
180
+ const editing = form.editing === r.name;
181
+ return (
182
+ <div
183
+ key={r.name}
184
+ className='vct-row'
185
+ role='button'
186
+ tabIndex={0}
187
+ aria-label={r.label}
188
+ onClick={() => !r.readOnly && form.startEdit(r.name)}
189
+ >
190
+ <span className='vct-label' style={{ color: c.text }}>
191
+ {r.label}
192
+ {r.required ?
193
+ <ReqoreIcon icon='Asterisk' size='9px' style={{ color: c.danger }} />
194
+ : null}
195
+ </span>
196
+ <span className='vct-value' style={{ minWidth: 0, color: c.muted }}>
197
+ <ValueView value={r.value} />
198
+ {r.status === 'invalid' || r.status === 'todo' ?
199
+ <span
200
+ className='vct-reason'
201
+ style={{ color: r.status === 'invalid' ? c.danger : c.warning }}
202
+ >
203
+ {r.reason}
204
+ </span>
205
+ : null}
206
+ </span>
207
+ <span className='vct-actions'>
208
+ <StatusDot status={r.status} />
209
+ {hasDesc ?
210
+ <button
211
+ type='button'
212
+ className='vct-info'
213
+ aria-label='Toggle description'
214
+ aria-expanded={open}
215
+ onClick={(e) => {
216
+ e.stopPropagation();
217
+ disc.toggle(r.name);
218
+ }}
219
+ >
220
+ <ReqoreIcon icon={open ? 'InformationFill' : 'InformationLine'} size='14px' />
221
+ </button>
222
+ : null}
223
+ </span>
224
+ {hasDesc && open ?
225
+ <div className='vct-desc'>
226
+ {r.shortDesc ? <div>{r.shortDesc}</div> : null}
227
+ {r.longDesc ?
228
+ <div style={{ opacity: 0.8, marginTop: r.shortDesc ? 4 : 0 }}>
229
+ {r.longDesc.replace(/[#`*]/g, '')}
230
+ </div>
231
+ : null}
232
+ </div>
233
+ : null}
234
+ {editing ? <InlineEdit row={r} onDone={form.stopEdit} /> : null}
235
+ </div>
236
+ );
237
+ })}
238
+ </div>
239
+ ))}
240
+ </Wrap>
241
+ );
242
+ };