@turquoisehealth/pit-viper 2.182.1-dev.5 → 2.183.1-dev.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.
- package/claude-plugin/skills/pit-viper/SKILL.md +28 -31
- package/package.json +1 -1
- package/pv-components/dist/stats/vue/visualizations/stats.html +1 -1
- package/pv-components/dist/vue/visualizations/components/tables/PvDataTable/table-components/FilterGroupMenu.vue.d.ts +21 -0
- package/pv-components/dist/vue/visualizations/components/tables/PvDataTable/types.d.ts +9 -0
- package/pv-components/dist/vue/visualizations/components/tables/PvDataTable/useSetFilter.d.ts +1 -0
- package/pv-components/dist/vue/visualizations/pv-components-visualizations.mjs +1610 -1426
- package/pv-components/dist/vue/visualizations/pv-components-visualizations.mjs.map +1 -1
- package/claude-plugin/skills/pit-viper/references/design-language.md +0 -80
- package/claude-plugin/skills/pit-viper/references/design-rules.md +0 -265
- package/claude-plugin/skills/pit-viper/references/html-patterns.md +0 -468
- package/claude-plugin/skills/pit-viper/references/layout-patterns.md +0 -367
- package/claude-plugin/skills/pit-viper/references/patterns-core.md +0 -97
- package/claude-plugin/skills/pit-viper/references/theme-guide.md +0 -160
- package/claude-plugin/skills/pit-viper/references/vue-guidelines.md +0 -526
|
@@ -1,526 +0,0 @@
|
|
|
1
|
-
# Vue Guidelines for Pit Viper
|
|
2
|
-
|
|
3
|
-
Use this reference when generating Vue SFC code (Vue Mode).
|
|
4
|
-
|
|
5
|
-
## Contents
|
|
6
|
-
|
|
7
|
-
- Adding Pit Viper (install, CSS, imports)
|
|
8
|
-
- File Structure (script/template/style order)
|
|
9
|
-
- Import Paths (base, visualizations, layout)
|
|
10
|
-
- Props, Emits, Generics Patterns
|
|
11
|
-
- v-model Patterns (input, select, modal, tabs)
|
|
12
|
-
- Naming Conventions
|
|
13
|
-
- Common Mistakes
|
|
14
|
-
- Styling (pv-* classes only)
|
|
15
|
-
- Tables & Charts (PvDataTable, PvChart)
|
|
16
|
-
- Anti-Patterns / Good Patterns
|
|
17
|
-
|
|
18
|
-
## Adding Pit Viper to a Vue Project
|
|
19
|
-
|
|
20
|
-
### Step 1: Install the package
|
|
21
|
-
|
|
22
|
-
```bash
|
|
23
|
-
npm install @turquoisehealth/pit-viper
|
|
24
|
-
# or
|
|
25
|
-
pnpm add @turquoisehealth/pit-viper
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
### Step 2: Import CSS in your app entry point
|
|
29
|
-
|
|
30
|
-
```typescript
|
|
31
|
-
// main.ts or App.vue
|
|
32
|
-
import '@turquoisehealth/pit-viper/pit-viper-v2.css'
|
|
33
|
-
// OR for consumer/marketing sites:
|
|
34
|
-
// import '@turquoisehealth/pit-viper/pit-viper-consumer.css'
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
### Step 3: Import and use components
|
|
38
|
-
|
|
39
|
-
```typescript
|
|
40
|
-
import { PvButton, PvInput } from '@turquoisehealth/pit-viper/components'
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
For complete setup instructions and sandboxes, see the [Storybook Introduction](https://pit-viper-storybook.netlify.app/?path=/docs/introduction--documentation).
|
|
44
|
-
|
|
45
|
-
## File Structure
|
|
46
|
-
|
|
47
|
-
Components use Vue 3 Composition API with TypeScript. Block order is strict:
|
|
48
|
-
|
|
49
|
-
1. `<script lang="ts">` — Only for generic type definitions (when needed)
|
|
50
|
-
2. `<script setup lang="ts">` — Component logic
|
|
51
|
-
3. `<template>` — Markup
|
|
52
|
-
4. `<style scoped>` — Only when absolutely necessary (prefer pv-* classes)
|
|
53
|
-
|
|
54
|
-
## Import Paths
|
|
55
|
-
|
|
56
|
-
Never use barrel exports. Import directly from component paths:
|
|
57
|
-
|
|
58
|
-
```typescript
|
|
59
|
-
// Base components
|
|
60
|
-
import { PvButton, PvInput, PvModal } from '@turquoisehealth/pit-viper/components'
|
|
61
|
-
|
|
62
|
-
// Visualization components (tables, charts)
|
|
63
|
-
import { PvDataTable, PvChart } from '@turquoisehealth/pit-viper/components/visualizations'
|
|
64
|
-
|
|
65
|
-
// Layout components (must import directly)
|
|
66
|
-
import PvSidePanel from '@turquoisehealth/pit-viper/components/layout/PvSidePanel/PvSidePanel.vue'
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
## Props Pattern
|
|
70
|
-
|
|
71
|
-
Define props as interfaces. Use `withDefaults` for default values. Props should be alphabetically ordered.
|
|
72
|
-
|
|
73
|
-
```typescript
|
|
74
|
-
export interface MyComponentProps {
|
|
75
|
-
disabled?: boolean;
|
|
76
|
-
label: string;
|
|
77
|
-
options: Option[];
|
|
78
|
-
placeholder?: string;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const props = withDefaults(defineProps<MyComponentProps>(), {
|
|
82
|
-
disabled: false,
|
|
83
|
-
options: () => [],
|
|
84
|
-
placeholder: 'Select...',
|
|
85
|
-
});
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
## Emits Pattern
|
|
89
|
-
|
|
90
|
-
Define emits with kebab-case event names and typed payloads:
|
|
91
|
-
|
|
92
|
-
```typescript
|
|
93
|
-
const emit = defineEmits<{
|
|
94
|
-
(e: 'value-change', value: string): void;
|
|
95
|
-
(e: 'item-select', item: Option): void;
|
|
96
|
-
}>();
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
## Generics Pattern
|
|
100
|
-
|
|
101
|
-
When using generics, declare interfaces in a separate `<script>` block:
|
|
102
|
-
|
|
103
|
-
```vue
|
|
104
|
-
<script lang="ts">
|
|
105
|
-
export interface ListProps<T> {
|
|
106
|
-
items: T[];
|
|
107
|
-
selected: T | null;
|
|
108
|
-
}
|
|
109
|
-
</script>
|
|
110
|
-
|
|
111
|
-
<script setup lang="ts" generic="T">
|
|
112
|
-
const props = defineProps<ListProps<T>>();
|
|
113
|
-
</script>
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
## v-model Patterns
|
|
117
|
-
|
|
118
|
-
### PvInput
|
|
119
|
-
```vue
|
|
120
|
-
<PvInput v-model="searchQuery" placeholder="Search..." />
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
### PvSelectButton
|
|
124
|
-
```vue
|
|
125
|
-
<PvSelectButton
|
|
126
|
-
v-model="selected"
|
|
127
|
-
:options="options"
|
|
128
|
-
label="Choose option"
|
|
129
|
-
placeholder="Select..."
|
|
130
|
-
/>
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
### PvModal
|
|
134
|
-
```vue
|
|
135
|
-
<PvModal v-model="isOpen" header="Confirm Action">
|
|
136
|
-
<template #body>
|
|
137
|
-
<p>Are you sure?</p>
|
|
138
|
-
</template>
|
|
139
|
-
<template #footer>
|
|
140
|
-
<PvButton variant="ghost" @click="isOpen = false">Cancel</PvButton>
|
|
141
|
-
<PvButton @click="confirm">Confirm</PvButton>
|
|
142
|
-
</template>
|
|
143
|
-
</PvModal>
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
### PvTabs
|
|
147
|
-
```vue
|
|
148
|
-
<PvTabs v-model="activeTab" :tabs="[
|
|
149
|
-
{ label: 'Overview', value: 'overview' },
|
|
150
|
-
{ label: 'Settings', value: 'settings' },
|
|
151
|
-
]" />
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
## Naming Conventions
|
|
155
|
-
|
|
156
|
-
Use PascalCase for component names AND HTML attributes (not kebab-case):
|
|
157
|
-
|
|
158
|
-
```vue
|
|
159
|
-
<!-- Correct -->
|
|
160
|
-
<PvButton leftIcon="plus" isLoading />
|
|
161
|
-
<PvSelectButton optionsVariant="checkbox" />
|
|
162
|
-
|
|
163
|
-
<!-- Incorrect -->
|
|
164
|
-
<pv-button left-icon="plus" is-loading />
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
## Common Mistakes
|
|
168
|
-
|
|
169
|
-
| Mistake | Correct Approach |
|
|
170
|
-
|---------|------------------|
|
|
171
|
-
| Using kebab-case for props | Use PascalCase: `leftIcon` not `left-icon` |
|
|
172
|
-
| Importing from barrel exports | Import directly from component path |
|
|
173
|
-
| Adding `<style scoped>` blocks | Use pv-* utility classes instead |
|
|
174
|
-
| Inline prop types in defineProps | Define interfaces in types.ts |
|
|
175
|
-
| Using raw `<button>`, `<input>` | Use PvButton, PvInput components |
|
|
176
|
-
| Guessing icon names | Verify at https://pitviper.turquoise.health/visual-style/icons/ |
|
|
177
|
-
|
|
178
|
-
## Styling
|
|
179
|
-
|
|
180
|
-
Use only Pit Viper utility classes. No custom CSS.
|
|
181
|
-
|
|
182
|
-
```vue
|
|
183
|
-
<!-- Correct -->
|
|
184
|
-
<div class="pv-flex pv-stack-16">
|
|
185
|
-
<h1 class="pv-heading-1">Title</h1>
|
|
186
|
-
<p class="pv-text-body-md pv-text-subdued">Description</p>
|
|
187
|
-
</div>
|
|
188
|
-
|
|
189
|
-
<!-- Incorrect -->
|
|
190
|
-
<div style="display: flex; margin-bottom: 16px;">
|
|
191
|
-
<h1 style="font-size: 20px;">Title</h1>
|
|
192
|
-
</div>
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
## Tables & Charts
|
|
196
|
-
|
|
197
|
-
### PvDataTable
|
|
198
|
-
|
|
199
|
-
Use for all tabular data. Never use raw `<table>` elements or custom table markup.
|
|
200
|
-
|
|
201
|
-
```typescript
|
|
202
|
-
import { PvDataTable } from '@turquoisehealth/pit-viper/components/visualizations'
|
|
203
|
-
|
|
204
|
-
const colDefs = [
|
|
205
|
-
{ field: 'name', headerName: 'Name', flex: 1 },
|
|
206
|
-
{ field: 'email', headerName: 'Email', flex: 1 },
|
|
207
|
-
{ field: 'role', headerName: 'Role', width: 120 },
|
|
208
|
-
]
|
|
209
|
-
|
|
210
|
-
const rowData = ref([
|
|
211
|
-
{ name: 'Alice', email: 'alice@example.com', role: 'Admin' },
|
|
212
|
-
{ name: 'Bob', email: 'bob@example.com', role: 'Editor' },
|
|
213
|
-
])
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
```vue
|
|
217
|
-
<PvDataTable
|
|
218
|
-
:rowData="rowData"
|
|
219
|
-
:colDefs="colDefs"
|
|
220
|
-
:loading="isLoading"
|
|
221
|
-
style="height: 400px;"
|
|
222
|
-
/>
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
### PvDataTableWithChart
|
|
226
|
-
|
|
227
|
-
Use when you need a table with an integrated chart visualization.
|
|
228
|
-
|
|
229
|
-
```typescript
|
|
230
|
-
import { PvDataTableWithChart } from '@turquoisehealth/pit-viper/components/visualizations'
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
### PvChart
|
|
234
|
-
|
|
235
|
-
Use for standalone charts. Never use D3, Chart.js, ApexCharts, or other charting libraries.
|
|
236
|
-
|
|
237
|
-
```typescript
|
|
238
|
-
import { PvChart } from '@turquoisehealth/pit-viper/components/visualizations'
|
|
239
|
-
|
|
240
|
-
const chartOptions = {
|
|
241
|
-
data: [
|
|
242
|
-
{ month: 'Jan', revenue: 50000 },
|
|
243
|
-
{ month: 'Feb', revenue: 62000 },
|
|
244
|
-
{ month: 'Mar', revenue: 58000 },
|
|
245
|
-
],
|
|
246
|
-
series: [
|
|
247
|
-
{
|
|
248
|
-
type: 'bar',
|
|
249
|
-
xKey: 'month',
|
|
250
|
-
yKey: 'revenue',
|
|
251
|
-
yName: 'Revenue',
|
|
252
|
-
},
|
|
253
|
-
],
|
|
254
|
-
}
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
```vue
|
|
258
|
-
<PvChart :options="chartOptions" style="height: 300px;" />
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
### Table & Chart Mistakes
|
|
262
|
-
|
|
263
|
-
| Mistake | Correct Approach |
|
|
264
|
-
|---------|------------------|
|
|
265
|
-
| Using raw `<table>` elements | Use PvDataTable component |
|
|
266
|
-
| Importing D3, Chart.js, ApexCharts | Use PvChart component |
|
|
267
|
-
| Custom table CSS/styling | PvDataTable has built-in Pit Viper theme |
|
|
268
|
-
| Building custom sorting/filtering | PvDataTable has AG Grid features built-in |
|
|
269
|
-
|
|
270
|
-
## Web Component Compatibility
|
|
271
|
-
|
|
272
|
-
If the component may be used as a web component:
|
|
273
|
-
|
|
274
|
-
- All non-primitive props (not string/number/boolean) must be optional
|
|
275
|
-
- Boolean props default to `false`
|
|
276
|
-
- Use `::slotted()` in styles for Shadow DOM
|
|
277
|
-
- No scoped or conditional slots (`v-if="$slots.name"`)
|
|
278
|
-
- Complex props must be reactive for post-initialization assignment
|
|
279
|
-
|
|
280
|
-
---
|
|
281
|
-
|
|
282
|
-
## Anti-Patterns
|
|
283
|
-
|
|
284
|
-
### Custom Classes on Components
|
|
285
|
-
|
|
286
|
-
Don't invent classes. Use pv-* utilities or inline styles for one-offs.
|
|
287
|
-
|
|
288
|
-
```vue
|
|
289
|
-
<!-- BAD: Custom classes -->
|
|
290
|
-
<template>
|
|
291
|
-
<div class="settings-container">
|
|
292
|
-
<PvCard class="settings-card">
|
|
293
|
-
<PvButton class="submit-btn" label="Save" />
|
|
294
|
-
</PvCard>
|
|
295
|
-
</div>
|
|
296
|
-
</template>
|
|
297
|
-
|
|
298
|
-
<style scoped>
|
|
299
|
-
.settings-container { padding: 24px; }
|
|
300
|
-
.settings-card { max-width: 500px; }
|
|
301
|
-
.submit-btn { margin-top: 16px; }
|
|
302
|
-
</style>
|
|
303
|
-
|
|
304
|
-
<!-- GOOD: Pit Viper classes + inline for one-offs -->
|
|
305
|
-
<template>
|
|
306
|
-
<div class="pv-inset-square-24">
|
|
307
|
-
<PvCard style="max-width: 500px;">
|
|
308
|
-
<div class="pv-stack-16">
|
|
309
|
-
<PvButton label="Save" />
|
|
310
|
-
</div>
|
|
311
|
-
</PvCard>
|
|
312
|
-
</div>
|
|
313
|
-
</template>
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
### Custom CSS / SCSS
|
|
317
|
-
|
|
318
|
-
Never add `<style>` blocks with custom styling. This defeats the design system.
|
|
319
|
-
|
|
320
|
-
```vue
|
|
321
|
-
<!-- BAD: Custom SCSS -->
|
|
322
|
-
<template>
|
|
323
|
-
<div class="error-label">
|
|
324
|
-
<PvIcon name="info-circle" />
|
|
325
|
-
<span class="error-label--text">{{ message }}</span>
|
|
326
|
-
</div>
|
|
327
|
-
</template>
|
|
328
|
-
|
|
329
|
-
<style lang="scss" scoped>
|
|
330
|
-
.error-label {
|
|
331
|
-
color: $red-base;
|
|
332
|
-
display: flex;
|
|
333
|
-
gap: 8px;
|
|
334
|
-
|
|
335
|
-
&--text {
|
|
336
|
-
@include text-medium;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
</style>
|
|
340
|
-
|
|
341
|
-
<!-- GOOD: Pit Viper classes only -->
|
|
342
|
-
<template>
|
|
343
|
-
<div class="pv-flex pv-text-critical" style="--flex-gap: 0.5rem;">
|
|
344
|
-
<PvIcon name="info-circle" />
|
|
345
|
-
<span class="pv-text-body-md">{{ message }}</span>
|
|
346
|
-
</div>
|
|
347
|
-
</template>
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
### Raw Tables with Custom Styling
|
|
351
|
-
|
|
352
|
-
Never build custom tables. Use PvDataTable.
|
|
353
|
-
|
|
354
|
-
```vue
|
|
355
|
-
<!-- BAD: Raw table with custom styling -->
|
|
356
|
-
<template>
|
|
357
|
-
<table class="my-table">
|
|
358
|
-
<thead>
|
|
359
|
-
<tr>
|
|
360
|
-
<th>Name</th>
|
|
361
|
-
<th>Email</th>
|
|
362
|
-
</tr>
|
|
363
|
-
</thead>
|
|
364
|
-
<tbody>
|
|
365
|
-
<tr v-for="user in users" :key="user.id">
|
|
366
|
-
<td>{{ user.name }}</td>
|
|
367
|
-
<td>{{ user.email }}</td>
|
|
368
|
-
</tr>
|
|
369
|
-
</tbody>
|
|
370
|
-
</table>
|
|
371
|
-
</template>
|
|
372
|
-
|
|
373
|
-
<style scoped>
|
|
374
|
-
.my-table { width: 100%; border-collapse: collapse; }
|
|
375
|
-
.my-table th { background: #f5f5f5; padding: 8px; }
|
|
376
|
-
.my-table td { border-bottom: 1px solid #eee; padding: 8px; }
|
|
377
|
-
</style>
|
|
378
|
-
|
|
379
|
-
<!-- GOOD: PvDataTable -->
|
|
380
|
-
<script setup lang="ts">
|
|
381
|
-
import { PvDataTable } from "@turquoisehealth/pit-viper/components/visualizations";
|
|
382
|
-
|
|
383
|
-
const colDefs = [
|
|
384
|
-
{ field: "name", headerName: "Name", flex: 1 },
|
|
385
|
-
{ field: "email", headerName: "Email", flex: 1 },
|
|
386
|
-
];
|
|
387
|
-
</script>
|
|
388
|
-
|
|
389
|
-
<template>
|
|
390
|
-
<PvDataTable :rowData="users" :colDefs="colDefs" style="height: 400px;" />
|
|
391
|
-
</template>
|
|
392
|
-
```
|
|
393
|
-
|
|
394
|
-
---
|
|
395
|
-
|
|
396
|
-
## Good Patterns
|
|
397
|
-
|
|
398
|
-
### Modal with Proper Structure
|
|
399
|
-
|
|
400
|
-
```vue
|
|
401
|
-
<script setup lang="ts">
|
|
402
|
-
import { PvButton, PvModal } from "@turquoisehealth/pit-viper/components";
|
|
403
|
-
import { ref } from "vue";
|
|
404
|
-
|
|
405
|
-
const showModal = defineModel<boolean>({ required: true });
|
|
406
|
-
|
|
407
|
-
const emit = defineEmits<{
|
|
408
|
-
(e: "confirm"): void;
|
|
409
|
-
}>();
|
|
410
|
-
|
|
411
|
-
const close = () => {
|
|
412
|
-
showModal.value = false;
|
|
413
|
-
};
|
|
414
|
-
</script>
|
|
415
|
-
|
|
416
|
-
<template>
|
|
417
|
-
<PvModal
|
|
418
|
-
v-model="showModal"
|
|
419
|
-
header="Delete Report?"
|
|
420
|
-
@close="close"
|
|
421
|
-
style="--flow-size: 16px; width: 298px"
|
|
422
|
-
>
|
|
423
|
-
<template #body>
|
|
424
|
-
<p class="pv-text-body-sm pv-text-subdued">
|
|
425
|
-
This action cannot be undone.
|
|
426
|
-
</p>
|
|
427
|
-
</template>
|
|
428
|
-
<template #footer>
|
|
429
|
-
<div class="pv-flex" style="--flex-justify: flex-end;">
|
|
430
|
-
<PvButton variant="ghost" @click="close" label="Cancel" size="lg" />
|
|
431
|
-
<PvButton variant="destructive" @click="emit('confirm')" label="Delete" size="lg" />
|
|
432
|
-
</div>
|
|
433
|
-
</template>
|
|
434
|
-
</PvModal>
|
|
435
|
-
</template>
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
**What makes this good:**
|
|
439
|
-
- Uses `label` attribute on buttons
|
|
440
|
-
- Proper modal structure with `#body` and `#footer` slots
|
|
441
|
-
- Semantic `variant="destructive"` for delete action
|
|
442
|
-
- Inline style for one-off width constraint
|
|
443
|
-
- CSS custom property `--flow-size` instead of custom class
|
|
444
|
-
|
|
445
|
-
### Form Actions with Feedback
|
|
446
|
-
|
|
447
|
-
```vue
|
|
448
|
-
<script setup lang="ts">
|
|
449
|
-
import { PvButton, PvToast } from "@turquoisehealth/pit-viper/components";
|
|
450
|
-
import { ref } from "vue";
|
|
451
|
-
|
|
452
|
-
const submitting = ref(false);
|
|
453
|
-
const submitError = ref<string | null>(null);
|
|
454
|
-
const submitSuccess = ref(false);
|
|
455
|
-
</script>
|
|
456
|
-
|
|
457
|
-
<template>
|
|
458
|
-
<div class="pv-flex-vertical pv-full-width">
|
|
459
|
-
<div class="pv-space-between pv-inset-block-24 pv-full-width">
|
|
460
|
-
<PvButton size="xl" label="Cancel" variant="secondary" @click="cancel" />
|
|
461
|
-
<PvButton
|
|
462
|
-
size="xl"
|
|
463
|
-
label="Save"
|
|
464
|
-
@click="submit"
|
|
465
|
-
:disabled="submitting"
|
|
466
|
-
:loading="submitting"
|
|
467
|
-
/>
|
|
468
|
-
</div>
|
|
469
|
-
<PvToast
|
|
470
|
-
v-if="submitSuccess"
|
|
471
|
-
icon="check-circle"
|
|
472
|
-
label="Saved successfully!"
|
|
473
|
-
variant="info"
|
|
474
|
-
/>
|
|
475
|
-
<PvToast
|
|
476
|
-
v-if="submitError"
|
|
477
|
-
icon="alert-circle"
|
|
478
|
-
:label="submitError"
|
|
479
|
-
variant="error"
|
|
480
|
-
/>
|
|
481
|
-
</div>
|
|
482
|
-
</template>
|
|
483
|
-
```
|
|
484
|
-
|
|
485
|
-
**What makes this good:**
|
|
486
|
-
- Uses `pv-space-between` for button layout
|
|
487
|
-
- Uses `pv-inset-block-24` for vertical padding
|
|
488
|
-
- Uses `:loading` prop instead of custom spinner
|
|
489
|
-
- Uses `PvToast` for feedback instead of custom alerts
|
|
490
|
-
|
|
491
|
-
### Card with Content Sections
|
|
492
|
-
|
|
493
|
-
```vue
|
|
494
|
-
<template>
|
|
495
|
-
<PvCard style="max-width: 500px;">
|
|
496
|
-
<template #header>
|
|
497
|
-
<h2 class="pv-heading-3">User Settings</h2>
|
|
498
|
-
</template>
|
|
499
|
-
|
|
500
|
-
<div class="pv-flex-vertical" style="--flex-gap: 1rem;">
|
|
501
|
-
<div>
|
|
502
|
-
<label class="pv-text-title-sm pv-stack-4">Name</label>
|
|
503
|
-
<PvInput v-model="name" placeholder="Full name" />
|
|
504
|
-
</div>
|
|
505
|
-
<div>
|
|
506
|
-
<label class="pv-text-title-sm pv-stack-4">Email</label>
|
|
507
|
-
<PvInput v-model="email" type="email" />
|
|
508
|
-
</div>
|
|
509
|
-
</div>
|
|
510
|
-
|
|
511
|
-
<template #footer>
|
|
512
|
-
<div class="pv-flex" style="--flex-justify: flex-end; --flex-gap: 0.5rem;">
|
|
513
|
-
<PvButton variant="ghost" label="Cancel" @click="cancel" />
|
|
514
|
-
<PvButton label="Save" @click="save" />
|
|
515
|
-
</div>
|
|
516
|
-
</template>
|
|
517
|
-
</PvCard>
|
|
518
|
-
</template>
|
|
519
|
-
```
|
|
520
|
-
|
|
521
|
-
**What makes this good:**
|
|
522
|
-
- Uses proper card slots (`#header`, default, `#footer`)
|
|
523
|
-
- Uses `--flex-gap` CSS custom property instead of custom class
|
|
524
|
-
- Uses `--flex-justify` for alignment
|
|
525
|
-
- Inline style for max-width constraint (one-off)
|
|
526
|
-
- Primary action on the right
|