@caipira/tamandua 0.0.1

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/.editorconfig +12 -0
  2. package/.prettierrc +5 -0
  3. package/.storybook/main.ts +25 -0
  4. package/.storybook/preview-body.html +3 -0
  5. package/.storybook/preview.ts +24 -0
  6. package/App.vue +1 -0
  7. package/Dockerfile +21 -0
  8. package/LICENSE +674 -0
  9. package/README.md +11 -0
  10. package/assets/icons/account.svg +1 -0
  11. package/assets/icons/alert-octagon-outline.svg +1 -0
  12. package/assets/icons/alert-octagon.svg +1 -0
  13. package/assets/icons/archive-outline.svg +1 -0
  14. package/assets/icons/archive.svg +1 -0
  15. package/assets/icons/arrow-left.svg +1 -0
  16. package/assets/icons/arrow-right.svg +1 -0
  17. package/assets/icons/bank-outline.svg +1 -0
  18. package/assets/icons/bank.svg +1 -0
  19. package/assets/icons/camera.svg +1 -0
  20. package/assets/icons/cards-outline.svg +1 -0
  21. package/assets/icons/cards-variant.svg +1 -0
  22. package/assets/icons/cart-outline.svg +1 -0
  23. package/assets/icons/chart-box-outline.svg +1 -0
  24. package/assets/icons/chart-box.svg +1 -0
  25. package/assets/icons/check-circle-outline.svg +1 -0
  26. package/assets/icons/check-circle.svg +1 -0
  27. package/assets/icons/check.svg +1 -0
  28. package/assets/icons/checkbox-dark.svg +1 -0
  29. package/assets/icons/checkbox-indeterminate-dark.svg +1 -0
  30. package/assets/icons/checkbox-indeterminate.svg +1 -0
  31. package/assets/icons/checkbox.svg +1 -0
  32. package/assets/icons/chevron-down.svg +1 -0
  33. package/assets/icons/chevron-left.svg +1 -0
  34. package/assets/icons/chevron-right.svg +1 -0
  35. package/assets/icons/chevron-up.svg +1 -0
  36. package/assets/icons/circle.svg +1 -0
  37. package/assets/icons/clock.svg +1 -0
  38. package/assets/icons/close-circle-outline.svg +1 -0
  39. package/assets/icons/close-circle.svg +1 -0
  40. package/assets/icons/close.svg +1 -0
  41. package/assets/icons/cog.svg +1 -0
  42. package/assets/icons/color-fill.svg +1 -0
  43. package/assets/icons/copy.svg +1 -0
  44. package/assets/icons/credit-card-plus.svg +1 -0
  45. package/assets/icons/credit-card.svg +1 -0
  46. package/assets/icons/currency.svg +1 -0
  47. package/assets/icons/database.svg +1 -0
  48. package/assets/icons/dots-grid.svg +1 -0
  49. package/assets/icons/dots-vertical.svg +1 -0
  50. package/assets/icons/email-open-outline.svg +1 -0
  51. package/assets/icons/email-outline.svg +1 -0
  52. package/assets/icons/eye-off.svg +1 -0
  53. package/assets/icons/eye.svg +1 -0
  54. package/assets/icons/file-document-plus-outline.svg +1 -0
  55. package/assets/icons/filmstrip.svg +1 -0
  56. package/assets/icons/filter.svg +1 -0
  57. package/assets/icons/fullscreen-exit.svg +1 -0
  58. package/assets/icons/fullscreen.svg +1 -0
  59. package/assets/icons/group.svg +1 -0
  60. package/assets/icons/image-album-outline.svg +1 -0
  61. package/assets/icons/image-album.svg +1 -0
  62. package/assets/icons/image-outline.svg +1 -0
  63. package/assets/icons/image.svg +1 -0
  64. package/assets/icons/info-outline.svg +1 -0
  65. package/assets/icons/key-chain.svg +1 -0
  66. package/assets/icons/key-variant.svg +1 -0
  67. package/assets/icons/key.svg +1 -0
  68. package/assets/icons/listbox-outline.svg +1 -0
  69. package/assets/icons/loading.svg +1 -0
  70. package/assets/icons/lock-outline.svg +1 -0
  71. package/assets/icons/lock.svg +1 -0
  72. package/assets/icons/logout.svg +1 -0
  73. package/assets/icons/menu-down.svg +1 -0
  74. package/assets/icons/menu-left.svg +1 -0
  75. package/assets/icons/menu-right.svg +1 -0
  76. package/assets/icons/menu.svg +1 -0
  77. package/assets/icons/minus-circle-outline.svg +1 -0
  78. package/assets/icons/minus-circle.svg +1 -0
  79. package/assets/icons/minus.svg +1 -0
  80. package/assets/icons/moon.svg +1 -0
  81. package/assets/icons/open-in-new.svg +1 -0
  82. package/assets/icons/pencil.svg +1 -0
  83. package/assets/icons/people.svg +1 -0
  84. package/assets/icons/piggy-bank-outline.svg +1 -0
  85. package/assets/icons/plus-circle-outline.svg +1 -0
  86. package/assets/icons/plus-circle.svg +1 -0
  87. package/assets/icons/plus.svg +1 -0
  88. package/assets/icons/qrcode-scan.svg +1 -0
  89. package/assets/icons/radio-dark.svg +1 -0
  90. package/assets/icons/radio.svg +1 -0
  91. package/assets/icons/refresh.svg +1 -0
  92. package/assets/icons/save.svg +1 -0
  93. package/assets/icons/search.svg +1 -0
  94. package/assets/icons/spotlight.svg +1 -0
  95. package/assets/icons/store-outline.svg +1 -0
  96. package/assets/icons/sun.svg +1 -0
  97. package/assets/icons/swap-horizontal.svg +1 -0
  98. package/assets/icons/swap-left.svg +1 -0
  99. package/assets/icons/swap-right.svg +1 -0
  100. package/assets/icons/swap.svg +1 -0
  101. package/assets/icons/system-theme.svg +1 -0
  102. package/assets/icons/tag-outline.svg +1 -0
  103. package/assets/icons/trash-can-outline.svg +1 -0
  104. package/assets/icons/trash-can.svg +1 -0
  105. package/assets/icons/upload.svg +1 -0
  106. package/assets/icons/user-circle.svg +1 -0
  107. package/assets/icons/zip-box.svg +1 -0
  108. package/assets/images/fs/apk.svg +11 -0
  109. package/assets/images/fs/bmp.svg +7 -0
  110. package/assets/images/fs/css.svg +8 -0
  111. package/assets/images/fs/doc.svg +9 -0
  112. package/assets/images/fs/docx.svg +9 -0
  113. package/assets/images/fs/folder-adwaita.svg +8 -0
  114. package/assets/images/fs/folder-black.svg +8 -0
  115. package/assets/images/fs/folder-brown.svg +8 -0
  116. package/assets/images/fs/folder-grey.svg +8 -0
  117. package/assets/images/fs/folder-nordic.svg +8 -0
  118. package/assets/images/fs/folder-orange.svg +8 -0
  119. package/assets/images/fs/folder-palebrown.svg +8 -0
  120. package/assets/images/fs/folder-paleorange.svg +8 -0
  121. package/assets/images/fs/folder-teal.svg +8 -0
  122. package/assets/images/fs/folder-white.svg +8 -0
  123. package/assets/images/fs/folder-yellow.svg +8 -0
  124. package/assets/images/fs/gif.svg +7 -0
  125. package/assets/images/fs/go.svg +9 -0
  126. package/assets/images/fs/ics.svg +24 -0
  127. package/assets/images/fs/iso.svg +10 -0
  128. package/assets/images/fs/jpeg.svg +7 -0
  129. package/assets/images/fs/jpg.svg +7 -0
  130. package/assets/images/fs/js.svg +9 -0
  131. package/assets/images/fs/json.svg +9 -0
  132. package/assets/images/fs/lua.svg +9 -0
  133. package/assets/images/fs/m4v.svg +7 -0
  134. package/assets/images/fs/md.svg +10 -0
  135. package/assets/images/fs/mov.svg +7 -0
  136. package/assets/images/fs/mp3.svg +9 -0
  137. package/assets/images/fs/mp4.svg +7 -0
  138. package/assets/images/fs/pdf.svg +9 -0
  139. package/assets/images/fs/pgp.svg +8 -0
  140. package/assets/images/fs/php.svg +9 -0
  141. package/assets/images/fs/png.svg +7 -0
  142. package/assets/images/fs/ppt.svg +9 -0
  143. package/assets/images/fs/py.svg +9 -0
  144. package/assets/images/fs/rar.svg +20 -0
  145. package/assets/images/fs/rpm.svg +7 -0
  146. package/assets/images/fs/rs.svg +9 -0
  147. package/assets/images/fs/sh.svg +9 -0
  148. package/assets/images/fs/tar.svg +20 -0
  149. package/assets/images/fs/txt.svg +8 -0
  150. package/assets/images/fs/unknown.svg +8 -0
  151. package/assets/images/fs/xls.svg +9 -0
  152. package/assets/images/fs/xlsx.svg +9 -0
  153. package/assets/images/fs/xml.svg +8 -0
  154. package/assets/images/fs/yaml.svg +9 -0
  155. package/assets/images/fs/zip.svg +20 -0
  156. package/components/Avatar/Avatar.story.ts +55 -0
  157. package/components/Avatar/Avatar.vue +82 -0
  158. package/components/Avatar/index.ts +12 -0
  159. package/components/Backdrop/Backdrop.vue +27 -0
  160. package/components/Backdrop/index.ts +12 -0
  161. package/components/Button/Button.story.ts +74 -0
  162. package/components/Button/Button.vue +230 -0
  163. package/components/Button/index.ts +12 -0
  164. package/components/ButtonCopy/ButtonCopy.vue +61 -0
  165. package/components/ButtonCopy/index.ts +12 -0
  166. package/components/Drawer/Drawer.vue +102 -0
  167. package/components/Drawer/index.ts +12 -0
  168. package/components/Dropdown/Dropdown.vue +258 -0
  169. package/components/Dropdown/index.ts +12 -0
  170. package/components/EventListener/EventListener.vue +12 -0
  171. package/components/FileDrop/FileDrop.vue +116 -0
  172. package/components/FileDrop/index.ts +12 -0
  173. package/components/Form/Form.spec.ts +72 -0
  174. package/components/Form/Form.vue +134 -0
  175. package/components/Form/index.ts +12 -0
  176. package/components/FormItem/FormItem.vue +85 -0
  177. package/components/FormItem/index.ts +12 -0
  178. package/components/GraphyEmpty/GraphyEmpty.vue +16 -0
  179. package/components/GraphyEmpty/index.ts +12 -0
  180. package/components/GraphyLabel/GraphyLabel.vue +34 -0
  181. package/components/GraphyLabel/index.ts +12 -0
  182. package/components/GraphyPrice/GraphyPrice.story.ts +37 -0
  183. package/components/GraphyPrice/GraphyPrice.vue +65 -0
  184. package/components/GraphyPrice/index.ts +12 -0
  185. package/components/GraphySubtitle/GraphySubtitle.vue +22 -0
  186. package/components/GraphySubtitle/index.ts +12 -0
  187. package/components/GraphyTitle/GraphyTitle.vue +13 -0
  188. package/components/GraphyTitle/index.ts +12 -0
  189. package/components/Icon/Icon.vue +84 -0
  190. package/components/Icon/index.ts +12 -0
  191. package/components/IconButton/IconButton.vue +168 -0
  192. package/components/IconButton/index.ts +12 -0
  193. package/components/InputAvatar/InputAvatar.vue +63 -0
  194. package/components/InputAvatar/index.ts +12 -0
  195. package/components/InputCheckbox/InputCheckbox.vue +77 -0
  196. package/components/InputCheckbox/index.ts +12 -0
  197. package/components/InputColor/InputColor.vue +54 -0
  198. package/components/InputColor/index.ts +12 -0
  199. package/components/InputDate/InputDate.story.ts +15 -0
  200. package/components/InputDate/InputDate.vue +368 -0
  201. package/components/InputDate/index.ts +12 -0
  202. package/components/InputMultiplier/InputMultiplier.vue +144 -0
  203. package/components/InputMultiplier/index.ts +12 -0
  204. package/components/InputPassword/InputPassword.vue +168 -0
  205. package/components/InputPassword/index.ts +12 -0
  206. package/components/InputPhone/InputPhone.vue +125 -0
  207. package/components/InputPhone/index.ts +12 -0
  208. package/components/InputPrice/InputPrice.vue +96 -0
  209. package/components/InputPrice/index.ts +12 -0
  210. package/components/InputRadio/InputRadio.vue +41 -0
  211. package/components/InputRadio/InputRadioGroup.story.ts +24 -0
  212. package/components/InputRadio/InputRadioGroup.vue +48 -0
  213. package/components/InputRadio/index.ts +14 -0
  214. package/components/InputSelect/InputSelect.story.ts +87 -0
  215. package/components/InputSelect/InputSelect.vue +507 -0
  216. package/components/InputSelect/index.ts +12 -0
  217. package/components/InputSwitch/InputSwitch.story.ts +34 -0
  218. package/components/InputSwitch/InputSwitch.vue +82 -0
  219. package/components/InputSwitch/index.ts +12 -0
  220. package/components/InputText/InputText.vue +62 -0
  221. package/components/InputText/index.ts +12 -0
  222. package/components/InputTextarea/InputTextarea.vue +64 -0
  223. package/components/InputTextarea/index.ts +12 -0
  224. package/components/LineChart/LineChart.vue +14 -0
  225. package/components/LineChart/index.ts +12 -0
  226. package/components/Modal/Modal.vue +106 -0
  227. package/components/Modal/index.ts +12 -0
  228. package/components/ModalForm/ModalForm.vue +141 -0
  229. package/components/ModalForm/index.ts +12 -0
  230. package/components/Pagination/Pagination.story.ts +15 -0
  231. package/components/Pagination/Pagination.vue +138 -0
  232. package/components/Pagination/index.ts +12 -0
  233. package/components/PieChart/PieChart.vue +14 -0
  234. package/components/PieChart/index.ts +12 -0
  235. package/components/Popconfirm/Popconfirm.vue +80 -0
  236. package/components/Popconfirm/index.ts +12 -0
  237. package/components/Popover/Popover.vue +133 -0
  238. package/components/Popover/index.ts +12 -0
  239. package/components/ProgressCircle/ProgressCircle.story.ts +31 -0
  240. package/components/ProgressCircle/ProgressCircle.vue +82 -0
  241. package/components/ProgressCircle/index.ts +12 -0
  242. package/components/ProgressLine/ProgressLine.story.ts +27 -0
  243. package/components/ProgressLine/ProgressLine.vue +104 -0
  244. package/components/ProgressLine/index.ts +12 -0
  245. package/components/SensitiveInfo/SensitiveInfo.vue +18 -0
  246. package/components/SensitiveInfo/index.ts +12 -0
  247. package/components/Tab/Tab.vue +58 -0
  248. package/components/Tab/index.ts +12 -0
  249. package/components/Table/Table.story.ts +32 -0
  250. package/components/Table/Table.vue +318 -0
  251. package/components/Table/index.ts +12 -0
  252. package/components/Tag/Tag.vue +73 -0
  253. package/components/Tag/index.ts +12 -0
  254. package/components/Toast/Toast.vue +75 -0
  255. package/components/Toast/index.ts +12 -0
  256. package/components/index.ts +43 -0
  257. package/components/plugins.ts +89 -0
  258. package/composables/index.ts +2 -0
  259. package/composables/useBreakpoints.ts +30 -0
  260. package/composables/useRender.ts +29 -0
  261. package/entrypoint.sh +19 -0
  262. package/enums/app.ts +5 -0
  263. package/enums/form.ts +25 -0
  264. package/enums/ui.ts +160 -0
  265. package/env.d.ts +8 -0
  266. package/i18n.ts +20 -0
  267. package/index.css +383 -0
  268. package/index.html +22 -0
  269. package/index.ts +14 -0
  270. package/main.ts +31 -0
  271. package/package.json +70 -0
  272. package/plugins/register-component.ts +5 -0
  273. package/postcss.config.js +6 -0
  274. package/services/clipboard.ts +5 -0
  275. package/services/date.ts +27 -0
  276. package/services/form/crud.ts +109 -0
  277. package/services/form/form-data-transformers.ts +148 -0
  278. package/services/form/form-json-transformers.ts +91 -0
  279. package/services/form/form-transformer.ts +54 -0
  280. package/services/form/form-value-transformers.ts +35 -0
  281. package/services/form/form.test.ts +98 -0
  282. package/services/form/form.ts +80 -0
  283. package/services/password.ts +309 -0
  284. package/services/ui.ts +43 -0
  285. package/tailwind.config.js +16 -0
  286. package/tsconfig.json +23 -0
  287. package/types/address.ts +44 -0
  288. package/types/api.ts +28 -0
  289. package/types/common.ts +5 -0
  290. package/types/form.ts +144 -0
  291. package/types/index.ts +5 -0
  292. package/types/ui.ts +55 -0
  293. package/types/website.ts +16 -0
  294. package/vite.config.mts +38 -0
  295. package/vitest.setup.ts +21 -0
