create-nala 3.0.0 → 3.0.3
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.
package/package.json
CHANGED
|
@@ -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).
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# NALA_APP_NAME
|
|
2
|
+
|
|
3
|
+
> Built with **Nala** — Enterprise Admin Dashboard Starter Template powered by Vue 3, Vite, TypeScript, and Tailwind CSS v4.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🚀 Quick Start
|
|
8
|
+
|
|
9
|
+
### 1. Install Dependencies
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# with pnpm (Recommended)
|
|
13
|
+
pnpm install
|
|
14
|
+
|
|
15
|
+
# with npm
|
|
16
|
+
npm install
|
|
17
|
+
|
|
18
|
+
# with yarn
|
|
19
|
+
yarn install
|
|
20
|
+
|
|
21
|
+
# with bun
|
|
22
|
+
bun install
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### 2. Environment Configuration
|
|
26
|
+
|
|
27
|
+
Copy the example environment file and customize as needed:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
cp .env.example .env
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 3. Start Development Server
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pnpm dev
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Open **http://localhost:5173** in your browser.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 🛠️ Available Scripts
|
|
44
|
+
|
|
45
|
+
| Script | Command | Description |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `pnpm dev` | `vite` | Starts local dev server with blazing fast HMR |
|
|
48
|
+
| `pnpm build` | `vue-tsc -b && vite build` | Type-checks & compiles production bundle to `dist/` |
|
|
49
|
+
| `pnpm preview` | `vite preview` | Previews production build locally |
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 📁 Project Structure
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
src/
|
|
57
|
+
├── assets/fonts/ # High-performance local Inter fonts (woff2)
|
|
58
|
+
├── components/
|
|
59
|
+
│ ├── layout/ # Admin shell (AppNavbar, AppSidebar, CommandPalette)
|
|
60
|
+
│ ├── ui/ # 34+ accessible UI primitives (Reka UI / shadcn-vue)
|
|
61
|
+
│ └── PageHeader.vue # Standardized page header component
|
|
62
|
+
├── composables/
|
|
63
|
+
│ ├── useFormatter.ts # Native Intl data formatting helpers (auto-imported)
|
|
64
|
+
│ └── useThemeConfig.ts # Runtime theme & layout state (auto-imported)
|
|
65
|
+
├── layouts/
|
|
66
|
+
│ ├── AdminLayout.vue # Main authenticated dashboard layout
|
|
67
|
+
│ └── AuthLayout.vue # Centered layout for login/register/auth flows
|
|
68
|
+
├── lib/
|
|
69
|
+
│ ├── axios.ts # Central Axios client with silent refresh & error toasts
|
|
70
|
+
│ ├── formatters.ts # Currency, Date, Number & Relative Time helpers
|
|
71
|
+
│ └── utils.ts # cn() class merger (clsx + tailwind-merge)
|
|
72
|
+
├── router/
|
|
73
|
+
│ └── index.ts # Route definitions & auth navigation guards
|
|
74
|
+
├── services/
|
|
75
|
+
│ ├── auth.service.ts # Authentication API service (with safe DEV mock fallback)
|
|
76
|
+
│ ├── user.service.ts # User profile API service
|
|
77
|
+
│ └── types.ts # Shared API response contracts
|
|
78
|
+
├── stores/
|
|
79
|
+
│ └── auth.ts # Pinia authentication & user session store
|
|
80
|
+
├── views/
|
|
81
|
+
│ ├── _starter/
|
|
82
|
+
│ │ └── BlankView.vue # Clean blank template for creating new pages
|
|
83
|
+
│ ├── auth/ # Login, Register, Forgot/Reset Password, OTP, Lock Screen
|
|
84
|
+
│ ├── dashboard/ # Clean starter dashboard with KPI cards & recent table
|
|
85
|
+
│ └── errors/ # 404, 500, and 403 error pages
|
|
86
|
+
├── App.vue # Root application component with Toaster
|
|
87
|
+
├── main.ts # App initialization & Pinia/Router registration
|
|
88
|
+
└── style.css # Tailwind CSS v4 & OKLCH color design tokens
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 📡 Connecting Your Backend API
|
|
94
|
+
|
|
95
|
+
This starter includes a production-grade Axios service layer in `src/lib/axios.ts` and `src/services/`.
|
|
96
|
+
|
|
97
|
+
1. Open `.env` and set your API endpoint:
|
|
98
|
+
```env
|
|
99
|
+
VITE_API_BASE_URL=https://api.yourcompany.com/v1
|
|
100
|
+
```
|
|
101
|
+
2. **Safe DEV Mock Fallback:**
|
|
102
|
+
While developing (`import.meta.env.DEV`), if your backend is offline, services will automatically fall back to safe in-memory mock data so you can build UI uninterrupted. In production (`pnpm build`), real API errors will be thrown normally.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## 🎨 Customizing Theme & Branding
|
|
107
|
+
|
|
108
|
+
- **Brand Colors:** Update `--primary` and `--ring` in `src/style.css` using standard `oklch` color format.
|
|
109
|
+
- **Default Theme Config:** Modify `DEFAULT_CONFIG` in `src/composables/useThemeConfig.ts`:
|
|
110
|
+
```ts
|
|
111
|
+
const DEFAULT_CONFIG: ThemeConfig = {
|
|
112
|
+
primaryColor: 'emerald', // emerald | indigo | violet | rose | amber | cyan | zinc
|
|
113
|
+
radius: '0.375', // 0 | 0.25 | 0.375 | 0.5 | 0.75 | 1.0
|
|
114
|
+
contentWidth: 'fluid', // fluid | boxed
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## 🤖 AI-Assisted Development
|
|
121
|
+
|
|
122
|
+
This project is configured out-of-the-box for AI coding assistants:
|
|
123
|
+
- **`AGENTS.md`** — Universal rules for Antigravity, Cursor, Claude Code, GitHub Copilot, Roo Code, and Windsurf.
|
|
124
|
+
- **`.agents/skills/nala-project/SKILL.md`** — Deep technical blueprints for building components, Pinia stores, and API services without hallucinations.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## 📄 License
|
|
129
|
+
|
|
130
|
+
This project is open-sourced software licensed under the [MIT License](./LICENSE).
|