@nyaruka/temba-components 0.129.8 → 0.129.10

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 (282) hide show
  1. package/CHANGELOG.md +37 -3
  2. package/demo/data/flows/sample-flow.json +186 -96
  3. package/demo/test-colorpicker.html +30 -0
  4. package/dist/temba-components.js +1126 -1111
  5. package/dist/temba-components.js.map +1 -1
  6. package/out-tsc/src/events.js.map +1 -1
  7. package/out-tsc/src/excellent/helpers.js +2 -2
  8. package/out-tsc/src/excellent/helpers.js.map +1 -1
  9. package/out-tsc/src/flow/CanvasNode.js +25 -7
  10. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  11. package/out-tsc/src/flow/Editor.js +11 -1
  12. package/out-tsc/src/flow/Editor.js.map +1 -1
  13. package/out-tsc/src/flow/NodeEditor.js +133 -290
  14. package/out-tsc/src/flow/NodeEditor.js.map +1 -1
  15. package/out-tsc/src/flow/actions/add_input_labels.js +40 -0
  16. package/out-tsc/src/flow/actions/add_input_labels.js.map +1 -1
  17. package/out-tsc/src/flow/actions/call_llm.js +56 -3
  18. package/out-tsc/src/flow/actions/call_llm.js.map +1 -1
  19. package/out-tsc/src/flow/actions/call_webhook.js +1 -1
  20. package/out-tsc/src/flow/actions/call_webhook.js.map +1 -1
  21. package/out-tsc/src/flow/actions/open_ticket.js +65 -3
  22. package/out-tsc/src/flow/actions/open_ticket.js.map +1 -1
  23. package/out-tsc/src/flow/actions/set_run_result.js +75 -0
  24. package/out-tsc/src/flow/actions/set_run_result.js.map +1 -1
  25. package/out-tsc/src/flow/config.js +4 -0
  26. package/out-tsc/src/flow/config.js.map +1 -1
  27. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js +227 -0
  28. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js.map +1 -0
  29. package/out-tsc/src/flow/nodes/split_by_ticket.js +18 -0
  30. package/out-tsc/src/flow/nodes/split_by_ticket.js.map +1 -0
  31. package/out-tsc/src/flow/nodes/wait_for_response.js +27 -1
  32. package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
  33. package/out-tsc/src/flow/types.js +0 -65
  34. package/out-tsc/src/flow/types.js.map +1 -1
  35. package/out-tsc/src/form/ArrayEditor.js +63 -117
  36. package/out-tsc/src/form/ArrayEditor.js.map +1 -1
  37. package/out-tsc/src/form/BaseListEditor.js +4 -3
  38. package/out-tsc/src/form/BaseListEditor.js.map +1 -1
  39. package/out-tsc/src/form/Checkbox.js +77 -24
  40. package/out-tsc/src/form/Checkbox.js.map +1 -1
  41. package/out-tsc/src/form/ColorPicker.js +28 -40
  42. package/out-tsc/src/form/ColorPicker.js.map +1 -1
  43. package/out-tsc/src/form/Completion.js +44 -53
  44. package/out-tsc/src/form/Completion.js.map +1 -1
  45. package/out-tsc/src/form/Compose.js +7 -8
  46. package/out-tsc/src/form/Compose.js.map +1 -1
  47. package/out-tsc/src/form/ContactSearch.js +3 -4
  48. package/out-tsc/src/form/ContactSearch.js.map +1 -1
  49. package/out-tsc/src/form/DatePicker.js +29 -36
  50. package/out-tsc/src/form/DatePicker.js.map +1 -1
  51. package/out-tsc/src/form/{FormField.js → FieldElement.js} +81 -53
  52. package/out-tsc/src/form/FieldElement.js.map +1 -0
  53. package/out-tsc/src/form/FieldRenderer.js +306 -0
  54. package/out-tsc/src/form/FieldRenderer.js.map +1 -0
  55. package/out-tsc/src/form/ImagePicker.js +122 -126
  56. package/out-tsc/src/form/ImagePicker.js.map +1 -1
  57. package/out-tsc/src/form/KeyValueEditor.js +41 -37
  58. package/out-tsc/src/form/KeyValueEditor.js.map +1 -1
  59. package/out-tsc/src/form/MessageEditor.js +55 -63
  60. package/out-tsc/src/form/MessageEditor.js.map +1 -1
  61. package/out-tsc/src/form/TembaSlider.js +3 -3
  62. package/out-tsc/src/form/TembaSlider.js.map +1 -1
  63. package/out-tsc/src/form/TemplateEditor.js +3 -3
  64. package/out-tsc/src/form/TemplateEditor.js.map +1 -1
  65. package/out-tsc/src/form/TextInput.js +23 -27
  66. package/out-tsc/src/form/TextInput.js.map +1 -1
  67. package/out-tsc/src/form/select/Select.js +57 -35
  68. package/out-tsc/src/form/select/Select.js.map +1 -1
  69. package/out-tsc/src/form/select/UserSelect.js +8 -9
  70. package/out-tsc/src/form/select/UserSelect.js.map +1 -1
  71. package/out-tsc/src/form/select/WorkspaceSelect.js +7 -8
  72. package/out-tsc/src/form/select/WorkspaceSelect.js.map +1 -1
  73. package/out-tsc/src/live/ContactChat.js +62 -44
  74. package/out-tsc/src/live/ContactChat.js.map +1 -1
  75. package/out-tsc/src/live/ContactFieldEditor.js.map +1 -1
  76. package/out-tsc/src/markdown.js +13 -11
  77. package/out-tsc/src/markdown.js.map +1 -1
  78. package/out-tsc/temba-modules.js +3 -2
  79. package/out-tsc/temba-modules.js.map +1 -1
  80. package/out-tsc/test/ActionHelper.js +2 -0
  81. package/out-tsc/test/ActionHelper.js.map +1 -1
  82. package/out-tsc/test/NodeHelper.js +148 -0
  83. package/out-tsc/test/NodeHelper.js.map +1 -0
  84. package/out-tsc/test/actions/call_llm.test.js +103 -0
  85. package/out-tsc/test/actions/call_llm.test.js.map +1 -0
  86. package/out-tsc/test/nodes/split_by_llm_categorize.test.js +532 -0
  87. package/out-tsc/test/nodes/split_by_llm_categorize.test.js.map +1 -0
  88. package/out-tsc/test/nodes/split_by_random.test.js +150 -0
  89. package/out-tsc/test/nodes/split_by_random.test.js.map +1 -0
  90. package/out-tsc/test/nodes/wait_for_digits.test.js +150 -0
  91. package/out-tsc/test/nodes/wait_for_digits.test.js.map +1 -0
  92. package/out-tsc/test/nodes/wait_for_response.test.js +171 -0
  93. package/out-tsc/test/nodes/wait_for_response.test.js.map +1 -0
  94. package/out-tsc/test/temba-add-input-labels.test.js +70 -0
  95. package/out-tsc/test/temba-add-input-labels.test.js.map +1 -0
  96. package/out-tsc/test/temba-checkbox.test.js +16 -0
  97. package/out-tsc/test/temba-checkbox.test.js.map +1 -1
  98. package/out-tsc/test/temba-field-renderer.test.js +296 -0
  99. package/out-tsc/test/temba-field-renderer.test.js.map +1 -0
  100. package/out-tsc/test/temba-integration-markdown.test.js +2 -4
  101. package/out-tsc/test/temba-integration-markdown.test.js.map +1 -1
  102. package/out-tsc/test/temba-markdown.test.js +1 -1
  103. package/out-tsc/test/temba-markdown.test.js.map +1 -1
  104. package/out-tsc/test/temba-node-editor.test.js +400 -0
  105. package/out-tsc/test/temba-node-editor.test.js.map +1 -1
  106. package/out-tsc/test/temba-select.test.js +6 -3
  107. package/out-tsc/test/temba-select.test.js.map +1 -1
  108. package/out-tsc/test/temba-slider.test.js +0 -1
  109. package/out-tsc/test/temba-slider.test.js.map +1 -1
  110. package/out-tsc/test/temba-webchat.test.js +1 -1
  111. package/out-tsc/test/temba-webchat.test.js.map +1 -1
  112. package/package.json +1 -1
  113. package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
  114. package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
  115. package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
  116. package/screenshots/truth/actions/call_llm/editor/information-extraction.png +0 -0
  117. package/screenshots/truth/actions/call_llm/editor/sentiment-analysis.png +0 -0
  118. package/screenshots/truth/actions/call_llm/editor/summarization.png +0 -0
  119. package/screenshots/truth/actions/call_llm/editor/translation-task.png +0 -0
  120. package/screenshots/truth/actions/call_llm/render/information-extraction.png +0 -0
  121. package/screenshots/truth/actions/call_llm/render/sentiment-analysis.png +0 -0
  122. package/screenshots/truth/actions/call_llm/render/summarization.png +0 -0
  123. package/screenshots/truth/actions/call_llm/render/translation-task.png +0 -0
  124. package/screenshots/truth/actions/remove_contact_groups/editor/cleanup-groups.png +0 -0
  125. package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
  126. package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.png +0 -0
  127. package/screenshots/truth/actions/remove_contact_groups/editor/multiple-groups.png +0 -0
  128. package/screenshots/truth/actions/remove_contact_groups/editor/remove-from-all-groups.png +0 -0
  129. package/screenshots/truth/actions/remove_contact_groups/editor/single-group.png +0 -0
  130. package/screenshots/truth/actions/send_email/editor/complex-business-email.png +0 -0
  131. package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
  132. package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
  133. package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
  134. package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
  135. package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
  136. package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
  137. package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
  138. package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
  139. package/screenshots/truth/checkbox/checkbox-label-background-hover.png +0 -0
  140. package/screenshots/truth/checkbox/checkbox-no-label-no-background-hover.png +0 -0
  141. package/screenshots/truth/checkbox/checkbox-with-help-text.png +0 -0
  142. package/screenshots/truth/checkbox/checked.png +0 -0
  143. package/screenshots/truth/checkbox/default.png +0 -0
  144. package/screenshots/truth/colorpicker/default.png +0 -0
  145. package/screenshots/truth/colorpicker/focused.png +0 -0
  146. package/screenshots/truth/colorpicker/initialized.png +0 -0
  147. package/screenshots/truth/colorpicker/selected.png +0 -0
  148. package/screenshots/truth/editor/router.png +0 -0
  149. package/screenshots/truth/editor/send_msg.png +0 -0
  150. package/screenshots/truth/editor/set_contact_language.png +0 -0
  151. package/screenshots/truth/editor/set_contact_name.png +0 -0
  152. package/screenshots/truth/editor/set_run_result.png +0 -0
  153. package/screenshots/truth/editor/wait.png +0 -0
  154. package/screenshots/truth/field-renderer/checkbox-checked.png +0 -0
  155. package/screenshots/truth/field-renderer/checkbox-unchecked.png +0 -0
  156. package/screenshots/truth/field-renderer/checkbox-with-errors.png +0 -0
  157. package/screenshots/truth/field-renderer/context-comparison.png +0 -0
  158. package/screenshots/truth/field-renderer/key-value-with-label.png +0 -0
  159. package/screenshots/truth/field-renderer/message-editor-with-label.png +0 -0
  160. package/screenshots/truth/field-renderer/select-multi.png +0 -0
  161. package/screenshots/truth/field-renderer/select-no-label.png +0 -0
  162. package/screenshots/truth/field-renderer/select-with-label.png +0 -0
  163. package/screenshots/truth/field-renderer/text-evaluated.png +0 -0
  164. package/screenshots/truth/field-renderer/text-no-label.png +0 -0
  165. package/screenshots/truth/field-renderer/text-with-errors.png +0 -0
  166. package/screenshots/truth/field-renderer/text-with-label.png +0 -0
  167. package/screenshots/truth/field-renderer/textarea-evaluated.png +0 -0
  168. package/screenshots/truth/field-renderer/textarea-with-label.png +0 -0
  169. package/screenshots/truth/integration/checkbox-markdown-errors.png +0 -0
  170. package/screenshots/truth/nodes/split_by_llm_categorize/editor/basic-categorization.png +0 -0
  171. package/screenshots/truth/nodes/split_by_llm_categorize/editor/custom-input-and-result-name.png +0 -0
  172. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  173. package/screenshots/truth/nodes/split_by_llm_categorize/editor/many-categories.png +0 -0
  174. package/screenshots/truth/nodes/split_by_llm_categorize/editor/minimal-categories.png +0 -0
  175. package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
  176. package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
  177. package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
  178. package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
  179. package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
  180. package/screenshots/truth/nodes/split_by_random/editor/ab-test-multiple-variants.png +0 -0
  181. package/screenshots/truth/nodes/split_by_random/editor/sampling-split.png +0 -0
  182. package/screenshots/truth/nodes/split_by_random/editor/three-way-split.png +0 -0
  183. package/screenshots/truth/nodes/split_by_random/editor/two-bucket-split.png +0 -0
  184. package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
  185. package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
  186. package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
  187. package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
  188. package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
  189. package/screenshots/truth/nodes/wait_for_digits/editor/phone-number-collection.png +0 -0
  190. package/screenshots/truth/nodes/wait_for_digits/editor/single-digit-with-timeout.png +0 -0
  191. package/screenshots/truth/nodes/wait_for_digits/editor/verification-code.png +0 -0
  192. package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
  193. package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
  194. package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
  195. package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
  196. package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
  197. package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
  198. package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
  199. package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
  200. package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
  201. package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
  202. package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
  203. package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
  204. package/screenshots/truth/omnibox/selected.png +0 -0
  205. package/screenshots/truth/run-list/basic.png +0 -0
  206. package/screenshots/truth/select/functions.png +0 -0
  207. package/screenshots/truth/select/multi-with-endpoint.png +0 -0
  208. package/screenshots/truth/select/search-enabled.png +0 -0
  209. package/src/events.ts +12 -6
  210. package/src/excellent/helpers.ts +2 -2
  211. package/src/flow/CanvasNode.ts +22 -1
  212. package/src/flow/Editor.ts +12 -1
  213. package/src/flow/NodeEditor.ts +186 -374
  214. package/src/flow/actions/add_input_labels.ts +45 -0
  215. package/src/flow/actions/call_llm.ts +57 -3
  216. package/src/flow/actions/call_webhook.ts +1 -1
  217. package/src/flow/actions/open_ticket.ts +74 -3
  218. package/src/flow/actions/set_run_result.ts +83 -0
  219. package/src/flow/config.ts +4 -0
  220. package/src/flow/nodes/split_by_llm_categorize.ts +277 -0
  221. package/src/flow/nodes/split_by_ticket.ts +19 -0
  222. package/src/flow/nodes/wait_for_response.ts +28 -1
  223. package/src/flow/types.ts +26 -127
  224. package/src/form/ArrayEditor.ts +79 -139
  225. package/src/form/BaseListEditor.ts +4 -4
  226. package/src/form/Checkbox.ts +81 -24
  227. package/src/form/ColorPicker.ts +31 -43
  228. package/src/form/Completion.ts +49 -56
  229. package/src/form/Compose.ts +8 -8
  230. package/src/form/ContactSearch.ts +3 -4
  231. package/src/form/DatePicker.ts +32 -38
  232. package/src/form/{FormField.ts → FieldElement.ts} +108 -55
  233. package/src/form/FieldRenderer.ts +466 -0
  234. package/src/form/ImagePicker.ts +107 -110
  235. package/src/form/KeyValueEditor.ts +43 -39
  236. package/src/form/MessageEditor.ts +61 -67
  237. package/src/form/TembaSlider.ts +3 -3
  238. package/src/form/TemplateEditor.ts +3 -3
  239. package/src/form/TextInput.ts +26 -29
  240. package/src/form/select/Select.ts +63 -37
  241. package/src/form/select/UserSelect.ts +10 -11
  242. package/src/form/select/WorkspaceSelect.ts +9 -10
  243. package/src/live/ContactChat.ts +62 -47
  244. package/src/live/ContactFieldEditor.ts +2 -2
  245. package/src/markdown.ts +19 -11
  246. package/src/store/flow-definition.d.ts +5 -2
  247. package/static/api/labels.json +31 -0
  248. package/static/api/topics.json +24 -9
  249. package/static/api/users.json +35 -16
  250. package/static/css/temba-components.css +3 -3
  251. package/stress-test.js +18 -13
  252. package/temba-modules.ts +3 -2
  253. package/test/ActionHelper.ts +2 -0
  254. package/test/NodeHelper.ts +184 -0
  255. package/test/actions/call_llm.test.ts +137 -0
  256. package/test/nodes/README.md +78 -0
  257. package/test/nodes/split_by_llm_categorize.test.ts +698 -0
  258. package/test/nodes/split_by_random.test.ts +177 -0
  259. package/test/nodes/wait_for_digits.test.ts +176 -0
  260. package/test/nodes/wait_for_response.test.ts +206 -0
  261. package/test/temba-add-input-labels.test.ts +87 -0
  262. package/test/temba-checkbox.test.ts +26 -0
  263. package/test/temba-field-renderer.test.ts +482 -0
  264. package/test/temba-integration-markdown.test.ts +2 -4
  265. package/test/temba-markdown.test.ts +1 -1
  266. package/test/temba-node-editor.test.ts +496 -0
  267. package/test/temba-select.test.ts +6 -6
  268. package/test/temba-slider.test.ts +0 -1
  269. package/test/temba-webchat.test.ts +1 -1
  270. package/test-assets/contacts/history.json +7 -20
  271. package/test-assets/select/llms.json +18 -0
  272. package/web-dev-mock.mjs +96 -6
  273. package/web-dev-server.config.mjs +29 -7
  274. package/out-tsc/src/form/FormElement.js +0 -67
  275. package/out-tsc/src/form/FormElement.js.map +0 -1
  276. package/out-tsc/src/form/FormField.js.map +0 -1
  277. package/out-tsc/test/temba-formfield.test.js +0 -94
  278. package/out-tsc/test/temba-formfield.test.js.map +0 -1
  279. package/src/form/FormElement.ts +0 -69
  280. package/test/temba-flow-editor.test.ts.backup +0 -563
  281. package/test/temba-formfield.test.ts +0 -121
  282. package/test/temba-utils-index.test.ts.backup +0 -1737
@@ -1,10 +1,10 @@
1
1
  import { html, css, PropertyValueMap } from 'lit';
2
- import { FormElement } from './FormElement';
2
+ import { FieldElement } from './FieldElement';
3
3
  import { property } from 'lit/decorators.js';
4
4
  import { getClasses, hslToHex } from '../utils';
5
5
  import { TextInput } from './TextInput';
6
6
 
7
- export class ColorPicker extends FormElement {
7
+ export class ColorPicker extends FieldElement {
8
8
  @property({ type: Boolean })
9
9
  expanded = false;
10
10
 
@@ -24,6 +24,8 @@ export class ColorPicker extends FormElement {
24
24
 
25
25
  static get styles() {
26
26
  return css`
27
+ ${super.styles}
28
+
27
29
  :host {
28
30
  color: var(--color-text);
29
31
  display: inline-block;
@@ -38,11 +40,6 @@ export class ColorPicker extends FormElement {
38
40
  width: 5em;
39
41
  }
40
42
 
41
- temba-field {
42
- display: block;
43
- width: 100%;
44
- }
45
-
46
43
  .wrapper {
47
44
  border: 1px solid var(--color-widget-border);
48
45
  padding: calc(var(--curvature) / 2);
@@ -221,46 +218,37 @@ export class ColorPicker extends FormElement {
221
218
  return value;
222
219
  }
223
220
 
224
- public render() {
221
+ protected renderWidget() {
225
222
  return html`
226
- <temba-field
227
- name=${this.name}
228
- .helpText=${this.helpText}
229
- .errors=${this.errors}
230
- .widgetOnly=${this.widgetOnly}
231
- .hideLabel=${this.hideLabel}
232
- .disabled=${this.disabled}
233
- >
234
- <div style="display:flex" tabindex="0">
235
- <div class=${getClasses({ wrapper: true, expanded: this.expanded })}>
236
- <div class=${getClasses({ 'picker-wrapper': true })}>
237
- <div
238
- class=${getClasses({
239
- preview: true,
240
- selecting: this.selecting
241
- })}
242
- style="color:${this.labelColor};background:${this.previewColor}"
243
- @click=${this.handlePreviewClick}
244
- >
245
- ${this.label}
246
- </div>
247
- <div
248
- class="color-picker"
249
- tabindex="0"
250
- @blur=${this.handleBlur}
251
- @mousemove=${this.handleMouseMove}
252
- @mouseout=${this.handleMouseOut}
253
- @click=${this.handleColorClick}
254
- ></div>
223
+ <div style="display:flex" tabindex="0">
224
+ <div class=${getClasses({ wrapper: true, expanded: this.expanded })}>
225
+ <div class=${getClasses({ 'picker-wrapper': true })}>
226
+ <div
227
+ class=${getClasses({
228
+ preview: true,
229
+ selecting: this.selecting
230
+ })}
231
+ style="color:${this.labelColor};background:${this.previewColor}"
232
+ @click=${this.handlePreviewClick}
233
+ >
234
+ ${this.label}
255
235
  </div>
256
- <temba-textinput
257
- value=${this.hex}
258
- @input=${this.handleHexInput}
259
- placeholder="#000000"
260
- ></temba-textinput>
236
+ <div
237
+ class="color-picker"
238
+ tabindex="0"
239
+ @blur=${this.handleBlur}
240
+ @mousemove=${this.handleMouseMove}
241
+ @mouseout=${this.handleMouseOut}
242
+ @click=${this.handleColorClick}
243
+ ></div>
261
244
  </div>
245
+ <temba-textinput
246
+ value=${this.hex}
247
+ @input=${this.handleHexInput}
248
+ placeholder="#000000"
249
+ ></temba-textinput>
262
250
  </div>
263
- </temba-field>
251
+ </div>
264
252
  `;
265
253
  }
266
254
  }
@@ -8,7 +8,7 @@ import {
8
8
  executeCompletionQuery
9
9
  } from '../excellent/helpers';
10
10
 
11
- import { FormElement } from './FormElement';
11
+ import { FieldElement } from './FieldElement';
12
12
  import { CompletionOption, Position } from '../interfaces';
13
13
  import { styleMap } from 'lit-html/directives/style-map.js';
14
14
  import { msg } from '@lit/localize';
@@ -16,9 +16,10 @@ import { msg } from '@lit/localize';
16
16
  /**
17
17
  * Completion is a text input that handles excellent completion options in a popup
18
18
  */
19
- export class Completion extends FormElement {
19
+ export class Completion extends FieldElement {
20
20
  static get styles() {
21
21
  return css`
22
+ ${super.styles}
22
23
  :host {
23
24
  display: block;
24
25
  }
@@ -69,7 +70,6 @@ export class Completion extends FormElement {
69
70
  }
70
71
 
71
72
  code {
72
- background: rgba(0, 0, 0, 0.1);
73
73
  padding: 1px 5px;
74
74
  border-radius: var(--curvature);
75
75
  }
@@ -106,9 +106,6 @@ export class Completion extends FormElement {
106
106
  @property({ type: String })
107
107
  name = '';
108
108
 
109
- @property({ type: String })
110
- value = '';
111
-
112
109
  @property({ type: Boolean })
113
110
  textarea: boolean;
114
111
 
@@ -259,7 +256,7 @@ export class Completion extends FormElement {
259
256
  }
260
257
  }
261
258
 
262
- public render(): TemplateResult {
259
+ protected renderWidget(): TemplateResult {
263
260
  const anchorStyles = this.anchorPosition
264
261
  ? {
265
262
  top: `${this.anchorPosition.top}px`,
@@ -270,55 +267,51 @@ export class Completion extends FormElement {
270
267
  const visible = this.options && this.options.length > 0;
271
268
 
272
269
  return html`
273
- <temba-field
274
- name=${this.name}
275
- .label=${this.label}
276
- .helpText=${this.helpText}
277
- .errors=${this.errors}
278
- .widgetOnly=${this.widgetOnly}
279
- >
280
- <div class="comp-container">
281
- <div id="anchor" style=${styleMap(anchorStyles)}></div>
282
- <temba-textinput
283
- name=${this.name}
284
- placeholder=${this.placeholder}
285
- gsm=${this.gsm}
286
- counter=${ifDefined(this.counter)}
287
- @keyup=${this.handleKeyUp}
288
- @click=${this.handleClick}
289
- @input=${this.handleInput}
290
- @blur=${this.handleOptionCanceled}
291
- maxlength="${ifDefined(this.maxLength)}"
292
- .value=${this.value}
293
- ?autogrow=${this.autogrow}
294
- ?textarea=${this.textarea}
295
- ?submitOnEnter=${this.submitOnEnter}
296
- style=${this.minHeight
297
- ? `--textarea-min-height: ${this.minHeight}px`
298
- : ''}
299
- >
300
- </temba-textinput>
301
- <temba-options
302
- @temba-selection=${this.handleOptionSelection}
303
- @temba-canceled=${this.handleOptionCanceled}
304
- .renderOption=${renderCompletionOption}
305
- .anchorTo=${this.anchorElement}
306
- .options=${this.options}
307
- ?visible=${visible}
308
- >
309
- ${this.currentFunction
310
- ? html`
311
- <div class="current-fn">
312
- ${renderCompletionOption(this.currentFunction, true)}
313
- </div>
314
- `
315
- : null}
316
- <div class="footer" style="${!visible ? 'display:none' : null}">
317
- ${msg('Tab to complete, enter to select')}
318
- </div>
319
- </temba-options>
320
- </div>
321
- </temba-field>
270
+ <div class="comp-container">
271
+ <div id="anchor" style=${styleMap(anchorStyles)}></div>
272
+ <temba-textinput
273
+ name=${this.name}
274
+ placeholder=${this.placeholder}
275
+ gsm=${this.gsm}
276
+ counter=${ifDefined(this.counter)}
277
+ @keyup=${this.handleKeyUp}
278
+ @click=${this.handleClick}
279
+ @input=${this.handleInput}
280
+ @blur=${this.handleOptionCanceled}
281
+ maxlength="${ifDefined(this.maxLength)}"
282
+ .value=${this.value}
283
+ ?autogrow=${this.autogrow}
284
+ ?textarea=${this.textarea}
285
+ ?submitOnEnter=${this.submitOnEnter}
286
+ style=${this.minHeight
287
+ ? `--textarea-min-height: ${this.minHeight}px`
288
+ : ''}
289
+ >
290
+ </temba-textinput>
291
+ <temba-options
292
+ @temba-selection=${this.handleOptionSelection}
293
+ @temba-canceled=${this.handleOptionCanceled}
294
+ .renderOption=${renderCompletionOption}
295
+ .anchorTo=${this.anchorElement}
296
+ .options=${this.options}
297
+ ?visible=${visible}
298
+ >
299
+ ${this.currentFunction
300
+ ? html`
301
+ <div class="current-fn">
302
+ ${renderCompletionOption(this.currentFunction, true)}
303
+ </div>
304
+ `
305
+ : null}
306
+ <div class="footer" style="${!visible ? 'display:none' : null}">
307
+ ${msg('Tab to complete, enter to select')}
308
+ </div>
309
+ </temba-options>
310
+ </div>
322
311
  `;
323
312
  }
313
+
314
+ public render(): TemplateResult {
315
+ return this.renderField();
316
+ }
324
317
  }
@@ -1,5 +1,5 @@
1
1
  import { TemplateResult, html, css } from 'lit';
2
- import { FormElement } from './FormElement';
2
+ import { FieldElement } from './FieldElement';
3
3
  import { property } from 'lit/decorators.js';
4
4
  import { Attachment, CustomEventType, Language, Shortcut } from '../interfaces';
5
5
  import { DEFAULT_MEDIA_ENDPOINT, getClasses } from '../utils';
@@ -20,7 +20,7 @@ export interface ComposeValue {
20
20
  variables: string[];
21
21
  }
22
22
 
23
- export class Compose extends FormElement {
23
+ export class Compose extends FieldElement {
24
24
  static get styles() {
25
25
  return css`
26
26
  :host {
@@ -544,12 +544,12 @@ export class Compose extends FormElement {
544
544
  }
545
545
 
546
546
  public render(): TemplateResult {
547
+ return this.renderField();
548
+ }
549
+
550
+ protected renderWidget(): TemplateResult {
547
551
  return html`
548
- <temba-field
549
- name=${this.name}
550
- .errors=${this.errors}
551
- .widgetOnly=${this.widgetOnly}
552
- .value=${this.value}
552
+ <div
553
553
  class=${getClasses({
554
554
  'active-template':
555
555
  !!this.currentTemplate &&
@@ -570,7 +570,7 @@ export class Compose extends FormElement {
570
570
  <div class="container">
571
571
  <div class="items actions">${this.getActions()}</div>
572
572
  </div>
573
- </temba-field>
573
+ </div>
574
574
  `;
575
575
  }
576
576
 
@@ -5,7 +5,7 @@ import { getClasses, postJSON, stopEvent, WebResponse } from '../utils';
5
5
  import { TextInput } from './TextInput';
6
6
  import '../display/Alert';
7
7
  import { Contact, CustomEventType } from '../interfaces';
8
- import { FormElement } from './FormElement';
8
+ import { FieldElement } from './FieldElement';
9
9
  import { Checkbox } from './Checkbox';
10
10
  import { msg } from '@lit/localize';
11
11
  import { OmniOption } from './select/Omnibox';
@@ -23,7 +23,7 @@ interface SummaryResponse {
23
23
  blockers: string[];
24
24
  }
25
25
 
26
- export class ContactSearch extends FormElement {
26
+ export class ContactSearch extends FieldElement {
27
27
  static get styles() {
28
28
  return css`
29
29
  :host {
@@ -475,7 +475,7 @@ export class ContactSearch extends FormElement {
475
475
  }
476
476
  }
477
477
 
478
- public render(): TemplateResult {
478
+ public renderWidget(): TemplateResult {
479
479
  let summary: TemplateResult;
480
480
  if (this.summary) {
481
481
  if (!this.summary.error) {
@@ -539,7 +539,6 @@ export class ContactSearch extends FormElement {
539
539
  this.advanced
540
540
  ? html`<div class="query">
541
541
  <temba-textinput
542
- .label=${this.label}
543
542
  .helpText=${this.helpText}
544
543
  .widgetOnly=${this.widgetOnly}
545
544
  .errors=${this.errors}
@@ -1,12 +1,13 @@
1
1
  import { TemplateResult, html, css, PropertyValueMap } from 'lit';
2
2
  import { property } from 'lit/decorators.js';
3
- import { FormElement } from './FormElement';
3
+ import { FieldElement } from './FieldElement';
4
4
  import { getClasses } from '../utils';
5
5
  import { DateTime } from 'luxon';
6
6
 
7
- export class DatePicker extends FormElement {
7
+ export class DatePicker extends FieldElement {
8
8
  static get styles() {
9
9
  return css`
10
+ ${super.styles}
10
11
  :host {
11
12
  display: block;
12
13
  }
@@ -205,7 +206,7 @@ export class DatePicker extends FormElement {
205
206
  this.shadowRoot.querySelector('input').focus();
206
207
  }
207
208
 
208
- public render(): TemplateResult {
209
+ protected renderWidget(): TemplateResult {
209
210
  const classes = getClasses({ unset: !this.value });
210
211
 
211
212
  let dateWidgetValue = null;
@@ -216,42 +217,35 @@ export class DatePicker extends FormElement {
216
217
  }
217
218
 
218
219
  return html`
219
- <temba-field
220
- class=${getClasses({ disabled: this.disabled })}
221
- name=${this.name}
222
- .label="${this.label}"
223
- .helpText="${this.helpText}"
224
- .errors=${this.errors}
225
- .widgetOnly=${this.widgetOnly}
226
- .hideLabel=${this.hideLabel}
227
- .disabled=${this.disabled}
228
- >
229
- <div class="container" @click=${this.handleClicked}>
230
- <slot name="prefix"></slot>
231
- <div class="input-wrapper">
232
- <input
233
- class=${classes}
234
- name=${this.label}
235
- value=${dateWidgetValue}
236
- type="${this.time ? 'datetime-local' : 'date'}"
237
- @change=${this.handleChange}
238
- min=${this.min || undefined}
239
- max=${this.max || undefined}
240
- />
241
- </div>
242
- ${this.time
243
- ? html`
244
- <div class="tz-wrapper">
245
- <div class="tz">
246
- <div class="label">Time Zone</div>
247
- <div class="zone">${this.timezoneFriendly}</div>
248
- </div>
249
- </div>
250
- `
251
- : null}
252
- <slot name="postfix"></slot>
220
+ <div class="container" @click=${this.handleClicked}>
221
+ <slot name="prefix"></slot>
222
+ <div class="input-wrapper">
223
+ <input
224
+ class=${classes}
225
+ name=${this.label}
226
+ value=${dateWidgetValue}
227
+ type="${this.time ? 'datetime-local' : 'date'}"
228
+ @change=${this.handleChange}
229
+ min=${this.min || undefined}
230
+ max=${this.max || undefined}
231
+ />
253
232
  </div>
254
- </temba-field>
233
+ ${this.time
234
+ ? html`
235
+ <div class="tz-wrapper">
236
+ <div class="tz">
237
+ <div class="label">Time Zone</div>
238
+ <div class="zone">${this.timezoneFriendly}</div>
239
+ </div>
240
+ </div>
241
+ `
242
+ : null}
243
+ <slot name="postfix"></slot>
244
+ </div>
255
245
  `;
256
246
  }
247
+
248
+ public render(): TemplateResult {
249
+ return this.renderField();
250
+ }
257
251
  }
@@ -1,12 +1,65 @@
1
- import { TemplateResult, html, css, LitElement } from 'lit';
1
+ import { TemplateResult, html, css } from 'lit';
2
2
  import { property } from 'lit/decorators.js';
3
- import { renderMarkdown } from '../markdown';
3
+ import { RapidElement } from '../RapidElement';
4
+ import { renderMarkdownInline } from '../markdown';
4
5
 
5
6
  /**
6
- * A small wrapper to display labels and help text in a smartmin style.
7
- * This exists so we can display things consistently before restyling.
7
+ * FieldElement is a base class for form components that provides built-in
8
+ * field wrapper functionality, eliminating the need for manual temba-field embedding.
9
+ *
10
+ * Components extending this class only need to implement renderWidget() with their
11
+ * specific widget content, and the field wrapper (label, errors, help text) is
12
+ * automatically handled.
8
13
  */
9
- export class FormField extends LitElement {
14
+ export abstract class FieldElement extends RapidElement {
15
+ @property({ type: String })
16
+ name = '';
17
+
18
+ @property({ type: String, attribute: 'help_text' })
19
+ helpText: string;
20
+
21
+ @property({ type: Boolean, attribute: 'widget_only' })
22
+ widgetOnly: boolean;
23
+
24
+ @property({ type: Array })
25
+ errors: string[];
26
+
27
+ // Use @property with custom getter/setter to handle both attribute and programmatic access
28
+ private _value: any = '';
29
+
30
+ @property({ type: String })
31
+ public get value() {
32
+ return this._value;
33
+ }
34
+
35
+ public set value(value) {
36
+ this._value = value;
37
+ }
38
+
39
+ @property({ attribute: false })
40
+ inputRoot: HTMLElement = this;
41
+
42
+ @property({ type: Boolean })
43
+ disabled = false;
44
+
45
+ static formAssociated = true;
46
+
47
+ protected internals: ElementInternals;
48
+
49
+ @property({ type: Boolean })
50
+ hideErrors = false;
51
+
52
+ @property({ type: Boolean, attribute: 'hide_label' })
53
+ hideLabel: boolean;
54
+
55
+ @property({ type: String })
56
+ label: string;
57
+
58
+ constructor() {
59
+ super();
60
+ this.internals = this.attachInternals();
61
+ }
62
+
10
63
  static get styles() {
11
64
  return css`
12
65
  :host {
@@ -29,19 +82,6 @@ export class FormField extends LitElement {
29
82
  line-height: normal;
30
83
  color: var(--color-text-help);
31
84
  margin-left: var(--help-text-margin-left);
32
- margin-top: -16px;
33
- opacity: 0;
34
- transition: opacity ease-in-out 100ms, margin-top ease-in-out 200ms;
35
- pointer-events: none;
36
- }
37
-
38
- .help-text.help-always {
39
- opacity: 1;
40
- margin-top: 6px;
41
- margin-left: var(--help-text-margin-left);
42
- }
43
-
44
- .field:focus-within .help-text {
45
85
  margin-top: 6px;
46
86
  opacity: 1;
47
87
  }
@@ -151,36 +191,13 @@ export class FormField extends LitElement {
151
191
  `;
152
192
  }
153
193
 
154
- @property({ type: Boolean, attribute: 'hide_label' })
155
- hideLabel = false;
156
-
157
- @property({ type: Boolean, attribute: 'widget_only' })
158
- widgetOnly = false;
159
-
160
- @property({ type: Array, attribute: false })
161
- errors: string[] = [];
162
-
163
- @property({ type: Boolean })
164
- hideErrors = false;
165
-
166
- @property({ type: String, attribute: 'help_text' })
167
- helpText = '';
168
-
169
- @property({ type: Boolean, attribute: 'help_always' })
170
- helpAlways = true;
171
-
172
- @property({ type: String })
173
- label = '';
174
-
175
- @property({ type: String })
176
- name = '';
177
-
178
- @property({ type: Boolean })
179
- disabled = false;
180
-
181
- updated(changedProperties: Map<string | number | symbol, unknown>): void {
194
+ updated(changedProperties: Map<string, any>): void {
182
195
  super.updated(changedProperties);
183
196
 
197
+ if (changedProperties.has('value')) {
198
+ this.internals.setFormValue(this.value);
199
+ }
200
+
184
201
  if (
185
202
  changedProperties.has('errors') ||
186
203
  changedProperties.has('hideErrors')
@@ -191,19 +208,50 @@ export class FormField extends LitElement {
191
208
  }
192
209
  }
193
210
 
194
- public render(): TemplateResult {
211
+ get form() {
212
+ return this.internals.form;
213
+ }
214
+
215
+ public setValue(value: any) {
216
+ this.value = this.serializeValue(value);
217
+ }
218
+
219
+ public getDeserializedValue(): any {
220
+ if (!this.value || this.value === '') {
221
+ return null;
222
+ }
223
+ return JSON.parse(this.value);
224
+ }
225
+
226
+ public serializeValue(value: any): string {
227
+ return JSON.stringify(value);
228
+ }
229
+
230
+ /**
231
+ * Abstract method that components must implement to render their specific widget content.
232
+ * This replaces the need to manually embed temba-field.
233
+ */
234
+ protected abstract renderWidget(): TemplateResult;
235
+
236
+ /**
237
+ * Renders the complete field including label, widget, errors, and help text.
238
+ * Components can override this for custom layouts, but typically should just implement renderWidget().
239
+ */
240
+ protected renderField(): TemplateResult {
195
241
  const hasErrors = !this.hideErrors && this.errors && this.errors.length > 0;
196
242
  const errors = hasErrors
197
243
  ? this.errors.map((error: string) => {
198
244
  return html`
199
- <div class="alert-error">${renderMarkdown(error)}</div>
245
+ <div class="alert-error">${renderMarkdownInline(error)}</div>
200
246
  `;
201
247
  })
202
248
  : [];
203
249
 
204
250
  if (this.widgetOnly) {
205
251
  return html`
206
- <div class="${this.disabled ? 'disabled' : ''}"><slot></slot></div>
252
+ <div class="${this.disabled ? 'disabled' : ''}">
253
+ ${this.renderWidget()}
254
+ </div>
207
255
  ${errors}
208
256
  `;
209
257
  }
@@ -221,18 +269,23 @@ export class FormField extends LitElement {
221
269
  >
222
270
  `
223
271
  : null}
224
- <div class="widget">
225
- <slot></slot>
226
- ${errors}
227
- </div>
272
+ <div class="widget">${this.renderWidget()} ${errors}</div>
228
273
  ${this.helpText && this.helpText !== 'None'
229
274
  ? html`
230
- <div class="help-text ${this.helpAlways ? 'help-always' : null}">
231
- ${this.helpText}
275
+ <div class="help-text">
276
+ ${renderMarkdownInline(this.helpText)}
232
277
  </div>
233
278
  `
234
279
  : null}
235
280
  </div>
236
281
  `;
237
282
  }
283
+
284
+ /**
285
+ * Main render method that automatically provides field wrapper functionality.
286
+ * Components extending FieldElement should not override this unless they need custom field layouts.
287
+ */
288
+ public render(): TemplateResult {
289
+ return this.renderField();
290
+ }
238
291
  }