@dxos/react-ui-form 0.8.4-main.fffef41 → 0.9.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 (301) hide show
  1. package/LICENSE +102 -5
  2. package/README.md +0 -1
  3. package/dist/lib/browser/index.mjs +4005 -2402
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/translations.mjs +87 -0
  7. package/dist/lib/browser/translations.mjs.map +7 -0
  8. package/dist/lib/node-esm/index.mjs +4005 -2402
  9. package/dist/lib/node-esm/index.mjs.map +4 -4
  10. package/dist/lib/node-esm/meta.json +1 -1
  11. package/dist/lib/node-esm/translations.mjs +89 -0
  12. package/dist/lib/node-esm/translations.mjs.map +7 -0
  13. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +8 -7
  14. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  15. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +73 -59
  16. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  17. package/dist/types/src/components/Form/Form.d.ts +135 -27
  18. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  19. package/dist/types/src/components/Form/Form.stories.d.ts +192 -137
  20. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  21. package/dist/types/src/components/Form/FormField.d.ts +52 -0
  22. package/dist/types/src/components/Form/FormField.d.ts.map +1 -0
  23. package/dist/types/src/components/Form/FormFieldComponent.d.ts +98 -0
  24. package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -0
  25. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +9 -0
  26. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -0
  27. package/dist/types/src/components/Form/FormFieldSet.d.ts +26 -0
  28. package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -0
  29. package/dist/types/src/components/Form/FormTooltipsContext.d.ts +19 -0
  30. package/dist/types/src/components/Form/FormTooltipsContext.d.ts.map +1 -0
  31. package/dist/types/src/components/Form/Layout/FormLayout.d.ts +50 -0
  32. package/dist/types/src/components/Form/Layout/FormLayout.d.ts.map +1 -0
  33. package/dist/types/src/components/Form/Layout/FormLayout.stories.d.ts +123 -0
  34. package/dist/types/src/components/Form/Layout/FormLayout.stories.d.ts.map +1 -0
  35. package/dist/types/src/components/Form/Layout/FormLayout.stories.test.d.ts +2 -0
  36. package/dist/types/src/components/Form/Layout/FormLayout.stories.test.d.ts.map +1 -0
  37. package/dist/types/src/components/Form/Layout/FormLayout.test.d.ts +2 -0
  38. package/dist/types/src/components/Form/Layout/FormLayout.test.d.ts.map +1 -0
  39. package/dist/types/src/components/Form/Layout/annotation.d.ts +30 -0
  40. package/dist/types/src/components/Form/Layout/annotation.d.ts.map +1 -0
  41. package/dist/types/src/components/Form/Layout/index.d.ts +4 -0
  42. package/dist/types/src/components/Form/Layout/index.d.ts.map +1 -0
  43. package/dist/types/src/components/Form/Layout/parser.d.ts +31 -0
  44. package/dist/types/src/components/Form/Layout/parser.d.ts.map +1 -0
  45. package/dist/types/src/components/Form/Layout/parser.test.d.ts +2 -0
  46. package/dist/types/src/components/Form/Layout/parser.test.d.ts.map +1 -0
  47. package/dist/types/src/components/Form/fields/ArrayField.d.ts +18 -0
  48. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -0
  49. package/dist/types/src/components/Form/fields/BooleanField.d.ts +4 -0
  50. package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -0
  51. package/dist/types/src/components/Form/fields/DateField.d.ts +4 -0
  52. package/dist/types/src/components/Form/fields/DateField.d.ts.map +1 -0
  53. package/dist/types/src/components/Form/fields/GeoPointField.d.ts +5 -0
  54. package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -0
  55. package/dist/types/src/components/Form/fields/InlineRefField.d.ts +13 -0
  56. package/dist/types/src/components/Form/fields/InlineRefField.d.ts.map +1 -0
  57. package/dist/types/src/components/Form/fields/MarkdownField.d.ts +11 -0
  58. package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -0
  59. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts +89 -0
  60. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts.map +1 -0
  61. package/dist/types/src/components/Form/fields/NumberField.d.ts +4 -0
  62. package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -0
  63. package/dist/types/src/components/Form/fields/RefField.d.ts +34 -0
  64. package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -0
  65. package/dist/types/src/components/Form/fields/RefField.test.d.ts +2 -0
  66. package/dist/types/src/components/Form/fields/RefField.test.d.ts.map +1 -0
  67. package/dist/types/src/components/Form/fields/SelectField.d.ts +13 -0
  68. package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -0
  69. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts +5 -0
  70. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -0
  71. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +89 -0
  72. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -0
  73. package/dist/types/src/components/Form/fields/TextAreaField.d.ts +4 -0
  74. package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -0
  75. package/dist/types/src/components/Form/fields/TextField.d.ts +4 -0
  76. package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -0
  77. package/dist/types/src/components/Form/fields/TupleField.d.ts +6 -0
  78. package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -0
  79. package/dist/types/src/components/Form/fields/index.d.ts +14 -0
  80. package/dist/types/src/components/Form/fields/index.d.ts.map +1 -0
  81. package/dist/types/src/components/Form/index.d.ts +5 -4
  82. package/dist/types/src/components/Form/index.d.ts.map +1 -1
  83. package/dist/types/src/components/Form/meta-tags.d.ts +30 -0
  84. package/dist/types/src/components/Form/meta-tags.d.ts.map +1 -0
  85. package/dist/types/src/components/Form/meta-tags.test.d.ts +2 -0
  86. package/dist/types/src/components/Form/meta-tags.test.d.ts.map +1 -0
  87. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +8 -0
  88. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -0
  89. package/dist/types/src/components/ObjectForm/ObjectForm.stories.d.ts +87 -0
  90. package/dist/types/src/components/ObjectForm/ObjectForm.stories.d.ts.map +1 -0
  91. package/dist/types/src/components/ObjectForm/index.d.ts +2 -0
  92. package/dist/types/src/components/ObjectForm/index.d.ts.map +1 -0
  93. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +39 -12
  94. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
  95. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +69 -58
  96. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
  97. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +9 -0
  98. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -0
  99. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +132 -0
  100. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -0
  101. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts +2 -0
  102. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts.map +1 -0
  103. package/dist/types/src/components/ObjectProperties/index.d.ts +2 -0
  104. package/dist/types/src/components/ObjectProperties/index.d.ts.map +1 -0
  105. package/dist/types/src/components/ObjectTree/ObjectTree.d.ts +9 -0
  106. package/dist/types/src/components/ObjectTree/ObjectTree.d.ts.map +1 -0
  107. package/dist/types/src/components/ObjectTree/ObjectTree.stories.d.ts +91 -0
  108. package/dist/types/src/components/ObjectTree/ObjectTree.stories.d.ts.map +1 -0
  109. package/dist/types/src/components/ObjectTree/index.d.ts +2 -0
  110. package/dist/types/src/components/ObjectTree/index.d.ts.map +1 -0
  111. package/dist/types/src/components/RefEditor/RefEditor.d.ts +73 -0
  112. package/dist/types/src/components/RefEditor/RefEditor.d.ts.map +1 -0
  113. package/dist/types/src/components/RefEditor/RefEditor.stories.d.ts +92 -0
  114. package/dist/types/src/components/RefEditor/RefEditor.stories.d.ts.map +1 -0
  115. package/dist/types/src/components/RefEditor/index.d.ts +3 -0
  116. package/dist/types/src/components/RefEditor/index.d.ts.map +1 -0
  117. package/dist/types/src/components/RefEditor/ref-editor-extension.d.ts +47 -0
  118. package/dist/types/src/components/RefEditor/ref-editor-extension.d.ts.map +1 -0
  119. package/dist/types/src/components/Settings/Settings.d.ts +51 -0
  120. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
  121. package/dist/types/src/components/Settings/Settings.stories.d.ts +89 -0
  122. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -0
  123. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts +43 -0
  124. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts.map +1 -0
  125. package/dist/types/src/components/Settings/SettingsItem.d.ts +16 -0
  126. package/dist/types/src/components/Settings/SettingsItem.d.ts.map +1 -0
  127. package/dist/types/src/components/Settings/SettingsSelectField.d.ts +18 -0
  128. package/dist/types/src/components/Settings/SettingsSelectField.d.ts.map +1 -0
  129. package/dist/types/src/components/Settings/index.d.ts +4 -0
  130. package/dist/types/src/components/Settings/index.d.ts.map +1 -0
  131. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +15 -19
  132. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  133. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +75 -64
  134. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  135. package/dist/types/src/components/index.d.ts +5 -1
  136. package/dist/types/src/components/index.d.ts.map +1 -1
  137. package/dist/types/src/components/testing.d.ts +13 -6
  138. package/dist/types/src/components/testing.d.ts.map +1 -1
  139. package/dist/types/src/hooks/index.d.ts +2 -3
  140. package/dist/types/src/hooks/index.d.ts.map +1 -1
  141. package/dist/types/src/hooks/useFormHandler.d.ts +82 -0
  142. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -0
  143. package/dist/types/src/hooks/useKeyHandler.d.ts +7 -0
  144. package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -0
  145. package/dist/types/src/index.d.ts +0 -2
  146. package/dist/types/src/index.d.ts.map +1 -1
  147. package/dist/types/src/translations.d.ts +70 -59
  148. package/dist/types/src/translations.d.ts.map +1 -1
  149. package/dist/types/src/util/field-type.d.ts +19 -0
  150. package/dist/types/src/util/field-type.d.ts.map +1 -0
  151. package/dist/types/src/util/field.d.ts +3 -3
  152. package/dist/types/src/util/field.d.ts.map +1 -1
  153. package/dist/types/src/util/formatting.d.ts +2 -2
  154. package/dist/types/src/util/formatting.d.ts.map +1 -1
  155. package/dist/types/src/util/index.d.ts +2 -1
  156. package/dist/types/src/util/index.d.ts.map +1 -1
  157. package/dist/types/src/util/properties.d.ts +35 -0
  158. package/dist/types/src/util/properties.d.ts.map +1 -0
  159. package/dist/types/src/util/properties.test.d.ts +2 -0
  160. package/dist/types/src/util/properties.test.d.ts.map +1 -0
  161. package/dist/types/src/util/refs.d.ts +2 -2
  162. package/dist/types/src/util/refs.d.ts.map +1 -1
  163. package/dist/types/tsconfig.tsbuildinfo +1 -1
  164. package/package.json +64 -52
  165. package/src/components/FieldEditor/FieldEditor.stories.tsx +44 -25
  166. package/src/components/FieldEditor/FieldEditor.test.tsx +4 -2
  167. package/src/components/FieldEditor/FieldEditor.tsx +91 -86
  168. package/src/components/Form/AUDIT.md +17 -0
  169. package/src/components/Form/Form.stories.tsx +147 -252
  170. package/src/components/Form/Form.tsx +461 -108
  171. package/src/components/Form/FormField.tsx +345 -0
  172. package/src/components/Form/FormFieldComponent.stories.tsx +65 -0
  173. package/src/components/Form/FormFieldComponent.tsx +253 -0
  174. package/src/components/Form/FormFieldSet.tsx +213 -0
  175. package/src/components/Form/FormTooltipsContext.ts +25 -0
  176. package/src/components/Form/Layout/FormLayout.stories.test.tsx +51 -0
  177. package/src/components/Form/Layout/FormLayout.stories.tsx +422 -0
  178. package/src/components/Form/Layout/FormLayout.test.ts +59 -0
  179. package/src/components/Form/Layout/FormLayout.tsx +251 -0
  180. package/src/components/Form/Layout/annotation.ts +38 -0
  181. package/src/components/Form/Layout/index.ts +7 -0
  182. package/src/components/Form/Layout/parser.test.ts +120 -0
  183. package/src/components/Form/Layout/parser.ts +167 -0
  184. package/src/components/Form/README.md +3 -0
  185. package/src/components/Form/fields/ArrayField.tsx +173 -0
  186. package/src/components/Form/fields/BooleanField.tsx +22 -0
  187. package/src/components/Form/fields/DateField.tsx +101 -0
  188. package/src/components/Form/fields/GeoPointField.tsx +124 -0
  189. package/src/components/Form/fields/InlineRefField.tsx +138 -0
  190. package/src/components/Form/fields/MarkdownField.stories.tsx +206 -0
  191. package/src/components/Form/fields/MarkdownField.tsx +167 -0
  192. package/src/components/Form/fields/NumberField.tsx +76 -0
  193. package/src/components/Form/fields/RefField.test.ts +45 -0
  194. package/src/components/Form/fields/RefField.tsx +266 -0
  195. package/src/components/Form/fields/SelectField.tsx +77 -0
  196. package/src/components/Form/{custom/SelectOptionsInput.stories.tsx → fields/SelectOptionField.stories.tsx} +23 -18
  197. package/src/components/Form/{custom/SelectOptionsInput.tsx → fields/SelectOptionField.tsx} +33 -26
  198. package/src/components/Form/fields/TextAreaField.tsx +38 -0
  199. package/src/components/Form/fields/TextField.tsx +38 -0
  200. package/src/components/Form/{custom/TupleInput.tsx → fields/TupleField.tsx} +17 -9
  201. package/src/components/Form/fields/index.ts +17 -0
  202. package/src/components/Form/index.ts +6 -4
  203. package/src/components/Form/meta-tags.test.ts +47 -0
  204. package/src/components/Form/meta-tags.ts +33 -0
  205. package/src/components/ObjectForm/ObjectForm.stories.tsx +62 -0
  206. package/src/components/ObjectForm/ObjectForm.tsx +111 -0
  207. package/src/components/{ControlSection → ObjectForm}/index.ts +1 -1
  208. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +26 -21
  209. package/src/components/ObjectPicker/ObjectPicker.tsx +123 -82
  210. package/src/components/ObjectProperties/ObjectProperties.stories.tsx +350 -0
  211. package/src/components/ObjectProperties/ObjectProperties.test.tsx +111 -0
  212. package/src/components/ObjectProperties/ObjectProperties.tsx +146 -0
  213. package/src/components/ObjectProperties/index.ts +5 -0
  214. package/src/components/ObjectTree/ObjectTree.stories.tsx +134 -0
  215. package/src/components/ObjectTree/ObjectTree.tsx +190 -0
  216. package/src/components/ObjectTree/index.ts +5 -0
  217. package/src/components/RefEditor/RefEditor.stories.tsx +148 -0
  218. package/src/components/RefEditor/RefEditor.tsx +275 -0
  219. package/src/components/RefEditor/index.ts +6 -0
  220. package/src/components/RefEditor/ref-editor-extension.ts +254 -0
  221. package/src/components/Settings/AUDIT.md +51 -0
  222. package/src/components/Settings/Settings.stories.tsx +104 -0
  223. package/src/components/Settings/Settings.tsx +81 -0
  224. package/src/components/Settings/SettingsFieldSet.tsx +257 -0
  225. package/src/components/Settings/SettingsItem.tsx +58 -0
  226. package/src/components/Settings/SettingsSelectField.tsx +59 -0
  227. package/src/components/Settings/index.ts +7 -0
  228. package/src/components/ViewEditor/ViewEditor.stories.tsx +70 -76
  229. package/src/components/ViewEditor/ViewEditor.test.tsx +17 -13
  230. package/src/components/ViewEditor/ViewEditor.tsx +340 -226
  231. package/src/components/index.ts +5 -1
  232. package/src/components/testing.tsx +24 -11
  233. package/src/hooks/index.ts +2 -3
  234. package/src/hooks/useFormHandler.ts +380 -0
  235. package/src/hooks/useKeyHandler.tsx +37 -0
  236. package/src/index.ts +0 -4
  237. package/src/translations.ts +76 -64
  238. package/src/util/field-type.ts +60 -0
  239. package/src/util/field.ts +25 -25
  240. package/src/util/formatting.ts +22 -22
  241. package/src/util/index.ts +2 -1
  242. package/src/util/properties.test.ts +153 -0
  243. package/src/util/properties.ts +100 -0
  244. package/src/util/refs.ts +16 -15
  245. package/src/vite-env.d.ts +10 -0
  246. package/dist/types/src/components/ControlSection/ControlSection.d.ts +0 -23
  247. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +0 -1
  248. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +0 -83
  249. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +0 -1
  250. package/dist/types/src/components/ControlSection/index.d.ts +0 -2
  251. package/dist/types/src/components/ControlSection/index.d.ts.map +0 -1
  252. package/dist/types/src/components/Form/ArrayField.d.ts +0 -16
  253. package/dist/types/src/components/Form/ArrayField.d.ts.map +0 -1
  254. package/dist/types/src/components/Form/Defaults.d.ts +0 -14
  255. package/dist/types/src/components/Form/Defaults.d.ts.map +0 -1
  256. package/dist/types/src/components/Form/FormActions.d.ts +0 -9
  257. package/dist/types/src/components/Form/FormActions.d.ts.map +0 -1
  258. package/dist/types/src/components/Form/FormContent.d.ts +0 -63
  259. package/dist/types/src/components/Form/FormContent.d.ts.map +0 -1
  260. package/dist/types/src/components/Form/FormContext.d.ts +0 -23
  261. package/dist/types/src/components/Form/FormContext.d.ts.map +0 -1
  262. package/dist/types/src/components/Form/Input.d.ts +0 -29
  263. package/dist/types/src/components/Form/Input.d.ts.map +0 -1
  264. package/dist/types/src/components/Form/RefField.d.ts +0 -22
  265. package/dist/types/src/components/Form/RefField.d.ts.map +0 -1
  266. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +0 -4
  267. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +0 -1
  268. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +0 -4
  269. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +0 -1
  270. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts +0 -78
  271. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +0 -1
  272. package/dist/types/src/components/Form/custom/TupleInput.d.ts +0 -6
  273. package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +0 -1
  274. package/dist/types/src/components/Form/custom/index.d.ts +0 -4
  275. package/dist/types/src/components/Form/custom/index.d.ts.map +0 -1
  276. package/dist/types/src/components/Form/factory.d.ts +0 -8
  277. package/dist/types/src/components/Form/factory.d.ts.map +0 -1
  278. package/dist/types/src/hooks/useForm.d.ts +0 -67
  279. package/dist/types/src/hooks/useForm.d.ts.map +0 -1
  280. package/dist/types/src/hooks/useQueryRefOptions.d.ts +0 -29
  281. package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +0 -1
  282. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +0 -8
  283. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +0 -1
  284. package/dist/types/src/util/schema.d.ts +0 -3
  285. package/dist/types/src/util/schema.d.ts.map +0 -1
  286. package/src/components/ControlSection/ControlSection.stories.tsx +0 -38
  287. package/src/components/ControlSection/ControlSection.tsx +0 -153
  288. package/src/components/Form/ArrayField.tsx +0 -125
  289. package/src/components/Form/Defaults.tsx +0 -205
  290. package/src/components/Form/FormActions.tsx +0 -54
  291. package/src/components/Form/FormContent.tsx +0 -362
  292. package/src/components/Form/FormContext.tsx +0 -110
  293. package/src/components/Form/Input.tsx +0 -54
  294. package/src/components/Form/RefField.tsx +0 -283
  295. package/src/components/Form/custom/GeoPointInput.tsx +0 -80
  296. package/src/components/Form/custom/index.ts +0 -7
  297. package/src/components/Form/factory.tsx +0 -31
  298. package/src/hooks/useForm.ts +0 -296
  299. package/src/hooks/useQueryRefOptions.ts +0 -62
  300. package/src/hooks/useRefQueryLookupHandler.ts +0 -39
  301. package/src/util/schema.ts +0 -12
