create-nala 3.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 (316) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +58 -0
  3. package/dist/index.js +227 -0
  4. package/package.json +48 -0
  5. package/template/.env.example +45 -0
  6. package/template/_gitignore +33 -0
  7. package/template/components.json +24 -0
  8. package/template/index.html +30 -0
  9. package/template/package.json +51 -0
  10. package/template/public/favicon.svg +15 -0
  11. package/template/public/icons.svg +24 -0
  12. package/template/public/img/avatar.webp +0 -0
  13. package/template/public/screenshot.png +0 -0
  14. package/template/src/App.vue +15 -0
  15. package/template/src/auto-imports.d.ts +691 -0
  16. package/template/src/components/AppLogo.vue +159 -0
  17. package/template/src/components/EmptyState.vue +60 -0
  18. package/template/src/components/PageHeader.vue +60 -0
  19. package/template/src/components/layout/AppNavbar.vue +159 -0
  20. package/template/src/components/layout/AppSidebar.vue +74 -0
  21. package/template/src/components/layout/CommandSearchDialog.vue +120 -0
  22. package/template/src/components/ui/accordion/Accordion.vue +15 -0
  23. package/template/src/components/ui/accordion/AccordionContent.vue +20 -0
  24. package/template/src/components/ui/accordion/AccordionItem.vue +20 -0
  25. package/template/src/components/ui/accordion/AccordionTrigger.vue +34 -0
  26. package/template/src/components/ui/accordion/index.ts +4 -0
  27. package/template/src/components/ui/alert/Alert.vue +21 -0
  28. package/template/src/components/ui/alert/AlertDescription.vue +17 -0
  29. package/template/src/components/ui/alert/AlertTitle.vue +17 -0
  30. package/template/src/components/ui/alert/index.ts +30 -0
  31. package/template/src/components/ui/alert-dialog/AlertDialog.vue +15 -0
  32. package/template/src/components/ui/alert-dialog/AlertDialogAction.vue +18 -0
  33. package/template/src/components/ui/alert-dialog/AlertDialogCancel.vue +25 -0
  34. package/template/src/components/ui/alert-dialog/AlertDialogContent.vue +44 -0
  35. package/template/src/components/ui/alert-dialog/AlertDialogDescription.vue +23 -0
  36. package/template/src/components/ui/alert-dialog/AlertDialogFooter.vue +22 -0
  37. package/template/src/components/ui/alert-dialog/AlertDialogHeader.vue +17 -0
  38. package/template/src/components/ui/alert-dialog/AlertDialogTitle.vue +21 -0
  39. package/template/src/components/ui/alert-dialog/AlertDialogTrigger.vue +12 -0
  40. package/template/src/components/ui/alert-dialog/index.ts +9 -0
  41. package/template/src/components/ui/avatar/Avatar.vue +50 -0
  42. package/template/src/components/ui/avatar/AvatarFallback.vue +21 -0
  43. package/template/src/components/ui/avatar/AvatarGroup.vue +69 -0
  44. package/template/src/components/ui/avatar/AvatarImage.vue +16 -0
  45. package/template/src/components/ui/avatar/index.ts +4 -0
  46. package/template/src/components/ui/badge/Badge.vue +40 -0
  47. package/template/src/components/ui/badge/index.ts +45 -0
  48. package/template/src/components/ui/breadcrumb/Breadcrumb.vue +17 -0
  49. package/template/src/components/ui/breadcrumb/BreadcrumbEllipsis.vue +23 -0
  50. package/template/src/components/ui/breadcrumb/BreadcrumbItem.vue +17 -0
  51. package/template/src/components/ui/breadcrumb/BreadcrumbLink.vue +21 -0
  52. package/template/src/components/ui/breadcrumb/BreadcrumbList.vue +17 -0
  53. package/template/src/components/ui/breadcrumb/BreadcrumbPage.vue +20 -0
  54. package/template/src/components/ui/breadcrumb/BreadcrumbSeparator.vue +22 -0
  55. package/template/src/components/ui/breadcrumb/index.ts +7 -0
  56. package/template/src/components/ui/button/Button.spec.ts +46 -0
  57. package/template/src/components/ui/button/Button.vue +31 -0
  58. package/template/src/components/ui/button/index.ts +41 -0
  59. package/template/src/components/ui/calendar/Calendar.vue +106 -0
  60. package/template/src/components/ui/calendar/index.ts +1 -0
  61. package/template/src/components/ui/card/Card.vue +29 -0
  62. package/template/src/components/ui/card/CardAction.vue +17 -0
  63. package/template/src/components/ui/card/CardContent.vue +17 -0
  64. package/template/src/components/ui/card/CardDescription.vue +17 -0
  65. package/template/src/components/ui/card/CardFooter.vue +17 -0
  66. package/template/src/components/ui/card/CardHeader.vue +29 -0
  67. package/template/src/components/ui/card/CardTitle.vue +17 -0
  68. package/template/src/components/ui/card/index.ts +7 -0
  69. package/template/src/components/ui/chart/AreaChart.vue +143 -0
  70. package/template/src/components/ui/chart/BarChart.vue +142 -0
  71. package/template/src/components/ui/chart/DonutChart.vue +75 -0
  72. package/template/src/components/ui/chart/LineChart.vue +132 -0
  73. package/template/src/components/ui/chart/index.ts +4 -0
  74. package/template/src/components/ui/checkbox/Checkbox.vue +57 -0
  75. package/template/src/components/ui/checkbox/index.ts +1 -0
  76. package/template/src/components/ui/collapsible/Collapsible.vue +15 -0
  77. package/template/src/components/ui/collapsible/CollapsibleContent.vue +18 -0
  78. package/template/src/components/ui/collapsible/CollapsibleTrigger.vue +12 -0
  79. package/template/src/components/ui/collapsible/index.ts +3 -0
  80. package/template/src/components/ui/command/Command.vue +88 -0
  81. package/template/src/components/ui/command/CommandDialog.vue +31 -0
  82. package/template/src/components/ui/command/CommandEmpty.vue +27 -0
  83. package/template/src/components/ui/command/CommandGroup.vue +45 -0
  84. package/template/src/components/ui/command/CommandInput.vue +39 -0
  85. package/template/src/components/ui/command/CommandItem.vue +76 -0
  86. package/template/src/components/ui/command/CommandList.vue +25 -0
  87. package/template/src/components/ui/command/CommandSeparator.vue +21 -0
  88. package/template/src/components/ui/command/CommandShortcut.vue +17 -0
  89. package/template/src/components/ui/command/index.ts +25 -0
  90. package/template/src/components/ui/context-menu/ContextMenu.vue +15 -0
  91. package/template/src/components/ui/context-menu/ContextMenuCheckboxItem.vue +36 -0
  92. package/template/src/components/ui/context-menu/ContextMenuContent.vue +33 -0
  93. package/template/src/components/ui/context-menu/ContextMenuGroup.vue +12 -0
  94. package/template/src/components/ui/context-menu/ContextMenuItem.vue +36 -0
  95. package/template/src/components/ui/context-menu/ContextMenuLabel.vue +25 -0
  96. package/template/src/components/ui/context-menu/ContextMenuRadioGroup.vue +18 -0
  97. package/template/src/components/ui/context-menu/ContextMenuRadioItem.vue +36 -0
  98. package/template/src/components/ui/context-menu/ContextMenuSeparator.vue +18 -0
  99. package/template/src/components/ui/context-menu/ContextMenuShortcut.vue +17 -0
  100. package/template/src/components/ui/context-menu/ContextMenuSub.vue +15 -0
  101. package/template/src/components/ui/context-menu/ContextMenuSubContent.vue +36 -0
  102. package/template/src/components/ui/context-menu/ContextMenuSubTrigger.vue +32 -0
  103. package/template/src/components/ui/context-menu/ContextMenuTrigger.vue +13 -0
  104. package/template/src/components/ui/context-menu/index.ts +14 -0
  105. package/template/src/components/ui/date-picker/DatePicker.vue +233 -0
  106. package/template/src/components/ui/date-picker/index.ts +1 -0
  107. package/template/src/components/ui/dialog/Dialog.vue +19 -0
  108. package/template/src/components/ui/dialog/DialogClose.vue +15 -0
  109. package/template/src/components/ui/dialog/DialogContent.vue +53 -0
  110. package/template/src/components/ui/dialog/DialogDescription.vue +23 -0
  111. package/template/src/components/ui/dialog/DialogFooter.vue +27 -0
  112. package/template/src/components/ui/dialog/DialogHeader.vue +17 -0
  113. package/template/src/components/ui/dialog/DialogOverlay.vue +21 -0
  114. package/template/src/components/ui/dialog/DialogScrollContent.vue +59 -0
  115. package/template/src/components/ui/dialog/DialogTitle.vue +23 -0
  116. package/template/src/components/ui/dialog/DialogTrigger.vue +15 -0
  117. package/template/src/components/ui/dialog/index.ts +10 -0
  118. package/template/src/components/ui/dropdown-menu/DropdownMenu.vue +19 -0
  119. package/template/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +39 -0
  120. package/template/src/components/ui/dropdown-menu/DropdownMenuContent.vue +41 -0
  121. package/template/src/components/ui/dropdown-menu/DropdownMenuGroup.vue +15 -0
  122. package/template/src/components/ui/dropdown-menu/DropdownMenuItem.vue +31 -0
  123. package/template/src/components/ui/dropdown-menu/DropdownMenuLabel.vue +23 -0
  124. package/template/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +21 -0
  125. package/template/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +40 -0
  126. package/template/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue +23 -0
  127. package/template/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue +17 -0
  128. package/template/src/components/ui/dropdown-menu/DropdownMenuSub.vue +18 -0
  129. package/template/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue +27 -0
  130. package/template/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +31 -0
  131. package/template/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue +17 -0
  132. package/template/src/components/ui/dropdown-menu/index.ts +16 -0
  133. package/template/src/components/ui/form/FormControl.vue +17 -0
  134. package/template/src/components/ui/form/FormDescription.vue +21 -0
  135. package/template/src/components/ui/form/FormItem.vue +23 -0
  136. package/template/src/components/ui/form/FormLabel.vue +25 -0
  137. package/template/src/components/ui/form/FormMessage.vue +23 -0
  138. package/template/src/components/ui/form/index.ts +7 -0
  139. package/template/src/components/ui/form/injectionKeys.ts +4 -0
  140. package/template/src/components/ui/form/useFormField.ts +30 -0
  141. package/template/src/components/ui/hover-card/HoverCard.vue +19 -0
  142. package/template/src/components/ui/hover-card/HoverCardContent.vue +43 -0
  143. package/template/src/components/ui/hover-card/HoverCardTrigger.vue +15 -0
  144. package/template/src/components/ui/hover-card/index.ts +3 -0
  145. package/template/src/components/ui/index.ts +47 -0
  146. package/template/src/components/ui/input/Input.vue +33 -0
  147. package/template/src/components/ui/input/InputAddon.vue +29 -0
  148. package/template/src/components/ui/input/InputGroup.vue +17 -0
  149. package/template/src/components/ui/input/InputIcon.vue +29 -0
  150. package/template/src/components/ui/input/index.ts +4 -0
  151. package/template/src/components/ui/kbd/Kbd.vue +30 -0
  152. package/template/src/components/ui/kbd/index.ts +1 -0
  153. package/template/src/components/ui/label/Label.vue +26 -0
  154. package/template/src/components/ui/label/index.ts +1 -0
  155. package/template/src/components/ui/loading-bar/LoadingBar.vue +41 -0
  156. package/template/src/components/ui/loading-bar/index.ts +2 -0
  157. package/template/src/components/ui/pagination/Pagination.vue +26 -0
  158. package/template/src/components/ui/pagination/PaginationContent.vue +22 -0
  159. package/template/src/components/ui/pagination/PaginationEllipsis.vue +25 -0
  160. package/template/src/components/ui/pagination/PaginationFirst.vue +33 -0
  161. package/template/src/components/ui/pagination/PaginationItem.vue +34 -0
  162. package/template/src/components/ui/pagination/PaginationLast.vue +33 -0
  163. package/template/src/components/ui/pagination/PaginationNext.vue +33 -0
  164. package/template/src/components/ui/pagination/PaginationPrevious.vue +33 -0
  165. package/template/src/components/ui/pagination/index.ts +8 -0
  166. package/template/src/components/ui/pin-input/PinInput.vue +28 -0
  167. package/template/src/components/ui/pin-input/PinInputGroup.vue +18 -0
  168. package/template/src/components/ui/pin-input/PinInputInput.vue +24 -0
  169. package/template/src/components/ui/pin-input/PinInputSeparator.vue +21 -0
  170. package/template/src/components/ui/pin-input/index.ts +4 -0
  171. package/template/src/components/ui/popover/Popover.vue +19 -0
  172. package/template/src/components/ui/popover/PopoverAnchor.vue +15 -0
  173. package/template/src/components/ui/popover/PopoverContent.vue +45 -0
  174. package/template/src/components/ui/popover/PopoverTrigger.vue +15 -0
  175. package/template/src/components/ui/popover/index.ts +4 -0
  176. package/template/src/components/ui/progress/Progress.vue +35 -0
  177. package/template/src/components/ui/progress/index.ts +1 -0
  178. package/template/src/components/ui/radio-group/RadioGroup.vue +23 -0
  179. package/template/src/components/ui/radio-group/RadioGroupItem.vue +30 -0
  180. package/template/src/components/ui/radio-group/index.ts +2 -0
  181. package/template/src/components/ui/scroll-area/ScrollArea.vue +40 -0
  182. package/template/src/components/ui/scroll-area/ScrollBar.vue +44 -0
  183. package/template/src/components/ui/scroll-area/index.ts +2 -0
  184. package/template/src/components/ui/select/Select.vue +19 -0
  185. package/template/src/components/ui/select/SelectContent.vue +51 -0
  186. package/template/src/components/ui/select/SelectGroup.vue +15 -0
  187. package/template/src/components/ui/select/SelectItem.vue +44 -0
  188. package/template/src/components/ui/select/SelectItemText.vue +15 -0
  189. package/template/src/components/ui/select/SelectLabel.vue +17 -0
  190. package/template/src/components/ui/select/SelectScrollDownButton.vue +26 -0
  191. package/template/src/components/ui/select/SelectScrollUpButton.vue +26 -0
  192. package/template/src/components/ui/select/SelectSeparator.vue +19 -0
  193. package/template/src/components/ui/select/SelectTrigger.vue +33 -0
  194. package/template/src/components/ui/select/SelectValue.vue +15 -0
  195. package/template/src/components/ui/select/index.ts +11 -0
  196. package/template/src/components/ui/separator/Separator.vue +29 -0
  197. package/template/src/components/ui/separator/index.ts +1 -0
  198. package/template/src/components/ui/sheet/Sheet.vue +19 -0
  199. package/template/src/components/ui/sheet/SheetClose.vue +15 -0
  200. package/template/src/components/ui/sheet/SheetContent.vue +62 -0
  201. package/template/src/components/ui/sheet/SheetDescription.vue +21 -0
  202. package/template/src/components/ui/sheet/SheetFooter.vue +16 -0
  203. package/template/src/components/ui/sheet/SheetHeader.vue +15 -0
  204. package/template/src/components/ui/sheet/SheetOverlay.vue +21 -0
  205. package/template/src/components/ui/sheet/SheetTitle.vue +21 -0
  206. package/template/src/components/ui/sheet/SheetTrigger.vue +15 -0
  207. package/template/src/components/ui/sheet/index.ts +8 -0
  208. package/template/src/components/ui/sidebar/Sidebar.vue +113 -0
  209. package/template/src/components/ui/sidebar/SidebarContent.vue +18 -0
  210. package/template/src/components/ui/sidebar/SidebarControl.vue +53 -0
  211. package/template/src/components/ui/sidebar/SidebarFooter.vue +18 -0
  212. package/template/src/components/ui/sidebar/SidebarGroup.vue +18 -0
  213. package/template/src/components/ui/sidebar/SidebarGroupAction.vue +27 -0
  214. package/template/src/components/ui/sidebar/SidebarGroupContent.vue +18 -0
  215. package/template/src/components/ui/sidebar/SidebarGroupLabel.vue +25 -0
  216. package/template/src/components/ui/sidebar/SidebarHeader.vue +18 -0
  217. package/template/src/components/ui/sidebar/SidebarInput.vue +22 -0
  218. package/template/src/components/ui/sidebar/SidebarInset.vue +21 -0
  219. package/template/src/components/ui/sidebar/SidebarMenu.vue +18 -0
  220. package/template/src/components/ui/sidebar/SidebarMenuAction.vue +35 -0
  221. package/template/src/components/ui/sidebar/SidebarMenuBadge.vue +26 -0
  222. package/template/src/components/ui/sidebar/SidebarMenuButton.vue +48 -0
  223. package/template/src/components/ui/sidebar/SidebarMenuButtonChild.vue +36 -0
  224. package/template/src/components/ui/sidebar/SidebarMenuItem.vue +18 -0
  225. package/template/src/components/ui/sidebar/SidebarMenuSkeleton.vue +35 -0
  226. package/template/src/components/ui/sidebar/SidebarMenuSub.vue +22 -0
  227. package/template/src/components/ui/sidebar/SidebarMenuSubButton.vue +36 -0
  228. package/template/src/components/ui/sidebar/SidebarMenuSubItem.vue +18 -0
  229. package/template/src/components/ui/sidebar/SidebarProvider.vue +136 -0
  230. package/template/src/components/ui/sidebar/SidebarRail.vue +33 -0
  231. package/template/src/components/ui/sidebar/SidebarSeparator.vue +19 -0
  232. package/template/src/components/ui/sidebar/SidebarTrigger.vue +27 -0
  233. package/template/src/components/ui/sidebar/index.ts +61 -0
  234. package/template/src/components/ui/sidebar/utils.ts +27 -0
  235. package/template/src/components/ui/skeleton/Skeleton.vue +17 -0
  236. package/template/src/components/ui/skeleton/index.ts +1 -0
  237. package/template/src/components/ui/slider/Slider.vue +42 -0
  238. package/template/src/components/ui/slider/index.ts +1 -0
  239. package/template/src/components/ui/sonner/Sonner.vue +42 -0
  240. package/template/src/components/ui/sonner/index.ts +2 -0
  241. package/template/src/components/ui/stepper/Stepper.vue +102 -0
  242. package/template/src/components/ui/stepper/index.ts +1 -0
  243. package/template/src/components/ui/switch/Switch.vue +38 -0
  244. package/template/src/components/ui/switch/index.ts +1 -0
  245. package/template/src/components/ui/table/Table.vue +16 -0
  246. package/template/src/components/ui/table/TableBody.vue +17 -0
  247. package/template/src/components/ui/table/TableCaption.vue +17 -0
  248. package/template/src/components/ui/table/TableCell.vue +22 -0
  249. package/template/src/components/ui/table/TableEmpty.vue +34 -0
  250. package/template/src/components/ui/table/TableFooter.vue +17 -0
  251. package/template/src/components/ui/table/TableHead.vue +17 -0
  252. package/template/src/components/ui/table/TableHeader.vue +17 -0
  253. package/template/src/components/ui/table/TableRow.vue +17 -0
  254. package/template/src/components/ui/table/index.ts +9 -0
  255. package/template/src/components/ui/table/utils.ts +10 -0
  256. package/template/src/components/ui/tabs/Tabs.vue +24 -0
  257. package/template/src/components/ui/tabs/TabsContent.vue +21 -0
  258. package/template/src/components/ui/tabs/TabsList.vue +24 -0
  259. package/template/src/components/ui/tabs/TabsTrigger.vue +26 -0
  260. package/template/src/components/ui/tabs/index.ts +4 -0
  261. package/template/src/components/ui/textarea/Textarea.vue +28 -0
  262. package/template/src/components/ui/textarea/index.ts +1 -0
  263. package/template/src/components/ui/timeline/Timeline.vue +73 -0
  264. package/template/src/components/ui/timeline/index.ts +1 -0
  265. package/template/src/components/ui/toggle/Toggle.vue +35 -0
  266. package/template/src/components/ui/toggle/index.ts +28 -0
  267. package/template/src/components/ui/toggle-group/ToggleGroup.vue +41 -0
  268. package/template/src/components/ui/toggle-group/ToggleGroupItem.vue +43 -0
  269. package/template/src/components/ui/toggle-group/index.ts +12 -0
  270. package/template/src/components/ui/tooltip/Tooltip.vue +19 -0
  271. package/template/src/components/ui/tooltip/TooltipContent.vue +34 -0
  272. package/template/src/components/ui/tooltip/TooltipProvider.vue +14 -0
  273. package/template/src/components/ui/tooltip/TooltipTrigger.vue +15 -0
  274. package/template/src/components/ui/tooltip/index.ts +4 -0
  275. package/template/src/components.d.ts +228 -0
  276. package/template/src/composables/useFormatter.ts +105 -0
  277. package/template/src/composables/useThemeConfig.ts +252 -0
  278. package/template/src/layouts/AdminLayout.vue +55 -0
  279. package/template/src/layouts/AuthLayout.vue +42 -0
  280. package/template/src/lib/axios.spec.ts +46 -0
  281. package/template/src/lib/axios.ts +243 -0
  282. package/template/src/lib/formatters.spec.ts +137 -0
  283. package/template/src/lib/formatters.ts +332 -0
  284. package/template/src/lib/loading-bar.ts +103 -0
  285. package/template/src/lib/utils.spec.ts +26 -0
  286. package/template/src/lib/utils.ts +7 -0
  287. package/template/src/lib/validation.spec.ts +78 -0
  288. package/template/src/main.ts +13 -0
  289. package/template/src/router/index.ts +147 -0
  290. package/template/src/services/auth.service.spec.ts +93 -0
  291. package/template/src/services/auth.service.ts +243 -0
  292. package/template/src/services/index.ts +3 -0
  293. package/template/src/services/types.ts +28 -0
  294. package/template/src/services/user.service.spec.ts +91 -0
  295. package/template/src/services/user.service.ts +283 -0
  296. package/template/src/stores/auth.spec.ts +52 -0
  297. package/template/src/stores/auth.ts +130 -0
  298. package/template/src/style.css +587 -0
  299. package/template/src/views/_starter/BlankView.vue +47 -0
  300. package/template/src/views/auth/ConfirmEmailView.vue +209 -0
  301. package/template/src/views/auth/ForgotPasswordView.vue +151 -0
  302. package/template/src/views/auth/LockScreenView.vue +175 -0
  303. package/template/src/views/auth/LoginView.vue +54 -0
  304. package/template/src/views/auth/RegisterView.vue +265 -0
  305. package/template/src/views/auth/ResetPasswordView.vue +260 -0
  306. package/template/src/views/auth/VerifyOtpView.vue +261 -0
  307. package/template/src/views/dashboard/IndexView.vue +159 -0
  308. package/template/src/views/errors/ComingSoonView.vue +241 -0
  309. package/template/src/views/errors/MaintenanceView.vue +252 -0
  310. package/template/src/views/errors/NotFoundView.vue +81 -0
  311. package/template/src/views/errors/ServerErrorView.vue +101 -0
  312. package/template/src/views/errors/UnauthorizedView.vue +64 -0
  313. package/template/tsconfig.app.json +27 -0
  314. package/template/tsconfig.json +19 -0
  315. package/template/tsconfig.node.json +23 -0
  316. package/template/vite.config.ts +46 -0