@@ -0,0 +1,258 @@
1
+ <script lang="ts" setup>
2
+ import { SelectOption } from "@/types/form";
3
+
4
+ import { getCurrentInstance, ref } from "vue";
5
+
6
+ export interface SelectEvent {
7
+ index: number;
8
+ option: SelectOption;
9
+ hold: boolean;
10
+ }
11
+
12
+ defineOptions({ name: "TDropdown" });
13
+
14
+ const props = withDefaults(
15
+ defineProps<{
16
+ items?: SelectOption[];
17
+ owner?: string;
18
+ multiple?: boolean;
19
+ isVisible?: boolean;
20
+
21
+ optionMarginClass?: string;
22
+ optionPaddingClass?: string;
23
+ optionRoundnessClass?: string;
24
+ wrapperRoundnessClass?: string;
25
+ wrapperPaddingClass?: string;
26
+ }>(),
27
+ {
28
+ items: () => [],
29
+ multiple: false,
30
+ isVisible: false,
31
+
32
+ optionMarginClass: "",
33
+ optionPaddingClass: "py-2 px-2",
34
+ optionRoundnessClass: "",
35
+ wrapperRoundnessClass: "rounded-md",
36
+ wrapperPaddingClass: "",
37
+ },
38
+ );
39
+
40
+ const emit = defineEmits<{
41
+ (e: "change", val: SelectEvent): void;
42
+ (e: "end-reached"): void;
43
+ (e: "created", val: ReturnType<typeof getCurrentInstance>): void;
44
+ }>();
45
+
46
+ const current = ref<number>(-1);
47
+ const listRef = ref<InstanceType<typeof HTMLUListElement> | null>(null);
48
+ const itemRefs = ref<InstanceType<typeof HTMLUListElement>[]>([]);
49
+ const movementSource = ref<"mouse" | "keyboard">("keyboard");
50
+
51
+ const onSelect = (
52
+ index: number = current.value,
53
+ option: SelectOption = props.items[index],
54
+ event: MouseEvent | undefined = undefined,
55
+ ) => {
56
+ event?.stopPropagation();
57
+
58
+ emit("change", {
59
+ index,
60
+ option,
61
+ hold: event?.ctrlKey ?? false,
62
+ });
63
+
64
+ if (option.action) {
65
+ option.action();
66
+ }
67
+ };
68
+
69
+ const listen = (e: KeyboardEvent) => {
70
+ if (!props.isVisible) {
71
+ return false;
72
+ }
73
+
74
+ e.preventDefault();
75
+
76
+ movementSource.value = "keyboard";
77
+
78
+ switch (e.key) {
79
+ case "ArrowUp":
80
+ case "ArrowDown":
81
+ return move(e);
82
+ case "Enter":
83
+ return onSelect();
84
+ }
85
+ };
86
+
87
+ const move = (e: KeyboardEvent) => {
88
+ const down = e.key === "ArrowDown";
89
+ const amountOfItems = props.items.length;
90
+
91
+ let newIndex = current.value;
92
+
93
+ const isNotAnOption = (): boolean => {
94
+ if (!props.items[newIndex]) {
95
+ return false;
96
+ }
97
+
98
+ return (
99
+ "isGroupLabel" in props.items[newIndex] &&
100
+ (props.items[newIndex].isGroupLabel as boolean)
101
+ );
102
+ };
103
+
104
+ const increase = () => {
105
+ if (newIndex === amountOfItems - 1) {
106
+ newIndex = 0; // If at the last item, jump to the first item
107
+ } else {
108
+ newIndex++; // Move to the next item
109
+ }
110
+ };
111
+
112
+ const decrease = () => {
113
+ if (newIndex === 0) {
114
+ newIndex = amountOfItems - 1; // If at the first item, jump to the last item
115
+ } else {
116
+ newIndex--; // Move to the previous item
117
+ }
118
+ };
119
+
120
+ if (down) {
121
+ do increase();
122
+ while (isNotAnOption());
123
+ } else {
124
+ do decrease();
125
+ while (isNotAnOption());
126
+ }
127
+
128
+ if (newIndex > -1 && newIndex < amountOfItems) {
129
+ current.value = newIndex;
130
+ }
131
+
132
+ const ul = listRef.value as HTMLElement;
133
+ const li = itemRefs.value[current.value] as HTMLUListElement;
134
+ const liPos = li.offsetTop;
135
+ const liHeight = li.offsetHeight;
136
+ const ulHeight = ul.offsetHeight;
137
+ const scroll = ul.scrollTop;
138
+
139
+ let height = 0;
140
+
141
+ li.focus();
142
+
143
+ if (liPos === 0 || scroll === 0) {
144
+ height = liPos - ulHeight + liHeight;
145
+ } else if (down && liPos + liHeight > ulHeight + scroll) {
146
+ height = scroll + liHeight;
147
+ } else if (!down && liPos < scroll) {
148
+ height = scroll - liHeight;
149
+ } else {
150
+ return true;
151
+ }
152
+
153
+ ul.scroll({ top: height });
154
+
155
+ return true;
156
+ };
157
+
158
+ const onScrollEnd = (e: MouseEvent) => {
159
+ const el = e.target as HTMLElement;
160
+ const li = itemRefs.value[0] as HTMLElement;
161
+
162
+ if (!el || !li) {
163
+ return;
164
+ }
165
+
166
+ if (el.scrollTop + el.clientHeight + li.offsetHeight >= el.scrollHeight) {
167
+ emit("end-reached");
168
+ }
169
+ };
170
+
171
+ defineExpose({ move });
172
+ </script>
173
+
174
+ <template>
175
+ <ul
176
+ ref="listRef"
177
+ role="listbox"
178
+ class="text-left max-h-60 min-w-[12rem] z-10 overflow-y-auto scrollbar select-none floatable"
179
+ :class="[props.wrapperRoundnessClass, props.wrapperPaddingClass]"
180
+ @scrollend="onScrollEnd"
181
+ @mousemove="() => (movementSource = 'mouse')"
182
+ @keydown="listen"
183
+ >
184
+ <!-- Empty placeholder -->
185
+ <li
186
+ v-if="items.length === 0"
187
+ role="option"
188
+ aria-disabled="true"
189
+ class="whitespace-nowrap text-gray-400"
190
+ :class="[
191
+ props.optionPaddingClass,
192
+ props.optionMarginClass,
193
+ props.optionRoundnessClass,
194
+ ]"
195
+ >
196
+ Nothing here
197
+ </li>
198
+
199
+ <template
200
+ v-else
201
+ v-for="(option, i) in items"
202
+ :key="i"
203
+ >
204
+ <!-- Group label -->
205
+ <li
206
+ v-if="option.isGroupLabel"
207
+ class="text-xs font-bold tracking-wider text-neutral-600"
208
+ :class="[
209
+ props.optionPaddingClass,
210
+ props.optionMarginClass,
211
+ props.optionRoundnessClass,
212
+ ]"
213
+ ref="itemRefs"
214
+ >
215
+ {{ option.label }}
216
+ </li>
217
+
218
+ <!-- Dropdown item -->
219
+ <li
220
+ v-else
221
+ :class="{
222
+ 'bg-hover': i === current && movementSource === 'keyboard',
223
+ 'hover:bg-hover': movementSource === 'mouse',
224
+ [props.optionMarginClass]: true,
225
+ [props.optionPaddingClass]: true,
226
+ [props.optionRoundnessClass]: true,
227
+ }"
228
+ class="flex text-base cursor-pointer whitespace-nowrap items-center focus-visible:outline-none"
229
+ role="option"
230
+ ref="itemRefs"
231
+ :tabindex="0"
232
+ :aria-selected="i === current"
233
+ @keydown.enter="onSelect(i, option)"
234
+ @click="(e) => onSelect(i, option, e)"
235
+ >
236
+ <!-- Slot for custom renderer -->
237
+ <slot
238
+ v-if="!!$slots.default"
239
+ v-bind="option"
240
+ />
241
+
242
+ <!-- Default renderer -->
243
+ <div
244
+ v-else
245
+ class="inline-flex items-center input-label"
246
+ >
247
+ <t-icon
248
+ v-if="option.icon"
249
+ :icon="option.icon"
250
+ class="mr-2"
251
+ size="xs"
252
+ />
253
+ {{ option.label }}
254
+ </div>
255
+ </li>
256
+ </template>
257
+ </ul>
258
+ </template>
@@ -0,0 +1,12 @@
1
+ import type { App, Plugin } from "vue";
2
+ import registerComponent from "@/plugins/register-component";
3
+
4
+ import Dropdown from "./Dropdown.vue";
5
+
6
+ export default {
7
+ install(app: App) {
8
+ registerComponent(app, Dropdown);
9
+ },
10
+ } as Plugin;
11
+
12
+ export { Dropdown };
@@ -0,0 +1,12 @@
1
+ <script lang="ts" setup>
2
+ import { useSlots } from "vue";
3
+ import { useRender } from "@/composables/useRender";
4
+
5
+ defineEmits<{
6
+ (e: "popoverClick"): void;
7
+ }>();
8
+
9
+ const slots = useSlots();
10
+
11
+ useRender(() => (slots.default ? slots.default() : [null]));
12
+ </script>
@@ -0,0 +1,116 @@
1
+ <script lang="ts" setup>
2
+ import { ref, inject, computed } from "vue";
3
+
4
+ defineOptions({ name: "TFileDrop" });
5
+
6
+ const props = withDefaults(
7
+ defineProps<{
8
+ types?: any;
9
+ multiple?: boolean;
10
+ modelValue?: File[];
11
+ }>(),
12
+ {
13
+ multiple: true,
14
+ modelValue: () => [],
15
+ },
16
+ );
17
+
18
+ const emit = defineEmits<{
19
+ (e: "update:modelValue", val: any): void;
20
+ }>();
21
+
22
+ const files = ref<File[]>([]);
23
+ const formStyle = inject("formStyle", { label: "", input: "" });
24
+
25
+ const hasFile = computed<boolean>(() => {
26
+ return props.modelValue.length > 0;
27
+ });
28
+
29
+ const extensions = computed(() => {
30
+ switch (props.types) {
31
+ default:
32
+ return "SVG, PNG, JPG or GIF";
33
+ }
34
+ });
35
+
36
+ const onDeleteFile = (fileName: string) => {};
37
+
38
+ const onFileChange = (e: Event) => {
39
+ e.stopPropagation();
40
+
41
+ const element: HTMLInputElement = e.target as HTMLInputElement;
42
+ const localFiles: FileList | null = element.files;
43
+
44
+ if (!localFiles || !localFiles.length) {
45
+ return;
46
+ }
47
+
48
+ files.value = Array.from(localFiles);
49
+ emit("update:modelValue", localFiles);
50
+ };
51
+ </script>
52
+
53
+ <template>
54
+ <div :class="{ [formStyle.input]: true }">
55
+ <div class="flex items-center justify-center w-full">
56
+ <label
57
+ for="dropzone-file"
58
+ class="flex flex-col items-center justify-center w-full h-full cursor-pointer border input-border border-dashed input-roundness input-text-color input-bg-color hover:bg-caipira-secondary"
59
+ >
60
+ <div
61
+ v-if="hasFile"
62
+ class="my-5"
63
+ >
64
+ <div
65
+ v-for="file in files"
66
+ :key="file.name"
67
+ class="flex items-center"
68
+ >
69
+ <img
70
+ class="fs mr-1"
71
+ :class="[file.name.split('.').pop()]"
72
+ />
73
+ <span class="text-xs">{{ file.name }}</span>
74
+ <t-icon
75
+ icon="close"
76
+ class="ml-2 text-red-400"
77
+ size="sm"
78
+ role="button"
79
+ @click="() => onDeleteFile(file.name)"
80
+ />
81
+ </div>
82
+ </div>
83
+ <div
84
+ v-else
85
+ class="flex items-center justify-evenly my-4 w-full"
86
+ >
87
+ <t-icon
88
+ class="dark:fill-gray-400"
89
+ icon="upload"
90
+ role="button"
91
+ size="lg"
92
+ />
93
+ <div class="text-center">
94
+ <p
95
+ class="mb-2 text-sm text-gray-500 dark:text-gray-400"
96
+ >
97
+ <span class="font-semibold">Click to upload</span>
98
+ or drag and drop
99
+ </p>
100
+ <p class="text-xs text-gray-500 dark:text-gray-400">
101
+ {{ extensions }}
102
+ </p>
103
+ </div>
104
+ </div>
105
+
106
+ <input
107
+ id="dropzone-file"
108
+ type="file"
109
+ class="hidden"
110
+ :multiple="multiple"
111
+ @change="onFileChange"
112
+ />
113
+ </label>
114
+ </div>
115
+ </div>
116
+ </template>
@@ -0,0 +1,12 @@
1
+ import type { App, Plugin } from "vue";
2
+ import registerComponent from "@/plugins/register-component";
3
+
4
+ import FileDrop from "./FileDrop.vue";
5
+
6
+ export default {
7
+ install(app: App) {
8
+ registerComponent(app, FileDrop);
9
+ },
10
+ } as Plugin;
11
+
12
+ export { FileDrop };
@@ -0,0 +1,72 @@
1
+ import { FormDataTypes } from "@/enums/form";
2
+
3
+ import { mount } from "@vue/test-utils";
4
+ import { describe, expect, it } from "vitest";
5
+
6
+ import Form from "./Form.vue";
7
+
8
+ describe("Form.vue", () => {
9
+ it("should render the form element", async () => {
10
+ const wrapper = mount(Form, {
11
+ props: {},
12
+ });
13
+
14
+ expect(wrapper.find("form").exists()).toBe(true);
15
+ });
16
+
17
+ it("should render the submit button regardless of the value of the show-submit prop", async () => {
18
+ const wrapper = mount(Form, {
19
+ props: {
20
+ showSubmit: false,
21
+ },
22
+ });
23
+
24
+ expect(wrapper.find('input[type="submit"]').exists()).toBe(true);
25
+ });
26
+
27
+ it('should emit the "submit" event with form data on submit', async () => {
28
+ const wrapper = mount(Form, {
29
+ props: {
30
+ schema: {
31
+ name: FormDataTypes.String,
32
+ },
33
+ },
34
+ });
35
+
36
+ await wrapper.find('input[type="submit"]').trigger("click");
37
+ expect(wrapper.emitted()).toHaveProperty("submit");
38
+
39
+ const payload = (wrapper.emitted().submit as any)[0][0];
40
+ expect(payload).toStrictEqual({
41
+ isValid: true,
42
+ form: new FormData(),
43
+ });
44
+ });
45
+
46
+ it("should set value correctly using setValue method", async () => {
47
+ const wrapper = mount(Form, {
48
+ props: {
49
+ schema: {
50
+ name: FormDataTypes.String,
51
+ },
52
+ },
53
+ });
54
+
55
+ wrapper.vm.setValue("name", "Bob");
56
+ expect(wrapper.vm.form).toEqual({ name: "Bob" });
57
+ });
58
+
59
+ it("should set values correctly using setValues method", async () => {
60
+ const wrapper = mount(Form, {
61
+ props: {
62
+ schema: {
63
+ name: FormDataTypes.String,
64
+ age: FormDataTypes.Number,
65
+ },
66
+ },
67
+ });
68
+
69
+ wrapper.vm.setValues({ name: "Alice", age: 30 });
70
+ expect(wrapper.vm.form).toEqual({ name: "Alice", age: 30 });
71
+ });
72
+ });
@@ -0,0 +1,134 @@
1
+ <script lang="ts" setup generic="T extends FormSchema">
2
+ import { ElementVariant } from "@/enums/ui";
3
+ import type { ButtonProps } from "@/components/Button/Button.vue";
4
+ import { FormSubmissionFormat } from "@/enums/form";
5
+ import {
6
+ FormStyle,
7
+ FormSchema,
8
+ FormInstance,
9
+ ValidationResult,
10
+ } from "@/types/form";
11
+
12
+ import { transformForm } from "@/services/form/form-transformer";
13
+ import { ref, provide, reactive, watch } from "vue";
14
+
15
+ defineOptions({ name: "TForm" });
16
+
17
+ const props = withDefaults(
18
+ defineProps<{
19
+ id?: string;
20
+ idKey?: string;
21
+ schema?: T;
22
+ formStyle?: FormStyle;
23
+ showSubmit?: boolean;
24
+ submitLabel?: string;
25
+ buttonProps?: ButtonProps;
26
+ buttonVariant?: ElementVariant;
27
+ submissionFormat?: FormSubmissionFormat;
28
+ buttonWrapperClasses?: string;
29
+ }>(),
30
+ {
31
+ id: "",
32
+ idKey: "id",
33
+ schema: () => ({}) as T,
34
+ formStyle: () => ({}),
35
+ showSubmit: false,
36
+ submitLabel: "Submit",
37
+ buttonVariant: ElementVariant.PRIMARY,
38
+ submissionFormat: FormSubmissionFormat.FormData,
39
+ buttonWrapperClasses: "",
40
+ },
41
+ );
42
+
43
+ const emit = defineEmits<{
44
+ (e: "submit", val: ValidationResult<FormSubmissionFormat>): void;
45
+ (e: "change", val: FormInstance<T>): void;
46
+ }>();
47
+
48
+ const form = reactive<FormInstance<T>>({} as FormInstance<T>);
49
+ const formReference = ref<HTMLFormElement | null>(null);
50
+
51
+ const onSubmit = async () => {
52
+ emit("submit", await submit());
53
+ };
54
+
55
+ const submit = async (): Promise<
56
+ ValidationResult<typeof props.submissionFormat>
57
+ > => {
58
+ const localForm: FormInstance<T> = { ...form } as FormInstance<T>;
59
+
60
+ if (props.id) {
61
+ localForm[props.idKey as keyof T] = props.id;
62
+ }
63
+
64
+ const transformedForm = await transformForm(
65
+ localForm,
66
+ props.schema,
67
+ props.submissionFormat,
68
+ );
69
+
70
+ return {
71
+ isValid: true,
72
+ form: transformedForm,
73
+ };
74
+ };
75
+
76
+ const reset = () => {
77
+ Object.assign(form, {});
78
+ };
79
+
80
+ const setValues = (values: Partial<FormInstance<T>>) => {
81
+ for (const [key, value] of Object.entries(values)) {
82
+ form[key as keyof T] = value;
83
+ }
84
+ };
85
+
86
+ const getValues = () => {
87
+ return form;
88
+ };
89
+
90
+ const setValue = <K extends keyof T>(name: K, value: T[K]) => {
91
+ form[name] = value;
92
+ };
93
+
94
+ const getValue = (key: string) => {
95
+ return form[key];
96
+ };
97
+
98
+ watch(form as FormInstance<T>, (newValue: FormInstance<T>) => {
99
+ emit("change", newValue);
100
+ });
101
+
102
+ provide("formStyle", props.formStyle);
103
+
104
+ defineExpose({
105
+ submit: onSubmit,
106
+ reset,
107
+ setValues,
108
+ getValues,
109
+ setValue,
110
+ getValue,
111
+ });
112
+ </script>
113
+
114
+ <template>
115
+ <form
116
+ @submit="onSubmit"
117
+ ref="formReference"
118
+ >
119
+ <slot :form="form" />
120
+ <div :class="[props.buttonWrapperClasses]">
121
+ <!-- Do not use v-if as the element must exist in the DOM
122
+ in order for the form submission with enter key to work -->
123
+ <t-button
124
+ v-show="props.showSubmit"
125
+ v-bind="props.buttonProps"
126
+ type="submit"
127
+ class="mt-4"
128
+ :variant="props.buttonVariant"
129
+ :label="props.submitLabel"
130
+ @click="onSubmit"
131
+ />
132
+ </div>
133
+ </form>
134
+ </template>
@@ -0,0 +1,12 @@
1
+ import type { App, Plugin } from "vue";
2
+ import registerComponent from "@/plugins/register-component";
3
+
4
+ import Form from "./Form.vue";
5
+
6
+ export default {
7
+ install(app: App) {
8
+ registerComponent(app, Form);
9
+ },
10
+ } as Plugin;
11
+
12
+ export { Form };