create-nala 3.0.0 → 3.0.4

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 (170) hide show
  1. package/package.json +6 -6
  2. package/template/.agents/skills/nala-project/SKILL.md +493 -0
  3. package/template/AGENTS.md +49 -0
  4. package/{LICENSE → template/LICENSE} +1 -1
  5. package/template/README.md +130 -0
  6. package/template/package.json +1 -1
  7. package/template/src/components/ui/alert/Alert.vue +8 -8
  8. package/template/src/components/ui/alert/AlertDescription.vue +5 -5
  9. package/template/src/components/ui/alert/AlertTitle.vue +5 -5
  10. package/template/src/components/ui/alert-dialog/AlertDialog.vue +6 -6
  11. package/template/src/components/ui/alert-dialog/AlertDialogAction.vue +9 -9
  12. package/template/src/components/ui/alert-dialog/AlertDialogCancel.vue +9 -9
  13. package/template/src/components/ui/alert-dialog/AlertDialogContent.vue +20 -20
  14. package/template/src/components/ui/alert-dialog/AlertDialogDescription.vue +10 -10
  15. package/template/src/components/ui/alert-dialog/AlertDialogFooter.vue +5 -5
  16. package/template/src/components/ui/alert-dialog/AlertDialogHeader.vue +5 -5
  17. package/template/src/components/ui/alert-dialog/AlertDialogTitle.vue +8 -8
  18. package/template/src/components/ui/alert-dialog/AlertDialogTrigger.vue +3 -3
  19. package/template/src/components/ui/alert-dialog/index.ts +8 -8
  20. package/template/src/components/ui/avatar/AvatarFallback.vue +8 -8
  21. package/template/src/components/ui/avatar/AvatarImage.vue +3 -3
  22. package/template/src/components/ui/breadcrumb/Breadcrumb.vue +4 -4
  23. package/template/src/components/ui/breadcrumb/BreadcrumbEllipsis.vue +6 -6
  24. package/template/src/components/ui/breadcrumb/BreadcrumbItem.vue +5 -5
  25. package/template/src/components/ui/breadcrumb/BreadcrumbLink.vue +7 -7
  26. package/template/src/components/ui/breadcrumb/BreadcrumbList.vue +5 -5
  27. package/template/src/components/ui/breadcrumb/BreadcrumbPage.vue +5 -5
  28. package/template/src/components/ui/breadcrumb/BreadcrumbSeparator.vue +6 -6
  29. package/template/src/components/ui/breadcrumb/index.ts +6 -6
  30. package/template/src/components/ui/button/Button.vue +15 -15
  31. package/template/src/components/ui/card/CardAction.vue +5 -5
  32. package/template/src/components/ui/card/CardContent.vue +5 -5
  33. package/template/src/components/ui/card/CardDescription.vue +5 -5
  34. package/template/src/components/ui/card/CardFooter.vue +5 -5
  35. package/template/src/components/ui/card/CardTitle.vue +5 -5
  36. package/template/src/components/ui/card/index.ts +6 -6
  37. package/template/src/components/ui/command/Command.vue +75 -75
  38. package/template/src/components/ui/command/CommandDialog.vue +14 -14
  39. package/template/src/components/ui/command/CommandEmpty.vue +14 -14
  40. package/template/src/components/ui/command/CommandGroup.vue +27 -27
  41. package/template/src/components/ui/command/CommandInput.vue +20 -20
  42. package/template/src/components/ui/command/CommandItem.vue +57 -57
  43. package/template/src/components/ui/command/CommandList.vue +10 -10
  44. package/template/src/components/ui/command/CommandSeparator.vue +8 -8
  45. package/template/src/components/ui/command/CommandShortcut.vue +5 -5
  46. package/template/src/components/ui/command/index.ts +24 -24
  47. package/template/src/components/ui/dialog/Dialog.vue +6 -6
  48. package/template/src/components/ui/dialog/DialogClose.vue +3 -3
  49. package/template/src/components/ui/dialog/DialogContent.vue +24 -24
  50. package/template/src/components/ui/dialog/DialogDescription.vue +10 -10
  51. package/template/src/components/ui/dialog/DialogFooter.vue +10 -10
  52. package/template/src/components/ui/dialog/DialogHeader.vue +5 -5
  53. package/template/src/components/ui/dialog/DialogOverlay.vue +8 -8
  54. package/template/src/components/ui/dialog/DialogScrollContent.vue +22 -22
  55. package/template/src/components/ui/dialog/DialogTitle.vue +10 -10
  56. package/template/src/components/ui/dialog/DialogTrigger.vue +3 -3
  57. package/template/src/components/ui/dialog/index.ts +9 -9
  58. package/template/src/components/ui/dropdown-menu/DropdownMenu.vue +6 -6
  59. package/template/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +16 -16
  60. package/template/src/components/ui/dropdown-menu/DropdownMenuGroup.vue +3 -3
  61. package/template/src/components/ui/dropdown-menu/DropdownMenuItem.vue +16 -16
  62. package/template/src/components/ui/dropdown-menu/DropdownMenuLabel.vue +9 -9
  63. package/template/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +9 -9
  64. package/template/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +17 -17
  65. package/template/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue +12 -12
  66. package/template/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue +5 -5
  67. package/template/src/components/ui/dropdown-menu/DropdownMenuSub.vue +9 -9
  68. package/template/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue +14 -14
  69. package/template/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +13 -13
  70. package/template/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue +5 -5
  71. package/template/src/components/ui/dropdown-menu/index.ts +15 -15
  72. package/template/src/components/ui/form/FormControl.vue +3 -3
  73. package/template/src/components/ui/form/FormDescription.vue +8 -8
  74. package/template/src/components/ui/form/FormItem.vue +11 -11
  75. package/template/src/components/ui/form/FormLabel.vue +8 -8
  76. package/template/src/components/ui/form/FormMessage.vue +10 -10
  77. package/template/src/components/ui/form/index.ts +6 -6
  78. package/template/src/components/ui/form/injectionKeys.ts +3 -3
  79. package/template/src/components/ui/form/useFormField.ts +29 -29
  80. package/template/src/components/ui/input/Input.vue +17 -17
  81. package/template/src/components/ui/label/Label.vue +8 -8
  82. package/template/src/components/ui/pagination/Pagination.vue +12 -12
  83. package/template/src/components/ui/pagination/PaginationContent.vue +8 -8
  84. package/template/src/components/ui/pagination/PaginationEllipsis.vue +9 -9
  85. package/template/src/components/ui/pagination/PaginationFirst.vue +17 -17
  86. package/template/src/components/ui/pagination/PaginationItem.vue +16 -16
  87. package/template/src/components/ui/pagination/PaginationLast.vue +17 -17
  88. package/template/src/components/ui/pagination/PaginationNext.vue +17 -17
  89. package/template/src/components/ui/pagination/PaginationPrevious.vue +17 -17
  90. package/template/src/components/ui/pagination/index.ts +7 -7
  91. package/template/src/components/ui/popover/Popover.vue +6 -6
  92. package/template/src/components/ui/popover/PopoverAnchor.vue +3 -3
  93. package/template/src/components/ui/popover/PopoverContent.vue +25 -25
  94. package/template/src/components/ui/popover/PopoverTrigger.vue +3 -3
  95. package/template/src/components/ui/popover/index.ts +3 -3
  96. package/template/src/components/ui/select/Select.vue +6 -6
  97. package/template/src/components/ui/select/SelectContent.vue +26 -26
  98. package/template/src/components/ui/select/SelectGroup.vue +3 -3
  99. package/template/src/components/ui/select/SelectItem.vue +16 -16
  100. package/template/src/components/ui/select/SelectItemText.vue +3 -3
  101. package/template/src/components/ui/select/SelectLabel.vue +5 -5
  102. package/template/src/components/ui/select/SelectScrollDownButton.vue +11 -11
  103. package/template/src/components/ui/select/SelectScrollUpButton.vue +11 -11
  104. package/template/src/components/ui/select/SelectSeparator.vue +8 -8
  105. package/template/src/components/ui/select/SelectTrigger.vue +13 -13
  106. package/template/src/components/ui/select/SelectValue.vue +3 -3
  107. package/template/src/components/ui/select/index.ts +10 -10
  108. package/template/src/components/ui/separator/Separator.vue +13 -13
  109. package/template/src/components/ui/sheet/Sheet.vue +6 -6
  110. package/template/src/components/ui/sheet/SheetClose.vue +3 -3
  111. package/template/src/components/ui/sheet/SheetContent.vue +29 -29
  112. package/template/src/components/ui/sheet/SheetDescription.vue +8 -8
  113. package/template/src/components/ui/sheet/SheetFooter.vue +3 -3
  114. package/template/src/components/ui/sheet/SheetHeader.vue +3 -3
  115. package/template/src/components/ui/sheet/SheetOverlay.vue +8 -8
  116. package/template/src/components/ui/sheet/SheetTitle.vue +8 -8
  117. package/template/src/components/ui/sheet/SheetTrigger.vue +3 -3
  118. package/template/src/components/ui/sheet/index.ts +7 -7
  119. package/template/src/components/ui/sidebar/Sidebar.vue +18 -18
  120. package/template/src/components/ui/sidebar/SidebarContent.vue +6 -6
  121. package/template/src/components/ui/sidebar/SidebarControl.vue +0 -3
  122. package/template/src/components/ui/sidebar/SidebarFooter.vue +5 -5
  123. package/template/src/components/ui/sidebar/SidebarGroup.vue +5 -5
  124. package/template/src/components/ui/sidebar/SidebarGroupAction.vue +7 -7
  125. package/template/src/components/ui/sidebar/SidebarGroupContent.vue +5 -5
  126. package/template/src/components/ui/sidebar/SidebarGroupLabel.vue +7 -7
  127. package/template/src/components/ui/sidebar/SidebarHeader.vue +5 -5
  128. package/template/src/components/ui/sidebar/SidebarInput.vue +6 -6
  129. package/template/src/components/ui/sidebar/SidebarInset.vue +5 -5
  130. package/template/src/components/ui/sidebar/SidebarMenu.vue +5 -5
  131. package/template/src/components/ui/sidebar/SidebarMenuAction.vue +10 -10
  132. package/template/src/components/ui/sidebar/SidebarMenuBadge.vue +5 -5
  133. package/template/src/components/ui/sidebar/SidebarMenuButton.vue +21 -21
  134. package/template/src/components/ui/sidebar/SidebarMenuButtonChild.vue +18 -18
  135. package/template/src/components/ui/sidebar/SidebarMenuItem.vue +5 -5
  136. package/template/src/components/ui/sidebar/SidebarMenuSkeleton.vue +12 -12
  137. package/template/src/components/ui/sidebar/SidebarMenuSub.vue +5 -5
  138. package/template/src/components/ui/sidebar/SidebarMenuSubButton.vue +12 -12
  139. package/template/src/components/ui/sidebar/SidebarMenuSubItem.vue +5 -5
  140. package/template/src/components/ui/sidebar/SidebarProvider.vue +4 -8
  141. package/template/src/components/ui/sidebar/SidebarRail.vue +8 -8
  142. package/template/src/components/ui/sidebar/SidebarSeparator.vue +6 -6
  143. package/template/src/components/ui/sidebar/SidebarTrigger.vue +10 -10
  144. package/template/src/components/ui/sidebar/utils.ts +1 -1
  145. package/template/src/components/ui/skeleton/Skeleton.vue +7 -7
  146. package/template/src/components/ui/sonner/Sonner.vue +5 -5
  147. package/template/src/components/ui/switch/Switch.vue +16 -16
  148. package/template/src/components/ui/table/Table.vue +5 -5
  149. package/template/src/components/ui/table/TableBody.vue +5 -5
  150. package/template/src/components/ui/table/TableCaption.vue +5 -5
  151. package/template/src/components/ui/table/TableCell.vue +5 -5
  152. package/template/src/components/ui/table/TableEmpty.vue +13 -13
  153. package/template/src/components/ui/table/TableFooter.vue +5 -5
  154. package/template/src/components/ui/table/TableHead.vue +5 -5
  155. package/template/src/components/ui/table/TableHeader.vue +5 -5
  156. package/template/src/components/ui/table/TableRow.vue +5 -5
  157. package/template/src/components/ui/table/index.ts +8 -8
  158. package/template/src/components/ui/table/utils.ts +9 -9
  159. package/template/src/components/ui/tabs/Tabs.vue +10 -10
  160. package/template/src/components/ui/tabs/TabsContent.vue +8 -8
  161. package/template/src/components/ui/tabs/TabsList.vue +8 -8
  162. package/template/src/components/ui/tabs/TabsTrigger.vue +10 -10
  163. package/template/src/components/ui/tabs/index.ts +3 -3
  164. package/template/src/components/ui/textarea/Textarea.vue +17 -17
  165. package/template/src/components/ui/tooltip/Tooltip.vue +6 -6
  166. package/template/src/components/ui/tooltip/TooltipContent.vue +17 -17
  167. package/template/src/components/ui/tooltip/TooltipProvider.vue +5 -5
  168. package/template/src/components/ui/tooltip/TooltipTrigger.vue +3 -3
  169. package/template/src/components/ui/tooltip/index.ts +3 -3
  170. package/template/src/lib/utils.ts +6 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nala",
