@codihaus/claude-skills 1.3.0 → 1.4.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.
|
@@ -326,11 +326,52 @@ export default async function ProductsPage() {
|
|
|
326
326
|
}
|
|
327
327
|
```
|
|
328
328
|
|
|
329
|
+
## Extension Development
|
|
330
|
+
|
|
331
|
+
For custom extensions (hooks, endpoints, interfaces, panels, modules):
|
|
332
|
+
|
|
333
|
+
**See:** `references/extensions.md`
|
|
334
|
+
|
|
335
|
+
Covers:
|
|
336
|
+
- API Extensions: Hooks, Endpoints, Operations
|
|
337
|
+
- App Extensions: Interfaces, Displays, Layouts, Panels, Modules, Themes
|
|
338
|
+
- Bundle structure and naming conventions
|
|
339
|
+
- Services API (ItemsService, FilesService, etc.)
|
|
340
|
+
- Composables (useApi, useStores, useItems)
|
|
341
|
+
|
|
342
|
+
### Quick Extension Example
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
// Hook: Run code on item create
|
|
346
|
+
export default ({ action }, { services, getSchema }) => {
|
|
347
|
+
const { ItemsService } = services;
|
|
348
|
+
|
|
349
|
+
action('items.create', async (meta, context) => {
|
|
350
|
+
const itemsService = new ItemsService('logs', {
|
|
351
|
+
schema: await getSchema(),
|
|
352
|
+
accountability: context.accountability,
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
await itemsService.createOne({
|
|
356
|
+
action: 'created',
|
|
357
|
+
collection: meta.collection,
|
|
358
|
+
item_id: meta.key,
|
|
359
|
+
}, { emitEvents: false });
|
|
360
|
+
});
|
|
361
|
+
};
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
```bash
|
|
365
|
+
# Create extension
|
|
366
|
+
npx create-directus-extension@latest
|
|
367
|
+
```
|
|
368
|
+
|
|
329
369
|
## Resources
|
|
330
370
|
|
|
331
371
|
### Official Docs
|
|
332
372
|
- Docs: https://docs.directus.io
|
|
333
373
|
- SDK: https://docs.directus.io/reference/sdk/
|
|
374
|
+
- Extensions: https://docs.directus.io/extensions/
|
|
334
375
|
|
|
335
376
|
### Context7 Queries
|
|
336
377
|
|
|
@@ -339,6 +380,8 @@ Query: "Directus SDK TypeScript setup"
|
|
|
339
380
|
Query: "Directus permissions best practices"
|
|
340
381
|
Query: "Directus flows automation"
|
|
341
382
|
Query: "Directus file uploads"
|
|
383
|
+
Query: "Directus hooks examples"
|
|
384
|
+
Query: "Directus custom endpoints"
|
|
342
385
|
```
|
|
343
386
|
|
|
344
387
|
### MCP Tools
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
# Directus Extension Development
|
|
2
|
+
|
|
3
|
+
Comprehensive guide for developing Directus extensions: hooks, endpoints, operations, interfaces, displays, layouts, panels, modules, and themes.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Create new extension
|
|
9
|
+
npx create-directus-extension@latest
|
|
10
|
+
|
|
11
|
+
# Enable auto-reload for development
|
|
12
|
+
# In docker-compose or .env:
|
|
13
|
+
EXTENSIONS_AUTO_RELOAD: true
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Extension Types
|
|
17
|
+
|
|
18
|
+
### API Extensions
|
|
19
|
+
| Type | Purpose | Example |
|
|
20
|
+
|------|---------|---------|
|
|
21
|
+
| **Hooks** | Run code on events (CRUD, schedule) | Send email on item create |
|
|
22
|
+
| **Endpoints** | Custom API routes | `/custom-api/products` |
|
|
23
|
+
| **Operations** | Custom Flow steps | Data transformation in Flows |
|
|
24
|
+
|
|
25
|
+
### App Extensions
|
|
26
|
+
| Type | Purpose | Example |
|
|
27
|
+
|------|---------|---------|
|
|
28
|
+
| **Interfaces** | Form inputs in Editor | Custom date picker |
|
|
29
|
+
| **Displays** | Single value display | Status badge |
|
|
30
|
+
| **Layouts** | Item listing views | Kanban board |
|
|
31
|
+
| **Panels** | Dashboard widgets | Analytics chart |
|
|
32
|
+
| **Modules** | Top-level areas | Custom admin section |
|
|
33
|
+
| **Themes** | Data Studio styling | Dark theme |
|
|
34
|
+
|
|
35
|
+
## Bundle Structure (Recommended)
|
|
36
|
+
|
|
37
|
+
Always use bundle extensions for organization:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
directus-extension-{bundle-name}/
|
|
41
|
+
├── package.json
|
|
42
|
+
├── src/
|
|
43
|
+
│ ├── utils/ # Shared utilities
|
|
44
|
+
│ ├── commons/ # Common code
|
|
45
|
+
│ ├── services/ # Shared services
|
|
46
|
+
│ ├── hook-{name}/ # Individual hooks
|
|
47
|
+
│ ├── endpoint-{name}/ # Individual endpoints
|
|
48
|
+
│ ├── interface-{name}/
|
|
49
|
+
│ └── ...
|
|
50
|
+
└── dist/
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Naming conventions:**
|
|
54
|
+
- `hook-send-email` or `send-email-hook`
|
|
55
|
+
- `endpoint-products` → API route: `/products`
|
|
56
|
+
- `interface-custom-input`
|
|
57
|
+
|
|
58
|
+
**Important:** Endpoint folder name includes prefix (`endpoint-products`), but `name` in package.json omits it (`products`).
|
|
59
|
+
|
|
60
|
+
## Core Patterns
|
|
61
|
+
|
|
62
|
+
### AsyncHandler (Required for Endpoints)
|
|
63
|
+
|
|
64
|
+
Always wrap endpoint handlers to prevent crashes:
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import type { RequestHandler, Request, Response, NextFunction } from 'express';
|
|
68
|
+
|
|
69
|
+
const asyncHandler = (fn: RequestHandler) => (req: Request, res: Response, next: NextFunction) =>
|
|
70
|
+
Promise.resolve(fn(req, res, next)).catch(next);
|
|
71
|
+
|
|
72
|
+
export default asyncHandler;
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Creating Services with Accountability
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
export default ({ action }, { services, getSchema }) => {
|
|
79
|
+
const { ItemsService } = services;
|
|
80
|
+
|
|
81
|
+
action('items.create', async (meta, context) => {
|
|
82
|
+
const itemsService = new ItemsService('collection_name', {
|
|
83
|
+
schema: await getSchema(),
|
|
84
|
+
accountability: context.accountability, // Track user actions
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Use emitEvents: false to prevent infinite loops
|
|
88
|
+
await itemsService.updateOne(meta.key, { processed: true }, { emitEvents: false });
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Hooks
|
|
96
|
+
|
|
97
|
+
### Filter Hook (Before Event)
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
export default ({ filter }, { services, getSchema }) => {
|
|
101
|
+
filter('items.create', async (payload, meta, context) => {
|
|
102
|
+
// Modify payload before creation
|
|
103
|
+
payload.modified_at = new Date();
|
|
104
|
+
return payload; // Must return payload
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Action Hook (After Event)
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
export default ({ action }, { services, getSchema, logger }) => {
|
|
113
|
+
action('items.create', async (meta, context) => {
|
|
114
|
+
logger.info(`Item ${meta.key} created in ${meta.collection}`);
|
|
115
|
+
|
|
116
|
+
// Side effects here
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Collection-Specific Hook
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
export default ({ action }) => {
|
|
125
|
+
// Only triggers for 'articles' collection
|
|
126
|
+
action('articles.items.create', async (meta, context) => {
|
|
127
|
+
console.log(`New article: ${meta.key}`);
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Schedule Hook (Cron)
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
export default ({ schedule }, { services, getSchema }) => {
|
|
136
|
+
// Every 15 minutes
|
|
137
|
+
schedule('*/15 * * * *', async () => {
|
|
138
|
+
console.log('Running scheduled task');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Daily at midnight
|
|
142
|
+
schedule('0 0 * * *', async () => {
|
|
143
|
+
// Cleanup old data
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Hook Events
|
|
149
|
+
|
|
150
|
+
**Filter Events:**
|
|
151
|
+
| Event | Payload | Use Case |
|
|
152
|
+
|-------|---------|----------|
|
|
153
|
+
| `items.create` | New item data | Modify before create |
|
|
154
|
+
| `items.update` | Updated fields | Validate changes |
|
|
155
|
+
| `items.delete` | Item keys | Prevent deletion |
|
|
156
|
+
| `auth.login` | Login payload | Custom auth validation |
|
|
157
|
+
|
|
158
|
+
**Action Events:**
|
|
159
|
+
| Event | Meta | Use Case |
|
|
160
|
+
|-------|------|----------|
|
|
161
|
+
| `items.create` | `key`, `collection`, `payload` | Send notifications |
|
|
162
|
+
| `items.update` | `keys`, `collection`, `payload` | Sync external systems |
|
|
163
|
+
| `items.delete` | `keys`, `collection` | Cleanup related data |
|
|
164
|
+
| `server.start` | `server` | Initialize resources |
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Endpoints
|
|
169
|
+
|
|
170
|
+
### Basic Endpoint
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
export default {
|
|
174
|
+
id: 'products',
|
|
175
|
+
handler: (router, { services, getSchema }) => {
|
|
176
|
+
const { ItemsService } = services;
|
|
177
|
+
|
|
178
|
+
router.get('/', asyncHandler(async (req, res) => {
|
|
179
|
+
const itemsService = new ItemsService('products', {
|
|
180
|
+
schema: await getSchema(),
|
|
181
|
+
accountability: req.accountability,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const items = await itemsService.readByQuery({ limit: 25 });
|
|
185
|
+
res.json({ data: items });
|
|
186
|
+
}));
|
|
187
|
+
|
|
188
|
+
router.get('/:id', asyncHandler(async (req, res) => {
|
|
189
|
+
const itemsService = new ItemsService('products', {
|
|
190
|
+
schema: await getSchema(),
|
|
191
|
+
accountability: req.accountability,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const item = await itemsService.readOne(req.params.id);
|
|
195
|
+
res.json({ data: item });
|
|
196
|
+
}));
|
|
197
|
+
|
|
198
|
+
router.post('/', asyncHandler(async (req, res) => {
|
|
199
|
+
const itemsService = new ItemsService('products', {
|
|
200
|
+
schema: await getSchema(),
|
|
201
|
+
accountability: req.accountability,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const key = await itemsService.createOne(req.body);
|
|
205
|
+
res.json({ data: { id: key } });
|
|
206
|
+
}));
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Services Reference
|
|
214
|
+
|
|
215
|
+
### ItemsService
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
const itemsService = new ItemsService('collection', {
|
|
219
|
+
schema: await getSchema(),
|
|
220
|
+
accountability: context.accountability,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Create
|
|
224
|
+
const id = await itemsService.createOne({ title: 'Hello' });
|
|
225
|
+
const ids = await itemsService.createMany([{ title: 'One' }, { title: 'Two' }]);
|
|
226
|
+
|
|
227
|
+
// Read
|
|
228
|
+
const item = await itemsService.readOne('item_id');
|
|
229
|
+
const items = await itemsService.readMany(['id1', 'id2']);
|
|
230
|
+
const results = await itemsService.readByQuery({
|
|
231
|
+
fields: ['id', 'title', 'author.*'],
|
|
232
|
+
filter: { status: { _eq: 'published' } },
|
|
233
|
+
sort: ['-date_created'],
|
|
234
|
+
limit: 10,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Update
|
|
238
|
+
await itemsService.updateOne('id', { title: 'Updated' });
|
|
239
|
+
await itemsService.updateMany(['id1', 'id2'], { status: 'archived' });
|
|
240
|
+
|
|
241
|
+
// Delete
|
|
242
|
+
await itemsService.deleteOne('id');
|
|
243
|
+
await itemsService.deleteMany(['id1', 'id2']);
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Other Services
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
const {
|
|
250
|
+
CollectionsService,
|
|
251
|
+
FieldsService,
|
|
252
|
+
RelationsService,
|
|
253
|
+
FilesService,
|
|
254
|
+
UsersService,
|
|
255
|
+
RolesService,
|
|
256
|
+
} = services;
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## App Extensions
|
|
262
|
+
|
|
263
|
+
### Interface
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
// index.ts
|
|
267
|
+
import { defineInterface } from '@directus/extensions-sdk';
|
|
268
|
+
import InterfaceComponent from './interface.vue';
|
|
269
|
+
|
|
270
|
+
export default defineInterface({
|
|
271
|
+
id: 'custom-input',
|
|
272
|
+
name: 'Custom Input',
|
|
273
|
+
icon: 'text_fields',
|
|
274
|
+
description: 'A custom input interface',
|
|
275
|
+
component: InterfaceComponent,
|
|
276
|
+
types: ['string'],
|
|
277
|
+
options: [
|
|
278
|
+
{
|
|
279
|
+
field: 'placeholder',
|
|
280
|
+
name: 'Placeholder',
|
|
281
|
+
type: 'string',
|
|
282
|
+
meta: { interface: 'input', width: 'full' },
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
});
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
```vue
|
|
289
|
+
<!-- interface.vue -->
|
|
290
|
+
<template>
|
|
291
|
+
<input
|
|
292
|
+
:value="value"
|
|
293
|
+
:placeholder="placeholder"
|
|
294
|
+
:disabled="disabled"
|
|
295
|
+
@input="emit('input', $event.target.value)"
|
|
296
|
+
/>
|
|
297
|
+
</template>
|
|
298
|
+
|
|
299
|
+
<script setup>
|
|
300
|
+
defineProps({
|
|
301
|
+
value: { type: String, default: null },
|
|
302
|
+
placeholder: { type: String, default: '' },
|
|
303
|
+
disabled: { type: Boolean, default: false },
|
|
304
|
+
collection: { type: String, required: true },
|
|
305
|
+
field: { type: String, required: true },
|
|
306
|
+
primaryKey: { type: String, default: null },
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
const emit = defineEmits(['input', 'setFieldValue']);
|
|
310
|
+
</script>
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Display
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
import { defineDisplay } from '@directus/extensions-sdk';
|
|
317
|
+
import DisplayComponent from './display.vue';
|
|
318
|
+
|
|
319
|
+
export default defineDisplay({
|
|
320
|
+
id: 'custom-display',
|
|
321
|
+
name: 'Custom Display',
|
|
322
|
+
icon: 'visibility',
|
|
323
|
+
component: DisplayComponent,
|
|
324
|
+
types: ['string'],
|
|
325
|
+
options: [],
|
|
326
|
+
});
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Panel (Dashboard)
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
import { definePanel } from '@directus/extensions-sdk';
|
|
333
|
+
import PanelComponent from './panel.vue';
|
|
334
|
+
|
|
335
|
+
export default definePanel({
|
|
336
|
+
id: 'custom-panel',
|
|
337
|
+
name: 'Custom Panel',
|
|
338
|
+
icon: 'dashboard',
|
|
339
|
+
component: PanelComponent,
|
|
340
|
+
minWidth: 12,
|
|
341
|
+
minHeight: 8,
|
|
342
|
+
options: [
|
|
343
|
+
{
|
|
344
|
+
field: 'collection',
|
|
345
|
+
name: 'Collection',
|
|
346
|
+
type: 'string',
|
|
347
|
+
meta: { interface: 'system-collection', width: 'full' },
|
|
348
|
+
},
|
|
349
|
+
],
|
|
350
|
+
});
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Module
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
import { defineModule } from '@directus/extensions-sdk';
|
|
357
|
+
import ModuleComponent from './module.vue';
|
|
358
|
+
|
|
359
|
+
export default defineModule({
|
|
360
|
+
id: 'custom-module',
|
|
361
|
+
name: 'Custom Module',
|
|
362
|
+
icon: 'extension',
|
|
363
|
+
routes: [
|
|
364
|
+
{ path: '', component: ModuleComponent },
|
|
365
|
+
{ path: 'subpage', component: () => import('./subpage.vue') },
|
|
366
|
+
],
|
|
367
|
+
preRegisterCheck(user) {
|
|
368
|
+
return user.role?.admin_access === true; // Only for admins
|
|
369
|
+
},
|
|
370
|
+
});
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## Composables (App Extensions)
|
|
376
|
+
|
|
377
|
+
```typescript
|
|
378
|
+
import { useApi, useStores, useCollection, useItems } from '@directus/extensions-sdk';
|
|
379
|
+
|
|
380
|
+
// API calls
|
|
381
|
+
const api = useApi();
|
|
382
|
+
const response = await api.get('/items/articles');
|
|
383
|
+
|
|
384
|
+
// Stores
|
|
385
|
+
const { useFieldsStore, useCollectionsStore, useUserStore } = useStores();
|
|
386
|
+
const fieldsStore = useFieldsStore();
|
|
387
|
+
const fields = fieldsStore.getFieldsForCollection('articles');
|
|
388
|
+
|
|
389
|
+
// Collection metadata
|
|
390
|
+
const { info, fields, primaryKeyField } = useCollection('articles');
|
|
391
|
+
|
|
392
|
+
// Fetch items
|
|
393
|
+
const { items, getItems, loading, itemCount } = useItems(ref('articles'), {
|
|
394
|
+
fields: ref(['*']),
|
|
395
|
+
limit: ref(25),
|
|
396
|
+
filter: ref(null),
|
|
397
|
+
});
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
## Best Practices
|
|
403
|
+
|
|
404
|
+
### DO
|
|
405
|
+
- Use TypeScript for all extensions
|
|
406
|
+
- Use Services over raw database queries
|
|
407
|
+
- Include `accountability` when creating services
|
|
408
|
+
- Use `emitEvents: false` to prevent infinite loops
|
|
409
|
+
- Wrap endpoint handlers with `asyncHandler`
|
|
410
|
+
- Use bundle extensions for organization
|
|
411
|
+
|
|
412
|
+
### DON'T
|
|
413
|
+
- Use `context.database` directly (use Services)
|
|
414
|
+
- Forget error handling in endpoints
|
|
415
|
+
- Create hooks that trigger themselves
|
|
416
|
+
- Expose admin functionality without permission checks
|
|
417
|
+
- Skip input validation in endpoints
|
|
418
|
+
|
|
419
|
+
## Validation & Publishing
|
|
420
|
+
|
|
421
|
+
```bash
|
|
422
|
+
# Validate extension
|
|
423
|
+
npx create-directus-extension@latest validate
|
|
424
|
+
|
|
425
|
+
# Link for development
|
|
426
|
+
npx create-directus-extension@latest link
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### package.json for Marketplace
|
|
430
|
+
|
|
431
|
+
```json
|
|
432
|
+
{
|
|
433
|
+
"name": "directus-extension-your-name",
|
|
434
|
+
"version": "1.0.0",
|
|
435
|
+
"keywords": ["directus-extension"],
|
|
436
|
+
"directus:extension": {
|
|
437
|
+
"type": "bundle",
|
|
438
|
+
"path": "dist/index.js",
|
|
439
|
+
"source": "src/index.ts",
|
|
440
|
+
"host": "^10.7.0",
|
|
441
|
+
"entries": []
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
```
|
|
@@ -453,11 +453,56 @@ export const useCartStore = defineStore('cart', () => {
|
|
|
453
453
|
});
|
|
454
454
|
```
|
|
455
455
|
|
|
456
|
+
## NuxtUI v4
|
|
457
|
+
|
|
458
|
+
For UI components, forms, and theming with NuxtUI v4:
|
|
459
|
+
|
|
460
|
+
**See:** `references/nuxtui-v4.md`
|
|
461
|
+
|
|
462
|
+
Covers:
|
|
463
|
+
- 130+ production-ready components
|
|
464
|
+
- Forms with validation (Zod, Yup, etc.)
|
|
465
|
+
- Theming and customization
|
|
466
|
+
- Dark/light mode
|
|
467
|
+
- Advanced UI patterns and animations
|
|
468
|
+
- v3 to v4 migration guide
|
|
469
|
+
|
|
470
|
+
### Quick NuxtUI Example
|
|
471
|
+
|
|
472
|
+
```bash
|
|
473
|
+
# Create with NuxtUI template
|
|
474
|
+
npm create nuxt@latest my-app -- -t ui
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
```vue
|
|
478
|
+
<script setup>
|
|
479
|
+
import { z } from 'zod'
|
|
480
|
+
|
|
481
|
+
const schema = z.object({
|
|
482
|
+
email: z.string().email(),
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
const state = reactive({ email: '' })
|
|
486
|
+
</script>
|
|
487
|
+
|
|
488
|
+
<template>
|
|
489
|
+
<UForm :state="state" :schema="schema" @submit="onSubmit">
|
|
490
|
+
<UFormField label="Email" name="email">
|
|
491
|
+
<UInput v-model="state.email" type="email" />
|
|
492
|
+
</UFormField>
|
|
493
|
+
<UButton type="submit" label="Submit" />
|
|
494
|
+
</UForm>
|
|
495
|
+
</template>
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
**Important:** NuxtUI v4 requires Nuxt 4 and `<UApp>` wrapper.
|
|
499
|
+
|
|
456
500
|
## Resources
|
|
457
501
|
|
|
458
502
|
### Official Docs
|
|
459
503
|
- Docs: https://nuxt.com/docs
|
|
460
504
|
- Modules: https://nuxt.com/modules
|
|
505
|
+
- NuxtUI: https://ui.nuxt.com
|
|
461
506
|
|
|
462
507
|
### Context7 Queries
|
|
463
508
|
|
|
@@ -466,4 +511,6 @@ Query: "Nuxt 3 useFetch best practices"
|
|
|
466
511
|
Query: "Nuxt 3 server routes authentication"
|
|
467
512
|
Query: "Nuxt 3 middleware patterns"
|
|
468
513
|
Query: "Nuxt 3 Pinia setup"
|
|
514
|
+
Query: "NuxtUI v4 components"
|
|
515
|
+
Query: "NuxtUI theming"
|
|
469
516
|
```
|
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
# NuxtUI v4
|
|
2
|
+
|
|
3
|
+
Comprehensive guide for NuxtUI v4 - 130+ production-ready components for Nuxt applications.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
### Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Create with template (recommended)
|
|
11
|
+
npm create nuxt@latest my-app -- -t ui
|
|
12
|
+
|
|
13
|
+
# Or add to existing project
|
|
14
|
+
pnpm add @nuxt/ui
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Configuration
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
// nuxt.config.ts
|
|
21
|
+
export default defineNuxtConfig({
|
|
22
|
+
modules: ['@nuxt/ui'],
|
|
23
|
+
ui: {
|
|
24
|
+
prefix: 'U', // Component prefix (default)
|
|
25
|
+
colorMode: true, // Enable dark/light mode
|
|
26
|
+
colors: {
|
|
27
|
+
primary: 'green',
|
|
28
|
+
neutral: 'slate',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```css
|
|
35
|
+
/* app/assets/css/main.css */
|
|
36
|
+
@import "tailwindcss";
|
|
37
|
+
@import "@nuxt/ui";
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```vue
|
|
41
|
+
<!-- app.vue - Required wrapper -->
|
|
42
|
+
<template>
|
|
43
|
+
<UApp>
|
|
44
|
+
<NuxtLayout>
|
|
45
|
+
<NuxtPage />
|
|
46
|
+
</NuxtLayout>
|
|
47
|
+
</UApp>
|
|
48
|
+
</template>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Important:** `<UApp>` is required for Toast, Tooltip, and Modal to work.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Common Components
|
|
56
|
+
|
|
57
|
+
### Button
|
|
58
|
+
|
|
59
|
+
```vue
|
|
60
|
+
<UButton
|
|
61
|
+
label="Click me"
|
|
62
|
+
color="primary" <!-- primary | secondary | success | error | warning | info | neutral -->
|
|
63
|
+
variant="solid" <!-- solid | outline | soft | subtle | ghost | link -->
|
|
64
|
+
size="md" <!-- xs | sm | md | lg | xl -->
|
|
65
|
+
icon="i-lucide-check"
|
|
66
|
+
:loading="isLoading"
|
|
67
|
+
:loading-auto="true" <!-- Auto loading on async @click -->
|
|
68
|
+
@click="handleClick"
|
|
69
|
+
/>
|
|
70
|
+
|
|
71
|
+
<!-- Icon only -->
|
|
72
|
+
<UButton icon="i-lucide-settings" square />
|
|
73
|
+
|
|
74
|
+
<!-- As link -->
|
|
75
|
+
<UButton to="/dashboard" label="Dashboard" />
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Input
|
|
79
|
+
|
|
80
|
+
```vue
|
|
81
|
+
<UInput
|
|
82
|
+
v-model="value"
|
|
83
|
+
placeholder="Enter text..."
|
|
84
|
+
type="text" <!-- text | email | password | number | etc. -->
|
|
85
|
+
icon="i-lucide-user"
|
|
86
|
+
:loading="isLoading"
|
|
87
|
+
:disabled="false"
|
|
88
|
+
/>
|
|
89
|
+
|
|
90
|
+
<!-- With model modifiers -->
|
|
91
|
+
<UInput v-model.nullable="value" /> <!-- Empty -> null -->
|
|
92
|
+
<UInput v-model.optional="value" /> <!-- Empty -> undefined -->
|
|
93
|
+
|
|
94
|
+
<!-- With slots -->
|
|
95
|
+
<UInput v-model="value">
|
|
96
|
+
<template #leading>
|
|
97
|
+
<UIcon name="i-lucide-search" />
|
|
98
|
+
</template>
|
|
99
|
+
<template #trailing>
|
|
100
|
+
<UButton icon="i-lucide-x" size="xs" variant="ghost" />
|
|
101
|
+
</template>
|
|
102
|
+
</UInput>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Form with Validation
|
|
106
|
+
|
|
107
|
+
```vue
|
|
108
|
+
<script setup>
|
|
109
|
+
import { z } from 'zod'
|
|
110
|
+
|
|
111
|
+
const schema = z.object({
|
|
112
|
+
email: z.string().email('Invalid email'),
|
|
113
|
+
password: z.string().min(8, 'Min 8 characters'),
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const state = reactive({
|
|
117
|
+
email: '',
|
|
118
|
+
password: '',
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
async function onSubmit(data) {
|
|
122
|
+
// data is validated and transformed
|
|
123
|
+
console.log('Valid:', data)
|
|
124
|
+
}
|
|
125
|
+
</script>
|
|
126
|
+
|
|
127
|
+
<template>
|
|
128
|
+
<UForm :state="state" :schema="schema" @submit="onSubmit">
|
|
129
|
+
<UFormField label="Email" name="email" required>
|
|
130
|
+
<UInput v-model="state.email" type="email" />
|
|
131
|
+
</UFormField>
|
|
132
|
+
|
|
133
|
+
<UFormField label="Password" name="password" required>
|
|
134
|
+
<UInput v-model="state.password" type="password" />
|
|
135
|
+
</UFormField>
|
|
136
|
+
|
|
137
|
+
<UButton type="submit" label="Submit" />
|
|
138
|
+
</UForm>
|
|
139
|
+
</template>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Modal
|
|
143
|
+
|
|
144
|
+
```vue
|
|
145
|
+
<template>
|
|
146
|
+
<UModal v-model:open="isOpen" title="Confirm">
|
|
147
|
+
<template #default>
|
|
148
|
+
<UButton @click="isOpen = true" label="Open Modal" />
|
|
149
|
+
</template>
|
|
150
|
+
|
|
151
|
+
<template #content="{ close }">
|
|
152
|
+
<p>Are you sure?</p>
|
|
153
|
+
</template>
|
|
154
|
+
|
|
155
|
+
<template #footer="{ close }">
|
|
156
|
+
<UButton @click="close" label="Cancel" variant="ghost" />
|
|
157
|
+
<UButton @click="confirm" label="Confirm" />
|
|
158
|
+
</template>
|
|
159
|
+
</UModal>
|
|
160
|
+
</template>
|
|
161
|
+
|
|
162
|
+
<!-- Programmatic -->
|
|
163
|
+
<script setup>
|
|
164
|
+
const modal = useModal()
|
|
165
|
+
|
|
166
|
+
function openModal() {
|
|
167
|
+
modal.open(MyModalComponent, { /* props */ })
|
|
168
|
+
}
|
|
169
|
+
</script>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Toast
|
|
173
|
+
|
|
174
|
+
```vue
|
|
175
|
+
<script setup>
|
|
176
|
+
const toast = useToast()
|
|
177
|
+
|
|
178
|
+
function showSuccess() {
|
|
179
|
+
toast.add({
|
|
180
|
+
title: 'Success!',
|
|
181
|
+
description: 'Changes saved.',
|
|
182
|
+
color: 'success',
|
|
183
|
+
icon: 'i-lucide-check-circle',
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
</script>
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Table
|
|
190
|
+
|
|
191
|
+
```vue
|
|
192
|
+
<script setup>
|
|
193
|
+
const columns = [
|
|
194
|
+
{ key: 'name', label: 'Name' },
|
|
195
|
+
{ key: 'email', label: 'Email' },
|
|
196
|
+
{ key: 'role', label: 'Role' },
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
const rows = [
|
|
200
|
+
{ name: 'John', email: 'john@example.com', role: 'Admin' },
|
|
201
|
+
]
|
|
202
|
+
</script>
|
|
203
|
+
|
|
204
|
+
<template>
|
|
205
|
+
<UTable :columns="columns" :rows="rows" />
|
|
206
|
+
</template>
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Select
|
|
210
|
+
|
|
211
|
+
```vue
|
|
212
|
+
<script setup>
|
|
213
|
+
const options = [
|
|
214
|
+
{ value: 'vue', label: 'Vue.js' },
|
|
215
|
+
{ value: 'react', label: 'React' },
|
|
216
|
+
]
|
|
217
|
+
const selected = ref('')
|
|
218
|
+
</script>
|
|
219
|
+
|
|
220
|
+
<template>
|
|
221
|
+
<USelect
|
|
222
|
+
v-model="selected"
|
|
223
|
+
:options="options"
|
|
224
|
+
placeholder="Select..."
|
|
225
|
+
value-attribute="value"
|
|
226
|
+
label-attribute="label"
|
|
227
|
+
/>
|
|
228
|
+
</template>
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Dropdown Menu
|
|
232
|
+
|
|
233
|
+
```vue
|
|
234
|
+
<template>
|
|
235
|
+
<UDropdownMenu>
|
|
236
|
+
<UButton label="Actions" trailing-icon="i-lucide-chevron-down" />
|
|
237
|
+
|
|
238
|
+
<template #content>
|
|
239
|
+
<UDropdownMenuItem label="Edit" icon="i-lucide-pencil" @click="edit" />
|
|
240
|
+
<UDropdownMenuItem label="Delete" icon="i-lucide-trash" color="error" />
|
|
241
|
+
</template>
|
|
242
|
+
</UDropdownMenu>
|
|
243
|
+
</template>
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Tabs
|
|
247
|
+
|
|
248
|
+
```vue
|
|
249
|
+
<script setup>
|
|
250
|
+
const items = [
|
|
251
|
+
{ label: 'Overview', value: 'overview' },
|
|
252
|
+
{ label: 'Settings', value: 'settings' },
|
|
253
|
+
]
|
|
254
|
+
const selected = ref('overview')
|
|
255
|
+
</script>
|
|
256
|
+
|
|
257
|
+
<template>
|
|
258
|
+
<UTabs v-model="selected" :items="items">
|
|
259
|
+
<template #overview>Overview content</template>
|
|
260
|
+
<template #settings>Settings content</template>
|
|
261
|
+
</UTabs>
|
|
262
|
+
</template>
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Theming
|
|
268
|
+
|
|
269
|
+
### Semantic Colors
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
// app.config.ts
|
|
273
|
+
export default defineAppConfig({
|
|
274
|
+
ui: {
|
|
275
|
+
colors: {
|
|
276
|
+
primary: 'green', // Main brand color
|
|
277
|
+
secondary: 'blue', // Alternative actions
|
|
278
|
+
success: 'emerald', // Success states
|
|
279
|
+
error: 'red', // Error states
|
|
280
|
+
warning: 'amber', // Warnings
|
|
281
|
+
info: 'sky', // Information
|
|
282
|
+
neutral: 'slate', // Text, backgrounds
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
})
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Custom CSS Variables
|
|
289
|
+
|
|
290
|
+
```css
|
|
291
|
+
/* main.css */
|
|
292
|
+
@import "tailwindcss";
|
|
293
|
+
@import "@nuxt/ui";
|
|
294
|
+
|
|
295
|
+
@theme {
|
|
296
|
+
/* Custom colors */
|
|
297
|
+
--color-brand-500: #10b981;
|
|
298
|
+
|
|
299
|
+
/* Custom fonts */
|
|
300
|
+
--font-sans: 'Inter', system-ui, sans-serif;
|
|
301
|
+
--font-mono: 'Fira Code', monospace;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
:root {
|
|
305
|
+
--color-gold: #d4af37;
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### Component Customization
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
// app.config.ts - Global defaults
|
|
313
|
+
export default defineAppConfig({
|
|
314
|
+
ui: {
|
|
315
|
+
button: {
|
|
316
|
+
slots: {
|
|
317
|
+
base: 'font-semibold transition-all',
|
|
318
|
+
},
|
|
319
|
+
defaultVariants: {
|
|
320
|
+
color: 'primary',
|
|
321
|
+
variant: 'solid',
|
|
322
|
+
size: 'md',
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
})
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
```vue
|
|
330
|
+
<!-- Per-component customization -->
|
|
331
|
+
<UButton
|
|
332
|
+
label="Custom"
|
|
333
|
+
:ui="{
|
|
334
|
+
base: 'rounded-full',
|
|
335
|
+
label: 'font-bold uppercase',
|
|
336
|
+
}"
|
|
337
|
+
/>
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### Color Mode (Dark/Light)
|
|
341
|
+
|
|
342
|
+
```vue
|
|
343
|
+
<template>
|
|
344
|
+
<!-- Toggle button -->
|
|
345
|
+
<UColorModeButton />
|
|
346
|
+
|
|
347
|
+
<!-- Select dropdown -->
|
|
348
|
+
<UColorModeSelect />
|
|
349
|
+
|
|
350
|
+
<!-- Switch -->
|
|
351
|
+
<UColorModeSwitch />
|
|
352
|
+
|
|
353
|
+
<!-- Theme-aware image -->
|
|
354
|
+
<UColorModeImage light="/logo-light.png" dark="/logo-dark.png" />
|
|
355
|
+
</template>
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## Advanced UI Patterns
|
|
361
|
+
|
|
362
|
+
### Page Load Animation
|
|
363
|
+
|
|
364
|
+
```vue
|
|
365
|
+
<script setup>
|
|
366
|
+
const isVisible = ref(false)
|
|
367
|
+
onMounted(() => {
|
|
368
|
+
setTimeout(() => { isVisible.value = true }, 50)
|
|
369
|
+
})
|
|
370
|
+
</script>
|
|
371
|
+
|
|
372
|
+
<template>
|
|
373
|
+
<div :class="{ 'opacity-0 translate-y-8': !isVisible, 'opacity-100 translate-y-0': isVisible }"
|
|
374
|
+
class="transition-all duration-700 ease-out">
|
|
375
|
+
<!-- Content -->
|
|
376
|
+
</div>
|
|
377
|
+
</template>
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Staggered Animations
|
|
381
|
+
|
|
382
|
+
```vue
|
|
383
|
+
<template>
|
|
384
|
+
<div v-for="(item, i) in items" :key="item.id"
|
|
385
|
+
:style="{ '--delay': `${i * 0.1}s` }"
|
|
386
|
+
class="animate-slide-up">
|
|
387
|
+
{{ item.title }}
|
|
388
|
+
</div>
|
|
389
|
+
</template>
|
|
390
|
+
|
|
391
|
+
<style>
|
|
392
|
+
.animate-slide-up {
|
|
393
|
+
animation: slideUp 0.6s ease-out var(--delay) forwards;
|
|
394
|
+
opacity: 0;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
@keyframes slideUp {
|
|
398
|
+
from { opacity: 0; transform: translateY(20px); }
|
|
399
|
+
to { opacity: 1; transform: translateY(0); }
|
|
400
|
+
}
|
|
401
|
+
</style>
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Gradient Mesh Background
|
|
405
|
+
|
|
406
|
+
```vue
|
|
407
|
+
<style>
|
|
408
|
+
.mesh-bg {
|
|
409
|
+
background:
|
|
410
|
+
radial-gradient(ellipse 800px 600px at 20% 20%, rgba(99, 102, 241, 0.15), transparent),
|
|
411
|
+
radial-gradient(ellipse 600px 800px at 80% 80%, rgba(168, 85, 247, 0.15), transparent);
|
|
412
|
+
}
|
|
413
|
+
</style>
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### Hover Effects
|
|
417
|
+
|
|
418
|
+
```vue
|
|
419
|
+
<style>
|
|
420
|
+
.card-hover {
|
|
421
|
+
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
.card-hover:hover {
|
|
425
|
+
transform: translateY(-4px);
|
|
426
|
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
|
|
427
|
+
}
|
|
428
|
+
</style>
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
## v3 to v4 Migration
|
|
434
|
+
|
|
435
|
+
### Component Renames
|
|
436
|
+
| v3 | v4 |
|
|
437
|
+
|----|-----|
|
|
438
|
+
| `UButtonGroup` | `UFieldGroup` |
|
|
439
|
+
| `UPageMarquee` | `UMarquee` |
|
|
440
|
+
| `UPageAccordion` | `UAccordion` |
|
|
441
|
+
|
|
442
|
+
### Model Modifiers
|
|
443
|
+
| v3 | v4 |
|
|
444
|
+
|----|-----|
|
|
445
|
+
| `.nullify` | `.nullable` or `.optional` |
|
|
446
|
+
|
|
447
|
+
### Nested Forms
|
|
448
|
+
```vue
|
|
449
|
+
<!-- v4 requires nested prop and name -->
|
|
450
|
+
<UForm :state="nestedState" nested name="address">
|
|
451
|
+
<!-- fields -->
|
|
452
|
+
</UForm>
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Module Changes
|
|
456
|
+
- Replace `@nuxt/ui-pro` with `@nuxt/ui`
|
|
457
|
+
- Merge `uiPro` config into `ui` config
|
|
458
|
+
|
|
459
|
+
---
|
|
460
|
+
|
|
461
|
+
## Icon Format
|
|
462
|
+
|
|
463
|
+
NuxtUI uses Iconify. Reference icons like:
|
|
464
|
+
|
|
465
|
+
```vue
|
|
466
|
+
<UIcon name="i-lucide-user" />
|
|
467
|
+
<UButton icon="i-heroicons-check" />
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
Browse icons: https://icones.js.org
|
|
471
|
+
|
|
472
|
+
---
|
|
473
|
+
|
|
474
|
+
## Dashboard Layout
|
|
475
|
+
|
|
476
|
+
```vue
|
|
477
|
+
<template>
|
|
478
|
+
<UDashboardGroup>
|
|
479
|
+
<UDashboardSidebar>
|
|
480
|
+
<!-- Navigation -->
|
|
481
|
+
</UDashboardSidebar>
|
|
482
|
+
|
|
483
|
+
<UDashboardPanel>
|
|
484
|
+
<UDashboardNavbar>
|
|
485
|
+
<!-- Header -->
|
|
486
|
+
</UDashboardNavbar>
|
|
487
|
+
|
|
488
|
+
<slot />
|
|
489
|
+
</UDashboardPanel>
|
|
490
|
+
</UDashboardGroup>
|
|
491
|
+
</template>
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
---
|
|
495
|
+
|
|
496
|
+
## Best Practices
|
|
497
|
+
|
|
498
|
+
### DO
|
|
499
|
+
- Use semantic colors (`primary`, not `green-500`)
|
|
500
|
+
- Use `<UApp>` wrapper (required for overlays)
|
|
501
|
+
- Use v4 model modifiers (`.nullable`, `.optional`)
|
|
502
|
+
- Use `:ui` prop for component customization
|
|
503
|
+
- Import styles in `main.css`
|
|
504
|
+
|
|
505
|
+
### DON'T
|
|
506
|
+
- Use v3 component names (`UButtonGroup`)
|
|
507
|
+
- Use `.nullify` modifier (use `.nullable`)
|
|
508
|
+
- Forget `nested` and `name` props for nested forms
|
|
509
|
+
- Hardcode colors (use semantic colors)
|
|
510
|
+
- Skip `<UApp>` wrapper
|
|
511
|
+
|
|
512
|
+
---
|
|
513
|
+
|
|
514
|
+
## Resources
|
|
515
|
+
|
|
516
|
+
- Components: https://ui.nuxt.com/docs/components
|
|
517
|
+
- Theming: https://ui.nuxt.com/docs/getting-started/theme
|
|
518
|
+
- Icons: https://icones.js.org
|
|
519
|
+
- Tailwind Colors: https://tailwindcss.com/docs/customizing-colors
|