qdadm 0.13.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.
Files changed (82) hide show
  1. package/CHANGELOG.md +270 -0
  2. package/LICENSE +21 -0
  3. package/README.md +166 -0
  4. package/package.json +48 -0
  5. package/src/assets/logo.svg +6 -0
  6. package/src/components/BoolCell.vue +28 -0
  7. package/src/components/dialogs/BulkStatusDialog.vue +43 -0
  8. package/src/components/dialogs/MultiStepDialog.vue +321 -0
  9. package/src/components/dialogs/SimpleDialog.vue +108 -0
  10. package/src/components/dialogs/UnsavedChangesDialog.vue +87 -0
  11. package/src/components/display/CardsGrid.vue +155 -0
  12. package/src/components/display/CopyableId.vue +92 -0
  13. package/src/components/display/EmptyState.vue +114 -0
  14. package/src/components/display/IntensityBar.vue +171 -0
  15. package/src/components/display/RichCardsGrid.vue +220 -0
  16. package/src/components/editors/JsonEditorFoldable.vue +467 -0
  17. package/src/components/editors/JsonStructuredField.vue +218 -0
  18. package/src/components/editors/JsonViewer.vue +91 -0
  19. package/src/components/editors/KeyValueEditor.vue +314 -0
  20. package/src/components/editors/LanguageEditor.vue +245 -0
  21. package/src/components/editors/ScopeEditor.vue +341 -0
  22. package/src/components/editors/VanillaJsonEditor.vue +185 -0
  23. package/src/components/forms/FormActions.vue +104 -0
  24. package/src/components/forms/FormField.vue +64 -0
  25. package/src/components/forms/FormTab.vue +217 -0
  26. package/src/components/forms/FormTabs.vue +108 -0
  27. package/src/components/index.js +44 -0
  28. package/src/components/layout/AppLayout.vue +430 -0
  29. package/src/components/layout/Breadcrumb.vue +106 -0
  30. package/src/components/layout/PageHeader.vue +75 -0
  31. package/src/components/layout/PageLayout.vue +93 -0
  32. package/src/components/lists/ActionButtons.vue +41 -0
  33. package/src/components/lists/ActionColumn.vue +37 -0
  34. package/src/components/lists/FilterBar.vue +53 -0
  35. package/src/components/lists/ListPage.vue +319 -0
  36. package/src/composables/index.js +19 -0
  37. package/src/composables/useApp.js +43 -0
  38. package/src/composables/useAuth.js +49 -0
  39. package/src/composables/useBareForm.js +143 -0
  40. package/src/composables/useBreadcrumb.js +221 -0
  41. package/src/composables/useDirtyState.js +103 -0
  42. package/src/composables/useEntityTitle.js +121 -0
  43. package/src/composables/useForm.js +254 -0
  44. package/src/composables/useGuardStore.js +37 -0
  45. package/src/composables/useJsonSyntax.js +101 -0
  46. package/src/composables/useListPageBuilder.js +1176 -0
  47. package/src/composables/useNavigation.js +89 -0
  48. package/src/composables/usePageBuilder.js +334 -0
  49. package/src/composables/useStatus.js +146 -0
  50. package/src/composables/useSubEditor.js +165 -0
  51. package/src/composables/useTabSync.js +110 -0
  52. package/src/composables/useUnsavedChangesGuard.js +122 -0
  53. package/src/entity/EntityManager.js +540 -0
  54. package/src/entity/index.js +11 -0
  55. package/src/entity/storage/ApiStorage.js +146 -0
  56. package/src/entity/storage/LocalStorage.js +220 -0
  57. package/src/entity/storage/MemoryStorage.js +201 -0
  58. package/src/entity/storage/index.js +10 -0
  59. package/src/index.js +29 -0
  60. package/src/kernel/Kernel.js +234 -0
  61. package/src/kernel/index.js +7 -0
  62. package/src/module/index.js +16 -0
  63. package/src/module/moduleRegistry.js +222 -0
  64. package/src/orchestrator/Orchestrator.js +141 -0
  65. package/src/orchestrator/index.js +8 -0
  66. package/src/orchestrator/useOrchestrator.js +61 -0
  67. package/src/plugin.js +142 -0
  68. package/src/styles/_alerts.css +48 -0
  69. package/src/styles/_code.css +33 -0
  70. package/src/styles/_dialogs.css +17 -0
  71. package/src/styles/_markdown.css +82 -0
  72. package/src/styles/_show-pages.css +84 -0
  73. package/src/styles/index.css +16 -0
  74. package/src/styles/main.css +845 -0
  75. package/src/styles/theme/components.css +286 -0
  76. package/src/styles/theme/index.css +10 -0
  77. package/src/styles/theme/tokens.css +125 -0
  78. package/src/styles/theme/utilities.css +172 -0
  79. package/src/utils/debugInjector.js +261 -0
  80. package/src/utils/formatters.js +165 -0
  81. package/src/utils/index.js +35 -0
  82. package/src/utils/transformers.js +105 -0