3
- "version": "3.0.0",
3
+ "version": "3.0.4",
4
4
  "description": "Scaffolding CLI tool for Nala Admin Dashboard Template",
5
5
  "author": "candrasp",
6
6
  "license": "MIT",
@@ -32,6 +32,10 @@
32
32
  "url": "https://github.com/candrasp/Nala.git",
33
33
  "directory": "packages/create-nala"
34
34
  },
35
+ "scripts": {
36
+ "build": "tsup",
37
+ "dev": "tsup --watch"
38
+ },
35
39
  "dependencies": {
36
40
  "@clack/prompts": "^0.9.1",
37
41
  "kolorist": "^1.8.0"
@@ -40,9 +44,5 @@
40
44
  "@types/node": "^24.13.3",
41
45
  "tsup": "^8.4.0",
42
46
  "typescript": "~6.0.2"
43
- },
44
- "scripts": {
45
- "build": "tsup",
46
- "dev": "tsup --watch"
47
47
  }
48
- }
48
+ }
@@ -0,0 +1,493 @@
1
+ ---
2
+ name: nala-project
3
+ description: >
4
+ Skill for the Nala Admin Dashboard Project - Single SPA application built with Vue 3,
5
+ Vite, TypeScript, Tailwind CSS v4, Reka UI (shadcn-vue), Pinia, and Axios.
6
+ Activates when building views, components, stores, API services, or scaffolding features.
7
+ ---
8
+
9
+ # Nala Project Skill (Starter Edition)
10
+
11
+ You are an expert frontend engineer building features for this **Nala Admin Dashboard** application. Follow these instructions strictly to ensure clean architecture, design consistency, and zero UI regressions.
12
+
13
+ ---
14
+
15
+ ## 🛠️ Tech Stack Overview
16
+
17
+ | Layer | Technology | Notes |
18
+ |---|---|---|
19
+ | **Framework** | Vue 3 (Composition API) | Always use `<script setup lang="ts">` |
20
+ | **Build Tool** | Vite 8 (`@vitejs/plugin-vue`) | Super-fast HMR & production bundler |
21
+ | **Styling** | Tailwind CSS v4 (`@tailwindcss/vite`) + `tw-animate-css` | OKLCH color tokens in `src/style.css` |
22
+ | **UI Primitives** | `reka-ui` + shadcn-vue (New York style) | 34+ primitive components in `src/components/ui/` |
23
+ | **Data Table** | `@tanstack/vue-table` | Headless table logic with sorting & pagination |
24
+ | **Form & Validation** | `vee-validate` + `@vee-validate/zod` + `zod` | Strongly-typed validation schemas |
25
+ | **Icons** | `@lucide/vue` | Must be imported explicitly (`import { Plus } from '@lucide/vue'`) |
26
+ | **State Management** | Pinia | Setup Stores pattern (`defineStore('name', () => { ... })`) |
27
+ | **Routing** | Vue Router v5 | HTML5 history mode with route guards |
28
+ | **API Client** | Axios + Enterprise Interceptors | Silent Token Refresh, Global Loading Bar, Auto Error Toast |
29
+ | **Notifications** | `vue-sonner` | Toast notifications via `toast.success()`, `toast.error()` |
30
+ | **Language & Types** | TypeScript 6 + `vue-tsc` | Strict static typing, zero `any` |
31
+
32
+ ---
33
+
34
+ ## 🛑 Strict Guardrails (MUST FOLLOW)
35
+
36
+ 1. **Composition API Only:** Never use Options API — always use `<script setup lang="ts">`.
37
+ 2. **Strict TypeScript:** No `any` type — define explicit TypeScript interfaces for all props, emits, and API payloads.
38
+ 3. **English Standard:** All user-facing UI text, form labels, placeholders, error messages, toast notifications, and in-code comments must be in **English**.
39
+ 4. **Tailwind CSS v4 Strict Compliance:**
40
+ - **Gradients:** ALWAYS use `bg-linear-to-r`, `bg-linear-to-b`, `bg-linear-to-tr` (NEVER use legacy v3 `bg-gradient-to-*`).
41
+ - **Sizing Scale:** NEVER write arbitrary bracket sizing like `max-w-[170px]`, `w-[300px]`, or `p-[1px]`. ALWAYS use Tailwind v4 scale (`max-w-44`, `max-w-28`, `w-75`, `max-h-75`, `p-px`).
42
+ - **Shadows:** Use `shadow-2xs`, `shadow-xs`, `shadow-sm`, `shadow-md`, `shadow-lg`.
43
+ 5. **No Scoped CSS:** Use Tailwind utility classes. Do not write `<style scoped>`.
44
+ 6. **Dialog Accessibility:** Every `DialogContent` MUST include `DialogTitle` and `DialogDescription` (use `sr-only` if visually hidden) to comply with Reka UI ARIA standards.
45
+
46
+ ---
47
+
48
+ ## ⚡ Auto-Import Guide Matrix
49
+
50
+ | Auto-Imported (No Import Needed) | Manual Import Required |
51
+ |---|---|
52
+ | Vue Reactivity (`ref`, `computed`, `watch`, `onMounted`, etc.) | Lucide Icons (`import { Plus, Search, Trash2 } from '@lucide/vue'`) |
53
+ | Vue Router (`useRoute`, `useRouter`) | Toast Notifications (`import { toast } from '@/components/ui/sonner'`) |
54
+ | VueUse (`useColorMode`, `useLocalStorage`, etc.) | Axios Client (`import { apiClient } from '@/lib/axios'`) |
55
+ | Pinia (`defineStore`, `storeToRefs`) | Form Validation (`import { z } from 'zod'`, `import { useForm } from 'vee-validate'`) |
56
+ | All UI Components (`<Button>`, `<Card>`, `<Dialog>`, `<Input>`, `<Table>`, `<PageHeader>`, etc.) | Class Merger (`import { cn } from '@/lib/utils'`) |
57
+
58
+ ---
59
+
60
+ ## 🎨 UI Design System & Component Blueprints
61
+
62
+ ### 1. Standard Page Header (`<PageHeader>`)
63
+ Always use the standardized `<PageHeader>` component at the top of every view:
64
+ ```vue
65
+ <PageHeader
66
+ title="Products"
67
+ description="Manage your product inventory and pricing."
68
+ >
69
+ <template #actions>
70
+ <Button size="sm" class="gap-1.5" @click="isDialogOpen = true">
71
+ <Plus class="h-3.5 w-3.5" />
72
+ Add Product
73
+ </Button>
74
+ </template>
75
+ </PageHeader>
76
+ ```
77
+
78
+ ### 2. Standard KPI Stats Cards Blueprint
79
+ When building summary metrics / stats cards, follow this exact blueprint:
80
+ ```vue
81
+ <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
82
+ <Card
83
+ v-for="stat in stats"
84
+ :key="stat.title"
85
+ flush
86
+ class="highlight-card shadow-xs border-border/80 bg-card hover:border-border transition-all duration-200"
87
+ >
88
+ <CardContent class="p-5 space-y-2">
89
+ <!-- 1. Top Row: Title + Icon -->
90
+ <div class="flex items-center justify-between">
91
+ <span class="text-xs font-medium text-muted-foreground">{{ stat.title }}</span>
92
+ <component :is="stat.icon" class="h-4 w-4 text-primary" />
93
+ </div>
94
+
95
+ <!-- 2. Value -->
96
+ <div class="text-2xl font-bold tracking-tight text-foreground">{{ stat.value }}</div>
97
+
98
+ <!-- 3. Trend Indicator -->
99
+ <p class="text-xs text-muted-foreground flex items-center gap-1">
100
+ <span
101
+ class="font-semibold inline-flex items-center gap-0.5"
102
+ :class="stat.positive ? 'text-emerald-500' : 'text-rose-500'"
103
+ >
104
+ <component :is="stat.positive ? ArrowUpRight : ArrowDownRight" class="h-3.5 w-3.5" />
105
+ {{ stat.change }}
106
+ </span>
107
+ <span>{{ stat.changeLabel }}</span>
108
+ </p>
109
+ </CardContent>
110
+ </Card>
111
+ </div>
112
+ ```
113
+
114
+ ### 3. Input Groups with Icons & Addons
115
+ Never write manual absolute positioning for input icons:
116
+ ```vue
117
+ <!-- Search Icon Input -->
118
+ <InputGroup>
119
+ <InputIcon side="left">
120
+ <Search class="h-3.5 w-3.5" />
121
+ </InputIcon>
122
+ <Input v-model="searchQuery" placeholder="Search..." class="pl-8 h-8 text-xs" />
123
+ </InputGroup>
124
+
125
+ <!-- URL Prefix / Suffix Addon -->
126
+ <InputGroup>
127
+ <InputAddon side="left">https://</InputAddon>
128
+ <Input placeholder="my-subdomain" class="rounded-none border-x-0" />
129
+ <InputAddon side="right">.nala.dev</InputAddon>
130
+ </InputGroup>
131
+ ```
132
+
133
+ ---
134
+
135
+ ## 🚀 Golden Blueprint: End-to-End CRUD Feature Pattern
136
+
137
+ Use this complete 4-file pattern when asked to build a new feature (e.g. "Products", "Invoices", "Customers"):
138
+
139
+ ### 1. API Service with Safe DEV Fallback (`src/services/product.service.ts`)
140
+ ```ts
141
+ import { apiClient } from '@/lib/axios'
142
+
143
+ export interface ProductItem {
144
+ id: string
145
+ name: string
146
+ sku: string
147
+ price: number
148
+ stock: number
149
+ status: 'active' | 'draft' | 'archived'
150
+ }
151
+
152
+ export interface CreateProductPayload {
153
+ name: string
154
+ sku: string
155
+ price: number
156
+ stock: number
157
+ }
158
+
159
+ // In-memory mock store for local development when backend is offline
160
+ const mockProducts: ProductItem[] = [
161
+ { id: '1', name: 'MacBook Pro 14"', sku: 'MBP-14', price: 1999, stock: 12, status: 'active' },
162
+ { id: '2', name: 'Dell XPS 15', sku: 'XPS-15', price: 1499, stock: 5, status: 'active' },
163
+ ]
164
+
165
+ export const productService = {
166
+ async getProducts(): Promise<ProductItem[]> {
167
+ try {
168
+ return await apiClient.get<ProductItem[]>('/products')
169
+ } catch (error) {
170
+ if (import.meta.env.DEV) {
171
+ console.warn('[DEV MOCK] Backend offline, returning mock products.')
172
+ return mockProducts
173
+ }
174
+ throw error
175
+ }
176
+ },
177
+
178
+ async createProduct(payload: CreateProductPayload): Promise<ProductItem> {
179
+ try {
180
+ return await apiClient.post<ProductItem>('/products', payload)
181
+ } catch (error) {
182
+ if (import.meta.env.DEV) {
183
+ const newItem: ProductItem = { id: String(Date.now()), ...payload, status: 'active' }
184
+ mockProducts.unshift(newItem)
185
+ return newItem
186
+ }
187
+ throw error
188
+ }
189
+ },
190
+
191
+ async deleteProduct(id: string): Promise<void> {
192
+ try {
193
+ await apiClient.delete(`/products/${id}`)
194
+ } catch (error) {
195
+ if (import.meta.env.DEV) {
196
+ const idx = mockProducts.findIndex(p => p.id === id)
197
+ if (idx !== -1) mockProducts.splice(idx, 1)
198
+ return
199
+ }
200
+ throw error
201
+ }
202
+ }
203
+ }
204
+ ```
205
+
206
+ ### 2. Pinia Store (`src/stores/product.ts`)
207
+ ```ts
208
+ import { defineStore } from 'pinia'
209
+ import { productService, type ProductItem, type CreateProductPayload } from '@/services/product.service'
210
+ import { toast } from '@/components/ui/sonner'
211
+
212
+ export const useProductStore = defineStore('product', () => {
213
+ const products = ref<ProductItem[]>([])
214
+ const isLoading = ref(false)
215
+
216
+ async function loadProducts() {
217
+ isLoading.value = true
218
+ try {
219
+ products.value = await productService.getProducts()
220
+ } catch {
221
+ toast.error('Failed to load products.')
222
+ } finally {
223
+ isLoading.value = false
224
+ }
225
+ }
226
+
227
+ async function addProduct(payload: CreateProductPayload) {
228
+ try {
229
+ const created = await productService.createProduct(payload)
230
+ products.value.unshift(created)
231
+ toast.success('Product created successfully!')
232
+ return true
233
+ } catch {
234
+ toast.error('Failed to create product.')
235
+ return false
236
+ }
237
+ }
238
+
239
+ async function removeProduct(id: string) {
240
+ try {
241
+ await productService.deleteProduct(id)
242
+ products.value = products.value.filter(p => p.id !== id)
243
+ toast.success('Product deleted.')
244
+ } catch {
245
+ toast.error('Failed to delete product.')
246
+ }
247
+ }
248
+
249
+ return { products, isLoading, loadProducts, addProduct, removeProduct }
250
+ })
251
+ ```
252
+
253
+ ### 3. Modal Form with Zod (`src/views/products/components/ProductFormDialog.vue`)
254
+ ```vue
255
+ <script setup lang="ts">
256
+ import { z } from 'zod'
257
+ import { useForm } from 'vee-validate'
258
+ import { toTypedSchema } from '@vee-validate/zod'
259
+ import { useProductStore } from '@/stores/product'
260
+ import { Loader2 } from '@lucide/vue'
261
+
262
+ const props = defineProps<{
263
+ open: boolean
264
+ }>()
265
+
266
+ const emit = defineEmits<{
267
+ (e: 'update:open', val: boolean): void
268
+ }>()
269
+
270
+ const productStore = useProductStore()
271
+
272
+ const productSchema = toTypedSchema(
273
+ z.object({
274
+ name: z.string().min(3, 'Product name must be at least 3 characters'),
275
+ sku: z.string().min(2, 'SKU must be at least 2 characters').toUpperCase(),
276
+ price: z.number({ invalid_type_error: 'Price is required' }).positive('Price must be greater than 0'),
277
+ stock: z.number({ invalid_type_error: 'Stock is required' }).int().min(0, 'Stock cannot be negative'),
278
+ })
279
+ )
280
+
281
+ const { handleSubmit, isSubmitting, resetForm } = useForm({
282
+ validationSchema: productSchema,
283
+ initialValues: {
284
+ name: '',
285
+ sku: '',
286
+ price: 0,
287
+ stock: 1,
288
+ }
289
+ })
290
+
291
+ const onSubmit = handleSubmit(async (values) => {
292
+ const success = await productStore.addProduct(values)
293
+ if (success) {
294
+ resetForm()
295
+ emit('update:open', false)
296
+ }
297
+ })
298
+ </script>
299
+
300
+ <template>
301
+ <Dialog :open="open" @update:open="emit('update:open', $event)">
302
+ <DialogContent class="sm:max-w-120">
303
+ <DialogHeader>
304
+ <DialogTitle>Add New Product</DialogTitle>
305
+ <DialogDescription>Fill in product details to add to inventory.</DialogDescription>
306
+ </DialogHeader>
307
+
308
+ <form @submit="onSubmit" class="space-y-4 py-2">
309
+ <FormField v-slot="{ componentField }" name="name">
310
+ <FormItem>
311
+ <FormLabel>Product Name</FormLabel>
312
+ <FormControl>
313
+ <Input v-bind="componentField" placeholder="e.g. MacBook Pro 14" />
314
+ </FormControl>
315
+ <FormMessage />
316
+ </FormItem>
317
+ </FormField>
318
+
319
+ <div class="grid grid-cols-2 gap-4">
320
+ <FormField v-slot="{ componentField }" name="sku">
321
+ <FormItem>
322
+ <FormLabel>SKU</FormLabel>
323
+ <FormControl>
324
+ <Input v-bind="componentField" placeholder="e.g. MBP-14" />
325
+ </FormControl>
326
+ <FormMessage />
327
+ </FormItem>
328
+ </FormField>
329
+
330
+ <FormField v-slot="{ componentField }" name="price">
331
+ <FormItem>
332
+ <FormLabel>Price ($)</FormLabel>
333
+ <FormControl>
334
+ <Input type="number" step="0.01" v-bind="componentField" placeholder="1999" />
335
+ </FormControl>
336
+ <FormMessage />
337
+ </FormItem>
338
+ </FormField>
339
+ </div>
340
+
341
+ <FormField v-slot="{ componentField }" name="stock">
342
+ <FormItem>
343
+ <FormLabel>Initial Stock</FormLabel>
344
+ <FormControl>
345
+ <Input type="number" v-bind="componentField" placeholder="10" />
346
+ </FormControl>
347
+ <FormMessage />
348
+ </FormItem>
349
+ </FormField>
350
+
351
+ <DialogFooter class="pt-4">
352
+ <Button type="button" variant="outline" @click="emit('update:open', false)">Cancel</Button>
353
+ <Button type="submit" :disabled="isSubmitting">
354
+ <Loader2 v-if="isSubmitting" class="h-3.5 w-3.5 animate-spin mr-1.5" />
355
+ Save Product
356
+ </Button>
357
+ </DialogFooter>
358
+ </form>
359
+ </DialogContent>
360
+ </Dialog>
361
+ </template>
362
+ ```
363
+
364
+ ### 4. Main Page View with Table & Skeleton (`src/views/products/IndexView.vue`)
365
+ ```vue
366
+ <script setup lang="ts">
367
+ import { Plus, Search, Trash2, Package } from '@lucide/vue'
368
+ import { useProductStore } from '@/stores/product'
369
+ import ProductFormDialog from './components/ProductFormDialog.vue'
370
+
371
+ const productStore = useProductStore()
372
+ const searchQuery = ref('')
373
+ const isDialogOpen = ref(false)
374
+
375
+ onMounted(() => {
376
+ productStore.loadProducts()
377
+ })
378
+
379
+ const filteredProducts = computed(() => {
380
+ if (!searchQuery.value) return productStore.products
381
+ return productStore.products.filter(p =>
382
+ p.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
383
+ p.sku.toLowerCase().includes(searchQuery.value.toLowerCase())
384
+ )
385
+ })
386
+ </script>
387
+
388
+ <template>
389
+ <div class="space-y-6 w-full mx-auto pb-10">
390
+ <PageHeader
391
+ title="Products"
392
+ description="Manage your product catalog and inventory."
393
+ >
394
+ <template #actions>
395
+ <Button size="sm" class="gap-1.5" @click="isDialogOpen = true">
396
+ <Plus class="h-3.5 w-3.5" />
397
+ Add Product
398
+ </Button>
399
+ </template>
400
+ </PageHeader>
401
+
402
+ <Card flush class="shadow-xs border">
403
+ <CardHeader section class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
404
+ <div>
405
+ <CardTitle class="text-base font-semibold">Product Catalog</CardTitle>
406
+ <CardDescription class="text-xs mt-0.5">Showing all active items in warehouse.</CardDescription>
407
+ </div>
408
+ <div class="w-full sm:w-64">
409
+ <InputGroup>
410
+ <InputIcon side="left">
411
+ <Search class="h-3.5 w-3.5" />
412
+ </InputIcon>
413
+ <Input v-model="searchQuery" placeholder="Search product or SKU..." class="pl-8 h-8 text-xs" />
414
+ </InputGroup>
415
+ </div>
416
+ </CardHeader>
417
+
418
+ <CardContent class="p-0">
419
+ <Table>
420
+ <TableHeader>
421
+ <TableRow>
422
+ <TableHead>Product</TableHead>
423
+ <TableHead>SKU</TableHead>
424
+ <TableHead>Price</TableHead>
425
+ <TableHead>Stock</TableHead>
426
+ <TableHead class="text-right">Actions</TableHead>
427
+ </TableRow>
428
+ </TableHeader>
429
+ <TableBody>
430
+ <!-- 1. Skeleton Loading -->
431
+ <template v-if="productStore.isLoading">
432
+ <TableRow v-for="n in 3" :key="n">
433
+ <TableCell><Skeleton class="h-4 w-40" /></TableCell>
434
+ <TableCell><Skeleton class="h-4 w-20" /></TableCell>
435
+ <TableCell><Skeleton class="h-4 w-16" /></TableCell>
436
+ <TableCell><Skeleton class="h-4 w-24" /></TableCell>
437
+ <TableCell class="text-right"><Skeleton class="h-8 w-8 ml-auto rounded-md" /></TableCell>
438
+ </TableRow>
439
+ </template>
440
+
441
+ <!-- 2. Data Rows -->
442
+ <template v-else>
443
+ <TableRow v-for="item in filteredProducts" :key="item.id">
444
+ <TableCell class="font-medium">{{ item.name }}</TableCell>
445
+ <TableCell class="font-mono text-xs text-muted-foreground">{{ item.sku }}</TableCell>
446
+ <TableCell>${{ item.price.toLocaleString() }}</TableCell>
447
+ <TableCell>{{ item.stock }} in stock</TableCell>
448
+ <TableCell class="text-right">
449
+ <Button variant="ghost" size="icon-sm" class="text-destructive hover:bg-destructive/10" @click="productStore.removeProduct(item.id)">
450
+ <Trash2 class="h-3.5 w-3.5" />
451
+ </Button>
452
+ </TableCell>
453
+ </TableRow>
454
+
455
+ <!-- 3. Empty State -->
456
+ <TableRow v-if="filteredProducts.length === 0">
457
+ <TableCell colspan="5" class="h-48 text-center text-muted-foreground">
458
+ <div class="flex flex-col items-center justify-center gap-2">
459
+ <Package class="h-8 w-8 text-muted-foreground/50" />
460
+ <p class="text-sm font-medium">No products found</p>
461
+ <p class="text-xs text-muted-foreground">Try adjusting your search query or add a new product.</p>
462
+ </div>
463
+ </TableCell>
464
+ </TableRow>
465
+ </template>
466
+ </TableBody>
467
+ </Table>
468
+ </CardContent>
469
+ </Card>
470
+
471
+ <ProductFormDialog v-model:open="isDialogOpen" />
472
+ </div>
473
+ </template>
474
+ ```
475
+
476
+ ### 5. Register in Router & Sidebar
477
+ - **`src/router/index.ts`:**
478
+ ```ts
479
+ {
480
+ path: 'products',
481
+ name: 'products',
482
+ component: () => import('@/views/products/IndexView.vue'),
483
+ }
484
+ ```
485
+ - **`src/components/layout/AppSidebar.vue`:**
486
+ ```ts
487
+ import { Package } from '@lucide/vue'
488
+
489
+ const mainNav = [
490
+ { name: 'Dashboard', routeName: 'dashboard', href: '/', icon: LayoutDashboard },
491
+ { name: 'Products', routeName: 'products', href: '/products', icon: Package },
492
+ ]
493
+ ```
@@ -0,0 +1,49 @@
1
+ # AGENTS.md — AI Coding Assistant Guidelines & Guardrails
2
+
3
+ > This document defines the strict architectural standards and rules for AI coding assistants (Antigravity, Cursor, Claude Code, GitHub Copilot, Roo Code, Windsurf) working on this **Nala** application.
4
+
5
+ ---
6
+
7
+ ## 🛑 Strict Rules (MUST FOLLOW)
8
+
9
+ 1. **Strict Composition API:** ALWAYS use `<script setup lang="ts">`. NEVER use the Options API.
10
+ 2. **Strict TypeScript Hygiene:** NEVER use `any`. Always define explicit TypeScript interfaces for props, emits, and API payloads.
11
+ 3. **English Standard:** All user-facing UI text, form labels, error messages, toast notifications, and in-code comments (JSDoc/inline notes) MUST be written in **English**.
12
+ 4. **Tailwind CSS v4 Compliance:**
13
+ - **Gradients:** ALWAYS use `bg-linear-to-r`, `bg-linear-to-b`, `bg-linear-to-tr` (NEVER use legacy v3 `bg-gradient-to-*`).
14
+ - **Sizing Scale:** NEVER write arbitrary bracket sizing like `max-w-[170px]`, `w-[300px]`, or `p-[1px]`. ALWAYS use Tailwind v4 scale (`max-w-44`, `max-w-28`, `w-75`, `max-h-75`, `p-px`).
15
+ - **Shadows:** Use `shadow-2xs`, `shadow-xs`, `shadow-sm`, `shadow-md`, `shadow-lg`.
16
+ 5. **No Scoped Styles:** Use Tailwind CSS utility classes instead of `<style scoped>`.
17
+ 6. **Package Installation:** NEVER install new npm packages autonomously. Always ask the developer for confirmation before running `pnpm add` or `npm install`.
18
+ 7. **Dialog Accessibility:** Every `DialogContent` MUST include `DialogTitle` and `DialogDescription` (use `sr-only` if visually hidden) to comply with Reka UI ARIA standards.
19
+ 8. **No Automatic Builds / Tests:** Do not run `pnpm build` or `pnpm test` autonomously after code generation unless explicitly instructed.
20
+
21
+ ---
22
+
23
+ ## ⚡ Auto-Import Matrix
24
+
25
+ The following modules are auto-imported via `unplugin-auto-import` and `unplugin-vue-components`:
26
+
27
+ | Category | Auto-Imported? | How to Use |
28
+ |---|:---:|---|
29
+ | Vue Reactivity (`ref`, `computed`, `watch`, `onMounted`, etc.) | ✅ Yes | Use directly: `const count = ref(0)` |
30
+ | Vue Router (`useRoute`, `useRouter`) | ✅ Yes | Use directly: `const router = useRouter()` |
31
+ | VueUse (`useColorMode`, `useLocalStorage`, etc.) | ✅ Yes | Use directly: `const mode = useColorMode()` |
32
+ | Pinia (`defineStore`, `storeToRefs`) | ✅ Yes | Use directly: `defineStore(...)` |
33
+ | UI Components (`Button`, `Card`, `Dialog`, `Input`, `Table`, etc.) | ✅ Yes | Use directly in template: `<Button>`, `<Card>` |
34
+ | Lucide Icons (`Plus`, `Search`, `Trash2`, etc.) | ⚠️ **No** | `import { Plus, Search } from '@lucide/vue'` |
35
+ | Toast Notifications (`toast`) | ⚠️ **No** | `import { toast } from '@/components/ui/sonner'` |
36
+ | API Client (`apiClient`) | ⚠️ **No** | `import { apiClient } from '@/lib/axios'` |
37
+ | Form Validation (`zod`, `useForm`) | ⚠️ **No** | `import { z } from 'zod'`, `import { useForm } from 'vee-validate'` |
38
+
39
+ ---
40
+
41
+ ## 📦 UI Primitives Catalog (`src/components/ui/`)
42
+
43
+ All 34+ primitive components in `src/components/ui/` are globally registered and ready to use:
44
+ - `Button`, `Badge`, `Avatar`, `Breadcrumb`, `Card`, `Checkbox`, `Dialog`, `DropdownMenu`
45
+ - `Form` (`FormField`, `FormItem`, `FormLabel`, `FormControl`, `FormMessage`)
46
+ - `Input`, `InputGroup`, `InputIcon`, `InputAddon`, `PinInput`, `Textarea`, `Select`, `Switch`
47
+ - `Table`, `Tabs`, `Sheet`, `Popover`, `Progress`, `Skeleton`, `Slider`, `Tooltip`, `Toaster`
48
+
49
+ For deep blueprints on adding new features and services, see [`.agents/skills/nala-project/SKILL.md`](./.agents/skills/nala-project/SKILL.md).
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 candrasp
3
+ Copyright (c) 2026 Nala Contributors
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal