@meistrari/tela-build 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. package/README.md +75 -0
  2. package/app.config.ts +73 -0
  3. package/components/tela/animated/animated-calculating-number.vue +16 -0
  4. package/components/tela/animated/animated-number.mdx +248 -0
  5. package/components/tela/animated/animated-number.stories.ts +52 -0
  6. package/components/tela/animated/animated-number.vue +23 -0
  7. package/components/tela/animated/animated-text.vue +124 -0
  8. package/components/tela/animated/animated-value.vue +68 -0
  9. package/components/tela/avatar/avatar.mdx +117 -0
  10. package/components/tela/avatar/avatar.stories.ts +62 -0
  11. package/components/tela/avatar/avatar.vue +71 -0
  12. package/components/tela/avatar/group/avatar-group.stories.ts +78 -0
  13. package/components/tela/avatar/group/avatar-group.vue +46 -0
  14. package/components/tela/badge/badge.mdx +154 -0
  15. package/components/tela/badge/badge.stories.ts +82 -0
  16. package/components/tela/badge/badge.vue +41 -0
  17. package/components/tela/button/button.mdx +155 -0
  18. package/components/tela/button/button.stories.ts +202 -0
  19. package/components/tela/button/button.vue +107 -0
  20. package/components/tela/card.vue +30 -0
  21. package/components/tela/chart/chart-bar.vue +58 -0
  22. package/components/tela/chat/chat.mdx +268 -0
  23. package/components/tela/chat/chat.stories.ts +253 -0
  24. package/components/tela/chat/command/index.vue +41 -0
  25. package/components/tela/chat/command/mention/index.vue +138 -0
  26. package/components/tela/chat/index.vue +112 -0
  27. package/components/tela/chat/pure-text-input/chat-text-input.vue +190 -0
  28. package/components/tela/chat/text-input/chat-text-input.stories.ts +128 -0
  29. package/components/tela/chat/text-input/index.vue +217 -0
  30. package/components/tela/chat/text-message/chat-text-message.stories.ts +138 -0
  31. package/components/tela/chat/text-message/index.vue +355 -0
  32. package/components/tela/chat/types.ts +19 -0
  33. package/components/tela/checkbox/checkbox-card.vue +30 -0
  34. package/components/tela/checkbox/checkbox.mdx +164 -0
  35. package/components/tela/checkbox/checkbox.stories.ts +104 -0
  36. package/components/tela/checkbox/checkbox.vue +43 -0
  37. package/components/tela/collapsible/Collapsible.vue +15 -0
  38. package/components/tela/collapsible/CollapsibleContent.vue +59 -0
  39. package/components/tela/collapsible/CollapsibleTrigger.vue +12 -0
  40. package/components/tela/collapsible/collapsible.mdx +157 -0
  41. package/components/tela/collapsible-section/collapsible-section.mdx +180 -0
  42. package/components/tela/collapsible-section/collapsible-section.stories.ts +53 -0
  43. package/components/tela/collapsible-section/collapsible-section.vue +51 -0
  44. package/components/tela/collapsible-section-with-actions.vue +98 -0
  45. package/components/tela/combobox/combobox-anchor.vue +24 -0
  46. package/components/tela/combobox/combobox-empty.vue +19 -0
  47. package/components/tela/combobox/combobox-group.vue +24 -0
  48. package/components/tela/combobox/combobox-indicator.vue +22 -0
  49. package/components/tela/combobox/combobox-input.vue +31 -0
  50. package/components/tela/combobox/combobox-item.vue +28 -0
  51. package/components/tela/combobox/combobox-label.vue +24 -0
  52. package/components/tela/combobox/combobox-list.vue +90 -0
  53. package/components/tela/combobox/combobox-module-selector.vue +366 -0
  54. package/components/tela/combobox/combobox-root.vue +15 -0
  55. package/components/tela/combobox/combobox-trigger.vue +12 -0
  56. package/components/tela/combobox/combobox.mdx +285 -0
  57. package/components/tela/combobox/combobox.stories.ts +232 -0
  58. package/components/tela/combobox/combobox.vue +497 -0
  59. package/components/tela/command/command-dialog.vue +22 -0
  60. package/components/tela/command/command-empty.vue +25 -0
  61. package/components/tela/command/command-group.vue +46 -0
  62. package/components/tela/command/command-input.vue +38 -0
  63. package/components/tela/command/command-item.vue +78 -0
  64. package/components/tela/command/command-list.vue +78 -0
  65. package/components/tela/command/command-separator.vue +23 -0
  66. package/components/tela/command/command-shortcut.vue +13 -0
  67. package/components/tela/command/command.vue +88 -0
  68. package/components/tela/command/dialog-base.vue +15 -0
  69. package/components/tela/command/dialog-content.vue +50 -0
  70. package/components/tela/command/utils.ts +15 -0
  71. package/components/tela/complex-table/complex-table-cell.stories.ts +145 -0
  72. package/components/tela/complex-table/complex-table-cell.vue +45 -0
  73. package/components/tela/complex-table/complex-table-header-cell.stories.ts +103 -0
  74. package/components/tela/complex-table/complex-table-header-cell.vue +48 -0
  75. package/components/tela/complex-table/complex-table-header.stories.ts +89 -0
  76. package/components/tela/complex-table/complex-table-header.vue +70 -0
  77. package/components/tela/complex-table/complex-table-row.vue +199 -0
  78. package/components/tela/complex-table/complex-table-virtualized.vue +326 -0
  79. package/components/tela/complex-table/complex-table.stories.ts +358 -0
  80. package/components/tela/complex-table/complex-table.vue +237 -0
  81. package/components/tela/complex-table/composables/table-common.ts +93 -0
  82. package/components/tela/complex-table/composables/table-selection.ts +87 -0
  83. package/components/tela/complex-table/composables/virtual-scroll.ts +252 -0
  84. package/components/tela/complex-table/styles/table-shared.css +170 -0
  85. package/components/tela/complex-table/types.ts +63 -0
  86. package/components/tela/complex-table/utils.ts +35 -0
  87. package/components/tela/confirm-button/confirm-button.vue +137 -0
  88. package/components/tela/confirmation-modal/confirmation-modal.vue +72 -0
  89. package/components/tela/copy-button.vue +86 -0
  90. package/components/tela/date-range-picker.vue +221 -0
  91. package/components/tela/dialog/dialog.mdx +170 -0
  92. package/components/tela/dialog/dialog.vue +182 -0
  93. package/components/tela/disabled-area.vue +16 -0
  94. package/components/tela/disclaimer/disclaimer.mdx +238 -0
  95. package/components/tela/disclaimer/disclaimer.stories.ts +196 -0
  96. package/components/tela/disclaimer/disclaimer.vue +125 -0
  97. package/components/tela/dropdown-menu/DropdownMenu.vue +121 -0
  98. package/components/tela/dropdown-menu/DropdownMenuCheckboxItem.vue +40 -0
  99. package/components/tela/dropdown-menu/DropdownMenuContent.vue +75 -0
  100. package/components/tela/dropdown-menu/DropdownMenuGroup.vue +12 -0
  101. package/components/tela/dropdown-menu/DropdownMenuItem.vue +137 -0
  102. package/components/tela/dropdown-menu/DropdownMenuLabel.vue +26 -0
  103. package/components/tela/dropdown-menu/DropdownMenuRadioGroup.vue +18 -0
  104. package/components/tela/dropdown-menu/DropdownMenuRadioItem.vue +40 -0
  105. package/components/tela/dropdown-menu/DropdownMenuRoot.vue +15 -0
  106. package/components/tela/dropdown-menu/DropdownMenuSeparator.vue +21 -0
  107. package/components/tela/dropdown-menu/DropdownMenuShortcut.vue +14 -0
  108. package/components/tela/dropdown-menu/DropdownMenuSub.vue +18 -0
  109. package/components/tela/dropdown-menu/DropdownMenuSubContent.vue +30 -0
  110. package/components/tela/dropdown-menu/DropdownMenuSubTrigger.vue +35 -0
  111. package/components/tela/dropdown-menu/DropdownMenuTrigger.vue +14 -0
  112. package/components/tela/dropdown-menu/dropdown-menu.mdx +265 -0
  113. package/components/tela/dropdown-menu/dropdown-menu.stories.ts +156 -0
  114. package/components/tela/expandable-input.vue +96 -0
  115. package/components/tela/file-drop.vue +37 -0
  116. package/components/tela/file-upload/file-upload.mdx +189 -0
  117. package/components/tela/file-upload/file-upload.stories.ts +48 -0
  118. package/components/tela/file-upload/file-upload.vue +205 -0
  119. package/components/tela/filters/checkbox-filter.stories.ts +218 -0
  120. package/components/tela/filters/checkbox-filter.vue +165 -0
  121. package/components/tela/filters/date-filter.stories.ts +258 -0
  122. package/components/tela/filters/date-filter.vue +200 -0
  123. package/components/tela/filters/user-filter.stories.ts +344 -0
  124. package/components/tela/filters/user-filter.vue +271 -0
  125. package/components/tela/hover-card/hover-card.mdx +221 -0
  126. package/components/tela/hover-card/hover-card.stories.ts +87 -0
  127. package/components/tela/hover-card/hover-card.vue +61 -0
  128. package/components/tela/icon/custom.vue +319 -0
  129. package/components/tela/icon/spinner.vue +12 -0
  130. package/components/tela/icon-button/icon-button.vue +114 -0
  131. package/components/tela/icon.vue +37 -0
  132. package/components/tela/initials.vue +28 -0
  133. package/components/tela/inline-input.vue +77 -0
  134. package/components/tela/input/input.mdx +182 -0
  135. package/components/tela/input/input.stories.ts +153 -0
  136. package/components/tela/input/tela-input.vue +240 -0
  137. package/components/tela/kbd/kbd-return.vue +6 -0
  138. package/components/tela/kbd/kbd.mdx +238 -0
  139. package/components/tela/kbd/kbd.vue +18 -0
  140. package/components/tela/label/label.mdx +121 -0
  141. package/components/tela/label/label.stories.ts +37 -0
  142. package/components/tela/label/label.vue +25 -0
  143. package/components/tela/link-decoration/link-decoration.vue +19 -0
  144. package/components/tela/live-label.vue +32 -0
  145. package/components/tela/long-press-button.vue +98 -0
  146. package/components/tela/menubar/menubar-content.vue +77 -0
  147. package/components/tela/menubar/menubar-item.vue +32 -0
  148. package/components/tela/menubar/menubar-label.vue +14 -0
  149. package/components/tela/menubar/menubar-menu.vue +12 -0
  150. package/components/tela/menubar/menubar-root.vue +30 -0
  151. package/components/tela/menubar/menubar-separator.vue +17 -0
  152. package/components/tela/menubar/menubar-shortcut.vue +14 -0
  153. package/components/tela/menubar/menubar-sub-content.vue +36 -0
  154. package/components/tela/menubar/menubar-sub-trigger.vue +28 -0
  155. package/components/tela/menubar/menubar-sub.vue +20 -0
  156. package/components/tela/menubar/menubar-trigger.vue +27 -0
  157. package/components/tela/menubar/menubar.vue +298 -0
  158. package/components/tela/modal/modal.mdx +145 -0
  159. package/components/tela/modal/modal.vue +242 -0
  160. package/components/tela/multiple-select/multiple-select.mdx +274 -0
  161. package/components/tela/multiple-select/multiple-select.stories.ts +325 -0
  162. package/components/tela/multiple-select/multiple-select.vue +666 -0
  163. package/components/tela/pane.vue +110 -0
  164. package/components/tela/popover/popover-content.vue +48 -0
  165. package/components/tela/popover/popover-trigger.vue +12 -0
  166. package/components/tela/popover/popover.mdx +239 -0
  167. package/components/tela/popover/popover.stories.ts +150 -0
  168. package/components/tela/popover/popover.vue +15 -0
  169. package/components/tela/popover-list/popover-list-nested.vue +104 -0
  170. package/components/tela/popover-list/popover-list.stories.ts +330 -0
  171. package/components/tela/popover-list/popover-list.vue +191 -0
  172. package/components/tela/radio-button.vue +66 -0
  173. package/components/tela/radio-group/radio-group-item.vue +40 -0
  174. package/components/tela/radio-group/radio-group-root.vue +26 -0
  175. package/components/tela/radio-group/radio-group.mdx +78 -0
  176. package/components/tela/radio-group/radio-group.stories.ts +106 -0
  177. package/components/tela/radio-group/radio-group.vue +23 -0
  178. package/components/tela/range-calendar.stories.ts +110 -0
  179. package/components/tela/range-calendar.vue +109 -0
  180. package/components/tela/scroll-area/scroll-area.mdx +183 -0
  181. package/components/tela/scroll-area/scroll-area.vue +30 -0
  182. package/components/tela/scroll-area/scroll-bar.vue +31 -0
  183. package/components/tela/segment-toggle.stories.ts +114 -0
  184. package/components/tela/segment-toggle.vue +66 -0
  185. package/components/tela/select-menu/select-menu-content.vue +106 -0
  186. package/components/tela/select-menu/select-menu-down-button.vue +20 -0
  187. package/components/tela/select-menu/select-menu-group.vue +16 -0
  188. package/components/tela/select-menu/select-menu-item.vue +40 -0
  189. package/components/tela/select-menu/select-menu-root.vue +15 -0
  190. package/components/tela/select-menu/select-menu-trigger.vue +34 -0
  191. package/components/tela/select-menu/select-menu-up-button.vue +20 -0
  192. package/components/tela/select-menu/select-menu-value.vue +12 -0
  193. package/components/tela/select-menu/select-menu.mdx +221 -0
  194. package/components/tela/select-menu/select-menu.stories.ts +91 -0
  195. package/components/tela/select-menu/select-menu.vue +165 -0
  196. package/components/tela/selector/selector.vue +47 -0
  197. package/components/tela/sheet/sheet-close.vue +12 -0
  198. package/components/tela/sheet/sheet-content.vue +57 -0
  199. package/components/tela/sheet/sheet-description.vue +23 -0
  200. package/components/tela/sheet/sheet-footer.vue +18 -0
  201. package/components/tela/sheet/sheet-header.vue +15 -0
  202. package/components/tela/sheet/sheet-root.vue +18 -0
  203. package/components/tela/sheet/sheet-title.vue +23 -0
  204. package/components/tela/sheet/sheet-trigger.vue +12 -0
  205. package/components/tela/sheet/sheet.client.vue +150 -0
  206. package/components/tela/sheet/sheet.mdx +176 -0
  207. package/components/tela/sheet/sheet.stories.ts +201 -0
  208. package/components/tela/sheet/variants.ts +22 -0
  209. package/components/tela/side-sheet/side-sheet.mdx +131 -0
  210. package/components/tela/side-sheet/side-sheet.stories.ts +134 -0
  211. package/components/tela/side-sheet/side-sheet.vue +106 -0
  212. package/components/tela/skeleton/skeleton.mdx +165 -0
  213. package/components/tela/skeleton/skeleton.stories.ts +35 -0
  214. package/components/tela/skeleton/skeleton.vue +45 -0
  215. package/components/tela/skeleton-icon.vue +24 -0
  216. package/components/tela/span.vue +24 -0
  217. package/components/tela/star-button.vue +70 -0
  218. package/components/tela/status/status-lean.vue +30 -0
  219. package/components/tela/status/status.mdx +187 -0
  220. package/components/tela/status/status.stories.ts +160 -0
  221. package/components/tela/status/status.vue +420 -0
  222. package/components/tela/status-bar/status-bar.mdx +178 -0
  223. package/components/tela/status-bar/status-bar.stories.ts +64 -0
  224. package/components/tela/status-bar/status-bar.vue +56 -0
  225. package/components/tela/status-bar/types.ts +5 -0
  226. package/components/tela/switch/switch.mdx +118 -0
  227. package/components/tela/switch/switch.stories.ts +80 -0
  228. package/components/tela/switch/switch.vue +56 -0
  229. package/components/tela/table/table-body.vue +13 -0
  230. package/components/tela/table/table-caption.vue +13 -0
  231. package/components/tela/table/table-cell.vue +20 -0
  232. package/components/tela/table/table-empty.vue +37 -0
  233. package/components/tela/table/table-footer.vue +13 -0
  234. package/components/tela/table/table-head.vue +13 -0
  235. package/components/tela/table/table-header.vue +13 -0
  236. package/components/tela/table/table-row.vue +13 -0
  237. package/components/tela/table/table.mdx +230 -0
  238. package/components/tela/table/table.stories.ts +384 -0
  239. package/components/tela/table/table.vue +15 -0
  240. package/components/tela/tabs/tabs-content.vue +20 -0
  241. package/components/tela/tabs/tabs-indicator.vue +22 -0
  242. package/components/tela/tabs/tabs-list.vue +23 -0
  243. package/components/tela/tabs/tabs-root.vue +15 -0
  244. package/components/tela/tabs/tabs-trigger.vue +27 -0
  245. package/components/tela/tabs/tabs.mdx +138 -0
  246. package/components/tela/tabs/tabs.stories.ts +72 -0
  247. package/components/tela/tabs/tabs.vue +61 -0
  248. package/components/tela/tags/tags-select.mdx +318 -0
  249. package/components/tela/tags/tags-select.stories.ts +47 -0
  250. package/components/tela/tags/tags-select.vue +637 -0
  251. package/components/tela/tags/tags.mdx +151 -0
  252. package/components/tela/tags/tags.stories.ts +118 -0
  253. package/components/tela/tags/tags.vue +112 -0
  254. package/components/tela/textarea/textarea.mdx +102 -0
  255. package/components/tela/textarea/textarea.stories.ts +50 -0
  256. package/components/tela/textarea/textarea.vue +34 -0
  257. package/components/tela/toggle-group.vue +91 -0
  258. package/components/tela/tooltip/tooltip-content.vue +45 -0
  259. package/components/tela/tooltip/tooltip-provider.vue +12 -0
  260. package/components/tela/tooltip/tooltip-root.vue +15 -0
  261. package/components/tela/tooltip/tooltip-trigger.vue +12 -0
  262. package/components/tela/tooltip/tooltip.mdx +196 -0
  263. package/components/tela/tooltip/tooltip.stories.ts +200 -0
  264. package/components/tela/tooltip/tooltip.vue +91 -0
  265. package/components/tela/tooltip-group/tooltip-group-trigger.vue +92 -0
  266. package/components/tela/tooltip-group/tooltip-group.mdx +236 -0
  267. package/components/tela/tooltip-group/tooltip-group.stories.ts +465 -0
  268. package/components/tela/tooltip-group/tooltip-group.vue +35 -0
  269. package/components/tela/transparent-input.vue +151 -0
  270. package/components/tela/variable-icon.vue +28 -0
  271. package/components/tela/variable-input.vue +77 -0
  272. package/components/tela/wide-button/wide-button.vue +40 -0
  273. package/components.json +18 -0
  274. package/composables/status-toast.ts +67 -0
  275. package/css/reset.css +386 -0
  276. package/css/text.css +22 -0
  277. package/lib/doc-generator.ts +903 -0
  278. package/lib/extractors/volar-extract.ts +186 -0
  279. package/lib/type-resolver.ts +402 -0
  280. package/lib/utils.ts +6 -0
  281. package/modules/tela-build-docs/index.ts +139 -0
  282. package/nuxt.config.ts +80 -0
  283. package/package.json +84 -0
  284. package/plugins/test-id.ts +7 -0
  285. package/tsconfig.json +7 -0
  286. package/types/custom-icon.ts +1 -0
  287. package/types/index.ts +2 -0
  288. package/types/status.ts +1 -0
  289. package/unocss.config.ts +89 -0
  290. package/utils/component-utils.ts +30 -0
  291. package/utils/design-tokens.ts +431 -0
  292. package/utils/fold.ts +8 -0
  293. package/utils/select-menu.ts +10 -0
  294. package/utils/status.ts +1 -0
  295. package/utils/without-keys.ts +34 -0
