@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,344 @@
1
+ import { ref } from 'vue'
2
+ import type { Meta, StoryObj } from '@storybook/vue3'
3
+ import UserFilter from './user-filter.vue'
4
+
5
+ const meta: Meta<typeof UserFilter> = {
6
+ title: 'Filters/UserFilter',
7
+ component: UserFilter,
8
+ tags: ['autodocs'],
9
+ parameters: {
10
+ layout: 'centered',
11
+ docs: {
12
+ description: {
13
+ component: 'A dropdown filter component for selecting users. Features a search bar, scrollable user list with avatars, and displays selected user avatars in the button. Supports multi-select with visual overflow indicators.',
14
+ },
15
+ },
16
+ },
17
+ argTypes: {
18
+ buttonLabel: {
19
+ control: 'text',
20
+ description: 'The label displayed on the filter button',
21
+ },
22
+ users: {
23
+ control: 'object',
24
+ description: 'Array of user/member objects to display as options',
25
+ },
26
+ isActive: {
27
+ control: 'boolean',
28
+ description: 'Whether the filter is currently active (has users selected)',
29
+ },
30
+ disabled: {
31
+ control: 'boolean',
32
+ description: 'Whether the filter button is disabled',
33
+ },
34
+ loading: {
35
+ control: 'boolean',
36
+ description: 'Whether the filter is in a loading state',
37
+ },
38
+ },
39
+ }
40
+
41
+ export default meta
42
+
43
+ type Story = StoryObj<typeof meta>
44
+
45
+ // Mock Member[] type structure (matching Member type from @meistrari/auth-sdk/utils)
46
+ const mockUsers = [
47
+ {
48
+ id: '1',
49
+ userId: '1',
50
+ organizationId: 'org-1',
51
+ role: 'member',
52
+ createdAt: new Date(),
53
+ user: {
54
+ id: '1',
55
+ name: 'Alice Johnson',
56
+ email: 'alice@example.com',
57
+ image: 'https://i.pravatar.cc/150?img=1',
58
+ },
59
+ },
60
+ {
61
+ id: '2',
62
+ userId: '2',
63
+ organizationId: 'org-1',
64
+ role: 'member',
65
+ createdAt: new Date(),
66
+ user: {
67
+ id: '2',
68
+ name: 'Bob Smith',
69
+ email: 'bob@example.com',
70
+ image: 'https://i.pravatar.cc/150?img=2',
71
+ },
72
+ },
73
+ {
74
+ id: '3',
75
+ userId: '3',
76
+ organizationId: 'org-1',
77
+ role: 'member',
78
+ createdAt: new Date(),
79
+ user: {
80
+ id: '3',
81
+ name: 'Carol Williams',
82
+ email: 'carol@example.com',
83
+ image: 'https://i.pravatar.cc/150?img=3',
84
+ },
85
+ },
86
+ {
87
+ id: '4',
88
+ userId: '4',
89
+ organizationId: 'org-1',
90
+ role: 'member',
91
+ createdAt: new Date(),
92
+ user: {
93
+ id: '4',
94
+ name: 'David Brown',
95
+ email: 'david@example.com',
96
+ image: 'https://i.pravatar.cc/150?img=4',
97
+ },
98
+ },
99
+ {
100
+ id: '5',
101
+ userId: '5',
102
+ organizationId: 'org-1',
103
+ role: 'member',
104
+ createdAt: new Date(),
105
+ user: {
106
+ id: '5',
107
+ name: 'Eve Davis',
108
+ email: 'eve@example.com',
109
+ image: 'https://i.pravatar.cc/150?img=5',
110
+ },
111
+ },
112
+ ]
113
+
114
+ export const Default: Story = {
115
+ render: args => ({
116
+ components: { UserFilter },
117
+ setup() {
118
+ const selected = ref({ identifier: 'user', label: 'User', value: undefined })
119
+ const isActive = ref(false)
120
+
121
+ function handleApply(value: any) {
122
+ const users = value.user
123
+ const userNames = users.map((email: string) => {
124
+ const member = mockUsers.find(u => u.user.email === email)
125
+ return member?.user.name || email
126
+ })
127
+
128
+ const label = userNames.length > 2
129
+ ? `${userNames[0]} & ${userNames.length - 1} others`
130
+ : userNames.join(' & ')
131
+
132
+ selected.value = {
133
+ identifier: 'user',
134
+ label,
135
+ value: users,
136
+ }
137
+ isActive.value = true
138
+ }
139
+
140
+ function handleReset() {
141
+ selected.value = { identifier: 'user', label: 'User', value: undefined }
142
+ isActive.value = false
143
+ }
144
+
145
+ return { selected, isActive, handleApply, handleReset, args }
146
+ },
147
+ template: `
148
+ <div class="flex flex-col gap-4 p-4">
149
+ <UserFilter
150
+ v-bind="args"
151
+ :selected="selected"
152
+ :is-active="isActive"
153
+ @apply="handleApply"
154
+ @reset="handleReset"
155
+ />
156
+ </div>
157
+ `,
158
+ }),
159
+ args: {
160
+ buttonLabel: 'User',
161
+ users: mockUsers as any,
162
+ isActive: false,
163
+ disabled: false,
164
+ loading: false,
165
+ },
166
+ }
167
+
168
+ export const SingleUserSelected: Story = {
169
+ render: args => ({
170
+ components: { UserFilter },
171
+ setup() {
172
+ const selected = ref({ identifier: 'user', label: 'Alice Johnson', value: ['alice@example.com'] })
173
+ const isActive = ref(true)
174
+
175
+ function handleApply(value: any) {
176
+ const users = value.user
177
+ const userNames = users.map((email: string) => {
178
+ const member = mockUsers.find(u => u.user.email === email)
179
+ return member?.user.name || email
180
+ })
181
+
182
+ const label = userNames.length > 2
183
+ ? `${userNames[0]} & ${userNames.length - 1} others`
184
+ : userNames.join(' & ')
185
+
186
+ selected.value = {
187
+ identifier: 'user',
188
+ label,
189
+ value: users,
190
+ }
191
+ isActive.value = true
192
+ }
193
+
194
+ function handleReset() {
195
+ selected.value = { identifier: 'user', label: 'User', value: [] }
196
+ isActive.value = false
197
+ }
198
+
199
+ return { selected, isActive, handleApply, handleReset, args }
200
+ },
201
+ template: `
202
+ <div class="flex flex-col gap-4 p-4">
203
+ <UserFilter
204
+ v-bind="args"
205
+ :selected="selected"
206
+ :is-active="isActive"
207
+ @apply="handleApply"
208
+ @reset="handleReset"
209
+ />
210
+ </div>
211
+ `,
212
+ }),
213
+ args: {
214
+ buttonLabel: 'Alice Johnson',
215
+ users: mockUsers as any,
216
+ isActive: true,
217
+ disabled: false,
218
+ loading: false,
219
+ },
220
+ }
221
+
222
+ export const TwoUsersSelected: Story = {
223
+ render: args => ({
224
+ components: { UserFilter },
225
+ setup() {
226
+ const selected = ref({ identifier: 'user', label: 'Alice Johnson & Bob Smith', value: ['alice@example.com', 'bob@example.com'] })
227
+ const isActive = ref(true)
228
+
229
+ function handleApply(value: any) {
230
+ const users = value.user
231
+ const userNames = users.map((email: string) => {
232
+ const member = mockUsers.find(u => u.user.email === email)
233
+ return member?.user.name || email
234
+ })
235
+
236
+ const label = userNames.length > 2
237
+ ? `${userNames[0]} & ${userNames.length - 1} others`
238
+ : userNames.join(' & ')
239
+
240
+ selected.value = {
241
+ identifier: 'user',
242
+ label,
243
+ value: users,
244
+ }
245
+ isActive.value = true
246
+ }
247
+
248
+ function handleReset() {
249
+ selected.value = { identifier: 'user', label: 'User', value: [] }
250
+ isActive.value = false
251
+ }
252
+
253
+ return { selected, isActive, handleApply, handleReset, args }
254
+ },
255
+ template: `
256
+ <div class="flex flex-col gap-4 p-4">
257
+ <UserFilter
258
+ v-bind="args"
259
+ :selected="selected"
260
+ :is-active="isActive"
261
+ @apply="handleApply"
262
+ @reset="handleReset"
263
+ />
264
+ </div>
265
+ `,
266
+ }),
267
+ args: {
268
+ buttonLabel: 'Alice Johnson & Bob Smith',
269
+ users: mockUsers as any,
270
+ isActive: true,
271
+ disabled: false,
272
+ loading: false,
273
+ },
274
+ }
275
+
276
+ export const MultipleUsersSelected: Story = {
277
+ render: args => ({
278
+ components: { UserFilter },
279
+ setup() {
280
+ const selected = ref({ identifier: 'user', label: 'Alice Johnson & 3 others', value: ['alice@example.com', 'bob@example.com', 'carol@example.com', 'david@example.com'] })
281
+ const isActive = ref(true)
282
+
283
+ function handleApply(value: any) {
284
+ const users = value.user
285
+ const userNames = users.map((email: string) => {
286
+ const member = mockUsers.find(u => u.user.email === email)
287
+ return member?.user.name || email
288
+ })
289
+
290
+ const label = userNames.length > 2
291
+ ? `${userNames[0]} & ${userNames.length - 1} others`
292
+ : userNames.join(' & ')
293
+
294
+ selected.value = {
295
+ identifier: 'user',
296
+ label,
297
+ value: users,
298
+ }
299
+ isActive.value = true
300
+ }
301
+
302
+ function handleReset() {
303
+ selected.value = { identifier: 'user', label: 'User', value: [] }
304
+ isActive.value = false
305
+ }
306
+
307
+ return { selected, isActive, handleApply, handleReset, args }
308
+ },
309
+ template: `
310
+ <div class="flex flex-col gap-4 p-4">
311
+ <UserFilter
312
+ v-bind="args"
313
+ :selected="selected"
314
+ :is-active="isActive"
315
+ @apply="handleApply"
316
+ @reset="handleReset"
317
+ />
318
+ </div>
319
+ `,
320
+ }),
321
+ args: {
322
+ buttonLabel: 'Alice Johnson & 3 others',
323
+ users: mockUsers as any,
324
+ isActive: true,
325
+ disabled: false,
326
+ loading: false,
327
+ },
328
+ }
329
+
330
+ export const Disabled: Story = {
331
+ ...Default,
332
+ args: {
333
+ ...Default.args,
334
+ disabled: true,
335
+ },
336
+ }
337
+
338
+ export const Loading: Story = {
339
+ ...Default,
340
+ args: {
341
+ ...Default.args,
342
+ loading: true,
343
+ },
344
+ }
@@ -0,0 +1,271 @@
1
+ <script setup lang="ts">
2
+ import type { Member } from '@meistrari/auth-sdk/utils'
3
+
4
+ const props = withDefaults(defineProps<{
5
+ disabled?: boolean
6
+ loading?: boolean
7
+ buttonLabel?: string
8
+ class?: string
9
+ selected: { identifier: string, label: string, value?: string | string[] }
10
+ isActive: boolean
11
+ users: Member[]
12
+ currentUserId?: string
13
+ searchPlaceholder?: string
14
+ applyLabel?: string
15
+ noOptionsFoundLabel?: string
16
+ meLabel?: string
17
+ }>(), {
18
+ searchPlaceholder: 'Search',
19
+ applyLabel: 'Apply',
20
+ noOptionsFoundLabel: 'No options found',
21
+ meLabel: 'Me',
22
+ })
23
+
24
+ const emit = defineEmits<{
25
+ (e: 'apply', value: Record<string, any>): void
26
+ (e: 'reset'): void
27
+ }>()
28
+
29
+ const userOptions = computed(() => {
30
+ return props.users.map((member: Member) => ({
31
+ id: member.userId,
32
+ label: member.user.name,
33
+ value: member.user.email,
34
+ avatar: { src: member.user.image },
35
+ isCurrentUser: member.userId === props.currentUserId,
36
+ }))
37
+ })
38
+
39
+ const filteredUserOptions = ref(userOptions.value)
40
+
41
+ const isDropdownOpen = ref(false)
42
+ const dropdownRef = useTemplateRef('dropdownEl')
43
+ const scrollableContainer = ref<HTMLElement>()
44
+
45
+ const searchQuery = ref<string>('')
46
+ const selectedUsers = ref<{ value: string, label: string }[]>(getInitialSelectedUsers())
47
+
48
+ const selectedUserAvatars = computed(() => {
49
+ if (props.selected?.value && Array.isArray(props.selected.value)) {
50
+ return props.selected.value.map((email: string) => {
51
+ const user = userOptions.value.find(option => option.value === email)
52
+ return {
53
+ image: user?.avatar.src,
54
+ alt: user?.label || email,
55
+ }
56
+ })
57
+ }
58
+ return []
59
+ })
60
+
61
+ const visibleImages = computed(() => selectedUserAvatars.value.slice(0, 2))
62
+ const remainingImages = computed(() => selectedUserAvatars.value.filter((_, index) => index >= 2))
63
+
64
+ const hasScrollbar = computed(() => {
65
+ if (!scrollableContainer.value) {
66
+ return false
67
+ }
68
+ return scrollableContainer.value.scrollHeight > scrollableContainer.value.clientHeight
69
+ })
70
+
71
+ function toggleUser(selectedUser: { value: string, label: string }) {
72
+ if (selectedUsers.value.some(u => u.value === selectedUser.value)) {
73
+ selectedUsers.value = selectedUsers.value.filter(u => u.value !== selectedUser.value)
74
+ }
75
+ else {
76
+ const isCurrentUser = userOptions.value.find(u => u.value === selectedUser.value)?.isCurrentUser
77
+ if (isCurrentUser) {
78
+ selectedUsers.value.unshift(selectedUser)
79
+ }
80
+ else {
81
+ selectedUsers.value.push(selectedUser)
82
+ }
83
+ }
84
+ }
85
+
86
+ function handleApply() {
87
+ const users = selectedUsers.value.map(user => user.value)
88
+ emit('apply', { user: users })
89
+ isDropdownOpen.value = false
90
+ }
91
+
92
+ function handleSearchInput() {
93
+ filteredUserOptions.value = userOptions.value.filter(user => user.label.toLowerCase().includes(searchQuery.value.toLowerCase()))
94
+ }
95
+
96
+ function getInitialSelectedUsers() {
97
+ if (props.selected?.value && Array.isArray(props.selected.value)) {
98
+ return props.selected.value.map((email: string) => {
99
+ const user = userOptions.value.find(option => option.value === email)
100
+ return { value: user?.value ?? '', label: user?.isCurrentUser ? props.meLabel : user?.label ?? '' }
101
+ })
102
+ }
103
+ return []
104
+ }
105
+
106
+ watch(isDropdownOpen, (isOpen) => {
107
+ if (isOpen) {
108
+ selectedUsers.value = getInitialSelectedUsers()
109
+ }
110
+ else {
111
+ searchQuery.value = ''
112
+ }
113
+ })
114
+
115
+ watch(() => props.isActive, (isActive) => {
116
+ if (!isActive && isDropdownOpen.value) {
117
+ isDropdownOpen.value = false
118
+ }
119
+ })
120
+
121
+ watch(() => props.users, () => {
122
+ filteredUserOptions.value = userOptions.value
123
+ })
124
+
125
+ onClickOutside(dropdownRef, () => {
126
+ isDropdownOpen.value = false
127
+ })
128
+ </script>
129
+
130
+ <template>
131
+ <div ref="dropdownEl" class="relative">
132
+ <TelaButton
133
+ :class="[props.class, isDropdownOpen && !props.isActive ? 'bg-gray-40! border-gray-240!' : '']"
134
+ class="group rounded-10px! flex items-center gap-6px focus:ring-0! px-12px!"
135
+ variant="secondary"
136
+ size="md"
137
+ h-32px
138
+ :disabled="props.disabled"
139
+ :loading="props.loading"
140
+ :leading="false"
141
+ @click="isDropdownOpen = !isDropdownOpen"
142
+ >
143
+ <div flex items-center gap-6px>
144
+ <TelaAvatar
145
+ v-for="(avatar, index) in visibleImages"
146
+ :key="avatar.alt"
147
+ :image="avatar.image"
148
+ :alt="avatar.alt"
149
+ size="2xs"
150
+ rounded-full
151
+ :style="{ marginRight: index === visibleImages.length - 1 ? '0' : '-6px', boxShadow: '0 0 0 2px #1B222B' }"
152
+ />
153
+ <div
154
+ v-if="remainingImages.length > 0"
155
+ class="relative"
156
+ :style="{ marginLeft: '-6px' }"
157
+ >
158
+ <TelaAvatar
159
+ :image="remainingImages[0]?.image"
160
+ alt="remaining images"
161
+ size="2xs"
162
+ rounded-full
163
+ :style="{
164
+ boxShadow: '0 0 0 2px #1B222B',
165
+ }"
166
+ />
167
+ <div
168
+ class="absolute inset-0 flex items-center justify-center text-white text-xs font-medium rounded-full" :style="{
169
+ background: 'rgba(3, 12, 22, 0.64)',
170
+ backdropFilter: 'blur(0.5px)',
171
+ }"
172
+ >
173
+ +{{ remainingImages.length }}
174
+ </div>
175
+ </div>
176
+ <span class="max-w-125px! truncate">{{ props.buttonLabel }}</span>
177
+ <TelaIcon
178
+ v-if="!props.isActive && !props.loading"
179
+ name="i-ph-caret-down" size="sm" text-black-1000
180
+ transition
181
+ :class="[
182
+ isDropdownOpen && 'rotate-180',
183
+ ]"
184
+ />
185
+ <TelaIcon v-if="props.isActive && !props.loading" name="i-ph-x" size="sm" @click.stop="emit('reset')" />
186
+ </div>
187
+ </TelaButton>
188
+
189
+ <div
190
+ v-if="isDropdownOpen"
191
+ class="absolute top-full left-0 z-50 mt-1 w-256px bg-white rounded-12px shadow-[0px_3px_12px_0px_rgba(15,_32,_48,_0.08),_0px_12px_52px_0px_rgba(3,_12,_20,_0.16)] border border-gray-120 flex flex-col"
192
+ >
193
+ <div p-8px border-b border-gray-120>
194
+ <div class="relative">
195
+ <TelaIcon
196
+ name="i-ph-magnifying-glass"
197
+ size="sm"
198
+ class="absolute left-8px top-1/2 -translate-y-1/2 text-gray-600"
199
+ />
200
+ <input
201
+ v-model="searchQuery"
202
+ type="text"
203
+ :placeholder="props.searchPlaceholder"
204
+ class="w-full h-32px pl-32px pr-8px rounded-8px border border-gray-120 text-14px focus:outline-none focus:border-gray-240"
205
+ @input="handleSearchInput"
206
+ >
207
+ </div>
208
+ </div>
209
+ <div
210
+ ref="scrollableContainer"
211
+ class="flex-1 overflow-y-auto max-h-256px"
212
+ flex="~ col"
213
+ gap-4px
214
+ :class="hasScrollbar ? 'pl-8px pr-4px' : 'px-8px'"
215
+ pt-8px
216
+ pb-8px
217
+ >
218
+ <div v-if="filteredUserOptions.length === 0" flex="~ col" items-center justify-center gap-12px p-16px>
219
+ <TelaIcon name="i-ph-magnifying-glass" size="24px" class="text-gray-400" />
220
+ <span text-14px text-gray-600>{{ props.noOptionsFoundLabel }}{{ searchQuery ? ` with '${searchQuery}'` : '' }}</span>
221
+ </div>
222
+ <div
223
+ v-for="userOption in filteredUserOptions"
224
+ v-else
225
+ :key="userOption.value"
226
+ cursor-pointer
227
+ flex="~ justify-between"
228
+ items-center
229
+ gap-12px
230
+ p-8px
231
+ rounded-8px
232
+ h-28px
233
+ class="hover:bg-gray-20"
234
+ @click="toggleUser({ value: userOption.value, label: userOption.isCurrentUser ? props.meLabel.toLowerCase() : userOption.label })"
235
+ >
236
+ <div flex="~" items-center gap-8px>
237
+ <TelaAvatar
238
+ size="2xs"
239
+ rounded-full
240
+ :image="userOption.avatar.src"
241
+ :alt="userOption.label"
242
+ />
243
+ <span
244
+ :title="userOption.label"
245
+ text-black-900 text-14px font-medium leading-tight max-w-160px truncate overflow-hidden text-ellipsis
246
+ >
247
+ {{ userOption.label }}
248
+ </span>
249
+ </div>
250
+ <TelaCheckbox
251
+ :model-value="selectedUsers.some(user => user.value === userOption.value)"
252
+ @update:model-value="toggleUser({ value: userOption.value, label: userOption.isCurrentUser ? props.meLabel.toLowerCase() : userOption.label })"
253
+ @click.stop
254
+ />
255
+ </div>
256
+ </div>
257
+ <div pb-8px px-8px pt-8px bg-white rounded-b-12px>
258
+ <TelaButton
259
+ variant="secondary"
260
+ w-full
261
+ size="sm"
262
+ rounded-lg
263
+ :disabled="selectedUsers.length === 0"
264
+ @click="handleApply"
265
+ >
266
+ {{ props.applyLabel }}
267
+ </TelaButton>
268
+ </div>
269
+ </div>
270
+ </div>
271
+ </template>