@@ -0,0 +1,148 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+ import React, { Fragment, useState } from 'react';
7
+
8
+ import { type Database, Filter, Obj } from '@dxos/echo';
9
+ import { random } from '@dxos/random';
10
+ import { useQuery } from '@dxos/react-client/echo';
11
+ import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
12
+ import { JsonHighlighter } from '@dxos/react-ui-syntax-highlighter';
13
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
14
+ import { type ValueGenerator, createObjectFactory } from '@dxos/schema/testing';
15
+ import { Person } from '@dxos/types';
16
+
17
+ import { translations } from '#translations';
18
+
19
+ import { EMAIL_REGEX } from './ref-editor-extension';
20
+ import { RefEditor, type RefEditorProps } from './RefEditor';
21
+
22
+ const generator: ValueGenerator = random as any;
23
+
24
+ random.seed(7);
25
+
26
+ /**
27
+ * Generate Person objects into the space. The generator does not populate array fields, so derive
28
+ * email addresses from each person's name (every other person gets a second work alias so the
29
+ * email-mode picker shows multiple entries per person).
30
+ */
31
+ const generatePeople = async (db: Database.Database, count: number) => {
32
+ const people = await createObjectFactory(db, generator)([{ type: Person.Person, count }]);
33
+ people.forEach((person, index) => {
34
+ Obj.update(person, (person: Obj.Mutable<Person.Person>) => {
35
+ const slug = (person.fullName ?? 'user')
36
+ .toLowerCase()
37
+ .replace(/[^a-z0-9]+/g, '.')
38
+ .replace(/^\.+|\.+$/g, '');
39
+ person.emails = [
40
+ { value: `${slug}@example.com` },
41
+ ...(index % 2 === 0 ? [{ label: 'work', value: `${slug}@work.example.com` }] : []),
42
+ ...(index % 4 === 0 ? [{ label: 'private', value: `${random.lorem.word()}@gmail.com` }] : []),
43
+ ];
44
+ });
45
+ });
46
+ return people;
47
+ };
48
+
49
+ /** Person email addresses (the values a person can be captured as in email mode). */
50
+ const getPersonValues = (object: Obj.Unknown): readonly string[] =>
51
+ Obj.instanceOf(Person.Person, object) ? (object.emails ?? []).map(({ value }) => value) : [];
52
+
53
+ /**
54
+ * Dense reference grid of the generated people and their email addresses.
55
+ */
56
+ const PeopleGrid = ({ db }: { db?: Database.Database }) => {
57
+ const people = useQuery(db, Filter.type(Person.Person));
58
+ return (
59
+ <div className='grid grid-cols-[max-content_1fr] gap-x-4 text-sm text-description'>
60
+ {people.flatMap((person) =>
61
+ (person.emails ?? []).map(({ value }) => (
62
+ <Fragment key={`${person.id}-${value}`}>
63
+ <span className='truncate'>{person.fullName}</span>
64
+ <span className='truncate'>{value}</span>
65
+ </Fragment>
66
+ )),
67
+ )}
68
+ </div>
69
+ );
70
+ };
71
+
72
+ type StoryProps = Partial<Pick<RefEditorProps, 'value' | 'mode' | 'activateOnTyping' | 'autoFocus'>>;
73
+
74
+ // NOTE: The Person config (type/match/getValues) is supplied in render, not args — Storybook's
75
+ // arg traversal attempts to mutate the proxied ECHO schema and crashes.
76
+ const DefaultStory = ({ value: initialValue, ...args }: StoryProps) => {
77
+ const { space } = useClientStory();
78
+ const [value, setValue] = useState(initialValue ?? '');
79
+
80
+ return (
81
+ <div className='flex flex-col gap-2'>
82
+ <RefEditor
83
+ {...args}
84
+ value={initialValue}
85
+ type={Person.Person}
86
+ match={EMAIL_REGEX}
87
+ getValues={getPersonValues}
88
+ icon='ph--user--regular'
89
+ db={space?.db}
90
+ onChange={setValue}
91
+ classNames='border'
92
+ />
93
+ <JsonHighlighter data={{ value }} classNames='text-xs' />
94
+ <PeopleGrid db={space?.db} />
95
+ </div>
96
+ );
97
+ };
98
+
99
+ const meta = {
100
+ title: 'ui/react-ui-form/RefEditor',
101
+ render: DefaultStory,
102
+ decorators: [
103
+ withTheme(),
104
+ withLayout({ layout: 'column', classNames: 'p-2', scroll: true }),
105
+ withClientProvider({
106
+ types: [Person.Person],
107
+ createIdentity: true,
108
+ createSpace: true,
109
+ onCreateSpace: async ({ space }) => {
110
+ await generatePeople(space.db, 8);
111
+ },
112
+ }),
113
+ ],
114
+ parameters: {
115
+ translations,
116
+ },
117
+ } satisfies Meta<typeof DefaultStory>;
118
+
119
+ export default meta;
120
+
121
+ type Story = StoryObj<typeof meta>;
122
+
123
+ export const Default: Story = {
124
+ args: {
125
+ autoFocus: true,
126
+ },
127
+ };
128
+
129
+ export const ActivateOnTyping: Story = {
130
+ args: {
131
+ autoFocus: true,
132
+ activateOnTyping: true,
133
+ },
134
+ };
135
+
136
+ export const EmailMode: Story = {
137
+ args: {
138
+ autoFocus: true,
139
+ mode: 'email',
140
+ activateOnTyping: true,
141
+ },
142
+ };
143
+
144
+ export const WithEmail: Story = {
145
+ args: {
146
+ value: 'someone@example.com ',
147
+ },
148
+ };
@@ -0,0 +1,275 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { type Extension, Prec } from '@codemirror/state';
6
+ import { type EditorView } from '@codemirror/view';
7
+ import React, { forwardRef, useCallback, useEffect, useMemo, useRef } from 'react';
8
+
9
+ import { type Database, Filter, Obj, type Type } from '@dxos/echo';
10
+ import { useQuery } from '@dxos/react-client/echo';
11
+ import { type ThemedClassName, useThemeContext, useTranslation } from '@dxos/react-ui';
12
+ import {
13
+ Editor,
14
+ type EditorController,
15
+ type EditorMenuProviderProps,
16
+ type EditorViewProps,
17
+ type UseEditorMenuProps,
18
+ } from '@dxos/react-ui-editor';
19
+ import { createBasicExtensions, createThemeExtensions, insertAtCursor, keymap } from '@dxos/ui-editor';
20
+ import { getHashHue } from '@dxos/ui-theme';
21
+
22
+ import { translationKey } from '#translations';
23
+
24
+ import {
25
+ EMAIL_REGEX,
26
+ NAME_ADDR_REGEX,
27
+ type RefEditorMode,
28
+ type RefInfo,
29
+ formatMailbox,
30
+ refEditor,
31
+ refEditorRedecorate,
32
+ } from './ref-editor-extension';
33
+
34
+ export type RefEditorProps = ThemedClassName<
35
+ {
36
+ /** Database queried for objects of {@link RefEditorProps.type}. */
37
+ db?: Database.Database;
38
+ /** ECHO object type whose instances are offered in the typeahead and referenced as `@<id>`. */
39
+ type: Type.AnyObj;
40
+ value?: string;
41
+ readonly?: boolean;
42
+ /**
43
+ * Content model:
44
+ * - `'ref'` (default): the picker only matches existing objects; selection inserts an
45
+ * `@<id>` reference (raw tokens accepted by `match` are also allowed).
46
+ * - `'email'`: the picker shows one item per (object, value) pair and selection captures the
47
+ * value; the content is an RFC 5322 mailbox list (`Name <email>, ...`) with the comma
48
+ * separators hidden inside the tags.
49
+ */
50
+ mode?: RefEditorMode;
51
+ /** Pluggable raw-token matcher (e.g. {@link EMAIL_REGEX}); matching tokens are accepted as values. */
52
+ match?: RegExp;
53
+ /** Display label for an object; defaults to `Obj.getLabel` falling back to the id. */
54
+ getLabel?: (object: Obj.Unknown) => string;
55
+ /** Values an object can be captured as in `'email'` mode (e.g. a person's email addresses). */
56
+ getValues?: (object: Obj.Unknown) => readonly string[];
57
+ /** Menu item icon. */
58
+ icon?: string;
59
+ /** Already-translated placeholder; defaults to a generic hint. */
60
+ placeholder?: string;
61
+ /** Open the typeahead while typing (the `@` trigger remains available but is not required). */
62
+ activateOnTyping?: boolean;
63
+ } & Omit<EditorViewProps, 'initialValue'> &
64
+ Pick<EditorMenuProviderProps, 'numItems'>
65
+ >;
66
+
67
+ /**
68
+ * Single-line input for a list of object references. Typing `@` (or any text, with
69
+ * `activateOnTyping`) opens a typeahead menu matching objects of the given type by label (and
70
+ * by their `getValues` values). The content model depends on {@link RefEditorProps.mode}.
71
+ * Resolved entries render as atomic tags with the object's label; raw tokens accepted by the
72
+ * pluggable `match` regexp render as neutral tags.
73
+ */
74
+ export const RefEditor = forwardRef<EditorController, RefEditorProps>(
75
+ (
76
+ {
77
+ db,
78
+ type,
79
+ value,
80
+ readonly,
81
+ mode = 'ref',
82
+ match,
83
+ getLabel,
84
+ getValues,
85
+ icon = 'ph--link--regular',
86
+ placeholder: placeholderProp,
87
+ activateOnTyping,
88
+ numItems = 8,
89
+ ...props
90
+ },
91
+ forwardedRef,
92
+ ) => {
93
+ const { t } = useTranslation(translationKey);
94
+ const { themeMode } = useThemeContext();
95
+ const getObjectLabel = useCallback(
96
+ (object: Obj.Unknown) => getLabel?.(object) ?? Obj.getLabel(object) ?? object.id,
97
+ [getLabel],
98
+ );
99
+
100
+ // Reads go through a ref so the decoration extension stays stable: recreating extensions
101
+ // destroys and recreates the editor, which would blur the input on data changes.
102
+ const filter = useMemo(() => Filter.type(type), [type]);
103
+ const objects = useQuery(db, filter);
104
+ const refs = useMemo(
105
+ () =>
106
+ new Map<string, RefInfo>(
107
+ objects.map((object) => [object.id, { label: getObjectLabel(object), hue: getHashHue(object.id) }]),
108
+ ),
109
+ [objects, getObjectLabel],
110
+ );
111
+ const refsRef = useRef(refs);
112
+ refsRef.current = refs;
113
+
114
+ // Internal controller (merged with the forwarded ref) used to re-resolve reference
115
+ // decorations when object data changes after the document was rendered.
116
+ const controllerRef = useRef<EditorController | null>(null);
117
+ const composedRef = useCallback(
118
+ (controller: EditorController | null) => {
119
+ controllerRef.current = controller;
120
+ if (typeof forwardedRef === 'function') {
121
+ forwardedRef(controller);
122
+ } else if (forwardedRef) {
123
+ forwardedRef.current = controller;
124
+ }
125
+ },
126
+ [forwardedRef],
127
+ );
128
+ useEffect(() => {
129
+ controllerRef.current?.view?.dispatch({ effects: refEditorRedecorate.of(null) });
130
+ }, [refs]);
131
+
132
+ const getMenu = useCallback<NonNullable<UseEditorMenuProps['getMenu']>>(
133
+ async ({ text }) => {
134
+ const query = (text ?? '').replace(/^@/, '').toLowerCase();
135
+ const matchesQuery = (object: Obj.Unknown) =>
136
+ !query ||
137
+ getObjectLabel(object).toLowerCase().includes(query) ||
138
+ (getValues?.(object) ?? []).some((entry) => entry.toLowerCase().includes(query));
139
+
140
+ if (mode === 'email') {
141
+ // One item per (object, value) pair; selection captures the value as an RFC 5322 mailbox.
142
+ const mailboxes = objects
143
+ .flatMap((object) => (getValues?.(object) ?? []).map((entry) => ({ object, entry })))
144
+ .filter(
145
+ ({ object, entry }) =>
146
+ !query || getObjectLabel(object).toLowerCase().includes(query) || entry.toLowerCase().includes(query),
147
+ );
148
+
149
+ return [
150
+ {
151
+ id: 'mailboxes',
152
+ items: mailboxes.slice(0, numItems * 2).map(({ object, entry }) => ({
153
+ id: `${object.id}-${entry}`,
154
+ label: `${getObjectLabel(object)} (${entry})`,
155
+ icon,
156
+ onSelect: ({ view, head }: { view: EditorView; head: number }) =>
157
+ insertMailbox(view, head, formatMailbox(getObjectLabel(object), entry)),
158
+ })),
159
+ },
160
+ ];
161
+ }
162
+
163
+ // 'ref' mode: only match existing objects.
164
+ return [
165
+ {
166
+ id: 'refs',
167
+ items: objects
168
+ .filter(matchesQuery)
169
+ .slice(0, numItems * 2)
170
+ .map((object) => {
171
+ const label = getObjectLabel(object);
172
+ const detail = getValues?.(object)?.[0];
173
+ return {
174
+ id: object.id,
175
+ label: detail && detail !== label ? `${label} (${detail})` : label,
176
+ icon,
177
+ onSelect: ({ view, head }: { view: EditorView; head: number }) =>
178
+ insertAtCursor(view, head, `@${object.id} `),
179
+ };
180
+ }),
181
+ },
182
+ ];
183
+ },
184
+ [objects, numItems, mode, icon, getObjectLabel, getValues],
185
+ );
186
+
187
+ // NOTE: The placeholder is supplied via the popover (Editor.Root) so the trigger hint and the
188
+ // empty-state hint are a single message; a second basic-extension placeholder would stack with it.
189
+ // Must be referentially stable: the menu extension is memoized on it, and a new extension
190
+ // identity destroys and recreates the editor on every keystroke.
191
+ const placeholder = useMemo(
192
+ () => ({
193
+ content: placeholderProp ?? t(activateOnTyping ? 'ref-editor-auto.placeholder' : 'ref-editor.placeholder'),
194
+ focusOnly: false,
195
+ }),
196
+ [t, placeholderProp, activateOnTyping],
197
+ );
198
+
199
+ const extensions = useMemo<Extension[]>(
200
+ () => [
201
+ createBasicExtensions({ readOnly: readonly, lineWrapping: false }),
202
+ createThemeExtensions({ themeMode, slots: { scroller: { className: 'scrollbar-none' } } }),
203
+ refEditor({ mode, match, getRef: (id) => refsRef.current.get(id) }),
204
+ Prec.highest(
205
+ keymap.of([
206
+ {
207
+ key: 'Enter',
208
+ // Commit the in-progress token when it matches (insert the mode's separator, the
209
+ // same gesture as typing it); always consume the key (single-line input).
210
+ run: (view) => {
211
+ const { head } = view.state.selection.main;
212
+ const line = view.state.doc.lineAt(head);
213
+ const before = line.text.slice(0, head - line.from);
214
+ if (mode === 'email') {
215
+ const segment = before.slice(before.lastIndexOf(',') + 1).trim();
216
+ if (NAME_ADDR_REGEX.test(segment) || (match ?? EMAIL_REGEX).test(segment)) {
217
+ insertAtCursor(view, head, ', ');
218
+ }
219
+ } else {
220
+ const token = before.slice(before.search(/\S+$/));
221
+ if (token && match?.test(token)) {
222
+ insertAtCursor(view, head, ' ');
223
+ }
224
+ }
225
+ return true;
226
+ },
227
+ },
228
+ ]),
229
+ ),
230
+ ],
231
+ [readonly, themeMode, mode, match],
232
+ );
233
+
234
+ return (
235
+ <Editor.Root
236
+ extensions={extensions}
237
+ numItems={numItems}
238
+ trigger='@'
239
+ activateOnTyping={activateOnTyping}
240
+ placeholder={placeholder}
241
+ // The menu owns its own matching (labels AND values); the built-in label filter would
242
+ // drop value-matched objects whose label doesn't start with the query.
243
+ filter={false}
244
+ getMenu={getMenu}
245
+ ref={composedRef}
246
+ >
247
+ <Editor.View
248
+ // TOOD(burdon): Use same style as react-ui Input.
249
+ classNames='border border-separator rounded-xs px-2'
250
+ {...props}
251
+ initialValue={value}
252
+ selectionEnd
253
+ />
254
+ </Editor.Root>
255
+ );
256
+ },
257
+ );
258
+
259
+ RefEditor.displayName = 'RefEditor';
260
+
261
+ /**
262
+ * Insert an RFC 5322 mailbox, replacing the in-progress segment (anything typed after the last
263
+ * comma — multi-word queries can leave words outside the popover's completion range).
264
+ */
265
+ const insertMailbox = (view: EditorView, head: number, mailbox: string) => {
266
+ const line = view.state.doc.lineAt(head);
267
+ const before = line.text.slice(0, head - line.from);
268
+ const lastComma = before.lastIndexOf(',');
269
+ const from = line.from + (lastComma === -1 ? 0 : lastComma + 1);
270
+ const insert = `${lastComma === -1 ? '' : ' '}${mailbox}, `;
271
+ view.dispatch({
272
+ changes: { from, to: head, insert },
273
+ selection: { anchor: from + insert.length, head: from + insert.length },
274
+ });
275
+ };
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ export * from './RefEditor';
6
+ export * from './ref-editor-extension';
@@ -0,0 +1,254 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { type EditorState, type Extension, RangeSetBuilder, StateEffect, StateField } from '@codemirror/state';
6
+ import { Decoration, type DecorationSet, EditorView, WidgetType } from '@codemirror/view';
7
+
8
+ import { Domino } from '@dxos/ui';
9
+ import { getHashHue, getStyles, mx } from '@dxos/ui-theme';
10
+
11
+ /**
12
+ * Display info for a resolved object reference.
13
+ */
14
+ export type RefInfo = {
15
+ label: string;
16
+ hue?: string;
17
+ };
18
+
19
+ /**
20
+ * Content model:
21
+ * - `'ref'`: whitespace-separated tokens; `@<id>` references to existing objects (and raw tokens
22
+ * accepted by the pluggable {@link RefEditorExtensionOptions.match} regexp).
23
+ * - `'email'`: comma-separated RFC 5322 mailboxes (`Name <email>` or bare `email`); the comma
24
+ * separators are consumed into the atomic tags so they are not visible.
25
+ */
26
+ export type RefEditorMode = 'ref' | 'email';
27
+
28
+ export type RefEditorExtensionOptions = {
29
+ mode?: RefEditorMode;
30
+ /** Resolve an object id referenced as `@<id>` to display info. */
31
+ getRef?: (id: string) => RefInfo | undefined;
32
+ /** Pluggable raw-token matcher (e.g. {@link EMAIL_REGEX}); matching tokens render as neutral tags. */
33
+ match?: RegExp;
34
+ };
35
+
36
+ /** Matches an object reference token (`@<object id>`). */
37
+ export const REF_REGEX = /^@([A-Za-z0-9]+)$/;
38
+
39
+ /** Matches a well-formed email address token. */
40
+ export const EMAIL_REGEX = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/;
41
+
42
+ /** Matches an RFC 5322 name-addr (`Name <email>`); group 1 = display name, group 2 = addr-spec. */
43
+ export const NAME_ADDR_REGEX = /^(.*?)\s*<([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})>$/;
44
+
45
+ /** Formats an RFC 5322 mailbox (`Name <email>`). */
46
+ export const formatMailbox = (name: string | undefined, email: string): string => (name ? `${name} <${email}>` : email);
47
+
48
+ /**
49
+ * Effect dispatched when reference data changes out-of-band (e.g. objects finish loading) so that
50
+ * already-rendered reference decorations re-resolve without a document change.
51
+ */
52
+ export const refEditorRedecorate = StateEffect.define<null>();
53
+
54
+ /**
55
+ * CodeMirror extension rendering reference-editor tokens as tags. Tags are atomic: the cursor
56
+ * cannot enter them and deletion removes the whole token.
57
+ * - `'ref'` mode: `@<id>` references resolve to the object's label (via
58
+ * {@link RefEditorExtensionOptions.getRef}); tokens accepted by the pluggable `match` regexp
59
+ * render as neutral tags.
60
+ * - `'email'` mode: RFC 5322 mailboxes render as tags labelled with the display name (falling
61
+ * back to the address); the trailing comma separator is hidden inside the tag's atomic range.
62
+ */
63
+ export const refEditor = ({ mode = 'ref', getRef, match }: RefEditorExtensionOptions = {}): Extension => {
64
+ const buildRefDecorations = (state: EditorState) => {
65
+ const deco = new RangeSetBuilder<Decoration>();
66
+ const text = state.doc.toString();
67
+ const tokenRegex = /\S+/g;
68
+ let tokenMatch: RegExpExecArray | null;
69
+ while ((tokenMatch = tokenRegex.exec(text))) {
70
+ const token = tokenMatch[0];
71
+ const from = tokenMatch.index;
72
+ const to = from + token.length;
73
+
74
+ const refMatch = token.match(REF_REGEX);
75
+ if (refMatch) {
76
+ const ref = getRef?.(refMatch[1]);
77
+ if (ref) {
78
+ deco.add(
79
+ from,
80
+ to,
81
+ Decoration.widget({
82
+ widget: new RefWidget(ref.label, ref.hue ?? getHashHue(refMatch[1])),
83
+ atomic: true,
84
+ }),
85
+ );
86
+ }
87
+ continue;
88
+ }
89
+
90
+ if (match?.test(token)) {
91
+ deco.add(
92
+ from,
93
+ to,
94
+ Decoration.widget({
95
+ widget: new TokenWidget(token),
96
+ atomic: true,
97
+ }),
98
+ );
99
+ }
100
+ }
101
+
102
+ return deco.finish();
103
+ };
104
+
105
+ const buildEmailDecorations = (state: EditorState) => {
106
+ const addrRegex = match ?? EMAIL_REGEX;
107
+ const deco = new RangeSetBuilder<Decoration>();
108
+ const text = state.doc.toString();
109
+ // Segments are comma-separated; a segment's atomic range includes its trailing comma (and
110
+ // following whitespace) so the separator never renders.
111
+ const segmentRegex = /[^,]+(?:,\s*)?|,\s*/g;
112
+ let segmentMatch: RegExpExecArray | null;
113
+ while ((segmentMatch = segmentRegex.exec(text))) {
114
+ const segment = segmentMatch[0];
115
+ const commaIndex = segment.indexOf(',');
116
+ const content = (commaIndex === -1 ? segment : segment.slice(0, commaIndex)).trim();
117
+ if (!content) {
118
+ continue;
119
+ }
120
+ const leading = segment.length - segment.trimStart().length;
121
+ const from = segmentMatch.index + leading;
122
+ const to = segmentMatch.index + segment.length;
123
+
124
+ const nameAddr = content.match(NAME_ADDR_REGEX);
125
+ if (nameAddr) {
126
+ deco.add(
127
+ from,
128
+ to,
129
+ Decoration.widget({
130
+ widget: new RefWidget(nameAddr[1] || nameAddr[2], getHashHue(nameAddr[2])),
131
+ atomic: true,
132
+ }),
133
+ );
134
+ } else if (addrRegex.test(content)) {
135
+ deco.add(
136
+ from,
137
+ to,
138
+ Decoration.widget({
139
+ widget: new TokenWidget(content),
140
+ atomic: true,
141
+ }),
142
+ );
143
+ }
144
+ }
145
+
146
+ return deco.finish();
147
+ };
148
+
149
+ const buildDecorations = mode === 'email' ? buildEmailDecorations : buildRefDecorations;
150
+
151
+ return [
152
+ styles,
153
+ StateField.define<DecorationSet>({
154
+ create: (state) => buildDecorations(state),
155
+ update: (deco, tr) => {
156
+ if (tr.docChanged || tr.effects.some((effect) => effect.is(refEditorRedecorate))) {
157
+ return buildDecorations(tr.state);
158
+ }
159
+
160
+ return deco;
161
+ },
162
+ provide: (field) => [
163
+ EditorView.decorations.from(field),
164
+ EditorView.atomicRanges.of((view) => {
165
+ const builder = new RangeSetBuilder<Decoration>();
166
+ const cursor = view.state.field(field).iter();
167
+ while (cursor.value) {
168
+ if (cursor.value.spec.atomic) {
169
+ builder.add(cursor.from, cursor.to, cursor.value);
170
+ }
171
+ cursor.next();
172
+ }
173
+
174
+ return builder.finish();
175
+ }),
176
+ ],
177
+ }),
178
+ ];
179
+ };
180
+
181
+ const lineHeight = '30px';
182
+
183
+ /**
184
+ * NOTE: The outer container vertically aligns the inner text with content in the outer div.
185
+ */
186
+ const container = (classNames: string, ...children: Domino<HTMLElement>[]) => {
187
+ const inner = Domino.of('span')
188
+ .classNames(mx('inline-flex h-[26px] border rounded-xs', classNames))
189
+ .append(...children);
190
+ return Domino.of('span').classNames('inline-flex h-[28px] align-middle').append(inner).root;
191
+ };
192
+
193
+ /**
194
+ * Resolved reference (object reference or RFC 5322 mailbox), rendered as the object's label.
195
+ */
196
+ class RefWidget extends WidgetType {
197
+ constructor(
198
+ private readonly _label: string,
199
+ private readonly _hue: string,
200
+ ) {
201
+ super();
202
+ }
203
+
204
+ override eq(other: this) {
205
+ return this._label === other._label && this._hue === other._hue;
206
+ }
207
+
208
+ override toDOM() {
209
+ const { bg: fill, border, surface } = getStyles(this._hue);
210
+ return container(
211
+ border,
212
+ Domino.of('span').classNames(mx('flex items-center px-1 text-black text-xs', fill)).text('@'),
213
+ Domino.of('span')
214
+ .classNames(mx('flex items-center px-1 text-subdued text-sm rounded-r-[3px]', surface))
215
+ .text(this._label),
216
+ );
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Raw token accepted by the pluggable matcher (e.g. an email address).
222
+ */
223
+ class TokenWidget extends WidgetType {
224
+ constructor(private readonly _token: string) {
225
+ super();
226
+ }
227
+
228
+ override eq(other: this) {
229
+ return this._token === other._token;
230
+ }
231
+
232
+ override toDOM() {
233
+ return container(
234
+ 'border-separator',
235
+ Domino.of('span').classNames('flex items-center px-1 text-subdued text-sm').text(this._token),
236
+ );
237
+ }
238
+ }
239
+
240
+ const styles = EditorView.theme({
241
+ '.cm-line': {
242
+ lineHeight,
243
+ },
244
+ // Match the standard Input block size (md density): 2.5rem, 2rem on pointer-fine devices.
245
+ '.cm-scroller': {
246
+ alignItems: 'center',
247
+ minHeight: '2.5rem',
248
+ },
249
+ '@media (pointer: fine)': {
250
+ '.cm-scroller': {
251
+ minHeight: '2rem',
252
+ },
253
+ },
254
+ });