@elyracode/stack-primevue 0.3.5

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/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # @elyracode/stack-primevue
2
+
3
+ Elyra extension for **PrimeVue 4** -- 80+ UI components for Vue 3 with Laravel/Inertia.
4
+
5
+ ## Install
6
+
7
+ ```
8
+ elyra install npm:@elyracode/stack-primevue
9
+ ```
10
+
11
+ ## What's included
12
+
13
+ - **Skills**: Comprehensive PrimeVue 4 component reference (DataTable, Dialog, Forms, Navigation, Toast, ConfirmDialog, theming, Laravel Inertia patterns, gotchas)
14
+ - **Commands**: `/primevue:info` -- show profile status
15
+
16
+ ## Usage
17
+
18
+ Just ask Elyra about PrimeVue components:
19
+
20
+ ```
21
+ > Build a products CRUD page with DataTable and Dialog
22
+ > Add a filterable sortable table with pagination
23
+ > Create a form with validation using PrimeVue inputs
24
+ > Set up PrimeVue with the Aura theme in my Laravel app
25
+ ```
26
+
27
+ The agent knows PrimeVue's component API, import patterns, theming system, and how to integrate with Laravel Inertia.
@@ -0,0 +1,13 @@
1
+ import type { ExtensionAPI } from "@elyracode/coding-agent";
2
+
3
+ export default function (elyra: ExtensionAPI) {
4
+ // The primevue skill is auto-loaded from the skills/ directory.
5
+ // This extension adds stack-aware commands.
6
+
7
+ elyra.registerCommand("primevue:info", {
8
+ description: "Show PrimeVue stack profile information",
9
+ handler: async (_args, ctx) => {
10
+ ctx.ui.notify("PrimeVue 4 profile loaded. Skills: primevue. Use /skill:primevue for full component reference.");
11
+ },
12
+ });
13
+ }
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@elyracode/stack-primevue",
3
+ "version": "0.3.5",
4
+ "description": "Elyra extension for PrimeVue 4 -- 80+ UI components for Vue 3 with Laravel/Inertia",
5
+ "type": "module",
6
+ "keywords": ["elyra-package", "primevue", "vue", "laravel", "inertia", "ui-components"],
7
+ "license": "MIT",
8
+ "author": "Knut W. Horne",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/kwhorne/elyra.git",
12
+ "directory": "packages/stack-primevue"
13
+ },
14
+ "elyra": {
15
+ "skills": ["./skills"],
16
+ "extensions": ["./extensions/index.ts"]
17
+ },
18
+ "peerDependencies": {
19
+ "@elyracode/coding-agent": "*",
20
+ "typebox": "*"
21
+ },
22
+ "scripts": {
23
+ "clean": "echo 'nothing to clean'",
24
+ "build": "echo 'nothing to build'",
25
+ "check": "echo 'nothing to check'"
26
+ }
27
+ }
@@ -0,0 +1,1199 @@
1
+ ---
2
+ name: primevue
3
+ description: Deep knowledge about PrimeVue 4 UI component library for Vue 3. Use when working with PrimeVue components, theming, forms, data tables, dialogs, or any PrimeVue UI patterns in Vue/Laravel/Inertia projects.
4
+ ---
5
+
6
+ # PrimeVue 4 Reference
7
+
8
+ ## Setup
9
+
10
+ ### Installation
11
+ ```bash
12
+ npm install primevue @primeuix/themes primeicons
13
+ ```
14
+
15
+ ### Plugin Configuration
16
+ ```typescript
17
+ // main.ts or app.ts
18
+ import { createApp } from 'vue'
19
+ import PrimeVue from 'primevue/config'
20
+ import Aura from '@primeuix/themes/aura'
21
+ import 'primeicons/primeicons.css'
22
+
23
+ const app = createApp(App)
24
+ app.use(PrimeVue, {
25
+ theme: {
26
+ preset: Aura
27
+ }
28
+ })
29
+ ```
30
+
31
+ ### With Laravel Inertia
32
+ ```typescript
33
+ // resources/js/app.ts
34
+ import { createApp, h } from 'vue'
35
+ import { createInertiaApp } from '@inertiajs/vue3'
36
+ import PrimeVue from 'primevue/config'
37
+ import Aura from '@primeuix/themes/aura'
38
+ import ToastService from 'primevue/toastservice'
39
+ import ConfirmationService from 'primevue/confirmationservice'
40
+ import 'primeicons/primeicons.css'
41
+
42
+ createInertiaApp({
43
+ resolve: name => {
44
+ const pages = import.meta.glob('./Pages/**/*.vue', { eager: true })
45
+ return pages[`./Pages/${name}.vue`]
46
+ },
47
+ setup({ el, App, props, plugin }) {
48
+ const app = createApp({ render: () => h(App, props) })
49
+ app.use(plugin)
50
+ app.use(PrimeVue, { theme: { preset: Aura } })
51
+ app.use(ToastService)
52
+ app.use(ConfirmationService)
53
+ app.mount(el)
54
+ },
55
+ })
56
+ ```
57
+
58
+ ### Available Theme Presets
59
+ - `Aura` -- Modern, clean default theme
60
+ - `Material` -- Material Design inspired
61
+ - `Lara` -- Bootstrap-like familiar style
62
+ - `Nora` -- Minimal, subtle design
63
+
64
+ Import any preset from `@primeuix/themes/<preset-name-lowercase>`:
65
+ ```typescript
66
+ import Aura from '@primeuix/themes/aura'
67
+ import Material from '@primeuix/themes/material'
68
+ import Lara from '@primeuix/themes/lara'
69
+ import Nora from '@primeuix/themes/nora'
70
+ ```
71
+
72
+ ## Component Import Pattern
73
+
74
+ PrimeVue uses individual component imports for tree-shaking. There is no barrel export -- import each component from its own path:
75
+
76
+ ```vue
77
+ <script setup lang="ts">
78
+ import Button from 'primevue/button'
79
+ import DataTable from 'primevue/datatable'
80
+ import Column from 'primevue/column'
81
+ import InputText from 'primevue/inputtext'
82
+ import Dialog from 'primevue/dialog'
83
+ </script>
84
+ ```
85
+
86
+ ## Core Components
87
+
88
+ ### Form Components
89
+
90
+ #### InputText
91
+ ```vue
92
+ <InputText v-model="name" placeholder="Enter name" />
93
+ <InputText v-model="email" type="email" :invalid="!!errors.email" />
94
+ ```
95
+
96
+ #### Password
97
+ ```vue
98
+ <Password v-model="password" toggleMask :feedback="true" />
99
+ ```
100
+
101
+ #### Textarea
102
+ ```vue
103
+ <Textarea v-model="description" rows="5" cols="30" autoResize />
104
+ ```
105
+
106
+ #### Select (Dropdown)
107
+ ```vue
108
+ <Select v-model="selectedCity" :options="cities" optionLabel="name"
109
+ optionValue="code" placeholder="Select a city" />
110
+ ```
111
+
112
+ With grouped options:
113
+ ```vue
114
+ <Select v-model="selectedCity" :options="groupedCities" optionLabel="label"
115
+ optionValue="value" optionGroupLabel="label" optionGroupChildren="items"
116
+ placeholder="Select a city" />
117
+ ```
118
+
119
+ #### AutoComplete
120
+ ```vue
121
+ <AutoComplete v-model="value" :suggestions="filteredItems"
122
+ @complete="search($event)" field="name" placeholder="Search..." />
123
+ ```
124
+
125
+ #### MultiSelect
126
+ ```vue
127
+ <MultiSelect v-model="selectedCities" :options="cities" optionLabel="name"
128
+ placeholder="Select Cities" :maxSelectedLabels="3" display="chip" />
129
+ ```
130
+
131
+ #### DatePicker (Calendar)
132
+ ```vue
133
+ <DatePicker v-model="date" dateFormat="yy-mm-dd" showIcon />
134
+ <DatePicker v-model="dateRange" selectionMode="range" :manualInput="false" />
135
+ <DatePicker v-model="dateTime" showTime hourFormat="24" />
136
+ ```
137
+
138
+ #### Checkbox
139
+ ```vue
140
+ <!-- Binary -->
141
+ <Checkbox v-model="accept" :binary="true" inputId="accept" />
142
+ <label for="accept">I accept</label>
143
+
144
+ <!-- Multiple values -->
145
+ <Checkbox v-model="selectedCategories" :value="category" inputId="cat1" />
146
+ ```
147
+
148
+ #### RadioButton
149
+ ```vue
150
+ <div v-for="option in options" :key="option.value">
151
+ <RadioButton v-model="selected" :value="option.value" :inputId="option.value" />
152
+ <label :for="option.value">{{ option.label }}</label>
153
+ </div>
154
+ ```
155
+
156
+ #### InputNumber
157
+ ```vue
158
+ <InputNumber v-model="price" mode="currency" currency="USD" locale="en-US" />
159
+ <InputNumber v-model="quantity" :min="0" :max="100" showButtons />
160
+ <InputNumber v-model="percent" prefix="%" :minFractionDigits="2" />
161
+ ```
162
+
163
+ #### ToggleSwitch
164
+ ```vue
165
+ <ToggleSwitch v-model="checked" />
166
+ ```
167
+
168
+ #### Slider
169
+ ```vue
170
+ <Slider v-model="value" :min="0" :max="100" />
171
+ <Slider v-model="rangeValues" range :min="0" :max="100" />
172
+ ```
173
+
174
+ #### ColorPicker
175
+ ```vue
176
+ <ColorPicker v-model="color" />
177
+ ```
178
+
179
+ #### InputMask
180
+ ```vue
181
+ <InputMask v-model="phone" mask="(999) 999-9999" placeholder="(999) 999-9999" />
182
+ <InputMask v-model="ssn" mask="999-99-9999" />
183
+ ```
184
+
185
+ #### Chips
186
+ ```vue
187
+ <Chips v-model="tags" separator="," placeholder="Add tags" />
188
+ ```
189
+
190
+ #### Editor (rich text)
191
+ ```vue
192
+ <Editor v-model="content" editorStyle="height: 320px" />
193
+ ```
194
+
195
+ #### FileUpload
196
+ ```vue
197
+ <!-- Basic -->
198
+ <FileUpload mode="basic" accept="image/*" :maxFileSize="1000000"
199
+ @upload="onUpload" :auto="true" chooseLabel="Upload" />
200
+
201
+ <!-- Advanced with drag-and-drop -->
202
+ <FileUpload name="files[]" url="/api/upload" :multiple="true"
203
+ accept="image/*" :maxFileSize="5000000">
204
+ <template #empty>
205
+ <p>Drag and drop files here to upload.</p>
206
+ </template>
207
+ </FileUpload>
208
+ ```
209
+
210
+ ### Data Display
211
+
212
+ #### DataTable
213
+ ```vue
214
+ <script setup lang="ts">
215
+ import DataTable from 'primevue/datatable'
216
+ import Column from 'primevue/column'
217
+
218
+ interface Product {
219
+ id: number
220
+ name: string
221
+ price: number
222
+ category: string
223
+ }
224
+
225
+ const props = defineProps<{ products: Product[] }>()
226
+ </script>
227
+
228
+ <template>
229
+ <DataTable :value="products" stripedRows paginator :rows="10"
230
+ :rowsPerPageOptions="[5, 10, 20, 50]"
231
+ sortMode="multiple" removableSort
232
+ v-model:filters="filters" filterDisplay="row">
233
+ <Column field="name" header="Name" sortable>
234
+ <template #filter="{ filterModel, filterCallback }">
235
+ <InputText v-model="filterModel.value" @input="filterCallback()" placeholder="Search" />
236
+ </template>
237
+ </Column>
238
+ <Column field="price" header="Price" sortable>
239
+ <template #body="{ data }">
240
+ {{ formatCurrency(data.price) }}
241
+ </template>
242
+ </Column>
243
+ <Column field="category" header="Category" sortable />
244
+ <Column header="Actions">
245
+ <template #body="{ data }">
246
+ <Button icon="pi pi-pencil" severity="info" text rounded @click="edit(data)" />
247
+ <Button icon="pi pi-trash" severity="danger" text rounded @click="remove(data)" />
248
+ </template>
249
+ </Column>
250
+ </DataTable>
251
+ </template>
252
+ ```
253
+
254
+ #### DataTable Filter Initialization
255
+ ```typescript
256
+ import { FilterMatchMode } from '@primevue/core/api'
257
+
258
+ const filters = ref({
259
+ global: { value: null, matchMode: FilterMatchMode.CONTAINS },
260
+ name: { value: null, matchMode: FilterMatchMode.STARTS_WITH },
261
+ category: { value: null, matchMode: FilterMatchMode.EQUALS },
262
+ })
263
+ ```
264
+
265
+ #### DataTable with Selection
266
+ ```vue
267
+ <DataTable v-model:selection="selectedProducts" :value="products"
268
+ selectionMode="multiple" dataKey="id">
269
+ <Column selectionMode="multiple" headerStyle="width: 3rem" />
270
+ <Column field="name" header="Name" />
271
+ </DataTable>
272
+ ```
273
+
274
+ #### DataTable with Expandable Rows
275
+ ```vue
276
+ <DataTable :value="products" v-model:expandedRows="expandedRows" dataKey="id">
277
+ <Column expander style="width: 5rem" />
278
+ <Column field="name" header="Name" />
279
+ <template #expansion="{ data }">
280
+ <div class="p-4">
281
+ <h5>Orders for {{ data.name }}</h5>
282
+ <DataTable :value="data.orders">
283
+ <Column field="id" header="Id" />
284
+ <Column field="amount" header="Amount" />
285
+ </DataTable>
286
+ </div>
287
+ </template>
288
+ </DataTable>
289
+ ```
290
+
291
+ #### DataTable with Lazy Loading (server-side)
292
+ ```vue
293
+ <DataTable :value="products" :lazy="true" :totalRecords="totalRecords"
294
+ :loading="loading" @page="onPage" @sort="onSort" @filter="onFilter"
295
+ paginator :rows="10" dataKey="id"
296
+ v-model:filters="filters" filterDisplay="row">
297
+ <Column field="name" header="Name" sortable />
298
+ </DataTable>
299
+ ```
300
+
301
+ ```typescript
302
+ function onPage(event: DataTablePageEvent) {
303
+ router.get(route('products.index'), {
304
+ page: event.page + 1,
305
+ per_page: event.rows,
306
+ }, { preserveState: true })
307
+ }
308
+ ```
309
+
310
+ #### DataTable with Inline Editing
311
+ ```vue
312
+ <DataTable :value="products" editMode="cell" @cell-edit-complete="onCellEditComplete">
313
+ <Column field="name" header="Name">
314
+ <template #editor="{ data, field }">
315
+ <InputText v-model="data[field]" autofocus />
316
+ </template>
317
+ </Column>
318
+ <Column field="price" header="Price">
319
+ <template #editor="{ data, field }">
320
+ <InputNumber v-model="data[field]" mode="currency" currency="USD" />
321
+ </template>
322
+ </Column>
323
+ </DataTable>
324
+ ```
325
+
326
+ #### Tree
327
+ ```vue
328
+ <Tree :value="nodes" selectionMode="checkbox" v-model:selectionKeys="selectedKeys" />
329
+ ```
330
+
331
+ #### TreeTable
332
+ ```vue
333
+ <TreeTable :value="nodes">
334
+ <Column field="name" header="Name" expander />
335
+ <Column field="size" header="Size" />
336
+ <Column field="type" header="Type" />
337
+ </TreeTable>
338
+ ```
339
+
340
+ #### Timeline
341
+ ```vue
342
+ <Timeline :value="events">
343
+ <template #content="{ item }">
344
+ <Card>
345
+ <template #title>{{ item.status }}</template>
346
+ <template #content>{{ item.date }}</template>
347
+ </Card>
348
+ </template>
349
+ </Timeline>
350
+ ```
351
+
352
+ #### OrderList
353
+ ```vue
354
+ <OrderList v-model="products" dataKey="id">
355
+ <template #option="{ option }">
356
+ <div class="flex items-center gap-2">
357
+ <span>{{ option.name }}</span>
358
+ </div>
359
+ </template>
360
+ </OrderList>
361
+ ```
362
+
363
+ ### Overlay Components
364
+
365
+ #### Dialog
366
+ ```vue
367
+ <Dialog v-model:visible="visible" header="Edit Product" :style="{ width: '450px' }" modal>
368
+ <div class="flex flex-col gap-4">
369
+ <div>
370
+ <label class="font-semibold">Name</label>
371
+ <InputText v-model="product.name" class="w-full" />
372
+ </div>
373
+ <div>
374
+ <label class="font-semibold">Price</label>
375
+ <InputNumber v-model="product.price" mode="currency" currency="USD" class="w-full" />
376
+ </div>
377
+ </div>
378
+ <template #footer>
379
+ <Button label="Cancel" severity="secondary" @click="visible = false" />
380
+ <Button label="Save" @click="save" />
381
+ </template>
382
+ </Dialog>
383
+ ```
384
+
385
+ #### ConfirmDialog
386
+ ```vue
387
+ <script setup>
388
+ import { useConfirm } from 'primevue/useconfirm'
389
+ const confirm = useConfirm()
390
+
391
+ function confirmDelete() {
392
+ confirm.require({
393
+ message: 'Are you sure you want to delete this?',
394
+ header: 'Confirm',
395
+ icon: 'pi pi-exclamation-triangle',
396
+ rejectProps: { label: 'Cancel', severity: 'secondary' },
397
+ acceptProps: { label: 'Delete', severity: 'danger' },
398
+ accept: () => { /* delete logic */ },
399
+ })
400
+ }
401
+ </script>
402
+ <template>
403
+ <ConfirmDialog />
404
+ <Button label="Delete" severity="danger" @click="confirmDelete" />
405
+ </template>
406
+ ```
407
+
408
+ #### ConfirmPopup
409
+ ```vue
410
+ <script setup>
411
+ import { useConfirm } from 'primevue/useconfirm'
412
+ const confirm = useConfirm()
413
+
414
+ function confirmAction(event: Event) {
415
+ confirm.require({
416
+ target: event.currentTarget as HTMLElement,
417
+ message: 'Proceed?',
418
+ accept: () => { /* proceed */ },
419
+ })
420
+ }
421
+ </script>
422
+ <template>
423
+ <ConfirmPopup />
424
+ <Button label="Confirm" @click="confirmAction($event)" />
425
+ </template>
426
+ ```
427
+
428
+ #### Toast
429
+ ```vue
430
+ <script setup>
431
+ import { useToast } from 'primevue/usetoast'
432
+ const toast = useToast()
433
+
434
+ function showSuccess() {
435
+ toast.add({ severity: 'success', summary: 'Saved', detail: 'Record updated', life: 3000 })
436
+ }
437
+ function showError() {
438
+ toast.add({ severity: 'error', summary: 'Error', detail: 'Something went wrong', life: 5000 })
439
+ }
440
+ </script>
441
+ <template>
442
+ <Toast />
443
+ <Button label="Save" @click="showSuccess" />
444
+ </template>
445
+ ```
446
+
447
+ Toast severity options: `success`, `info`, `warn`, `error`, `secondary`, `contrast`.
448
+
449
+ #### Popover (Overlay Panel)
450
+ ```vue
451
+ <script setup>
452
+ import { ref } from 'vue'
453
+ import Popover from 'primevue/popover'
454
+
455
+ const op = ref()
456
+ function toggle(event: Event) {
457
+ op.value.toggle(event)
458
+ }
459
+ </script>
460
+ <template>
461
+ <Button icon="pi pi-info-circle" @click="toggle" />
462
+ <Popover ref="op">
463
+ <p>Additional information here.</p>
464
+ </Popover>
465
+ </template>
466
+ ```
467
+
468
+ #### Drawer (Sidebar)
469
+ ```vue
470
+ <Drawer v-model:visible="visible" header="Menu" position="left">
471
+ <Menu :model="menuItems" />
472
+ </Drawer>
473
+ ```
474
+
475
+ Position options: `left`, `right`, `top`, `bottom`, `full`.
476
+
477
+ ### Navigation
478
+
479
+ #### Menubar
480
+ ```vue
481
+ <Menubar :model="items">
482
+ <template #start>
483
+ <img src="/logo.svg" alt="Logo" class="h-8" />
484
+ </template>
485
+ <template #end>
486
+ <Button icon="pi pi-user" rounded text />
487
+ </template>
488
+ </Menubar>
489
+ ```
490
+
491
+ Menu item model:
492
+ ```typescript
493
+ const items = ref([
494
+ {
495
+ label: 'Home',
496
+ icon: 'pi pi-home',
497
+ command: () => router.visit('/')
498
+ },
499
+ {
500
+ label: 'Products',
501
+ icon: 'pi pi-box',
502
+ items: [
503
+ { label: 'List', icon: 'pi pi-list', command: () => router.visit('/products') },
504
+ { label: 'Create', icon: 'pi pi-plus', command: () => router.visit('/products/create') },
505
+ ]
506
+ },
507
+ ])
508
+ ```
509
+
510
+ #### TabMenu
511
+ ```vue
512
+ <TabMenu :model="tabs" />
513
+ ```
514
+
515
+ #### Breadcrumb
516
+ ```vue
517
+ <Breadcrumb :home="{ icon: 'pi pi-home', to: '/' }" :model="breadcrumbs" />
518
+ ```
519
+
520
+ ```typescript
521
+ const breadcrumbs = ref([
522
+ { label: 'Products', to: '/products' },
523
+ { label: 'Edit' },
524
+ ])
525
+ ```
526
+
527
+ #### Steps
528
+ ```vue
529
+ <Steps :model="steps" :activeStep="activeStep" />
530
+ ```
531
+
532
+ ```typescript
533
+ const steps = ref([
534
+ { label: 'Details' },
535
+ { label: 'Pricing' },
536
+ { label: 'Confirm' },
537
+ ])
538
+ ```
539
+
540
+ #### ContextMenu
541
+ ```vue
542
+ <script setup>
543
+ import { ref } from 'vue'
544
+ import ContextMenu from 'primevue/contextmenu'
545
+
546
+ const cm = ref()
547
+ const items = ref([
548
+ { label: 'Edit', icon: 'pi pi-pencil' },
549
+ { label: 'Delete', icon: 'pi pi-trash' },
550
+ ])
551
+ </script>
552
+ <template>
553
+ <div @contextmenu="cm.show($event)">
554
+ Right-click here
555
+ </div>
556
+ <ContextMenu ref="cm" :model="items" />
557
+ </template>
558
+ ```
559
+
560
+ #### MegaMenu
561
+ ```vue
562
+ <MegaMenu :model="megaItems" orientation="horizontal" />
563
+ ```
564
+
565
+ #### PanelMenu
566
+ ```vue
567
+ <PanelMenu :model="items" />
568
+ ```
569
+
570
+ #### TieredMenu
571
+ ```vue
572
+ <TieredMenu :model="items" popup ref="menu" />
573
+ <Button label="Menu" @click="menu.toggle($event)" />
574
+ ```
575
+
576
+ ### Button Variants
577
+ ```vue
578
+ <Button label="Primary" />
579
+ <Button label="Secondary" severity="secondary" />
580
+ <Button label="Success" severity="success" />
581
+ <Button label="Info" severity="info" />
582
+ <Button label="Warning" severity="warning" />
583
+ <Button label="Danger" severity="danger" />
584
+ <Button label="Help" severity="help" />
585
+ <Button label="Contrast" severity="contrast" />
586
+
587
+ <!-- Variants -->
588
+ <Button label="Outlined" outlined />
589
+ <Button label="Text" text />
590
+ <Button label="Rounded" rounded />
591
+ <Button label="Raised" raised />
592
+ <Button icon="pi pi-check" />
593
+ <Button label="Loading" :loading="isLoading" />
594
+
595
+ <!-- Link button -->
596
+ <Button label="Link" link />
597
+
598
+ <!-- Icon-only -->
599
+ <Button icon="pi pi-check" rounded aria-label="Confirm" />
600
+
601
+ <!-- Sizes -->
602
+ <Button label="Small" size="small" />
603
+ <Button label="Large" size="large" />
604
+ ```
605
+
606
+ #### SplitButton
607
+ ```vue
608
+ <SplitButton label="Save" :model="splitItems" @click="save" />
609
+ ```
610
+
611
+ ```typescript
612
+ const splitItems = ref([
613
+ { label: 'Save as Draft', command: () => saveDraft() },
614
+ { label: 'Export', command: () => exportData() },
615
+ ])
616
+ ```
617
+
618
+ #### SpeedDial
619
+ ```vue
620
+ <SpeedDial :model="dialItems" direction="up" />
621
+ ```
622
+
623
+ ### Layout Components
624
+
625
+ #### Card
626
+ ```vue
627
+ <Card>
628
+ <template #title>Title</template>
629
+ <template #subtitle>Subtitle</template>
630
+ <template #content>
631
+ <p>Content here</p>
632
+ </template>
633
+ <template #footer>
634
+ <Button label="Save" />
635
+ <Button label="Cancel" severity="secondary" />
636
+ </template>
637
+ </Card>
638
+ ```
639
+
640
+ #### Tabs
641
+ ```vue
642
+ <Tabs value="0">
643
+ <TabList>
644
+ <Tab value="0">General</Tab>
645
+ <Tab value="1">Security</Tab>
646
+ <Tab value="2">Notifications</Tab>
647
+ </TabList>
648
+ <TabPanels>
649
+ <TabPanel value="0">General content</TabPanel>
650
+ <TabPanel value="1">Security content</TabPanel>
651
+ <TabPanel value="2">Notification content</TabPanel>
652
+ </TabPanels>
653
+ </Tabs>
654
+ ```
655
+
656
+ #### Accordion
657
+ ```vue
658
+ <Accordion>
659
+ <AccordionPanel value="0">
660
+ <AccordionHeader>Section 1</AccordionHeader>
661
+ <AccordionContent>Content 1</AccordionContent>
662
+ </AccordionPanel>
663
+ <AccordionPanel value="1">
664
+ <AccordionHeader>Section 2</AccordionHeader>
665
+ <AccordionContent>Content 2</AccordionContent>
666
+ </AccordionPanel>
667
+ </Accordion>
668
+ ```
669
+
670
+ #### Fieldset
671
+ ```vue
672
+ <Fieldset legend="Address" :toggleable="true">
673
+ <p>Content here</p>
674
+ </Fieldset>
675
+ ```
676
+
677
+ #### Panel
678
+ ```vue
679
+ <Panel header="Settings" :toggleable="true">
680
+ <p>Panel content</p>
681
+ </Panel>
682
+ ```
683
+
684
+ #### Divider
685
+ ```vue
686
+ <Divider />
687
+ <Divider layout="vertical" />
688
+ <Divider align="center">
689
+ <span class="text-sm text-surface-500">OR</span>
690
+ </Divider>
691
+ ```
692
+
693
+ #### Splitter
694
+ ```vue
695
+ <Splitter style="height: 300px">
696
+ <SplitterPanel :size="40">Left panel</SplitterPanel>
697
+ <SplitterPanel :size="60">Right panel</SplitterPanel>
698
+ </Splitter>
699
+ ```
700
+
701
+ #### ScrollPanel
702
+ ```vue
703
+ <ScrollPanel style="width: 100%; height: 300px">
704
+ <p>Scrollable content...</p>
705
+ </ScrollPanel>
706
+ ```
707
+
708
+ ### Message / Feedback
709
+
710
+ #### Message
711
+ ```vue
712
+ <Message severity="success">Operation completed</Message>
713
+ <Message severity="info">Information message</Message>
714
+ <Message severity="warn">Warning message</Message>
715
+ <Message severity="error">Error occurred</Message>
716
+ <Message severity="secondary">Secondary message</Message>
717
+ <Message severity="contrast">Contrast message</Message>
718
+ ```
719
+
720
+ #### InlineMessage
721
+ ```vue
722
+ <InlineMessage severity="error">Username is required</InlineMessage>
723
+ ```
724
+
725
+ #### Tag
726
+ ```vue
727
+ <Tag value="Active" severity="success" />
728
+ <Tag value="Pending" severity="warn" />
729
+ <Tag value="Inactive" severity="danger" />
730
+ <Tag icon="pi pi-check" value="Approved" severity="success" rounded />
731
+ ```
732
+
733
+ #### Badge
734
+ ```vue
735
+ <Badge value="4" severity="danger" />
736
+ <Button label="Inbox" badge="3" badgeSeverity="danger" />
737
+
738
+ <!-- Overlay badge -->
739
+ <OverlayBadge value="2">
740
+ <i class="pi pi-bell" style="font-size: 1.5rem" />
741
+ </OverlayBadge>
742
+ ```
743
+
744
+ #### ProgressBar
745
+ ```vue
746
+ <ProgressBar :value="50" />
747
+ <ProgressBar mode="indeterminate" style="height: 6px" />
748
+ ```
749
+
750
+ #### ProgressSpinner
751
+ ```vue
752
+ <ProgressSpinner style="width: 50px; height: 50px" />
753
+ ```
754
+
755
+ #### Skeleton
756
+ ```vue
757
+ <Skeleton width="100%" height="2rem" class="mb-2" />
758
+ <Skeleton shape="circle" size="4rem" />
759
+ <Skeleton width="10rem" class="mb-2" />
760
+ ```
761
+
762
+ #### Chip
763
+ ```vue
764
+ <Chip label="Vue" icon="pi pi-check" />
765
+ <Chip label="Removable" removable @remove="onRemove" />
766
+ ```
767
+
768
+ #### Avatar
769
+ ```vue
770
+ <Avatar label="KH" shape="circle" />
771
+ <Avatar image="/user.jpg" shape="circle" size="large" />
772
+
773
+ <AvatarGroup>
774
+ <Avatar image="/user1.jpg" shape="circle" />
775
+ <Avatar image="/user2.jpg" shape="circle" />
776
+ <Avatar label="+3" shape="circle" />
777
+ </AvatarGroup>
778
+ ```
779
+
780
+ #### BlockUI
781
+ ```vue
782
+ <BlockUI :blocked="blocked">
783
+ <Panel header="Content">
784
+ <p>This will be blocked</p>
785
+ </Panel>
786
+ </BlockUI>
787
+ ```
788
+
789
+ ### Misc Components
790
+
791
+ #### Image
792
+ ```vue
793
+ <Image src="/product.jpg" alt="Product" width="250" preview />
794
+ ```
795
+
796
+ #### Galleria
797
+ ```vue
798
+ <Galleria :value="images" :numVisible="5" containerStyle="max-width: 640px">
799
+ <template #item="{ item }">
800
+ <img :src="item.src" :alt="item.alt" style="width: 100%" />
801
+ </template>
802
+ <template #thumbnail="{ item }">
803
+ <img :src="item.thumbnailSrc" :alt="item.alt" />
804
+ </template>
805
+ </Galleria>
806
+ ```
807
+
808
+ #### Carousel
809
+ ```vue
810
+ <Carousel :value="products" :numVisible="3" :numScroll="1">
811
+ <template #item="{ data }">
812
+ <Card>
813
+ <template #title>{{ data.name }}</template>
814
+ <template #content>{{ data.description }}</template>
815
+ </Card>
816
+ </template>
817
+ </Carousel>
818
+ ```
819
+
820
+ #### VirtualScroller
821
+ ```vue
822
+ <VirtualScroller :items="largeList" :itemSize="50" style="height: 400px">
823
+ <template #item="{ item, options }">
824
+ <div :class="{ 'surface-hover': options.odd }" class="p-2">
825
+ {{ item.name }}
826
+ </div>
827
+ </template>
828
+ </VirtualScroller>
829
+ ```
830
+
831
+ #### Tooltip (directive)
832
+ ```vue
833
+ <Button v-tooltip="'Save changes'" label="Save" />
834
+ <Button v-tooltip.top="'Top tooltip'" label="Top" />
835
+ <Button v-tooltip.bottom="'Bottom'" label="Bottom" />
836
+ ```
837
+
838
+ #### Ripple (directive)
839
+ ```vue
840
+ <div v-ripple class="p-4 cursor-pointer">Click me</div>
841
+ ```
842
+
843
+ ## Theming with Tailwind CSS
844
+
845
+ PrimeVue 4 uses a design token system that integrates with Tailwind:
846
+
847
+ ```typescript
848
+ app.use(PrimeVue, {
849
+ theme: {
850
+ preset: Aura,
851
+ options: {
852
+ prefix: 'p',
853
+ darkModeSelector: '.dark',
854
+ cssLayer: {
855
+ name: 'primevue',
856
+ order: 'tailwind-base, primevue, tailwind-utilities'
857
+ }
858
+ }
859
+ }
860
+ })
861
+ ```
862
+
863
+ ### Dark Mode
864
+ PrimeVue respects the `darkModeSelector` option. With `'.dark'`, toggle via:
865
+ ```typescript
866
+ document.documentElement.classList.toggle('dark')
867
+ ```
868
+
869
+ ### Customizing Theme Tokens
870
+ ```typescript
871
+ import { definePreset } from '@primeuix/themes'
872
+ import Aura from '@primeuix/themes/aura'
873
+
874
+ const MyPreset = definePreset(Aura, {
875
+ semantic: {
876
+ primary: {
877
+ 50: '{indigo.50}',
878
+ 100: '{indigo.100}',
879
+ 200: '{indigo.200}',
880
+ 300: '{indigo.300}',
881
+ 400: '{indigo.400}',
882
+ 500: '{indigo.500}',
883
+ 600: '{indigo.600}',
884
+ 700: '{indigo.700}',
885
+ 800: '{indigo.800}',
886
+ 900: '{indigo.900}',
887
+ 950: '{indigo.950}',
888
+ }
889
+ }
890
+ })
891
+
892
+ app.use(PrimeVue, { theme: { preset: MyPreset } })
893
+ ```
894
+
895
+ ### Tailwind CSS Layer Configuration
896
+ Add to `tailwind.config.js` / CSS:
897
+ ```css
898
+ @layer tailwind-base, primevue, tailwind-utilities;
899
+
900
+ @layer tailwind-base {
901
+ @tailwind base;
902
+ }
903
+ @layer tailwind-utilities {
904
+ @tailwind components;
905
+ @tailwind utilities;
906
+ }
907
+ ```
908
+
909
+ ## PrimeIcons
910
+
911
+ Common icons:
912
+ ```vue
913
+ <i class="pi pi-check" />
914
+ <i class="pi pi-times" />
915
+ <i class="pi pi-search" />
916
+ <i class="pi pi-user" />
917
+ <i class="pi pi-cog" />
918
+ <i class="pi pi-home" />
919
+ <i class="pi pi-file" />
920
+ <i class="pi pi-trash" />
921
+ <i class="pi pi-pencil" />
922
+ <i class="pi pi-plus" />
923
+ <i class="pi pi-minus" />
924
+ <i class="pi pi-download" />
925
+ <i class="pi pi-upload" />
926
+ <i class="pi pi-eye" />
927
+ <i class="pi pi-eye-slash" />
928
+ <i class="pi pi-filter" />
929
+ <i class="pi pi-sort-alt" />
930
+ <i class="pi pi-refresh" />
931
+ <i class="pi pi-bell" />
932
+ <i class="pi pi-calendar" />
933
+ <i class="pi pi-chart-bar" />
934
+ <i class="pi pi-envelope" />
935
+ <i class="pi pi-exclamation-triangle" />
936
+ <i class="pi pi-info-circle" />
937
+ <i class="pi pi-spinner pi-spin" />
938
+ ```
939
+
940
+ Spin animation: add `pi-spin` class to any icon.
941
+
942
+ ## Common Patterns with Laravel Inertia
943
+
944
+ ### CRUD Page with DataTable + Dialog
945
+ ```vue
946
+ <script setup lang="ts">
947
+ import { ref } from 'vue'
948
+ import { useForm, router } from '@inertiajs/vue3'
949
+ import DataTable from 'primevue/datatable'
950
+ import Column from 'primevue/column'
951
+ import Button from 'primevue/button'
952
+ import Dialog from 'primevue/dialog'
953
+ import InputText from 'primevue/inputtext'
954
+ import InputNumber from 'primevue/inputnumber'
955
+ import Toast from 'primevue/toast'
956
+ import ConfirmDialog from 'primevue/confirmdialog'
957
+ import { useToast } from 'primevue/usetoast'
958
+ import { useConfirm } from 'primevue/useconfirm'
959
+
960
+ interface Product {
961
+ id: number
962
+ name: string
963
+ price: number
964
+ }
965
+
966
+ const props = defineProps<{ products: Product[] }>()
967
+ const toast = useToast()
968
+ const confirm = useConfirm()
969
+
970
+ const dialogVisible = ref(false)
971
+ const editMode = ref(false)
972
+ const form = useForm({ id: 0, name: '', price: 0 })
973
+
974
+ function openNew() {
975
+ form.reset()
976
+ editMode.value = false
977
+ dialogVisible.value = true
978
+ }
979
+
980
+ function openEdit(product: Product) {
981
+ form.id = product.id
982
+ form.name = product.name
983
+ form.price = product.price
984
+ editMode.value = true
985
+ dialogVisible.value = true
986
+ }
987
+
988
+ function save() {
989
+ if (editMode.value) {
990
+ form.put(route('products.update', form.id), {
991
+ onSuccess: () => {
992
+ dialogVisible.value = false
993
+ toast.add({ severity: 'success', summary: 'Updated', life: 3000 })
994
+ },
995
+ })
996
+ } else {
997
+ form.post(route('products.store'), {
998
+ onSuccess: () => {
999
+ dialogVisible.value = false
1000
+ toast.add({ severity: 'success', summary: 'Created', life: 3000 })
1001
+ },
1002
+ })
1003
+ }
1004
+ }
1005
+
1006
+ function destroy(product: Product) {
1007
+ confirm.require({
1008
+ message: `Delete ${product.name}?`,
1009
+ header: 'Confirm',
1010
+ acceptProps: { label: 'Delete', severity: 'danger' },
1011
+ rejectProps: { label: 'Cancel', severity: 'secondary' },
1012
+ accept: () => {
1013
+ router.delete(route('products.destroy', product.id), {
1014
+ onSuccess: () => {
1015
+ toast.add({ severity: 'success', summary: 'Deleted', life: 3000 })
1016
+ },
1017
+ })
1018
+ },
1019
+ })
1020
+ }
1021
+ </script>
1022
+
1023
+ <template>
1024
+ <Toast />
1025
+ <ConfirmDialog />
1026
+
1027
+ <div class="flex justify-between items-center mb-4">
1028
+ <h1 class="text-2xl font-bold">Products</h1>
1029
+ <Button label="New Product" icon="pi pi-plus" @click="openNew" />
1030
+ </div>
1031
+
1032
+ <DataTable :value="products" stripedRows paginator :rows="10"
1033
+ :rowsPerPageOptions="[5, 10, 20, 50]">
1034
+ <Column field="name" header="Name" sortable />
1035
+ <Column field="price" header="Price" sortable>
1036
+ <template #body="{ data }">
1037
+ ${{ data.price.toFixed(2) }}
1038
+ </template>
1039
+ </Column>
1040
+ <Column header="Actions" style="width: 8rem">
1041
+ <template #body="{ data }">
1042
+ <Button icon="pi pi-pencil" text rounded severity="info" @click="openEdit(data)" />
1043
+ <Button icon="pi pi-trash" text rounded severity="danger" @click="destroy(data)" />
1044
+ </template>
1045
+ </Column>
1046
+ </DataTable>
1047
+
1048
+ <Dialog v-model:visible="dialogVisible"
1049
+ :header="editMode ? 'Edit Product' : 'New Product'"
1050
+ :style="{ width: '400px' }" modal>
1051
+ <div class="flex flex-col gap-4">
1052
+ <div>
1053
+ <label class="font-semibold block mb-1">Name</label>
1054
+ <InputText v-model="form.name" class="w-full" :invalid="!!form.errors.name" />
1055
+ <small v-if="form.errors.name" class="text-red-500">{{ form.errors.name }}</small>
1056
+ </div>
1057
+ <div>
1058
+ <label class="font-semibold block mb-1">Price</label>
1059
+ <InputNumber v-model="form.price" mode="currency" currency="USD" class="w-full"
1060
+ :invalid="!!form.errors.price" />
1061
+ <small v-if="form.errors.price" class="text-red-500">{{ form.errors.price }}</small>
1062
+ </div>
1063
+ </div>
1064
+ <template #footer>
1065
+ <Button label="Cancel" severity="secondary" @click="dialogVisible = false" />
1066
+ <Button label="Save" :loading="form.processing" @click="save" />
1067
+ </template>
1068
+ </Dialog>
1069
+ </template>
1070
+ ```
1071
+
1072
+ ### Layout with Menubar + Sidebar
1073
+ ```vue
1074
+ <script setup lang="ts">
1075
+ import { ref } from 'vue'
1076
+ import { router, usePage } from '@inertiajs/vue3'
1077
+ import Menubar from 'primevue/menubar'
1078
+ import Drawer from 'primevue/drawer'
1079
+ import Menu from 'primevue/menu'
1080
+ import Button from 'primevue/button'
1081
+ import Avatar from 'primevue/avatar'
1082
+
1083
+ const sidebarVisible = ref(false)
1084
+ const page = usePage()
1085
+
1086
+ const menuItems = ref([
1087
+ { label: 'Dashboard', icon: 'pi pi-home', command: () => router.visit('/dashboard') },
1088
+ { label: 'Products', icon: 'pi pi-box', command: () => router.visit('/products') },
1089
+ { label: 'Orders', icon: 'pi pi-shopping-cart', command: () => router.visit('/orders') },
1090
+ { label: 'Customers', icon: 'pi pi-users', command: () => router.visit('/customers') },
1091
+ { separator: true },
1092
+ { label: 'Settings', icon: 'pi pi-cog', command: () => router.visit('/settings') },
1093
+ ])
1094
+
1095
+ const navItems = ref([
1096
+ { label: 'Home', icon: 'pi pi-home', command: () => router.visit('/') },
1097
+ {
1098
+ label: 'Products',
1099
+ icon: 'pi pi-box',
1100
+ items: [
1101
+ { label: 'All Products', command: () => router.visit('/products') },
1102
+ { label: 'Categories', command: () => router.visit('/categories') },
1103
+ ]
1104
+ },
1105
+ ])
1106
+ </script>
1107
+
1108
+ <template>
1109
+ <Menubar :model="navItems">
1110
+ <template #start>
1111
+ <Button icon="pi pi-bars" text rounded @click="sidebarVisible = true" class="mr-2" />
1112
+ <span class="font-bold text-lg">MyApp</span>
1113
+ </template>
1114
+ <template #end>
1115
+ <Avatar :label="page.props.auth.user.name[0]" shape="circle" />
1116
+ </template>
1117
+ </Menubar>
1118
+
1119
+ <Drawer v-model:visible="sidebarVisible" header="Navigation">
1120
+ <Menu :model="menuItems" class="w-full border-0" />
1121
+ </Drawer>
1122
+
1123
+ <main class="p-4">
1124
+ <slot />
1125
+ </main>
1126
+ </template>
1127
+ ```
1128
+
1129
+ ### Form Page with Validation Errors
1130
+ ```vue
1131
+ <script setup lang="ts">
1132
+ import { useForm } from '@inertiajs/vue3'
1133
+ import InputText from 'primevue/inputtext'
1134
+ import Select from 'primevue/select'
1135
+ import Textarea from 'primevue/textarea'
1136
+ import DatePicker from 'primevue/datepicker'
1137
+ import Button from 'primevue/button'
1138
+
1139
+ const props = defineProps<{ categories: { id: number; name: string }[] }>()
1140
+
1141
+ const form = useForm({
1142
+ name: '',
1143
+ category_id: null as number | null,
1144
+ description: '',
1145
+ published_at: null as Date | null,
1146
+ })
1147
+
1148
+ function submit() {
1149
+ form.post(route('articles.store'))
1150
+ }
1151
+ </script>
1152
+
1153
+ <template>
1154
+ <form @submit.prevent="submit" class="max-w-lg flex flex-col gap-4">
1155
+ <div>
1156
+ <label class="font-semibold block mb-1">Name</label>
1157
+ <InputText v-model="form.name" class="w-full" :invalid="!!form.errors.name" />
1158
+ <small v-if="form.errors.name" class="text-red-500">{{ form.errors.name }}</small>
1159
+ </div>
1160
+
1161
+ <div>
1162
+ <label class="font-semibold block mb-1">Category</label>
1163
+ <Select v-model="form.category_id" :options="categories"
1164
+ optionLabel="name" optionValue="id"
1165
+ placeholder="Select category" class="w-full"
1166
+ :invalid="!!form.errors.category_id" />
1167
+ <small v-if="form.errors.category_id" class="text-red-500">{{ form.errors.category_id }}</small>
1168
+ </div>
1169
+
1170
+ <div>
1171
+ <label class="font-semibold block mb-1">Description</label>
1172
+ <Textarea v-model="form.description" rows="4" class="w-full" autoResize />
1173
+ </div>
1174
+
1175
+ <div>
1176
+ <label class="font-semibold block mb-1">Publish Date</label>
1177
+ <DatePicker v-model="form.published_at" dateFormat="yy-mm-dd" showIcon class="w-full" />
1178
+ </div>
1179
+
1180
+ <div class="flex gap-2">
1181
+ <Button type="submit" label="Save" :loading="form.processing" />
1182
+ <Button type="button" label="Reset" severity="secondary" @click="form.reset()" />
1183
+ </div>
1184
+ </form>
1185
+ </template>
1186
+ ```
1187
+
1188
+ ## Common Gotchas
1189
+
1190
+ 1. **Import each component individually** -- no global registration. Use `import Button from 'primevue/button'`, not from a barrel export.
1191
+ 2. **Services need plugin registration** -- Toast and ConfirmDialog require `app.use(ToastService)` and `app.use(ConfirmationService)` in app.ts before use.
1192
+ 3. **CSS layer order matters with Tailwind** -- Set `cssLayer` in theme config so Tailwind utilities can override PrimeVue styles. Without it, PrimeVue styles may override Tailwind classes.
1193
+ 4. **v-model naming** -- Some components use named v-model: `v-model:visible` (Dialog, Drawer), `v-model:selection` (DataTable), `v-model:expandedRows` (DataTable), `v-model:filters` (DataTable), `v-model:selectionKeys` (Tree).
1194
+ 5. **DataTable needs `dataKey`** -- Always set `dataKey="id"` when using selection, expandable rows, or row reordering.
1195
+ 6. **Theme presets are imported from `@primeuix/themes`** -- not from `primevue`. The old `primevue/themes/*` path no longer works in v4.
1196
+ 7. **PrimeIcons** -- Install separately: `npm install primeicons`. Import in app.ts: `import 'primeicons/primeicons.css'`.
1197
+ 8. **Component rename in v4** -- `Dropdown` is now `Select`. `Calendar` is now `DatePicker`. `OverlayPanel` is now `Popover`. `Sidebar` is now `Drawer`. `InputSwitch` is now `ToggleSwitch`. `TabView`/`TabPanel` are now `Tabs`/`TabList`/`Tab`/`TabPanels`/`TabPanel`. `AccordionTab` is now `AccordionPanel`/`AccordionHeader`/`AccordionContent`.
1198
+ 9. **FilterMatchMode import** -- In v4, import from `@primevue/core/api`, not from `primevue/api`.
1199
+ 10. **No global CSS import** -- PrimeVue 4 does not require `import 'primevue/resources/themes/...'` or `import 'primevue/resources/primevue.min.css'`. The theme preset handles everything.