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.
- package/CHANGELOG.md +270 -0
- package/LICENSE +21 -0
- package/README.md +166 -0
- package/package.json +48 -0
- package/src/assets/logo.svg +6 -0
- package/src/components/BoolCell.vue +28 -0
- package/src/components/dialogs/BulkStatusDialog.vue +43 -0
- package/src/components/dialogs/MultiStepDialog.vue +321 -0
- package/src/components/dialogs/SimpleDialog.vue +108 -0
- package/src/components/dialogs/UnsavedChangesDialog.vue +87 -0
- package/src/components/display/CardsGrid.vue +155 -0
- package/src/components/display/CopyableId.vue +92 -0
- package/src/components/display/EmptyState.vue +114 -0
- package/src/components/display/IntensityBar.vue +171 -0
- package/src/components/display/RichCardsGrid.vue +220 -0
- package/src/components/editors/JsonEditorFoldable.vue +467 -0
- package/src/components/editors/JsonStructuredField.vue +218 -0
- package/src/components/editors/JsonViewer.vue +91 -0
- package/src/components/editors/KeyValueEditor.vue +314 -0
- package/src/components/editors/LanguageEditor.vue +245 -0
- package/src/components/editors/ScopeEditor.vue +341 -0
- package/src/components/editors/VanillaJsonEditor.vue +185 -0
- package/src/components/forms/FormActions.vue +104 -0
- package/src/components/forms/FormField.vue +64 -0
- package/src/components/forms/FormTab.vue +217 -0
- package/src/components/forms/FormTabs.vue +108 -0
- package/src/components/index.js +44 -0
- package/src/components/layout/AppLayout.vue +430 -0
- package/src/components/layout/Breadcrumb.vue +106 -0
- package/src/components/layout/PageHeader.vue +75 -0
- package/src/components/layout/PageLayout.vue +93 -0
- package/src/components/lists/ActionButtons.vue +41 -0
- package/src/components/lists/ActionColumn.vue +37 -0
- package/src/components/lists/FilterBar.vue +53 -0
- package/src/components/lists/ListPage.vue +319 -0
- package/src/composables/index.js +19 -0
- package/src/composables/useApp.js +43 -0
- package/src/composables/useAuth.js +49 -0
- package/src/composables/useBareForm.js +143 -0
- package/src/composables/useBreadcrumb.js +221 -0
- package/src/composables/useDirtyState.js +103 -0
- package/src/composables/useEntityTitle.js +121 -0
- package/src/composables/useForm.js +254 -0
- package/src/composables/useGuardStore.js +37 -0
- package/src/composables/useJsonSyntax.js +101 -0
- package/src/composables/useListPageBuilder.js +1176 -0
- package/src/composables/useNavigation.js +89 -0
- package/src/composables/usePageBuilder.js +334 -0
- package/src/composables/useStatus.js +146 -0
- package/src/composables/useSubEditor.js +165 -0
- package/src/composables/useTabSync.js +110 -0
- package/src/composables/useUnsavedChangesGuard.js +122 -0
- package/src/entity/EntityManager.js +540 -0
- package/src/entity/index.js +11 -0
- package/src/entity/storage/ApiStorage.js +146 -0
- package/src/entity/storage/LocalStorage.js +220 -0
- package/src/entity/storage/MemoryStorage.js +201 -0
- package/src/entity/storage/index.js +10 -0
- package/src/index.js +29 -0
- package/src/kernel/Kernel.js +234 -0
- package/src/kernel/index.js +7 -0
- package/src/module/index.js +16 -0
- package/src/module/moduleRegistry.js +222 -0
- package/src/orchestrator/Orchestrator.js +141 -0
- package/src/orchestrator/index.js +8 -0
- package/src/orchestrator/useOrchestrator.js +61 -0
- package/src/plugin.js +142 -0
- package/src/styles/_alerts.css +48 -0
- package/src/styles/_code.css +33 -0
- package/src/styles/_dialogs.css +17 -0
- package/src/styles/_markdown.css +82 -0
- package/src/styles/_show-pages.css +84 -0
- package/src/styles/index.css +16 -0
- package/src/styles/main.css +845 -0
- package/src/styles/theme/components.css +286 -0
- package/src/styles/theme/index.css +10 -0
- package/src/styles/theme/tokens.css +125 -0
- package/src/styles/theme/utilities.css +172 -0
- package/src/utils/debugInjector.js +261 -0
- package/src/utils/formatters.js +165 -0
- package/src/utils/index.js +35 -0
- package/src/utils/transformers.js +105 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* CopyableId - Read-only ID field with copy-to-clipboard functionality
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* <CopyableId :value="entityId" label="ID" />
|
|
7
|
+
* <CopyableId :value="apiKey" label="API Key" />
|
|
8
|
+
*/
|
|
9
|
+
import { ref } from 'vue'
|
|
10
|
+
import { useToast } from 'primevue/usetoast'
|
|
11
|
+
import Button from 'primevue/button'
|
|
12
|
+
|
|
13
|
+
const props = defineProps({
|
|
14
|
+
value: {
|
|
15
|
+
type: String,
|
|
16
|
+
required: true
|
|
17
|
+
},
|
|
18
|
+
label: {
|
|
19
|
+
type: String,
|
|
20
|
+
default: 'ID'
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const toast = useToast()
|
|
25
|
+
const copied = ref(false)
|
|
26
|
+
|
|
27
|
+
async function copyToClipboard() {
|
|
28
|
+
try {
|
|
29
|
+
await navigator.clipboard.writeText(props.value)
|
|
30
|
+
copied.value = true
|
|
31
|
+
toast.add({
|
|
32
|
+
severity: 'success',
|
|
33
|
+
summary: 'Copied',
|
|
34
|
+
detail: `${props.label} copied to clipboard`,
|
|
35
|
+
life: 2000
|
|
36
|
+
})
|
|
37
|
+
setTimeout(() => {
|
|
38
|
+
copied.value = false
|
|
39
|
+
}, 2000)
|
|
40
|
+
} catch {
|
|
41
|
+
toast.add({
|
|
42
|
+
severity: 'error',
|
|
43
|
+
summary: 'Error',
|
|
44
|
+
detail: 'Failed to copy to clipboard',
|
|
45
|
+
life: 3000
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<template>
|
|
52
|
+
<div class="form-field">
|
|
53
|
+
<label class="form-field-label">{{ label }}</label>
|
|
54
|
+
<div class="copyable-id">
|
|
55
|
+
<Button
|
|
56
|
+
type="button"
|
|
57
|
+
:icon="copied ? 'pi pi-check' : 'pi pi-copy'"
|
|
58
|
+
:severity="copied ? 'success' : 'secondary'"
|
|
59
|
+
size="small"
|
|
60
|
+
text
|
|
61
|
+
rounded
|
|
62
|
+
@click="copyToClipboard"
|
|
63
|
+
:aria-label="`Copy ${label}`"
|
|
64
|
+
class="copyable-id-button"
|
|
65
|
+
/>
|
|
66
|
+
<code class="copyable-id-value">{{ value }}</code>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</template>
|
|
70
|
+
|
|
71
|
+
<style scoped>
|
|
72
|
+
.copyable-id {
|
|
73
|
+
display: inline-flex;
|
|
74
|
+
align-items: center;
|
|
75
|
+
gap: 0.5rem;
|
|
76
|
+
background: var(--p-surface-100);
|
|
77
|
+
padding: 0.5rem 0.75rem;
|
|
78
|
+
border-radius: 4px;
|
|
79
|
+
max-width: 100%;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.copyable-id-value {
|
|
83
|
+
font-family: monospace;
|
|
84
|
+
font-size: 0.875rem;
|
|
85
|
+
color: var(--p-surface-700);
|
|
86
|
+
word-break: break-all;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.copyable-id-button {
|
|
90
|
+
flex-shrink: 0;
|
|
91
|
+
}
|
|
92
|
+
</style>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* EmptyState - Reusable empty state display
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* <EmptyState
|
|
7
|
+
* icon="pi-users"
|
|
8
|
+
* title="No agents found"
|
|
9
|
+
* description="Assign agents from their profile page"
|
|
10
|
+
* />
|
|
11
|
+
*
|
|
12
|
+
* <!-- With slot for custom content -->
|
|
13
|
+
* <EmptyState icon="pi-inbox" title="No items">
|
|
14
|
+
* <Button label="Create Item" @click="create" />
|
|
15
|
+
* </EmptyState>
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
defineProps({
|
|
19
|
+
// PrimeIcon name (without "pi-" prefix if you want, both work)
|
|
20
|
+
icon: {
|
|
21
|
+
type: String,
|
|
22
|
+
default: 'inbox'
|
|
23
|
+
},
|
|
24
|
+
// Main message
|
|
25
|
+
title: {
|
|
26
|
+
type: String,
|
|
27
|
+
default: 'No items found'
|
|
28
|
+
},
|
|
29
|
+
// Secondary description (optional)
|
|
30
|
+
description: {
|
|
31
|
+
type: String,
|
|
32
|
+
default: ''
|
|
33
|
+
},
|
|
34
|
+
// Size variant
|
|
35
|
+
size: {
|
|
36
|
+
type: String,
|
|
37
|
+
default: 'md', // sm, md, lg
|
|
38
|
+
validator: (v) => ['sm', 'md', 'lg'].includes(v)
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// Normalize icon name (accept both "pi-inbox" and "inbox")
|
|
43
|
+
function getIconClass(icon) {
|
|
44
|
+
if (icon.startsWith('pi-')) {
|
|
45
|
+
return `pi ${icon}`
|
|
46
|
+
}
|
|
47
|
+
return `pi pi-${icon}`
|
|
48
|
+
}
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<template>
|
|
52
|
+
<div class="empty-state" :class="`empty-state--${size}`">
|
|
53
|
+
<i :class="getIconClass(icon)" class="empty-state-icon" />
|
|
54
|
+
<p class="empty-state-title">{{ title }}</p>
|
|
55
|
+
<small v-if="description" class="empty-state-description">{{ description }}</small>
|
|
56
|
+
<div v-if="$slots.default" class="empty-state-actions">
|
|
57
|
+
<slot />
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</template>
|
|
61
|
+
|
|
62
|
+
<style scoped>
|
|
63
|
+
.empty-state {
|
|
64
|
+
display: flex;
|
|
65
|
+
flex-direction: column;
|
|
66
|
+
align-items: center;
|
|
67
|
+
justify-content: center;
|
|
68
|
+
text-align: center;
|
|
69
|
+
padding: 2rem;
|
|
70
|
+
color: var(--text-color-secondary);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.empty-state--sm {
|
|
74
|
+
padding: 1rem;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.empty-state--lg {
|
|
78
|
+
padding: 3rem;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.empty-state-icon {
|
|
82
|
+
font-size: 2rem;
|
|
83
|
+
margin-bottom: 0.5rem;
|
|
84
|
+
opacity: 0.5;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.empty-state--sm .empty-state-icon {
|
|
88
|
+
font-size: 1.5rem;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.empty-state--lg .empty-state-icon {
|
|
92
|
+
font-size: 3rem;
|
|
93
|
+
margin-bottom: 1rem;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.empty-state-title {
|
|
97
|
+
margin: 0 0 0.25rem 0;
|
|
98
|
+
font-weight: 500;
|
|
99
|
+
color: var(--text-color-secondary);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.empty-state--lg .empty-state-title {
|
|
103
|
+
font-size: 1.1rem;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.empty-state-description {
|
|
107
|
+
opacity: 0.7;
|
|
108
|
+
max-width: 300px;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.empty-state-actions {
|
|
112
|
+
margin-top: 1rem;
|
|
113
|
+
}
|
|
114
|
+
</style>
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed, onMounted, inject } from 'vue'
|
|
3
|
+
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
value: {
|
|
6
|
+
type: Number,
|
|
7
|
+
required: true
|
|
8
|
+
},
|
|
9
|
+
showValue: {
|
|
10
|
+
type: Boolean,
|
|
11
|
+
default: true
|
|
12
|
+
},
|
|
13
|
+
size: {
|
|
14
|
+
type: String,
|
|
15
|
+
default: 'md', // sm, md, lg
|
|
16
|
+
validator: (v) => ['sm', 'md', 'lg'].includes(v)
|
|
17
|
+
},
|
|
18
|
+
// Threshold configuration
|
|
19
|
+
threshold: {
|
|
20
|
+
type: Number,
|
|
21
|
+
default: null // If provided, use static threshold instead of loading from API
|
|
22
|
+
},
|
|
23
|
+
configEndpoint: {
|
|
24
|
+
type: String,
|
|
25
|
+
default: null // Endpoint to load threshold config from API
|
|
26
|
+
},
|
|
27
|
+
configThresholdKey: {
|
|
28
|
+
type: String,
|
|
29
|
+
default: 'threshold' // Key in config response containing threshold value
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
// Get API adapter (optional - only needed if loading from endpoint)
|
|
34
|
+
const api = inject('apiAdapter', null)
|
|
35
|
+
|
|
36
|
+
// Threshold value
|
|
37
|
+
const loadedThreshold = ref(8) // default fallback
|
|
38
|
+
const configLoaded = ref(false)
|
|
39
|
+
|
|
40
|
+
// Computed threshold - use prop if provided, otherwise loaded value
|
|
41
|
+
const currentThreshold = computed(() => props.threshold ?? loadedThreshold.value)
|
|
42
|
+
|
|
43
|
+
// Singleton pattern - load config once for all instances
|
|
44
|
+
let configPromise = null
|
|
45
|
+
|
|
46
|
+
async function loadConfig() {
|
|
47
|
+
// Skip if using static threshold or no endpoint configured
|
|
48
|
+
if (props.threshold !== null || !props.configEndpoint) {
|
|
49
|
+
configLoaded.value = true
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Skip if no API adapter
|
|
54
|
+
if (!api) {
|
|
55
|
+
console.warn('[IntensityBar] apiAdapter not provided, using default threshold')
|
|
56
|
+
configLoaded.value = true
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (configPromise) return configPromise
|
|
61
|
+
|
|
62
|
+
configPromise = api.request('GET', props.configEndpoint)
|
|
63
|
+
.then(data => {
|
|
64
|
+
if (data?.[props.configThresholdKey]) {
|
|
65
|
+
loadedThreshold.value = data[props.configThresholdKey]
|
|
66
|
+
}
|
|
67
|
+
configLoaded.value = true
|
|
68
|
+
})
|
|
69
|
+
.catch(() => {
|
|
70
|
+
console.warn('[IntensityBar] Could not load config, using defaults')
|
|
71
|
+
configLoaded.value = true
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
return configPromise
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Calculate bar properties based on threshold tiers
|
|
78
|
+
const barProps = computed(() => {
|
|
79
|
+
const threshold = currentThreshold.value
|
|
80
|
+
const tier = Math.floor(props.value / threshold)
|
|
81
|
+
const withinTier = props.value % threshold
|
|
82
|
+
const percentage = (withinTier / threshold) * 100
|
|
83
|
+
|
|
84
|
+
// Color: green (tier 0), orange (tier 1), red (tier 2+)
|
|
85
|
+
let color = 'var(--p-green-500)'
|
|
86
|
+
let severity = 'success'
|
|
87
|
+
if (tier === 1) {
|
|
88
|
+
color = 'var(--p-orange-500)'
|
|
89
|
+
severity = 'warning'
|
|
90
|
+
} else if (tier >= 2) {
|
|
91
|
+
color = 'var(--p-red-500)'
|
|
92
|
+
severity = 'danger'
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { percentage, tier, color, severity, threshold }
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
// Size classes
|
|
99
|
+
const sizeClass = computed(() => `intensity-bar--${props.size}`)
|
|
100
|
+
|
|
101
|
+
onMounted(() => {
|
|
102
|
+
loadConfig()
|
|
103
|
+
})
|
|
104
|
+
</script>
|
|
105
|
+
|
|
106
|
+
<template>
|
|
107
|
+
<div class="intensity-container" :class="sizeClass">
|
|
108
|
+
<div
|
|
109
|
+
class="intensity-bar"
|
|
110
|
+
:style="{ '--bar-color': barProps.color, '--bar-width': barProps.percentage + '%' }"
|
|
111
|
+
:title="`Tier ${barProps.tier} (threshold: ${barProps.threshold})`"
|
|
112
|
+
>
|
|
113
|
+
<div class="intensity-bar-fill"></div>
|
|
114
|
+
</div>
|
|
115
|
+
<span v-if="showValue" class="intensity-value" :style="{ color: barProps.color }">
|
|
116
|
+
{{ value.toFixed(2) }}
|
|
117
|
+
</span>
|
|
118
|
+
</div>
|
|
119
|
+
</template>
|
|
120
|
+
|
|
121
|
+
<style scoped>
|
|
122
|
+
.intensity-container {
|
|
123
|
+
display: flex;
|
|
124
|
+
align-items: center;
|
|
125
|
+
gap: 8px;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.intensity-bar {
|
|
129
|
+
background: var(--p-surface-200);
|
|
130
|
+
border-radius: 4px;
|
|
131
|
+
overflow: hidden;
|
|
132
|
+
/* Default size (md) */
|
|
133
|
+
width: 80px;
|
|
134
|
+
height: 8px;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.intensity-bar--sm .intensity-bar {
|
|
138
|
+
width: 60px;
|
|
139
|
+
height: 6px;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.intensity-bar--lg .intensity-bar {
|
|
143
|
+
width: 120px;
|
|
144
|
+
height: 10px;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.intensity-bar-fill {
|
|
148
|
+
height: 100%;
|
|
149
|
+
width: var(--bar-width);
|
|
150
|
+
background: var(--bar-color);
|
|
151
|
+
border-radius: 4px;
|
|
152
|
+
transition: width 0.3s ease, background 0.3s ease;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.intensity-value {
|
|
156
|
+
font-weight: 600;
|
|
157
|
+
min-width: 45px;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.intensity-bar--sm .intensity-value {
|
|
161
|
+
font-size: 0.8em;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.intensity-bar--md .intensity-value {
|
|
165
|
+
font-size: 0.9em;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.intensity-bar--lg .intensity-value {
|
|
169
|
+
font-size: 1em;
|
|
170
|
+
}
|
|
171
|
+
</style>
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* RichCardsGrid - Grid of rich stat cards with title, value, subtitle
|
|
4
|
+
*
|
|
5
|
+
* Props:
|
|
6
|
+
* - cards: Array of card objects
|
|
7
|
+
* - columns: Number of columns (2, 3, 4, or 'auto')
|
|
8
|
+
*
|
|
9
|
+
* Card object:
|
|
10
|
+
* {
|
|
11
|
+
* name: 'users', // unique key
|
|
12
|
+
* title: 'Users', // card title
|
|
13
|
+
* value: 42, // main value
|
|
14
|
+
* subtitle: '10 active', // optional subtitle
|
|
15
|
+
* icon: 'pi pi-users', // optional icon
|
|
16
|
+
* severity: 'success', // optional: success, danger, warning, info
|
|
17
|
+
* onClick: () => {}, // optional click handler
|
|
18
|
+
* to: '/dashboard/users', // optional router link
|
|
19
|
+
* custom: true // use slot instead
|
|
20
|
+
* }
|
|
21
|
+
*/
|
|
22
|
+
import { useRouter } from 'vue-router'
|
|
23
|
+
|
|
24
|
+
const router = useRouter()
|
|
25
|
+
|
|
26
|
+
defineProps({
|
|
27
|
+
cards: {
|
|
28
|
+
type: Array,
|
|
29
|
+
default: () => []
|
|
30
|
+
},
|
|
31
|
+
columns: {
|
|
32
|
+
type: [Number, String],
|
|
33
|
+
default: 'auto',
|
|
34
|
+
validator: (v) => ['auto', 2, 3, 4, 5, 6].includes(v)
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
function handleClick(card) {
|
|
39
|
+
if (card.onClick) {
|
|
40
|
+
card.onClick()
|
|
41
|
+
} else if (card.to) {
|
|
42
|
+
router.push(card.to)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isClickable(card) {
|
|
47
|
+
return card.onClick || card.to
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getSeverityClass(severity) {
|
|
51
|
+
if (!severity) return ''
|
|
52
|
+
return `rich-card--${severity}`
|
|
53
|
+
}
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<template>
|
|
57
|
+
<div v-if="cards.length > 0" class="rich-cards-grid" :class="`rich-cards-grid--cols-${columns}`">
|
|
58
|
+
<template v-for="card in cards" :key="card.name">
|
|
59
|
+
<!-- Custom card: render slot -->
|
|
60
|
+
<div v-if="card.custom" class="rich-card rich-card--custom" :class="card.class">
|
|
61
|
+
<slot :name="`card-${card.name}`" :card="card" ></slot>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<!-- Rich stat card -->
|
|
65
|
+
<div
|
|
66
|
+
v-else
|
|
67
|
+
class="rich-card"
|
|
68
|
+
:class="[
|
|
69
|
+
getSeverityClass(card.severity),
|
|
70
|
+
{ 'rich-card--clickable': isClickable(card) },
|
|
71
|
+
card.class
|
|
72
|
+
]"
|
|
73
|
+
@click="handleClick(card)"
|
|
74
|
+
>
|
|
75
|
+
<div class="rich-card-header">
|
|
76
|
+
<span class="rich-card-title">{{ card.title }}</span>
|
|
77
|
+
<i v-if="card.icon" :class="card.icon" class="rich-card-icon"></i>
|
|
78
|
+
</div>
|
|
79
|
+
<div class="rich-card-value">{{ card.value }}</div>
|
|
80
|
+
<div v-if="card.subtitle" class="rich-card-subtitle">{{ card.subtitle }}</div>
|
|
81
|
+
</div>
|
|
82
|
+
</template>
|
|
83
|
+
</div>
|
|
84
|
+
</template>
|
|
85
|
+
|
|
86
|
+
<style scoped>
|
|
87
|
+
.rich-cards-grid {
|
|
88
|
+
display: grid;
|
|
89
|
+
gap: 1rem;
|
|
90
|
+
margin-bottom: 1.5rem;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.rich-cards-grid--cols-auto {
|
|
94
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.rich-cards-grid--cols-2 {
|
|
98
|
+
grid-template-columns: repeat(2, 1fr);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.rich-cards-grid--cols-3 {
|
|
102
|
+
grid-template-columns: repeat(3, 1fr);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.rich-cards-grid--cols-4 {
|
|
106
|
+
grid-template-columns: repeat(4, 1fr);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.rich-cards-grid--cols-5 {
|
|
110
|
+
grid-template-columns: repeat(5, 1fr);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.rich-cards-grid--cols-6 {
|
|
114
|
+
grid-template-columns: repeat(6, 1fr);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.rich-card {
|
|
118
|
+
background: var(--p-surface-0);
|
|
119
|
+
border: 1px solid var(--p-surface-200);
|
|
120
|
+
border-radius: 0.5rem;
|
|
121
|
+
padding: 1.25rem;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.rich-card--clickable {
|
|
125
|
+
cursor: pointer;
|
|
126
|
+
transition: border-color 0.2s, box-shadow 0.2s, transform 0.2s;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.rich-card--clickable:hover {
|
|
130
|
+
border-color: var(--p-primary-300);
|
|
131
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
132
|
+
transform: translateY(-2px);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.rich-card-header {
|
|
136
|
+
display: flex;
|
|
137
|
+
justify-content: space-between;
|
|
138
|
+
align-items: center;
|
|
139
|
+
margin-bottom: 0.75rem;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.rich-card-title {
|
|
143
|
+
font-size: 0.875rem;
|
|
144
|
+
font-weight: 500;
|
|
145
|
+
color: var(--p-surface-600);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.rich-card-icon {
|
|
149
|
+
font-size: 1.25rem;
|
|
150
|
+
color: var(--p-surface-400);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.rich-card-value {
|
|
154
|
+
font-size: 2rem;
|
|
155
|
+
font-weight: 700;
|
|
156
|
+
color: var(--p-surface-900);
|
|
157
|
+
line-height: 1.2;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.rich-card-subtitle {
|
|
161
|
+
font-size: 0.8rem;
|
|
162
|
+
color: var(--p-surface-500);
|
|
163
|
+
margin-top: 0.25rem;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* Severity variants */
|
|
167
|
+
.rich-card--success .rich-card-value {
|
|
168
|
+
color: var(--p-green-600);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.rich-card--success .rich-card-icon {
|
|
172
|
+
color: var(--p-green-500);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.rich-card--danger .rich-card-value {
|
|
176
|
+
color: var(--p-red-600);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.rich-card--danger .rich-card-icon {
|
|
180
|
+
color: var(--p-red-500);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.rich-card--warning .rich-card-value {
|
|
184
|
+
color: var(--p-orange-600);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.rich-card--warning .rich-card-icon {
|
|
188
|
+
color: var(--p-orange-500);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.rich-card--info .rich-card-value {
|
|
192
|
+
color: var(--p-blue-600);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.rich-card--info .rich-card-icon {
|
|
196
|
+
color: var(--p-blue-500);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
@media (max-width: 1024px) {
|
|
200
|
+
.rich-cards-grid--cols-5,
|
|
201
|
+
.rich-cards-grid--cols-6 {
|
|
202
|
+
grid-template-columns: repeat(3, 1fr);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
@media (max-width: 768px) {
|
|
207
|
+
.rich-cards-grid--cols-3,
|
|
208
|
+
.rich-cards-grid--cols-4,
|
|
209
|
+
.rich-cards-grid--cols-5,
|
|
210
|
+
.rich-cards-grid--cols-6 {
|
|
211
|
+
grid-template-columns: repeat(2, 1fr);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
@media (max-width: 480px) {
|
|
216
|
+
.rich-cards-grid {
|
|
217
|
+
grid-template-columns: 1fr;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
</style>
|