@@ -0,0 +1,112 @@
1
+ <script setup lang="ts">
2
+ import { ref, onMounted } from 'vue'
3
+ import { TreeRoot } from 'radix-vue'
4
+ import ChatTextMessage from './text-message/index.vue'
5
+ import ChatTextInput from './text-input/index.vue'
6
+ import type { Message, User } from './types'
7
+
8
+ const props = defineProps<{
9
+ messages: Message[]
10
+ currentUser: User
11
+ otherUsers: User[]
12
+ loading?: boolean
13
+ allowReply?: boolean
14
+ }>()
15
+
16
+ const emit = defineEmits<{
17
+ (e: 'delete', id: string): void
18
+ (e: 'update', message: Message): void
19
+ (e: 'add', message: Message): void
20
+ (e: 'reply', message: Message): void
21
+ }>()
22
+
23
+ const expandedValues = ref<string[]>([])
24
+ const replyingTo = ref<Message | null>(null)
25
+
26
+ function handleSendMessage(content: string) {
27
+ const newMessage: Message = {
28
+ id: crypto.randomUUID(),
29
+ content,
30
+ timestamp: new Date(),
31
+ author: props.currentUser.name,
32
+ authorEmail: props.currentUser.email,
33
+ isReply: replyingTo.value !== null,
34
+ parentId: replyingTo.value?.id ?? undefined,
35
+ avatarUrl: props.currentUser.avatarUrl,
36
+ }
37
+ emit('add', newMessage)
38
+ replyingTo.value = null
39
+ }
40
+
41
+ const chatTextInputRef = useTemplateRef('chat-text-input')
42
+
43
+ function handleReply(message: Message) {
44
+ expandedValues.value.push(message.id)
45
+ replyingTo.value = message
46
+ chatTextInputRef.value?.$el.querySelector('textarea')?.focus()
47
+ }
48
+
49
+ onMounted(() => {
50
+ // Initialize all top-level messages as expanded
51
+ expandedValues.value = props.messages.map(msg => msg.id)
52
+ })
53
+ </script>
54
+
55
+ <template>
56
+ <div flex="~ col" bg-white rounded-lg shadow-lg>
57
+ <div v-if="messages.length > 0 && !loading" flex-1 flex-col overflow-y-auto p-24px gap-6>
58
+ <TreeRoot
59
+ v-model:expanded="expandedValues"
60
+ orientation="vertical"
61
+ :value="expandedValues"
62
+ :get-key="(value: Record<string, any>) => value.id"
63
+ class="w-full"
64
+ >
65
+ <ChatTextMessage
66
+ v-for="message in messages"
67
+ :id="message.id"
68
+ :key="message.id"
69
+ :content="message.content"
70
+ :timestamp="message.timestamp"
71
+ :author="message.author"
72
+ :author-email="message.authorEmail"
73
+ :avatar-url="message.avatarUrl"
74
+ :depth="1"
75
+ :replies="message.replies"
76
+ :allow-reply="props.allowReply"
77
+ :current-user="props.currentUser"
78
+ :other-users="props.otherUsers"
79
+ :is-chat="true"
80
+ :is-expanded="expandedValues.some(value => value === message.id)"
81
+ @reply="() => handleReply(message)"
82
+ @delete="emit('delete', $event)"
83
+ />
84
+ </TreeRoot>
85
+ </div>
86
+ <div v-else flex justify-center items-center flex-1 flex-col>
87
+ <slot v-if="loading" name="loading">
88
+ <div h-500px flex justify-center items-center>
89
+ <TelaIconSpinner class="h-24px w-24px" animate-spin />
90
+ </div>
91
+ </slot>
92
+ <slot v-else-if="!messages.length" name="empty">
93
+ <div flex flex-col justify-center items-center h-full h-500px>
94
+ <span body-16-semibold text-gray-700 text-center>No messages yet</span>
95
+ <span body-14-regular text-gray-600 text-center>Start a conversation by sending a message.</span>
96
+ </div>
97
+ </slot>
98
+ </div>
99
+
100
+ <div p-16px b-t="0.5px gray-200">
101
+ <ChatTextInput
102
+ ref="chat-text-input"
103
+ :message-count="messages.length"
104
+ :current-user="props.currentUser"
105
+ :other-users="props.otherUsers"
106
+ :replying-to="replyingTo ? { content: replyingTo.content, author: replyingTo.author } : undefined"
107
+ @send-message="handleSendMessage"
108
+ @cancel-reply="() => replyingTo = null"
109
+ />
110
+ </div>
111
+ </div>
112
+ </template>
@@ -0,0 +1,190 @@
1
+ <script setup lang="ts">
2
+ import { ref, watch, withDefaults } from 'vue'
3
+ import type { User } from '../types'
4
+
5
+ interface ChatInputProps {
6
+ replyingTo?: {
7
+ content: string
8
+ author: string
9
+ }
10
+ currentUser: User
11
+ otherUsers: User[]
12
+ placeholder?: string
13
+ messageCount?: number
14
+ }
15
+
16
+ const props = withDefaults(defineProps<ChatInputProps>(), {
17
+ placeholder: 'Leave a note',
18
+ })
19
+
20
+ const emit = defineEmits<{
21
+ sendMessage: [message: string]
22
+ cancelReply: []
23
+ }>()
24
+
25
+ const localPlaceholder = ref(props.placeholder)
26
+ const message = ref('')
27
+ const displayMessage = ref('')
28
+ const isSending = ref(false)
29
+ const shouldShowCheckIcon = ref(false)
30
+ const showMentionModal = ref(false)
31
+ const cursorPosition = ref(0)
32
+ const mentions = ref<Array<{ start: number; email: string; name: string }>>([])
33
+
34
+ function handleInput(e: Event) {
35
+ const target = e.target as HTMLTextAreaElement
36
+ cursorPosition.value = target.selectionStart || 0
37
+ displayMessage.value = target.value
38
+
39
+ // Reconstruct message with emails
40
+ let finalMessage = displayMessage.value
41
+ mentions.value.sort((a, b) => b.start - a.start).forEach((mention) => {
42
+ finalMessage = finalMessage.slice(0, mention.start) + mention.email + finalMessage.slice(mention.start + (mention.name?.length ?? 0))
43
+ })
44
+ message.value = finalMessage
45
+
46
+ // Check if @ was just typed
47
+ if (target.value[cursorPosition.value - 1] === '@') {
48
+ showMentionModal.value = true
49
+ }
50
+ }
51
+
52
+ function insertMention(user: User) {
53
+ const beforeCursor = displayMessage.value.slice(0, cursorPosition.value)
54
+ const afterCursor = displayMessage.value.slice(cursorPosition.value)
55
+
56
+ // Store mention data
57
+ mentions.value.push({
58
+ start: cursorPosition.value,
59
+ email: user.email,
60
+ name: user.name,
61
+ })
62
+
63
+ // Update display message with name
64
+ displayMessage.value = `${beforeCursor}${user.name} ${afterCursor}`
65
+
66
+ // Reconstruct message with email
67
+ let finalMessage = displayMessage.value
68
+ mentions.value.sort((a, b) => b.start - a.start).forEach((mention) => {
69
+ finalMessage = finalMessage.slice(0, mention.start) + mention.email + finalMessage.slice(mention.start + (mention.name?.length ?? 0))
70
+ })
71
+ message.value = finalMessage
72
+
73
+ showMentionModal.value = false
74
+ }
75
+
76
+ async function handleSubmit(e: Event) {
77
+ e.preventDefault()
78
+ if (message.value.trim()) {
79
+ isSending.value = true
80
+ emit('sendMessage', message.value)
81
+ await new Promise(resolve => setTimeout(resolve, 500))
82
+ shouldShowCheckIcon.value = true
83
+ isSending.value = false
84
+ message.value = ''
85
+ displayMessage.value = ''
86
+ mentions.value = []
87
+ await new Promise(resolve => setTimeout(resolve, 1000))
88
+ shouldShowCheckIcon.value = false
89
+ }
90
+ }
91
+
92
+ function resolveContent(content: string) {
93
+ if (props.currentUser && props.otherUsers) {
94
+ return content.replace(/@([\w.-]+@[\w.-]+\.\w+)/g, (match, email) => {
95
+ const user = props.otherUsers?.find(user => user.email === email)
96
+ return user ? `@${user.name}` : match
97
+ })
98
+ }
99
+ return content
100
+ }
101
+
102
+ const { 'Meta+Enter': metaEnterKey } = useMagicKeys({
103
+ passive: false,
104
+ })
105
+
106
+ watch(() => props.messageCount, (count) => {
107
+ localPlaceholder.value = (count ?? 0) > 0 ? 'Leave another note' : 'Leave a note'
108
+ }, { immediate: true })
109
+
110
+ watch(metaEnterKey, (press) => {
111
+ if (press && message.value.trim())
112
+ handleSubmit(new Event('submit'))
113
+ })
114
+
115
+ watch(displayMessage, (value) => {
116
+ if (value === '') {
117
+ showMentionModal.value = false
118
+ }
119
+ })
120
+ </script>
121
+
122
+ <template>
123
+ <div
124
+ flex="~ col"
125
+ h-full
126
+ focus-within="b-1px b-gray-400 ring-2px ring-gray-200"
127
+ transition
128
+ rounded-8px
129
+ relative
130
+ @submit.prevent="handleSubmit"
131
+ >
132
+ <div v-if="replyingTo" mb-4px p-8px bg-gray-50 rounded-8px b="~ gray-200" body-12-regular relative>
133
+ <TelaButton
134
+ variant="ghost"
135
+ size="sm"
136
+ icon="i-ph-x-circle"
137
+ color="secondary"
138
+ absolute top-8px right-8px
139
+ @click="$emit('cancelReply')"
140
+ />
141
+ <div text-gray-600 mb-4px>
142
+ Replying to {{ replyingTo.author }}
143
+ </div>
144
+ <div text-gray-700 line-clamp-1>
145
+ {{ resolveContent(replyingTo.content) }}
146
+ </div>
147
+ </div>
148
+ <TelaChatMention
149
+ :members="otherUsers"
150
+ :is-open="showMentionModal && otherUsers.length > 0"
151
+ @close="showMentionModal = false"
152
+ @select="insertMention"
153
+ >
154
+ <template #trigger>
155
+ <TelaInput
156
+ v-model="displayMessage"
157
+ type="textarea"
158
+ w-full
159
+ :placeholder="localPlaceholder"
160
+ :disabled="isSending"
161
+ @input="handleInput"
162
+ >
163
+ <template v-if="message?.trim() || isSending" #trailing>
164
+ <TelaIconButton
165
+ v-if="isSending"
166
+ color="primary"
167
+ icon="i-ph-circle-notch-light"
168
+ animate-spin
169
+ :disabled="true"
170
+ />
171
+ <TelaIconButton
172
+ v-else
173
+ color="primary"
174
+ icon="i-ph-paper-plane-tilt"
175
+ :disabled="!message.trim()"
176
+ @click="handleSubmit"
177
+ />
178
+ </template>
179
+ <template v-else-if="shouldShowCheckIcon" #trailing>
180
+ <TelaIconButton
181
+ color="primary"
182
+ icon="i-ph-check"
183
+ :disabled="true"
184
+ />
185
+ </template>
186
+ </TelaInput>
187
+ </template>
188
+ </TelaChatMention>
189
+ </div>
190
+ </template>
@@ -0,0 +1,128 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3'
2
+ import { fn } from '@storybook/test'
3
+ import ChatInput from './index.vue'
4
+ import type { User } from '../types'
5
+
6
+ const meta: Meta<typeof ChatInput> = {
7
+ title: 'Components/Chat/TextInput',
8
+ component: ChatInput,
9
+ tags: ['autodocs'],
10
+ parameters: {
11
+ layout: 'centered',
12
+ docs: {
13
+ description: {
14
+ component: 'A chat text input component for sending messages in chat interfaces. Supports reply functionality, mentions, user autocomplete, and message sending. Integrates with chat systems to provide a complete messaging input experience.',
15
+ },
16
+ },
17
+ },
18
+ argTypes: {
19
+ replyingTo: {
20
+ control: 'object',
21
+ description: 'Message object being replied to. When provided, shows reply preview above input.',
22
+ },
23
+ placeholder: {
24
+ control: 'text',
25
+ description: 'Placeholder text displayed in the input field.',
26
+ },
27
+ currentUser: {
28
+ control: 'object',
29
+ description: 'Current user object with `id`, `name`, `email`, and optionally `avatarUrl` properties.',
30
+ },
31
+ otherUsers: {
32
+ control: 'object',
33
+ description: 'Array of other users for mention autocomplete functionality.',
34
+ },
35
+ onSendMessage: {
36
+ action: 'send-message',
37
+ description: 'Event handler called when a message is sent.',
38
+ },
39
+ onCancelReply: {
40
+ action: 'cancel-reply',
41
+ description: 'Event handler called when reply is cancelled.',
42
+ },
43
+ },
44
+ args: {
45
+ onSendMessage: fn(),
46
+ onCancelReply: fn(),
47
+ },
48
+ } satisfies Meta<typeof ChatInput>
49
+
50
+ export default meta
51
+ type Story = StoryObj<typeof meta>
52
+
53
+ const mockMembers: User[] = [
54
+ {
55
+ id: '1',
56
+ name: 'Jane Smith',
57
+ email: 'jane@example.com',
58
+ avatarUrl: 'https://example.com/avatar1.png',
59
+ },
60
+ {
61
+ id: '2',
62
+ name: 'John Doe',
63
+ email: 'john@example.com',
64
+ avatarUrl: 'https://example.com/avatar2.png',
65
+ },
66
+ ]
67
+
68
+ export const Default: Story = {
69
+ args: {
70
+ otherUsers: mockMembers,
71
+ currentUser: mockMembers[0],
72
+ },
73
+ }
74
+
75
+ export const WithReply: Story = {
76
+ args: {
77
+ replyingTo: {
78
+ content: 'Yep, this is still happening. @thierry can we review the parser?',
79
+ author: 'Guilherme Oliveira',
80
+ },
81
+ otherUsers: mockMembers,
82
+ currentUser: mockMembers[0],
83
+ },
84
+ }
85
+
86
+ export const WithCustomPlaceholder: Story = {
87
+ args: {
88
+ placeholder: 'Write a message...',
89
+ otherUsers: mockMembers,
90
+ currentUser: mockMembers[0],
91
+ },
92
+ }
93
+
94
+ export const WithLongReply: Story = {
95
+ args: {
96
+ replyingTo: {
97
+ content: 'I was expecting that the CPF field would come as expected, and not empty. The original file has it already so it\'s really odd that it didn\'t appear. We should investigate this further to understand why the parser is not picking up this field correctly.',
98
+ author: 'Lucas Siqueira',
99
+ },
100
+ otherUsers: mockMembers,
101
+ currentUser: mockMembers[0],
102
+ },
103
+ }
104
+
105
+ export const Focused: Story = {
106
+ args: {
107
+ otherUsers: mockMembers,
108
+ currentUser: mockMembers[0],
109
+ },
110
+ play: async ({ canvasElement }) => {
111
+ const input = canvasElement.querySelector('textarea')
112
+ input?.focus()
113
+ },
114
+ }
115
+
116
+ export const WithContent: Story = {
117
+ args: {
118
+ otherUsers: mockMembers,
119
+ currentUser: mockMembers[0],
120
+ },
121
+ play: async ({ canvasElement }) => {
122
+ const input = canvasElement.querySelector('textarea')
123
+ if (input) {
124
+ input.value = 'This is a sample message that I\'m typing...'
125
+ input.dispatchEvent(new Event('input'))
126
+ }
127
+ },
128
+ }
@@ -0,0 +1,217 @@
1
+ <script setup lang="ts">
2
+ import { ref, watch, withDefaults } from 'vue'
3
+ import type { User } from '../types'
4
+
5
+ interface ChatInputProps {
6
+ replyingTo?: {
7
+ content: string
8
+ author: string
9
+ }
10
+ currentUser: User
11
+ otherUsers: User[]
12
+ placeholder?: string
13
+ messageCount?: number
14
+ }
15
+
16
+ const props = withDefaults(defineProps<ChatInputProps>(), {
17
+ placeholder: 'Leave a note',
18
+ })
19
+
20
+ const emit = defineEmits<{
21
+ sendMessage: [message: string]
22
+ cancelReply: []
23
+ }>()
24
+
25
+ const localPlaceholder = ref(props.placeholder)
26
+ const message = ref('')
27
+ const displayMessage = ref('')
28
+ const searchTerm = ref<string | null>(null)
29
+ const cursorPosition = ref(0)
30
+ const mentions = ref<Array<{ start: number; email: string; name: string }>>([])
31
+
32
+ const isSending = ref(false)
33
+ const shouldShowCheckIcon = ref(false)
34
+ const showMentionModal = ref(false)
35
+
36
+ function handleInput(e: Event) {
37
+ const target = e.target as HTMLTextAreaElement
38
+ cursorPosition.value = target.selectionStart || 0
39
+ displayMessage.value = target.value
40
+
41
+ // Check if cursor is right after @ or if there's an @ with text being typed after it
42
+ const textBeforeCursor = target.value.slice(0, cursorPosition.value)
43
+ const match = textBeforeCursor.match(/@(\S*)$/)
44
+ searchTerm.value = match?.[0] ?? null
45
+
46
+ // Clean up mentions that are no longer in the display message
47
+ mentions.value = mentions.value.filter(mention =>
48
+ displayMessage.value.includes(`@${mention.name}`),
49
+ )
50
+
51
+ // Reconstruct message with emails
52
+ let finalMessage = displayMessage.value
53
+ mentions.value.sort((a, b) => b.start - a.start).forEach((mention) => {
54
+ finalMessage = `${finalMessage.slice(0, mention.start)}@${mention.email}${finalMessage.slice(mention.start + mention.name.length + 1)}` // +1 for @
55
+ })
56
+ message.value = finalMessage
57
+
58
+ showMentionModal.value = !!match
59
+ }
60
+
61
+ function insertMention(user: User) {
62
+ const beforeCursor = displayMessage.value.slice(0, cursorPosition.value)
63
+ const afterCursor = displayMessage.value.slice(cursorPosition.value)
64
+
65
+ // Find the @ position we're replacing
66
+ const lastAtIndex = beforeCursor.lastIndexOf('@')
67
+ if (lastAtIndex === -1)
68
+ return
69
+
70
+ // Store mention data with @ position
71
+ mentions.value.push({
72
+ start: lastAtIndex,
73
+ email: user.email,
74
+ name: user.name,
75
+ })
76
+
77
+ // Update display message with @name
78
+ displayMessage.value = `${beforeCursor.slice(0, lastAtIndex)}@${user.name} ${afterCursor}`
79
+
80
+ // Update cursor position after insertion
81
+ cursorPosition.value = lastAtIndex + user.name.length + 2 // +2 for @ and space
82
+
83
+ // Reconstruct message with email
84
+ let finalMessage = displayMessage.value
85
+ mentions.value.sort((a, b) => b.start - a.start).forEach((mention) => {
86
+ finalMessage = `${finalMessage.slice(0, mention.start)}@${mention.email}${finalMessage.slice(mention.start + mention.name.length + 1)}` // +1 for @
87
+ })
88
+ message.value = finalMessage
89
+
90
+ showMentionModal.value = false
91
+ }
92
+
93
+ async function handleSubmit(e: Event) {
94
+ e.preventDefault()
95
+ if (message.value.trim()) {
96
+ isSending.value = true
97
+ emit('sendMessage', message.value)
98
+ await new Promise(resolve => setTimeout(resolve, 500))
99
+ shouldShowCheckIcon.value = true
100
+ isSending.value = false
101
+ message.value = ''
102
+ displayMessage.value = ''
103
+ mentions.value = []
104
+ await new Promise(resolve => setTimeout(resolve, 1000))
105
+ shouldShowCheckIcon.value = false
106
+ }
107
+ }
108
+
109
+ function resolveContent(content: string) {
110
+ if (props.currentUser && props.otherUsers) {
111
+ return content.replace(/@([\w.-]+@[\w.-]+\.\w+)/g, (match, email) => {
112
+ const user = props.otherUsers?.find(user => user.email === email)
113
+ return user ? `@${user.name}` : match
114
+ })
115
+ }
116
+ return content
117
+ }
118
+
119
+ const { 'Meta+Enter': metaEnterKey } = useMagicKeys({
120
+ passive: false,
121
+ })
122
+
123
+ watch(() => props.messageCount, (count) => {
124
+ localPlaceholder.value = (count ?? 0) > 0 ? 'Leave another note' : 'Leave a note'
125
+ }, { immediate: true })
126
+
127
+ watch(metaEnterKey, (press) => {
128
+ if (press && message.value.trim())
129
+ handleSubmit(new Event('submit'))
130
+ })
131
+
132
+ watch(displayMessage, (value) => {
133
+ if (value === '') {
134
+ showMentionModal.value = false
135
+ }
136
+ })
137
+ </script>
138
+
139
+ <template>
140
+ <div
141
+ flex="~ col"
142
+ h-full
143
+ transition
144
+ rounded-8px
145
+ relative
146
+ >
147
+ <div
148
+ v-if="replyingTo" p="8px b-16px" bg-gray-200
149
+ mb--8px
150
+ b="t-0.5px x-0.5px gray-200"
151
+ rounded-t-8px body-12-regular relative
152
+ >
153
+ <TelaIconButton
154
+ variant="ghost"
155
+ size="xs"
156
+ icon="i-ph-x-circle"
157
+ color="secondary"
158
+ absolute top-6px right-6px
159
+ @click="$emit('cancelReply')"
160
+ />
161
+ <div text-gray-600 mb-4px>
162
+ Replying to {{ replyingTo.author }}
163
+ </div>
164
+ <div text-gray-700 line-clamp-1>
165
+ {{ resolveContent(replyingTo.content) }}
166
+ </div>
167
+ </div>
168
+ <TelaChatCommand
169
+ :commands="['@']"
170
+ :mention-props="{
171
+ members: props.otherUsers,
172
+ isOpen: showMentionModal,
173
+ onClose: () => showMentionModal = false,
174
+ onSelect: insertMention,
175
+ chatMessage: displayMessage,
176
+ searchTerm: searchTerm ?? '',
177
+ }"
178
+ >
179
+ <template #mention>
180
+ <TelaInput
181
+ v-model="displayMessage"
182
+ type="textarea"
183
+ w-full
184
+ input-font-class="body-14-medium text-gray-800 leading-20px"
185
+ :placeholder="localPlaceholder"
186
+ :disabled="isSending"
187
+ @input="handleInput"
188
+ @keydown.enter.prevent="handleSubmit"
189
+ >
190
+ <template v-if="message?.trim() || isSending" #trailing>
191
+ <TelaIconButton
192
+ v-if="isSending"
193
+ color="primary"
194
+ icon="i-ph-circle-notch-light"
195
+ animate-spin
196
+ :disabled="true"
197
+ />
198
+ <TelaIconButton
199
+ v-else
200
+ color="primary"
201
+ icon="i-ph-paper-plane-tilt"
202
+ :disabled="!message.trim()"
203
+ @click="handleSubmit"
204
+ />
205
+ </template>
206
+ <template v-else-if="shouldShowCheckIcon" #trailing>
207
+ <TelaIconButton
208
+ color="primary"
209
+ icon="i-ph-check"
210
+ :disabled="true"
211
+ />
212
+ </template>
213
+ </TelaInput>
214
+ </template>
215
+ </TelaChatCommand>
216
+ </div>
217
+ </template>