@@ -0,0 +1,321 @@
1
+ <script setup>
2
+ /**
3
+ * MultiStepDialog - Reusable multi-step form dialog
4
+ *
5
+ * Features:
6
+ * - Step indicator with labels
7
+ * - Automatic navigation buttons (Cancel, Back, Next, Submit)
8
+ * - Per-step validation
9
+ * - Linear progression mode
10
+ * - Loading state for submit
11
+ * - Customizable button labels
12
+ *
13
+ * Usage:
14
+ * <MultiStepDialog
15
+ * v-model:visible="showDialog"
16
+ * v-model:step="currentStep"
17
+ * title="Create Item"
18
+ * :steps="[
19
+ * { label: 'Step 1', valid: !!field1 },
20
+ * { label: 'Step 2', valid: canSubmit }
21
+ * ]"
22
+ * :loading="submitting"
23
+ * @submit="handleSubmit"
24
+ * @cancel="handleCancel"
25
+ * >
26
+ * <template #step-1>
27
+ * <!-- Step 1 content -->
28
+ * </template>
29
+ * <template #step-2>
30
+ * <!-- Step 2 content -->
31
+ * </template>
32
+ * </MultiStepDialog>
33
+ *
34
+ * Props:
35
+ * - visible: boolean (v-model) - controls dialog visibility
36
+ * - step: number (v-model) - current step (1-indexed)
37
+ * - title: string - dialog title
38
+ * - steps: Array<{ label: string, valid?: boolean }> - step definitions
39
+ * - loading: boolean - shows loading state on submit button
40
+ * - width: string - dialog width (default: '800px')
41
+ * - linear: boolean - require validation before next step (default: true)
42
+ * - submitLabel: string - label for submit button (default: 'Submit')
43
+ * - cancelLabel: string - label for cancel button (default: 'Cancel')
44
+ * - nextLabel: string - label for next button (default: 'Next')
45
+ * - backLabel: string - label for back button (default: 'Back')
46
+ * - showBackOnFirst: boolean - show back button on first step (default: false)
47
+ * - hideStepIndicator: boolean - hide the step indicator (default: false)
48
+ *
49
+ * Events:
50
+ * - submit: emitted when submit button is clicked on last step
51
+ * - cancel: emitted when cancel button is clicked
52
+ * - step-change: emitted when step changes, payload: { from, to }
53
+ *
54
+ * Slots:
55
+ * - step-{n}: content for step n (1-indexed)
56
+ * - step-{n}-header: optional header content for step n (shown above step content)
57
+ * - actions: override action buttons (receives { step, isFirst, isLast, canProceed, goNext, goPrev, submit, cancel })
58
+ */
59
+ import { computed, watch } from 'vue'
60
+ import Dialog from 'primevue/dialog'
61
+ import Button from 'primevue/button'
62
+ import Stepper from 'primevue/stepper'
63
+ import StepList from 'primevue/steplist'
64
+ import Step from 'primevue/step'
65
+ import StepPanels from 'primevue/steppanels'
66
+ import StepPanel from 'primevue/steppanel'
67
+
68
+ const props = defineProps({
69
+ visible: {
70
+ type: Boolean,
71
+ default: false
72
+ },
73
+ step: {
74
+ type: Number,
75
+ default: 1
76
+ },
77
+ title: {
78
+ type: String,
79
+ default: ''
80
+ },
81
+ steps: {
82
+ type: Array,
83
+ required: true,
84
+ validator: (v) => v.every(s => typeof s.label === 'string')
85
+ },
86
+ loading: {
87
+ type: Boolean,
88
+ default: false
89
+ },
90
+ width: {
91
+ type: String,
92
+ default: '800px'
93
+ },
94
+ linear: {
95
+ type: Boolean,
96
+ default: true
97
+ },
98
+ submitLabel: {
99
+ type: String,
100
+ default: 'Submit'
101
+ },
102
+ cancelLabel: {
103
+ type: String,
104
+ default: 'Cancel'
105
+ },
106
+ nextLabel: {
107
+ type: String,
108
+ default: 'Next'
109
+ },
110
+ backLabel: {
111
+ type: String,
112
+ default: 'Back'
113
+ },
114
+ showBackOnFirst: {
115
+ type: Boolean,
116
+ default: false
117
+ },
118
+ hideStepIndicator: {
119
+ type: Boolean,
120
+ default: false
121
+ }
122
+ })
123
+
124
+ const emit = defineEmits(['update:visible', 'update:step', 'submit', 'cancel', 'step-change'])
125
+
126
+ // Current step (1-indexed)
127
+ const currentStep = computed({
128
+ get: () => props.step,
129
+ set: (v) => emit('update:step', v)
130
+ })
131
+
132
+ // Step helpers
133
+ const isFirstStep = computed(() => currentStep.value === 1)
134
+ const isLastStep = computed(() => currentStep.value === props.steps.length)
135
+
136
+ // Current step definition
137
+ const currentStepDef = computed(() => props.steps[currentStep.value - 1] || {})
138
+
139
+ // Can proceed to next step (validation)
140
+ const canProceed = computed(() => {
141
+ if (!props.linear) return true
142
+ return currentStepDef.value.valid !== false
143
+ })
144
+
145
+ // Can submit (last step validation)
146
+ const canSubmit = computed(() => {
147
+ return currentStepDef.value.valid !== false
148
+ })
149
+
150
+ // Navigation
151
+ function goNext() {
152
+ if (!isLastStep.value && canProceed.value) {
153
+ const from = currentStep.value
154
+ const to = currentStep.value + 1
155
+ currentStep.value = to
156
+ emit('step-change', { from, to })
157
+ }
158
+ }
159
+
160
+ function goPrev() {
161
+ if (!isFirstStep.value) {
162
+ const from = currentStep.value
163
+ const to = currentStep.value - 1
164
+ currentStep.value = to
165
+ emit('step-change', { from, to })
166
+ }
167
+ }
168
+
169
+ function goToStep(stepNumber) {
170
+ if (stepNumber >= 1 && stepNumber <= props.steps.length) {
171
+ // In linear mode, can only go back or to current+1 if valid
172
+ if (props.linear && stepNumber > currentStep.value) {
173
+ if (stepNumber > currentStep.value + 1 || !canProceed.value) {
174
+ return
175
+ }
176
+ }
177
+ const from = currentStep.value
178
+ currentStep.value = stepNumber
179
+ emit('step-change', { from, to: stepNumber })
180
+ }
181
+ }
182
+
183
+ function submit() {
184
+ if (canSubmit.value) {
185
+ emit('submit')
186
+ }
187
+ }
188
+
189
+ function cancel() {
190
+ emit('cancel')
191
+ emit('update:visible', false)
192
+ }
193
+
194
+ // Reset to first step when dialog opens
195
+ watch(() => props.visible, (newVal, oldVal) => {
196
+ if (newVal && !oldVal) {
197
+ // Don't reset if step is explicitly set
198
+ // currentStep.value = 1
199
+ }
200
+ })
201
+
202
+ // Expose for slot actions
203
+ const slotActions = computed(() => ({
204
+ step: currentStep.value,
205
+ isFirst: isFirstStep.value,
206
+ isLast: isLastStep.value,
207
+ canProceed: canProceed.value,
208
+ canSubmit: canSubmit.value,
209
+ goNext,
210
+ goPrev,
211
+ goToStep,
212
+ submit,
213
+ cancel
214
+ }))
215
+ </script>
216
+
217
+ <template>
218
+ <Dialog
219
+ :visible="props.visible"
220
+ @update:visible="emit('update:visible', $event)"
221
+ :header="title"
222
+ :style="{ width: width }"
223
+ :modal="true"
224
+ :closable="!loading"
225
+ :closeOnEscape="!loading"
226
+ >
227
+ <div class="multi-step-dialog">
228
+ <Stepper v-model:value="currentStep" :linear="linear">
229
+ <!-- Step indicator -->
230
+ <StepList v-if="!hideStepIndicator">
231
+ <Step
232
+ v-for="(stepDef, idx) in steps"
233
+ :key="idx"
234
+ :value="idx + 1"
235
+ >
236
+ {{ stepDef.label }}
237
+ </Step>
238
+ </StepList>
239
+
240
+ <!-- Step panels -->
241
+ <StepPanels>
242
+ <StepPanel
243
+ v-for="(stepDef, idx) in steps"
244
+ :key="idx"
245
+ :value="idx + 1"
246
+ >
247
+ <div class="step-content">
248
+ <!-- Optional step header slot -->
249
+ <slot :name="`step-${idx + 1}-header`" v-bind="slotActions" />
250
+
251
+ <!-- Step content slot -->
252
+ <slot :name="`step-${idx + 1}`" v-bind="slotActions" />
253
+
254
+ <!-- Action buttons -->
255
+ <div class="step-actions">
256
+ <slot name="actions" v-bind="slotActions">
257
+ <!-- Back button -->
258
+ <Button
259
+ v-if="!isFirstStep || showBackOnFirst"
260
+ :label="backLabel"
261
+ icon="pi pi-arrow-left"
262
+ severity="secondary"
263
+ @click="goPrev"
264
+ :disabled="loading || isFirstStep"
265
+ />
266
+
267
+ <!-- Cancel button -->
268
+ <Button
269
+ :label="cancelLabel"
270
+ severity="secondary"
271
+ @click="cancel"
272
+ :disabled="loading"
273
+ />
274
+
275
+ <!-- Next button (not on last step) -->
276
+ <Button
277
+ v-if="!isLastStep"
278
+ :label="nextLabel"
279
+ icon="pi pi-arrow-right"
280
+ iconPos="right"
281
+ @click="goNext"
282
+ :disabled="!canProceed"
283
+ />
284
+
285
+ <!-- Submit button (only on last step) -->
286
+ <Button
287
+ v-if="isLastStep"
288
+ :label="submitLabel"
289
+ icon="pi pi-check"
290
+ @click="submit"
291
+ :loading="loading"
292
+ :disabled="!canSubmit"
293
+ />
294
+ </slot>
295
+ </div>
296
+ </div>
297
+ </StepPanel>
298
+ </StepPanels>
299
+ </Stepper>
300
+ </div>
301
+ </Dialog>
302
+ </template>
303
+
304
+ <style scoped>
305
+ .multi-step-dialog {
306
+ padding: 0.5rem 0;
307
+ }
308
+
309
+ .step-content {
310
+ padding: 1rem 0;
311
+ }
312
+
313
+ .step-actions {
314
+ display: flex;
315
+ justify-content: flex-end;
316
+ gap: 0.5rem;
317
+ margin-top: 1.5rem;
318
+ padding-top: 1rem;
319
+ border-top: 1px solid var(--p-surface-200);
320
+ }
321
+ </style>
@@ -0,0 +1,108 @@
1
+ <script setup>
2
+ /**
3
+ * SimpleDialog - Wrapper for common dialog patterns
4
+ *
5
+ * Simplifies the most common dialog use case:
6
+ * - Modal with title
7
+ * - Content slot
8
+ * - Cancel/Confirm footer buttons
9
+ *
10
+ * Usage:
11
+ * <SimpleDialog
12
+ * v-model:visible="showDialog"
13
+ * title="Confirm Action"
14
+ * :loading="saving"
15
+ * @confirm="onSave"
16
+ * >
17
+ * <p>Are you sure?</p>
18
+ * </SimpleDialog>
19
+ *
20
+ * For read-only dialogs (no confirm button):
21
+ * <SimpleDialog v-model:visible="showDialog" title="View" :showConfirm="false">
22
+ * <p>Content here</p>
23
+ * </SimpleDialog>
24
+ *
25
+ * For dialogs with extra action buttons:
26
+ * <SimpleDialog v-model:visible="showDialog" title="View Key" :showConfirm="false">
27
+ * <p>{{ key }}</p>
28
+ * <template #actions>
29
+ * <Button label="Copy" icon="pi pi-copy" @click="copyKey" />
30
+ * </template>
31
+ * </SimpleDialog>
32
+ */
33
+ import Dialog from 'primevue/dialog'
34
+ import Button from 'primevue/button'
35
+
36
+ const props = defineProps({
37
+ visible: { type: Boolean, default: false },
38
+ title: { type: String, required: true },
39
+ width: { type: String, default: '500px' },
40
+
41
+ // Footer buttons
42
+ showCancel: { type: Boolean, default: true },
43
+ showConfirm: { type: Boolean, default: true },
44
+ cancelLabel: { type: String, default: 'Cancel' },
45
+ confirmLabel: { type: String, default: 'Confirm' },
46
+ confirmIcon: { type: String, default: null },
47
+ confirmSeverity: { type: String, default: null },
48
+ confirmDisabled: { type: Boolean, default: false },
49
+ loading: { type: Boolean, default: false },
50
+
51
+ // Dialog behavior
52
+ closable: { type: Boolean, default: true },
53
+ dismissableMask: { type: Boolean, default: false }
54
+ })
55
+
56
+ const emit = defineEmits(['update:visible', 'confirm', 'cancel'])
57
+
58
+ function onCancel() {
59
+ emit('cancel')
60
+ emit('update:visible', false)
61
+ }
62
+
63
+ function onConfirm() {
64
+ emit('confirm')
65
+ }
66
+
67
+ // Allow closing via escape/mask only when not loading
68
+ const canClose = computed(() => props.closable && !props.loading)
69
+ </script>
70
+
71
+ <script>
72
+ import { computed } from 'vue'
73
+ </script>
74
+
75
+ <template>
76
+ <Dialog
77
+ :visible="visible"
78
+ :header="title"
79
+ :modal="true"
80
+ :style="{ width }"
81
+ :closable="canClose"
82
+ :closeOnEscape="canClose"
83
+ :dismissableMask="dismissableMask && canClose"
84
+ @update:visible="$emit('update:visible', $event)"
85
+ >
86
+ <slot />
87
+
88
+ <template #footer v-if="showCancel || showConfirm || $slots.actions">
89
+ <slot name="actions" />
90
+ <Button
91
+ v-if="showCancel"
92
+ :label="cancelLabel"
93
+ severity="secondary"
94
+ @click="onCancel"
95
+ :disabled="loading"
96
+ />
97
+ <Button
98
+ v-if="showConfirm"
99
+ :label="confirmLabel"
100
+ :icon="confirmIcon"
101
+ :severity="confirmSeverity"
102
+ :loading="loading"
103
+ :disabled="confirmDisabled"
104
+ @click="onConfirm"
105
+ />
106
+ </template>
107
+ </Dialog>
108
+ </template>
@@ -0,0 +1,87 @@
1
+ <script setup>
2
+ /**
3
+ * UnsavedChangesDialog - Confirm leaving with unsaved changes
4
+ *
5
+ * Uses SimpleDialog with custom actions:
6
+ * - Save & Leave (optional): saves then navigates
7
+ * - Leave: discards changes and navigates
8
+ * - Stay: cancels navigation
9
+ */
10
+ import SimpleDialog from './SimpleDialog.vue'
11
+ import Button from 'primevue/button'
12
+
13
+ const props = defineProps({
14
+ visible: { type: Boolean, default: false },
15
+ saving: { type: Boolean, default: false },
16
+ message: { type: String, default: 'You have unsaved changes that will be lost.' },
17
+ hasOnSave: { type: Boolean, default: false }
18
+ })
19
+
20
+ const emit = defineEmits(['update:visible', 'saveAndLeave', 'leave', 'stay'])
21
+
22
+ function onStay() {
23
+ emit('stay')
24
+ emit('update:visible', false)
25
+ }
26
+
27
+ function onLeave() {
28
+ emit('leave')
29
+ }
30
+
31
+ function onSaveAndLeave() {
32
+ emit('saveAndLeave')
33
+ }
34
+ </script>
35
+
36
+ <template>
37
+ <SimpleDialog
38
+ :visible="visible"
39
+ title="Unsaved Changes"
40
+ width="400px"
41
+ :closable="false"
42
+ :showCancel="true"
43
+ :showConfirm="true"
44
+ cancelLabel="Stay"
45
+ confirmLabel="Leave"
46
+ confirmSeverity="danger"
47
+ :loading="saving"
48
+ @cancel="onStay"
49
+ @confirm="onLeave"
50
+ @update:visible="$emit('update:visible', $event)"
51
+ >
52
+ <div class="dialog-content">
53
+ <i class="pi pi-exclamation-triangle dialog-icon"></i>
54
+ <p>{{ message }}</p>
55
+ </div>
56
+
57
+ <template #actions>
58
+ <Button
59
+ v-if="hasOnSave"
60
+ label="Save & Leave"
61
+ icon="pi pi-save"
62
+ severity="success"
63
+ :loading="saving"
64
+ @click="onSaveAndLeave"
65
+ />
66
+ </template>
67
+ </SimpleDialog>
68
+ </template>
69
+
70
+ <style scoped>
71
+ .dialog-content {
72
+ display: flex;
73
+ align-items: flex-start;
74
+ gap: 1rem;
75
+ }
76
+
77
+ .dialog-icon {
78
+ font-size: 2rem;
79
+ color: var(--p-orange-500);
80
+ flex-shrink: 0;
81
+ }
82
+
83
+ .dialog-content p {
84
+ margin: 0;
85
+ line-height: 1.5;
86
+ }
87
+ </style>
@@ -0,0 +1,155 @@
1
+ <script setup>
2
+ /**
3
+ * CardsGrid - Dynamic card zone component
4
+ *
5
+ * Props:
6
+ * - cards: Array of card objects from useCardsZone composable
7
+ * - columns: Number of columns (2, 3, 4, or 'auto')
8
+ *
9
+ * Card types:
10
+ * 1. Simple stat card (default):
11
+ * { name: 'total', value: 42, label: 'Total', severity: 'success', icon: 'pi pi-check' }
12
+ *
13
+ * 2. Custom card with slot:
14
+ * { name: 'preview', custom: true }
15
+ * Then use: <template #card-preview>...</template>
16
+ *
17
+ * If no cards provided, renders nothing.
18
+ */
19
+ defineProps({
20
+ cards: {
21
+ type: Array,
22
+ default: () => []
23
+ },
24
+ columns: {
25
+ type: [Number, String],
26
+ default: 'auto',
27
+ validator: (v) => ['auto', 2, 3, 4].includes(v)
28
+ }
29
+ })
30
+
31
+ function getSeverityClass(severity) {
32
+ if (!severity) return ''
33
+ return `stat-value--${severity}`
34
+ }
35
+ </script>
36
+
37
+ <template>
38
+ <div v-if="cards.length > 0" class="cards-grid" :class="`cards-grid--cols-${columns}`">
39
+ <template v-for="card in cards" :key="card.name">
40
+ <!-- Custom card: render slot -->
41
+ <div v-if="card.custom" class="card-custom" :class="card.class">
42
+ <slot :name="`card-${card.name}`" :card="card" ></slot>
43
+ </div>
44
+
45
+ <!-- Simple stat card -->
46
+ <div
47
+ v-else
48
+ class="stat-card"
49
+ :class="[{ 'stat-card--clickable': card.onClick }, card.class]"
50
+ @click="card.onClick?.()"
51
+ >
52
+ <div class="stat-value" :class="getSeverityClass(card.severity)">
53
+ <i v-if="card.icon" :class="card.icon" class="stat-icon"></i>
54
+ {{ card.value }}
55
+ </div>
56
+ <div class="stat-label">{{ card.label }}</div>
57
+ </div>
58
+ </template>
59
+ </div>
60
+ </template>
61
+
62
+ <style scoped>
63
+ .cards-grid {
64
+ display: grid;
65
+ gap: 1rem;
66
+ margin-bottom: 1.5rem;
67
+ }
68
+
69
+ .cards-grid--cols-auto {
70
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
71
+ }
72
+
73
+ .cards-grid--cols-2 {
74
+ grid-template-columns: repeat(2, 1fr);
75
+ }
76
+
77
+ .cards-grid--cols-3 {
78
+ grid-template-columns: repeat(3, 1fr);
79
+ }
80
+
81
+ .cards-grid--cols-4 {
82
+ grid-template-columns: repeat(4, 1fr);
83
+ }
84
+
85
+ .stat-card,
86
+ .card-custom {
87
+ background: var(--p-surface-0);
88
+ border: 1px solid var(--p-surface-200);
89
+ border-radius: 0.5rem;
90
+ padding: 1.25rem;
91
+ }
92
+
93
+ .stat-card {
94
+ text-align: center;
95
+ }
96
+
97
+ .stat-card--clickable {
98
+ cursor: pointer;
99
+ transition: border-color 0.2s, box-shadow 0.2s;
100
+ }
101
+
102
+ .stat-card--clickable:hover {
103
+ border-color: var(--p-primary-300);
104
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
105
+ }
106
+
107
+ .stat-value {
108
+ font-size: 2rem;
109
+ font-weight: 600;
110
+ color: var(--p-primary-500);
111
+ display: flex;
112
+ align-items: center;
113
+ justify-content: center;
114
+ gap: 0.5rem;
115
+ }
116
+
117
+ .stat-value--success {
118
+ color: var(--p-green-500);
119
+ }
120
+
121
+ .stat-value--danger {
122
+ color: var(--p-red-500);
123
+ }
124
+
125
+ .stat-value--warning {
126
+ color: var(--p-orange-500);
127
+ }
128
+
129
+ .stat-value--info {
130
+ color: var(--p-blue-500);
131
+ }
132
+
133
+ .stat-icon {
134
+ font-size: 1.5rem;
135
+ }
136
+
137
+ .stat-label {
138
+ font-size: 0.875rem;
139
+ color: var(--p-surface-600);
140
+ margin-top: 0.25rem;
141
+ }
142
+
143
+ @media (max-width: 768px) {
144
+ .cards-grid--cols-3,
145
+ .cards-grid--cols-4 {
146
+ grid-template-columns: repeat(2, 1fr);
147
+ }
148
+ }
149
+
150
+ @media (max-width: 480px) {
151
+ .cards-grid {
152
+ grid-template-columns: 1fr;
153
+ }
154
+ }
155
+ </style>