@@ -0,0 +1,332 @@
1
+ /**
2
+ * Enterprise Data Formatting Utilities
3
+ * Uses native browser Intl APIs for optimal performance without external libraries.
4
+ *
5
+ * Global defaults can be configured via environment variables:
6
+ * - VITE_DEFAULT_LOCALE (e.g. "en-US", "id-ID", "de-DE") → default: "en-US"
7
+ * - VITE_DEFAULT_CURRENCY (e.g. "USD", "IDR", "EUR") → default: "USD"
8
+ */
9
+
10
+ /** Global locale — override via VITE_DEFAULT_LOCALE in .env */
11
+ const DEFAULT_LOCALE: string = import.meta.env.VITE_DEFAULT_LOCALE ?? 'en-US'
12
+
13
+ /** Global currency — override via VITE_DEFAULT_CURRENCY in .env */
14
+ const DEFAULT_CURRENCY: string = import.meta.env.VITE_DEFAULT_CURRENCY ?? 'USD'
15
+
16
+ /** Global timezone — override via VITE_DEFAULT_TIMEZONE in .env (e.g. "Asia/Jakarta") */
17
+ const DEFAULT_TIMEZONE: string = import.meta.env.VITE_DEFAULT_TIMEZONE ?? 'Asia/Jakarta'
18
+
19
+ /** Global time format — override via VITE_DEFAULT_TIME_FORMAT in .env ('24h' | '12h' | 'auto') */
20
+ export type TimeFormat = '24h' | '12h' | 'auto'
21
+ const DEFAULT_TIME_FORMAT: TimeFormat = (import.meta.env.VITE_DEFAULT_TIME_FORMAT as TimeFormat) ?? '24h'
22
+
23
+ export interface CurrencyFormatOptions {
24
+ currency?: string
25
+ locale?: string
26
+ minimumFractionDigits?: number
27
+ maximumFractionDigits?: number
28
+ }
29
+
30
+ export interface DateTimeFormatOptions {
31
+ locale?: string
32
+ timeZone?: string
33
+ format?: TimeFormat
34
+ dateStyle?: 'short' | 'medium' | 'long' | 'full'
35
+ timeStyle?: 'short' | 'medium' | 'long'
36
+ showSeconds?: boolean
37
+ }
38
+
39
+ export interface TimeOnlyFormatOptions {
40
+ locale?: string
41
+ timeZone?: string
42
+ format?: TimeFormat
43
+ showSeconds?: boolean
44
+ }
45
+
46
+ /**
47
+ * Format numeric value to localized currency
48
+ * @example formatCurrency(1500, { currency: 'USD' }) => "$1,500.00"
49
+ * @example formatCurrency(50000, { currency: 'IDR', locale: 'id-ID' }) => "Rp 50.000"
50
+ */
51
+ export function formatCurrency(
52
+ value: number | string | null | undefined,
53
+ options: CurrencyFormatOptions = {},
54
+ ): string {
55
+ if (value === null || value === undefined || value === '') return '-'
56
+ const numeric = typeof value === 'string' ? Number(value) : value
57
+ if (Number.isNaN(numeric)) return '-'
58
+
59
+ const {
60
+ currency = DEFAULT_CURRENCY,
61
+ locale = DEFAULT_LOCALE,
62
+ minimumFractionDigits = currency === 'IDR' ? 0 : 2,
63
+ maximumFractionDigits = currency === 'IDR' ? 0 : 2,
64
+ } = options
65
+
66
+ try {
67
+ return new Intl.NumberFormat(locale, {
68
+ style: 'currency',
69
+ currency,
70
+ minimumFractionDigits,
71
+ maximumFractionDigits,
72
+ }).format(numeric)
73
+ } catch {
74
+ return `${currency} ${numeric.toLocaleString()}`
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Format numeric value with thousands separators
80
+ * @example formatNumber(1250000) => "1,250,000"
81
+ */
82
+ export function formatNumber(
83
+ value: number | string | null | undefined,
84
+ locale = DEFAULT_LOCALE,
85
+ ): string {
86
+ if (value === null || value === undefined || value === '') return '-'
87
+ const numeric = typeof value === 'string' ? Number(value) : value
88
+ if (Number.isNaN(numeric)) return '-'
89
+
90
+ return new Intl.NumberFormat(locale).format(numeric)
91
+ }
92
+
93
+ /**
94
+ * Format large numbers to compact notation (e.g., 1.5K, 2.4M)
95
+ * @example formatCompactNumber(15400) => "15.4K"
96
+ */
97
+ export function formatCompactNumber(
98
+ value: number | string | null | undefined,
99
+ locale = DEFAULT_LOCALE,
100
+ ): string {
101
+ if (value === null || value === undefined || value === '') return '-'
102
+ const numeric = typeof value === 'string' ? Number(value) : value
103
+ if (Number.isNaN(numeric)) return '-'
104
+
105
+ return new Intl.NumberFormat(locale, {
106
+ notation: 'compact',
107
+ compactDisplay: 'short',
108
+ maximumFractionDigits: 1,
109
+ }).format(numeric)
110
+ }
111
+
112
+ /**
113
+ * Format date string / Date object to localized readable string
114
+ * @example formatDate('2026-08-21T07:00:00Z') => "Aug 21, 2026"
115
+ * @example formatDate('2026-08-21', 'full') => "Friday, August 21, 2026"
116
+ */
117
+ export function formatDate(
118
+ date: string | number | Date | null | undefined,
119
+ style: 'short' | 'medium' | 'long' | 'full' = 'medium',
120
+ locale = DEFAULT_LOCALE,
121
+ ): string {
122
+ if (!date) return '-'
123
+ const parsedDate = date instanceof Date ? date : new Date(date)
124
+ if (Number.isNaN(parsedDate.getTime())) return '-'
125
+
126
+ const dateStyleMap: Record<'short' | 'medium' | 'long' | 'full', Intl.DateTimeFormatOptions['dateStyle']> = {
127
+ short: 'short',
128
+ medium: 'medium',
129
+ long: 'long',
130
+ full: 'full',
131
+ }
132
+
133
+ return new Intl.DateTimeFormat(locale, {
134
+ dateStyle: dateStyleMap[style],
135
+ }).format(parsedDate)
136
+ }
137
+
138
+ /**
139
+ * Helper to resolve hour12 boolean from '24h' | '12h' | 'auto'
140
+ */
141
+ function resolveHour12(format: TimeFormat = DEFAULT_TIME_FORMAT): boolean | undefined {
142
+ if (format === '24h') return false
143
+ if (format === '12h') return true
144
+ return undefined // auto: browser locale determines 12h/24h
145
+ }
146
+
147
+ /**
148
+ * Format date to include time string with timezone and 24h/12h clock support
149
+ * @example formatDateTime('2026-08-21T07:30:00Z') => "Aug 21, 2026, 14:30" (if 24h & WIB)
150
+ * @example formatDateTime(date, { format: '12h' }) => "Aug 21, 2026, 02:30 PM"
151
+ */
152
+ export function formatDateTime(
153
+ date: string | number | Date | null | undefined,
154
+ options: DateTimeFormatOptions | string = {},
155
+ ): string {
156
+ if (!date) return '-'
157
+ const parsedDate = date instanceof Date ? date : new Date(date)
158
+ if (Number.isNaN(parsedDate.getTime())) return '-'
159
+
160
+ const opts: DateTimeFormatOptions = typeof options === 'string' ? { locale: options } : options
161
+ const {
162
+ locale = DEFAULT_LOCALE,
163
+ timeZone = DEFAULT_TIMEZONE,
164
+ format = DEFAULT_TIME_FORMAT,
165
+ dateStyle = 'medium',
166
+ timeStyle = 'short',
167
+ } = opts
168
+
169
+ return new Intl.DateTimeFormat(locale, {
170
+ dateStyle,
171
+ timeStyle,
172
+ timeZone,
173
+ hour12: resolveHour12(format),
174
+ }).format(parsedDate)
175
+ }
176
+
177
+ /**
178
+ * Format time only (without date) with 24h / 12h clock support
179
+ * @example formatTime('2026-08-21T07:30:00Z') => "14:30" (if 24h & WIB)
180
+ * @example formatTime('2026-08-21T07:30:00Z', { format: '12h' }) => "02:30 PM"
181
+ * @example formatTime('2026-08-21T07:30:00Z', { showSeconds: true }) => "14:30:00"
182
+ */
183
+ export function formatTime(
184
+ date: string | number | Date | null | undefined,
185
+ options: TimeOnlyFormatOptions = {},
186
+ ): string {
187
+ if (!date) return '-'
188
+ const parsedDate = date instanceof Date ? date : new Date(date)
189
+ if (Number.isNaN(parsedDate.getTime())) return '-'
190
+
191
+ const {
192
+ locale = DEFAULT_LOCALE,
193
+ timeZone = DEFAULT_TIMEZONE,
194
+ format = DEFAULT_TIME_FORMAT,
195
+ showSeconds = false,
196
+ } = options
197
+
198
+ return new Intl.DateTimeFormat(locale, {
199
+ hour: '2-digit',
200
+ minute: '2-digit',
201
+ ...(showSeconds ? { second: '2-digit' } : {}),
202
+ timeZone,
203
+ hour12: resolveHour12(format),
204
+ }).format(parsedDate)
205
+ }
206
+
207
+ /**
208
+ * Format relative time (e.g. "5 minutes ago", "in 2 hours", "kemarin")
209
+ * Fully locale-aware including the "just now" / "baru saja" fallback phrase.
210
+ */
211
+ export function formatRelativeTime(
212
+ date: string | number | Date | null | undefined,
213
+ locale = DEFAULT_LOCALE,
214
+ ): string {
215
+ if (!date) return '-'
216
+ const parsedDate = date instanceof Date ? date : new Date(date)
217
+ if (Number.isNaN(parsedDate.getTime())) return '-'
218
+
219
+ const diffInSeconds = Math.round((parsedDate.getTime() - Date.now()) / 1000)
220
+ const absDiff = Math.abs(diffInSeconds)
221
+
222
+ // Locale-aware "just now" phrase map — add more locales as needed
223
+ const justNowPhrases: Record<string, string> = {
224
+ 'id-ID': 'baru saja',
225
+ 'id': 'baru saja',
226
+ 'de-DE': 'gerade eben',
227
+ 'fr-FR': "à l'instant",
228
+ 'ja-JP': 'たった今',
229
+ 'zh-CN': '刚才',
230
+ }
231
+ const justNow = justNowPhrases[locale] ?? 'just now'
232
+
233
+ if (absDiff < 45) return justNow
234
+
235
+ const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' })
236
+
237
+ const intervals = [
238
+ { unit: 'year', seconds: 31536000 },
239
+ { unit: 'month', seconds: 2592000 },
240
+ { unit: 'week', seconds: 604800 },
241
+ { unit: 'day', seconds: 86400 },
242
+ { unit: 'hour', seconds: 3600 },
243
+ { unit: 'minute', seconds: 60 },
244
+ ] as const
245
+
246
+ for (const { unit, seconds } of intervals) {
247
+ if (absDiff >= seconds) {
248
+ const delta = Math.round(diffInSeconds / seconds)
249
+ return rtf.format(delta, unit)
250
+ }
251
+ }
252
+
253
+ return justNow
254
+ }
255
+
256
+ /**
257
+ * Format raw bytes into human-readable data units (KB, MB, GB)
258
+ * Uses locale-aware decimal separator (e.g. "1,5 MB" for id-ID, "1.5 MB" for en-US)
259
+ * @example formatBytes(1048576) => "1 MB"
260
+ * @example formatBytes(1536, 1, 'id-ID') => "1,5 KB"
261
+ */
262
+ export function formatBytes(
263
+ bytes: number | null | undefined,
264
+ decimals = 2,
265
+ locale = DEFAULT_LOCALE,
266
+ ): string {
267
+ if (bytes === null || bytes === undefined || Number.isNaN(bytes)) return '-'
268
+ if (bytes === 0) return '0 B'
269
+
270
+ const k = 1024
271
+ const dm = decimals < 0 ? 0 : decimals
272
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
273
+ const i = Math.floor(Math.log(Math.abs(bytes)) / Math.log(k))
274
+ const value = bytes / Math.pow(k, i)
275
+
276
+ const formatted = new Intl.NumberFormat(locale, {
277
+ minimumFractionDigits: 0,
278
+ maximumFractionDigits: dm,
279
+ }).format(value)
280
+
281
+ return `${formatted} ${sizes[i]}`
282
+ }
283
+
284
+ /**
285
+ * Format date with time and explicit timezone (important for Indonesia: WIB/WITA/WIT)
286
+ * @param timeZone IANA timezone string. Indonesia: 'Asia/Jakarta' (WIB), 'Asia/Makassar' (WITA), 'Asia/Jayapura' (WIT)
287
+ * @example formatDateTimeWithTz(date, 'Asia/Jakarta', '24h') => "21 Agt 2026, 14.30"
288
+ */
289
+ export function formatDateTimeWithTz(
290
+ date: string | number | Date | null | undefined,
291
+ timeZone: string = DEFAULT_TIMEZONE,
292
+ format: TimeFormat = DEFAULT_TIME_FORMAT,
293
+ locale = DEFAULT_LOCALE,
294
+ ): string {
295
+ if (!date) return '-'
296
+ const parsedDate = date instanceof Date ? date : new Date(date)
297
+ if (Number.isNaN(parsedDate.getTime())) return '-'
298
+
299
+ return new Intl.DateTimeFormat(locale, {
300
+ dateStyle: 'medium',
301
+ timeStyle: 'short',
302
+ timeZone,
303
+ hour12: resolveHour12(format),
304
+ }).format(parsedDate)
305
+ }
306
+
307
+ /**
308
+ * Get 2-letter uppercase initials from a full name (safe for Avatar fallback)
309
+ * @example getInitials("Olivia Martin") => "OM"
310
+ * @example getInitials("John") => "JO"
311
+ */
312
+ export function getInitials(name?: string | null): string {
313
+ if (!name || typeof name !== 'string') return 'NA'
314
+ const trimmed = name.trim()
315
+ if (!trimmed) return 'NA'
316
+
317
+ const parts = trimmed.split(/\s+/).filter(Boolean)
318
+ if (parts.length === 1) {
319
+ return parts[0].slice(0, 2).toUpperCase()
320
+ }
321
+ return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase()
322
+ }
323
+
324
+ /**
325
+ * Truncate long strings with ellipsis
326
+ * @example truncate("This is a very long text", 10) => "This is a..."
327
+ */
328
+ export function truncate(text: string | null | undefined, length = 30): string {
329
+ if (!text) return ''
330
+ if (text.length <= length) return text
331
+ return `${text.slice(0, length)}...`
332
+ }
@@ -0,0 +1,103 @@
1
+ import { ref } from 'vue'
2
+
3
+ export interface LoadingBarState {
4
+ progress: number
5
+ status: 'idle' | 'loading' | 'success' | 'error'
6
+ visible: boolean
7
+ }
8
+
9
+ const progress = ref(0)
10
+ const status = ref<'idle' | 'loading' | 'success' | 'error'>('idle')
11
+ const visible = ref(false)
12
+
13
+ let timer: ReturnType<typeof setInterval> | null = null
14
+ let activeRequests = 0
15
+
16
+ function clearTimer() {
17
+ if (timer) {
18
+ clearInterval(timer)
19
+ timer = null
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Global Top Loading Bar Controller
25
+ * Supports multi-request counting and automatic micro-increments.
26
+ */
27
+ export const loadingBar = {
28
+ progress,
29
+ status,
30
+ visible,
31
+
32
+ /**
33
+ * Start or increment progress
34
+ */
35
+ start() {
36
+ activeRequests++
37
+ if (activeRequests > 1) return
38
+
39
+ clearTimer()
40
+ status.value = 'loading'
41
+ visible.value = true
42
+ progress.value = 10
43
+
44
+ timer = setInterval(() => {
45
+ if (progress.value < 85) {
46
+ // Slow down as progress approaches 85%
47
+ const inc = Math.max(1, (90 - progress.value) / 10)
48
+ progress.value = Math.min(85, progress.value + inc)
49
+ }
50
+ }, 150)
51
+ },
52
+
53
+ /**
54
+ * Finish loading with a smooth transition to 100%
55
+ */
56
+ finish() {
57
+ activeRequests = Math.max(0, activeRequests - 1)
58
+ if (activeRequests > 0) return
59
+
60
+ clearTimer()
61
+ progress.value = 100
62
+ status.value = 'success'
63
+
64
+ setTimeout(() => {
65
+ visible.value = false
66
+ setTimeout(() => {
67
+ progress.value = 0
68
+ status.value = 'idle'
69
+ }, 200)
70
+ }, 300)
71
+ },
72
+
73
+ /**
74
+ * Fail loading bar with an error state (red bar)
75
+ */
76
+ fail() {
77
+ activeRequests = Math.max(0, activeRequests - 1)
78
+ if (activeRequests > 0) return
79
+
80
+ clearTimer()
81
+ progress.value = 100
82
+ status.value = 'error'
83
+
84
+ setTimeout(() => {
85
+ visible.value = false
86
+ setTimeout(() => {
87
+ progress.value = 0
88
+ status.value = 'idle'
89
+ }, 200)
90
+ }, 400)
91
+ },
92
+
93
+ /**
94
+ * Force reset loading bar
95
+ */
96
+ reset() {
97
+ activeRequests = 0
98
+ clearTimer()
99
+ visible.value = false
100
+ progress.value = 0
101
+ status.value = 'idle'
102
+ },
103
+ }
@@ -0,0 +1,26 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { cn } from './utils'
3
+
4
+ describe('cn utility', () => {
5
+ it('merges class names correctly', () => {
6
+ const result = cn('px-2 py-1', 'bg-blue-500')
7
+ expect(result).toBe('px-2 py-1 bg-blue-500')
8
+ })
9
+
10
+ it('handles conditional class names', () => {
11
+ const isActive = true
12
+ const isDisabled = false
13
+ const result = cn('btn', isActive && 'btn-active', isDisabled && 'btn-disabled')
14
+ expect(result).toBe('btn btn-active')
15
+ })
16
+
17
+ it('resolves tailwind class conflicts using tailwind-merge', () => {
18
+ const result = cn('p-4', 'p-2', 'text-red-500', 'text-blue-500')
19
+ expect(result).toBe('p-2 text-blue-500')
20
+ })
21
+
22
+ it('handles array and object inputs', () => {
23
+ const result = cn(['base-class', 'extra-class'], { 'is-open': true, 'is-hidden': false })
24
+ expect(result).toBe('base-class extra-class is-open')
25
+ })
26
+ })
@@ -0,0 +1,7 @@
1
+ import type { ClassValue } from "clsx"
2
+ import { clsx } from "clsx"
3
+ import { twMerge } from "tailwind-merge"
4
+
5
+ export function cn(...inputs: ClassValue[]) {
6
+ return twMerge(clsx(inputs))
7
+ }
@@ -0,0 +1,78 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { z } from 'zod'
3
+
4
+ const loginSchema = z.object({
5
+ email: z.string().min(1, 'Email is required').email('Invalid email address'),
6
+ password: z.string().min(6, 'Password must be at least 6 characters'),
7
+ })
8
+
9
+ const userFormSchema = z.object({
10
+ name: z.string().min(2, 'Name must be at least 2 characters'),
11
+ email: z.string().email('Invalid email address'),
12
+ role: z.enum(['Admin', 'Developer', 'Editor', 'Viewer']),
13
+ status: z.enum(['Active', 'Pending', 'Suspended']),
14
+ twoFactor: z.boolean().default(false),
15
+ })
16
+
17
+ describe('Form Validation Schemas (Zod)', () => {
18
+ describe('loginSchema', () => {
19
+ it('validates correct email and password', () => {
20
+ const validData = {
21
+ email: 'admin@nala.dev',
22
+ password: 'password123',
23
+ }
24
+ const result = loginSchema.safeParse(validData)
25
+ expect(result.success).toBe(true)
26
+ })
27
+
28
+ it('rejects invalid email formats', () => {
29
+ const invalidData = {
30
+ email: 'not-an-email',
31
+ password: 'password123',
32
+ }
33
+ const result = loginSchema.safeParse(invalidData)
34
+ expect(result.success).toBe(false)
35
+ if (!result.success) {
36
+ expect(result.error.issues[0].message).toBe('Invalid email address')
37
+ }
38
+ })
39
+
40
+ it('rejects short passwords', () => {
41
+ const shortPass = {
42
+ email: 'admin@nala.dev',
43
+ password: '123',
44
+ }
45
+ const result = loginSchema.safeParse(shortPass)
46
+ expect(result.success).toBe(false)
47
+ if (!result.success) {
48
+ expect(result.error.issues[0].message).toBe('Password must be at least 6 characters')
49
+ }
50
+ })
51
+ })
52
+
53
+ describe('userFormSchema', () => {
54
+ it('accepts valid user creation payload', () => {
55
+ const validUser = {
56
+ name: 'Olivia Martin',
57
+ email: 'olivia@supabase.io',
58
+ role: 'Admin',
59
+ status: 'Active',
60
+ twoFactor: true,
61
+ }
62
+ const result = userFormSchema.safeParse(validUser)
63
+ expect(result.success).toBe(true)
64
+ })
65
+
66
+ it('rejects invalid role selection', () => {
67
+ const invalidUser = {
68
+ name: 'Olivia Martin',
69
+ email: 'olivia@supabase.io',
70
+ role: 'SuperMaster',
71
+ status: 'Active',
72
+ twoFactor: true,
73
+ }
74
+ const result = userFormSchema.safeParse(invalidUser)
75
+ expect(result.success).toBe(false)
76
+ })
77
+ })
78
+ })
@@ -0,0 +1,13 @@
1
+ import { createApp } from 'vue'
2
+ import { createPinia } from 'pinia'
3
+ import 'vue-sonner/style.css'
4
+ import './style.css'
5
+ import App from './App.vue'
6
+ import router from './router'
7
+
8
+ const app = createApp(App)
9
+
10
+ app.use(createPinia())
11
+ app.use(router)
12
+
13
+ app.mount('#app')