@conduction/nextcloud-vue 0.1.0-beta.11 → 0.1.0-beta.12
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/dist/nextcloud-vue.cjs +67614 -0
- package/dist/nextcloud-vue.cjs.js +13518 -13617
- package/dist/nextcloud-vue.cjs.js.map +1 -1
- package/dist/nextcloud-vue.cjs.map +1 -0
- package/dist/nextcloud-vue.css +1796 -1800
- package/dist/nextcloud-vue.esm.js +13518 -13617
- package/dist/nextcloud-vue.esm.js.map +1 -1
- package/package.json +3 -2
- package/src/components/CnActionsBar/CnActionsBar.vue +254 -254
- package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +570 -570
- package/src/components/CnAdvancedFormDialog/CnDataTab.vue +217 -217
- package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +121 -121
- package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +422 -422
- package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +247 -247
- package/src/components/CnCard/CnCard.vue +415 -415
- package/src/components/CnCardGrid/CnCardGrid.vue +156 -156
- package/src/components/CnCellRenderer/CnCellRenderer.vue +132 -132
- package/src/components/CnChartWidget/CnChartWidget.vue +346 -346
- package/src/components/CnConfigurationCard/CnConfigurationCard.vue +77 -77
- package/src/components/CnContextMenu/CnContextMenu.vue +142 -142
- package/src/components/CnCopyDialog/CnCopyDialog.vue +266 -266
- package/src/components/CnDashboardGrid/CnDashboardGrid.vue +229 -229
- package/src/components/CnDashboardPage/CnDashboardPage.vue +397 -397
- package/src/components/CnDataTable/CnDataTable.vue +362 -362
- package/src/components/CnDeleteDialog/CnDeleteDialog.vue +177 -177
- package/src/components/CnDetailCard/CnDetailCard.vue +225 -225
- package/src/components/CnDetailGrid/CnDetailGrid.vue +256 -256
- package/src/components/CnDetailPage/CnDetailPage.vue +432 -432
- package/src/components/CnFacetSidebar/CnFacetSidebar.vue +234 -234
- package/src/components/CnFilterBar/CnFilterBar.vue +153 -153
- package/src/components/CnFormDialog/CnFormDialog.vue +1047 -1047
- package/src/components/CnIcon/CnIcon.vue +89 -89
- package/src/components/CnIndexPage/CnIndexPage.vue +981 -980
- package/src/components/CnIndexSidebar/CnIndexSidebar.vue +536 -536
- package/src/components/CnInfoWidget/CnInfoWidget.vue +219 -219
- package/src/components/CnItemCard/CnItemCard.vue +134 -134
- package/src/components/CnJsonViewer/CnJsonViewer.vue +312 -312
- package/src/components/CnKpiGrid/CnKpiGrid.vue +93 -93
- package/src/components/CnMassActionBar/CnMassActionBar.vue +161 -161
- package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +327 -327
- package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +245 -245
- package/src/components/CnMassExportDialog/CnMassExportDialog.vue +191 -191
- package/src/components/CnMassImportDialog/CnMassImportDialog.vue +494 -494
- package/src/components/CnNoteCard/CnNoteCard.vue +149 -149
- package/src/components/CnNotesCard/CnNotesCard.vue +416 -416
- package/src/components/CnObjectCard/CnObjectCard.vue +294 -294
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +854 -854
- package/src/components/CnObjectMetadataWidget/CnObjectMetadataWidget.vue +289 -289
- package/src/components/CnObjectSidebar/CnAuditTrailTab.vue +369 -369
- package/src/components/CnObjectSidebar/CnFilesTab.vue +287 -287
- package/src/components/CnObjectSidebar/CnNotesTab.vue +250 -250
- package/src/components/CnObjectSidebar/CnObjectSidebar.vue +255 -255
- package/src/components/CnObjectSidebar/CnTagsTab.vue +259 -259
- package/src/components/CnObjectSidebar/CnTasksTab.vue +483 -483
- package/src/components/CnPageHeader/CnPageHeader.vue +61 -61
- package/src/components/CnPagination/CnPagination.vue +253 -253
- package/src/components/CnProgressBar/CnProgressBar.vue +262 -262
- package/src/components/CnRegisterMapping/CnRegisterMapping.vue +793 -793
- package/src/components/CnRowActions/CnRowActions.vue +95 -95
- package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +226 -226
- package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +788 -788
- package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +305 -305
- package/src/components/CnSchemaFormDialog/CnSchemaPropertyActions.vue +1398 -1398
- package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +236 -236
- package/src/components/CnSettingsCard/CnSettingsCard.vue +92 -92
- package/src/components/CnSettingsSection/CnSettingsSection.vue +267 -267
- package/src/components/CnStatsBlock/CnStatsBlock.vue +437 -437
- package/src/components/CnStatsPanel/CnStatsPanel.vue +321 -321
- package/src/components/CnStatusBadge/CnStatusBadge.vue +90 -90
- package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +545 -545
- package/src/components/CnTableWidget/CnTableWidget.vue +333 -333
- package/src/components/CnTasksCard/CnTasksCard.vue +374 -374
- package/src/components/CnTileWidget/CnTileWidget.vue +159 -159
- package/src/components/CnTimelineStages/CnTimelineStages.vue +294 -294
- package/src/components/CnUserActionMenu/CnUserActionMenu.vue +436 -436
- package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +313 -313
- package/src/components/CnWidgetRenderer/CnWidgetRenderer.vue +180 -180
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +248 -248
|
@@ -1,415 +1,415 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div
|
|
3
|
-
class="cn-card"
|
|
4
|
-
:class="rootClasses"
|
|
5
|
-
:style="activeStyles"
|
|
6
|
-
@click="onClick">
|
|
7
|
-
<div class="cn-card__header">
|
|
8
|
-
<h2 class="cn-card__title">
|
|
9
|
-
<slot name="icon">
|
|
10
|
-
<component :is="icon" v-if="icon" :size="iconSize" />
|
|
11
|
-
</slot>
|
|
12
|
-
<span ref="titleText" v-tooltip.bottom="computedTooltip" class="cn-card__title-text">{{ title }}</span>
|
|
13
|
-
</h2>
|
|
14
|
-
<div v-if="$slots.actions || $scopedSlots.actions" class="cn-card__actions">
|
|
15
|
-
<slot name="actions" />
|
|
16
|
-
</div>
|
|
17
|
-
<slot name="labels">
|
|
18
|
-
<span v-if="labels.length > 0" class="cn-card__labels">
|
|
19
|
-
<CnStatusBadge
|
|
20
|
-
v-for="(label, i) in labels"
|
|
21
|
-
:key="i"
|
|
22
|
-
:label="label.text"
|
|
23
|
-
:variant="label.variant || 'default'"
|
|
24
|
-
:solid="true" />
|
|
25
|
-
</span>
|
|
26
|
-
</slot>
|
|
27
|
-
</div>
|
|
28
|
-
|
|
29
|
-
<div class="cn-card__body">
|
|
30
|
-
<slot name="description">
|
|
31
|
-
<p v-if="description"
|
|
32
|
-
class="cn-card__description"
|
|
33
|
-
:style="descriptionStyle">
|
|
34
|
-
{{ description }}
|
|
35
|
-
</p>
|
|
36
|
-
</slot>
|
|
37
|
-
|
|
38
|
-
<div v-if="$slots.default || $scopedSlots.default" class="cn-card__content">
|
|
39
|
-
<slot />
|
|
40
|
-
</div>
|
|
41
|
-
|
|
42
|
-
<slot name="stats">
|
|
43
|
-
<div v-if="stats.length > 0" class="cn-card__stats">
|
|
44
|
-
<div v-for="(stat, i) in stats" :key="i" class="cn-card__stat">
|
|
45
|
-
<span class="cn-card__stat-label">{{ stat.label }}:</span>
|
|
46
|
-
<span class="cn-card__stat-value">{{ stat.value }}</span>
|
|
47
|
-
</div>
|
|
48
|
-
</div>
|
|
49
|
-
</slot>
|
|
50
|
-
</div>
|
|
51
|
-
|
|
52
|
-
<!-- Footer -->
|
|
53
|
-
<slot name="footer">
|
|
54
|
-
<div v-if="hasFooterContent" class="cn-card__footer">
|
|
55
|
-
<a
|
|
56
|
-
v-for="(link, i) in footerLinks"
|
|
57
|
-
:key="'link-' + i"
|
|
58
|
-
:href="link.url"
|
|
59
|
-
target="_blank"
|
|
60
|
-
rel="noopener noreferrer"
|
|
61
|
-
class="cn-card__footer-link">
|
|
62
|
-
<slot :name="'footer-link-icon-' + i" />
|
|
63
|
-
{{ link.label || link.url }}
|
|
64
|
-
</a>
|
|
65
|
-
<CnStatusBadge
|
|
66
|
-
v-for="(tag, i) in normalizedTags"
|
|
67
|
-
:key="'tag-' + i"
|
|
68
|
-
:label="tag.text"
|
|
69
|
-
:variant="tag.variant || 'default'"
|
|
70
|
-
size="small" />
|
|
71
|
-
</div>
|
|
72
|
-
</slot>
|
|
73
|
-
</div>
|
|
74
|
-
</template>
|
|
75
|
-
|
|
76
|
-
<script>
|
|
77
|
-
import { CnStatusBadge } from '../CnStatusBadge/index.js'
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* CnCard — Generic prop-driven card component.
|
|
81
|
-
*
|
|
82
|
-
* A flexible card for displaying entities with a title, icon, description,
|
|
83
|
-
* labels/badges, stats, and an optional active highlight state. Unlike
|
|
84
|
-
* CnObjectCard (schema-driven), CnCard takes direct props and is ideal
|
|
85
|
-
* for known, fixed-structure entities.
|
|
86
|
-
*
|
|
87
|
-
* @example Basic usage
|
|
88
|
-
* <CnCard
|
|
89
|
-
* title="My Source"
|
|
90
|
-
* description="A PostgreSQL data source"
|
|
91
|
-
* :icon="DatabaseArrowRightOutline"
|
|
92
|
-
* :stats="[{ label: 'Type', value: 'PostgreSQL' }]">
|
|
93
|
-
* <template #actions>
|
|
94
|
-
* <NcActions><NcActionButton @click="edit">Edit</NcActionButton></NcActions>
|
|
95
|
-
* </template>
|
|
96
|
-
* </CnCard>
|
|
97
|
-
*
|
|
98
|
-
* @example With labels and active state
|
|
99
|
-
* <CnCard
|
|
100
|
-
* title="My Organisation"
|
|
101
|
-
* :icon="OfficeBuilding"
|
|
102
|
-
* :active="isActive"
|
|
103
|
-
* active-variant="success"
|
|
104
|
-
* :labels="[
|
|
105
|
-
* { text: 'Default', variant: 'warning' },
|
|
106
|
-
* { text: 'Active', variant: 'success' },
|
|
107
|
-
* ]"
|
|
108
|
-
* :stats="[
|
|
109
|
-
* { label: 'Members', value: 12 },
|
|
110
|
-
* { label: 'Owner', value: 'Admin' },
|
|
111
|
-
* ]" />
|
|
112
|
-
*/
|
|
113
|
-
export default {
|
|
114
|
-
name: 'CnCard',
|
|
115
|
-
|
|
116
|
-
components: {
|
|
117
|
-
CnStatusBadge,
|
|
118
|
-
},
|
|
119
|
-
|
|
120
|
-
props: {
|
|
121
|
-
/** Card title text */
|
|
122
|
-
title: {
|
|
123
|
-
type: String,
|
|
124
|
-
default: '',
|
|
125
|
-
},
|
|
126
|
-
/** Description text, displayed with line-clamp truncation */
|
|
127
|
-
description: {
|
|
128
|
-
type: String,
|
|
129
|
-
default: '',
|
|
130
|
-
},
|
|
131
|
-
/** Tooltip text for the title. If not set, falls back to description */
|
|
132
|
-
titleTooltip: {
|
|
133
|
-
type: String,
|
|
134
|
-
default: '',
|
|
135
|
-
},
|
|
136
|
-
/** Icon component (e.g., imported MDI icon). Rendered via <component :is> */
|
|
137
|
-
icon: {
|
|
138
|
-
type: [Object, Function],
|
|
139
|
-
default: null,
|
|
140
|
-
},
|
|
141
|
-
/** Icon size in pixels */
|
|
142
|
-
iconSize: {
|
|
143
|
-
type: Number,
|
|
144
|
-
default: 20,
|
|
145
|
-
},
|
|
146
|
-
/**
|
|
147
|
-
* Array of badge/label objects displayed inline with the title.
|
|
148
|
-
* Each entry: { text: string, variant?: string }
|
|
149
|
-
* Variant maps to CnStatusBadge variants: 'default'|'primary'|'success'|'warning'|'error'|'info'
|
|
150
|
-
* For labels with icons, use the #labels slot override and render CnStatusBadge
|
|
151
|
-
* manually with its #icon slot.
|
|
152
|
-
*/
|
|
153
|
-
labels: {
|
|
154
|
-
type: Array,
|
|
155
|
-
default: () => [],
|
|
156
|
-
},
|
|
157
|
-
/**
|
|
158
|
-
* Array of stat rows displayed as label:value pairs.
|
|
159
|
-
* Each entry: { label: string, value: string|number }
|
|
160
|
-
*/
|
|
161
|
-
stats: {
|
|
162
|
-
type: Array,
|
|
163
|
-
default: () => [],
|
|
164
|
-
},
|
|
165
|
-
/** Maximum lines for description truncation (CSS line-clamp) */
|
|
166
|
-
descriptionLines: {
|
|
167
|
-
type: Number,
|
|
168
|
-
default: 3,
|
|
169
|
-
},
|
|
170
|
-
/** Whether the card is in an active/highlighted state */
|
|
171
|
-
active: {
|
|
172
|
-
type: Boolean,
|
|
173
|
-
default: false,
|
|
174
|
-
},
|
|
175
|
-
/**
|
|
176
|
-
* Color variant for the active state border and background.
|
|
177
|
-
* Maps to Nextcloud CSS variables.
|
|
178
|
-
*/
|
|
179
|
-
activeVariant: {
|
|
180
|
-
type: String,
|
|
181
|
-
default: 'success',
|
|
182
|
-
validator: (v) => ['success', 'primary', 'warning', 'error', 'info'].includes(v),
|
|
183
|
-
},
|
|
184
|
-
/** Whether the card is clickable (adds hover effect and cursor pointer) */
|
|
185
|
-
clickable: {
|
|
186
|
-
type: Boolean,
|
|
187
|
-
default: false,
|
|
188
|
-
},
|
|
189
|
-
/**
|
|
190
|
-
* Array of footer link objects. Each entry: { url: string, label?: string }
|
|
191
|
-
* Links are rendered as clickable anchors. Use the #footer-link-icon-{index} slot
|
|
192
|
-
* to add an icon before a specific link.
|
|
193
|
-
*/
|
|
194
|
-
footerLinks: {
|
|
195
|
-
type: Array,
|
|
196
|
-
default: () => [],
|
|
197
|
-
},
|
|
198
|
-
/**
|
|
199
|
-
* Array of tag items for the footer. Accepts either strings or objects.
|
|
200
|
-
* String entries are converted to { text: string, variant: 'default' }.
|
|
201
|
-
* Object entries: { text: string, variant?: string }
|
|
202
|
-
*/
|
|
203
|
-
tags: {
|
|
204
|
-
type: Array,
|
|
205
|
-
default: () => [],
|
|
206
|
-
},
|
|
207
|
-
},
|
|
208
|
-
|
|
209
|
-
data() {
|
|
210
|
-
return {
|
|
211
|
-
isTitleEllipsized: false,
|
|
212
|
-
}
|
|
213
|
-
},
|
|
214
|
-
|
|
215
|
-
computed: {
|
|
216
|
-
computedTooltip() {
|
|
217
|
-
if (this.titleTooltip) return this.titleTooltip
|
|
218
|
-
return this.isTitleEllipsized ? this.title : ''
|
|
219
|
-
},
|
|
220
|
-
|
|
221
|
-
rootClasses() {
|
|
222
|
-
return {
|
|
223
|
-
'cn-card--active': this.active,
|
|
224
|
-
'cn-card--clickable': this.clickable,
|
|
225
|
-
}
|
|
226
|
-
},
|
|
227
|
-
|
|
228
|
-
descriptionStyle() {
|
|
229
|
-
return {
|
|
230
|
-
'-webkit-line-clamp': this.descriptionLines,
|
|
231
|
-
'line-clamp': this.descriptionLines,
|
|
232
|
-
}
|
|
233
|
-
},
|
|
234
|
-
|
|
235
|
-
normalizedTags() {
|
|
236
|
-
return this.tags.map(tag =>
|
|
237
|
-
typeof tag === 'string' ? { text: tag, variant: 'default' } : tag,
|
|
238
|
-
)
|
|
239
|
-
},
|
|
240
|
-
|
|
241
|
-
hasFooterContent() {
|
|
242
|
-
return this.footerLinks.length > 0 || this.tags.length > 0
|
|
243
|
-
},
|
|
244
|
-
|
|
245
|
-
activeStyles() {
|
|
246
|
-
if (!this.active) return {}
|
|
247
|
-
const variantMap = {
|
|
248
|
-
success: 'var(--color-success)',
|
|
249
|
-
primary: 'var(--color-primary-element)',
|
|
250
|
-
warning: 'var(--color-warning)',
|
|
251
|
-
error: 'var(--color-error)',
|
|
252
|
-
info: 'var(--color-info)',
|
|
253
|
-
}
|
|
254
|
-
return {
|
|
255
|
-
'--cn-card-active-border': variantMap[this.activeVariant] || variantMap.success,
|
|
256
|
-
}
|
|
257
|
-
},
|
|
258
|
-
},
|
|
259
|
-
|
|
260
|
-
mounted() {
|
|
261
|
-
this.checkTitleEllipsis()
|
|
262
|
-
this._resizeObserver = new ResizeObserver(() => this.checkTitleEllipsis())
|
|
263
|
-
this._resizeObserver.observe(this.$el)
|
|
264
|
-
},
|
|
265
|
-
|
|
266
|
-
beforeDestroy() {
|
|
267
|
-
if (this._resizeObserver) {
|
|
268
|
-
this._resizeObserver.disconnect()
|
|
269
|
-
}
|
|
270
|
-
},
|
|
271
|
-
|
|
272
|
-
methods: {
|
|
273
|
-
onClick(event) {
|
|
274
|
-
if (this.clickable) {
|
|
275
|
-
this.$emit('click', event)
|
|
276
|
-
}
|
|
277
|
-
},
|
|
278
|
-
|
|
279
|
-
checkTitleEllipsis() {
|
|
280
|
-
const el = this.$refs.titleText
|
|
281
|
-
this.isTitleEllipsized = el ? el.scrollWidth > el.clientWidth : false
|
|
282
|
-
},
|
|
283
|
-
},
|
|
284
|
-
}
|
|
285
|
-
</script>
|
|
286
|
-
|
|
287
|
-
<style scoped lang="scss">
|
|
288
|
-
.cn-card {
|
|
289
|
-
padding: 16px;
|
|
290
|
-
border: 1px solid var(--color-border);
|
|
291
|
-
border-radius: var(--border-radius-large);
|
|
292
|
-
background: var(--color-main-background);
|
|
293
|
-
height: 100%;
|
|
294
|
-
display: flex;
|
|
295
|
-
flex-direction: column;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
.cn-card--active {
|
|
299
|
-
border: 2px solid var(--cn-card-active-border);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
.cn-card--clickable {
|
|
303
|
-
cursor: pointer;
|
|
304
|
-
transition: box-shadow 0.2s ease, border-color 0.2s ease;
|
|
305
|
-
|
|
306
|
-
&:hover {
|
|
307
|
-
border-color: var(--color-primary-element);
|
|
308
|
-
box-shadow: 0 2px 8px var(--color-box-shadow);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
.cn-card__header {
|
|
313
|
-
display: grid;
|
|
314
|
-
grid-template-columns: 1fr auto;
|
|
315
|
-
grid-template-rows: auto auto;
|
|
316
|
-
align-items: center;
|
|
317
|
-
border-bottom: 1px solid var(--color-border);
|
|
318
|
-
padding-block-end: 1rem;
|
|
319
|
-
margin-block-end: 0.5rem;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
.cn-card__title {
|
|
323
|
-
display: flex;
|
|
324
|
-
align-items: center;
|
|
325
|
-
gap: 6px;
|
|
326
|
-
font-size: 16px;
|
|
327
|
-
margin: 0;
|
|
328
|
-
min-width: 0;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
.cn-card__title-text {
|
|
332
|
-
overflow: hidden;
|
|
333
|
-
text-overflow: ellipsis;
|
|
334
|
-
white-space: nowrap;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
.cn-card__labels {
|
|
338
|
-
grid-column: 1 / -1;
|
|
339
|
-
display: flex;
|
|
340
|
-
gap: 4px;
|
|
341
|
-
flex-wrap: wrap;
|
|
342
|
-
margin-top: 6px;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
.cn-card__actions {
|
|
346
|
-
flex-shrink: 0;
|
|
347
|
-
margin-inline-start: 0.25rem;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
.cn-card__body {
|
|
351
|
-
flex-grow: 1;
|
|
352
|
-
display: flex;
|
|
353
|
-
flex-direction: column;
|
|
354
|
-
justify-content: space-between;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
.cn-card__description {
|
|
358
|
-
color: var(--color-text-lighter);
|
|
359
|
-
margin-bottom: 12px;
|
|
360
|
-
word-wrap: break-word;
|
|
361
|
-
overflow-wrap: break-word;
|
|
362
|
-
display: -webkit-box;
|
|
363
|
-
-webkit-box-orient: vertical;
|
|
364
|
-
overflow: hidden;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
.cn-card__content {
|
|
368
|
-
margin-bottom: 12px;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
.cn-card__stats {
|
|
372
|
-
display: flex;
|
|
373
|
-
flex-direction: column;
|
|
374
|
-
gap: 4px;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
.cn-card__stat {
|
|
378
|
-
display: flex;
|
|
379
|
-
justify-content: space-between;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
.cn-card__stat-label {
|
|
383
|
-
color: var(--color-text-lighter);
|
|
384
|
-
font-size: 12px;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
.cn-card__stat-value {
|
|
388
|
-
font-weight: 600;
|
|
389
|
-
font-size: 12px;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
.cn-card__footer {
|
|
393
|
-
display: flex;
|
|
394
|
-
flex-wrap: wrap;
|
|
395
|
-
gap: 8px;
|
|
396
|
-
align-items: center;
|
|
397
|
-
padding-top: 8px;
|
|
398
|
-
margin-top: 8px;
|
|
399
|
-
border-top: 1px solid var(--color-border);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
.cn-card__footer-link {
|
|
403
|
-
display: inline-flex;
|
|
404
|
-
align-items: center;
|
|
405
|
-
gap: 4px;
|
|
406
|
-
font-size: 0.85em;
|
|
407
|
-
color: var(--color-primary-element);
|
|
408
|
-
text-decoration: none;
|
|
409
|
-
transition: color 0.2s;
|
|
410
|
-
|
|
411
|
-
&:hover {
|
|
412
|
-
text-decoration: underline;
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
</style>
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="cn-card"
|
|
4
|
+
:class="rootClasses"
|
|
5
|
+
:style="activeStyles"
|
|
6
|
+
@click="onClick">
|
|
7
|
+
<div class="cn-card__header">
|
|
8
|
+
<h2 class="cn-card__title">
|
|
9
|
+
<slot name="icon">
|
|
10
|
+
<component :is="icon" v-if="icon" :size="iconSize" />
|
|
11
|
+
</slot>
|
|
12
|
+
<span ref="titleText" v-tooltip.bottom="computedTooltip" class="cn-card__title-text">{{ title }}</span>
|
|
13
|
+
</h2>
|
|
14
|
+
<div v-if="$slots.actions || $scopedSlots.actions" class="cn-card__actions">
|
|
15
|
+
<slot name="actions" />
|
|
16
|
+
</div>
|
|
17
|
+
<slot name="labels">
|
|
18
|
+
<span v-if="labels.length > 0" class="cn-card__labels">
|
|
19
|
+
<CnStatusBadge
|
|
20
|
+
v-for="(label, i) in labels"
|
|
21
|
+
:key="i"
|
|
22
|
+
:label="label.text"
|
|
23
|
+
:variant="label.variant || 'default'"
|
|
24
|
+
:solid="true" />
|
|
25
|
+
</span>
|
|
26
|
+
</slot>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div class="cn-card__body">
|
|
30
|
+
<slot name="description">
|
|
31
|
+
<p v-if="description"
|
|
32
|
+
class="cn-card__description"
|
|
33
|
+
:style="descriptionStyle">
|
|
34
|
+
{{ description }}
|
|
35
|
+
</p>
|
|
36
|
+
</slot>
|
|
37
|
+
|
|
38
|
+
<div v-if="$slots.default || $scopedSlots.default" class="cn-card__content">
|
|
39
|
+
<slot />
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<slot name="stats">
|
|
43
|
+
<div v-if="stats.length > 0" class="cn-card__stats">
|
|
44
|
+
<div v-for="(stat, i) in stats" :key="i" class="cn-card__stat">
|
|
45
|
+
<span class="cn-card__stat-label">{{ stat.label }}:</span>
|
|
46
|
+
<span class="cn-card__stat-value">{{ stat.value }}</span>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</slot>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<!-- Footer -->
|
|
53
|
+
<slot name="footer">
|
|
54
|
+
<div v-if="hasFooterContent" class="cn-card__footer">
|
|
55
|
+
<a
|
|
56
|
+
v-for="(link, i) in footerLinks"
|
|
57
|
+
:key="'link-' + i"
|
|
58
|
+
:href="link.url"
|
|
59
|
+
target="_blank"
|
|
60
|
+
rel="noopener noreferrer"
|
|
61
|
+
class="cn-card__footer-link">
|
|
62
|
+
<slot :name="'footer-link-icon-' + i" />
|
|
63
|
+
{{ link.label || link.url }}
|
|
64
|
+
</a>
|
|
65
|
+
<CnStatusBadge
|
|
66
|
+
v-for="(tag, i) in normalizedTags"
|
|
67
|
+
:key="'tag-' + i"
|
|
68
|
+
:label="tag.text"
|
|
69
|
+
:variant="tag.variant || 'default'"
|
|
70
|
+
size="small" />
|
|
71
|
+
</div>
|
|
72
|
+
</slot>
|
|
73
|
+
</div>
|
|
74
|
+
</template>
|
|
75
|
+
|
|
76
|
+
<script>
|
|
77
|
+
import { CnStatusBadge } from '../CnStatusBadge/index.js'
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* CnCard — Generic prop-driven card component.
|
|
81
|
+
*
|
|
82
|
+
* A flexible card for displaying entities with a title, icon, description,
|
|
83
|
+
* labels/badges, stats, and an optional active highlight state. Unlike
|
|
84
|
+
* CnObjectCard (schema-driven), CnCard takes direct props and is ideal
|
|
85
|
+
* for known, fixed-structure entities.
|
|
86
|
+
*
|
|
87
|
+
* @example Basic usage
|
|
88
|
+
* <CnCard
|
|
89
|
+
* title="My Source"
|
|
90
|
+
* description="A PostgreSQL data source"
|
|
91
|
+
* :icon="DatabaseArrowRightOutline"
|
|
92
|
+
* :stats="[{ label: 'Type', value: 'PostgreSQL' }]">
|
|
93
|
+
* <template #actions>
|
|
94
|
+
* <NcActions><NcActionButton @click="edit">Edit</NcActionButton></NcActions>
|
|
95
|
+
* </template>
|
|
96
|
+
* </CnCard>
|
|
97
|
+
*
|
|
98
|
+
* @example With labels and active state
|
|
99
|
+
* <CnCard
|
|
100
|
+
* title="My Organisation"
|
|
101
|
+
* :icon="OfficeBuilding"
|
|
102
|
+
* :active="isActive"
|
|
103
|
+
* active-variant="success"
|
|
104
|
+
* :labels="[
|
|
105
|
+
* { text: 'Default', variant: 'warning' },
|
|
106
|
+
* { text: 'Active', variant: 'success' },
|
|
107
|
+
* ]"
|
|
108
|
+
* :stats="[
|
|
109
|
+
* { label: 'Members', value: 12 },
|
|
110
|
+
* { label: 'Owner', value: 'Admin' },
|
|
111
|
+
* ]" />
|
|
112
|
+
*/
|
|
113
|
+
export default {
|
|
114
|
+
name: 'CnCard',
|
|
115
|
+
|
|
116
|
+
components: {
|
|
117
|
+
CnStatusBadge,
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
props: {
|
|
121
|
+
/** Card title text */
|
|
122
|
+
title: {
|
|
123
|
+
type: String,
|
|
124
|
+
default: '',
|
|
125
|
+
},
|
|
126
|
+
/** Description text, displayed with line-clamp truncation */
|
|
127
|
+
description: {
|
|
128
|
+
type: String,
|
|
129
|
+
default: '',
|
|
130
|
+
},
|
|
131
|
+
/** Tooltip text for the title. If not set, falls back to description */
|
|
132
|
+
titleTooltip: {
|
|
133
|
+
type: String,
|
|
134
|
+
default: '',
|
|
135
|
+
},
|
|
136
|
+
/** Icon component (e.g., imported MDI icon). Rendered via <component :is> */
|
|
137
|
+
icon: {
|
|
138
|
+
type: [Object, Function],
|
|
139
|
+
default: null,
|
|
140
|
+
},
|
|
141
|
+
/** Icon size in pixels */
|
|
142
|
+
iconSize: {
|
|
143
|
+
type: Number,
|
|
144
|
+
default: 20,
|
|
145
|
+
},
|
|
146
|
+
/**
|
|
147
|
+
* Array of badge/label objects displayed inline with the title.
|
|
148
|
+
* Each entry: { text: string, variant?: string }
|
|
149
|
+
* Variant maps to CnStatusBadge variants: 'default'|'primary'|'success'|'warning'|'error'|'info'
|
|
150
|
+
* For labels with icons, use the #labels slot override and render CnStatusBadge
|
|
151
|
+
* manually with its #icon slot.
|
|
152
|
+
*/
|
|
153
|
+
labels: {
|
|
154
|
+
type: Array,
|
|
155
|
+
default: () => [],
|
|
156
|
+
},
|
|
157
|
+
/**
|
|
158
|
+
* Array of stat rows displayed as label:value pairs.
|
|
159
|
+
* Each entry: { label: string, value: string|number }
|
|
160
|
+
*/
|
|
161
|
+
stats: {
|
|
162
|
+
type: Array,
|
|
163
|
+
default: () => [],
|
|
164
|
+
},
|
|
165
|
+
/** Maximum lines for description truncation (CSS line-clamp) */
|
|
166
|
+
descriptionLines: {
|
|
167
|
+
type: Number,
|
|
168
|
+
default: 3,
|
|
169
|
+
},
|
|
170
|
+
/** Whether the card is in an active/highlighted state */
|
|
171
|
+
active: {
|
|
172
|
+
type: Boolean,
|
|
173
|
+
default: false,
|
|
174
|
+
},
|
|
175
|
+
/**
|
|
176
|
+
* Color variant for the active state border and background.
|
|
177
|
+
* Maps to Nextcloud CSS variables.
|
|
178
|
+
*/
|
|
179
|
+
activeVariant: {
|
|
180
|
+
type: String,
|
|
181
|
+
default: 'success',
|
|
182
|
+
validator: (v) => ['success', 'primary', 'warning', 'error', 'info'].includes(v),
|
|
183
|
+
},
|
|
184
|
+
/** Whether the card is clickable (adds hover effect and cursor pointer) */
|
|
185
|
+
clickable: {
|
|
186
|
+
type: Boolean,
|
|
187
|
+
default: false,
|
|
188
|
+
},
|
|
189
|
+
/**
|
|
190
|
+
* Array of footer link objects. Each entry: { url: string, label?: string }
|
|
191
|
+
* Links are rendered as clickable anchors. Use the #footer-link-icon-{index} slot
|
|
192
|
+
* to add an icon before a specific link.
|
|
193
|
+
*/
|
|
194
|
+
footerLinks: {
|
|
195
|
+
type: Array,
|
|
196
|
+
default: () => [],
|
|
197
|
+
},
|
|
198
|
+
/**
|
|
199
|
+
* Array of tag items for the footer. Accepts either strings or objects.
|
|
200
|
+
* String entries are converted to { text: string, variant: 'default' }.
|
|
201
|
+
* Object entries: { text: string, variant?: string }
|
|
202
|
+
*/
|
|
203
|
+
tags: {
|
|
204
|
+
type: Array,
|
|
205
|
+
default: () => [],
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
data() {
|
|
210
|
+
return {
|
|
211
|
+
isTitleEllipsized: false,
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
computed: {
|
|
216
|
+
computedTooltip() {
|
|
217
|
+
if (this.titleTooltip) return this.titleTooltip
|
|
218
|
+
return this.isTitleEllipsized ? this.title : ''
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
rootClasses() {
|
|
222
|
+
return {
|
|
223
|
+
'cn-card--active': this.active,
|
|
224
|
+
'cn-card--clickable': this.clickable,
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
descriptionStyle() {
|
|
229
|
+
return {
|
|
230
|
+
'-webkit-line-clamp': this.descriptionLines,
|
|
231
|
+
'line-clamp': this.descriptionLines,
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
normalizedTags() {
|
|
236
|
+
return this.tags.map(tag =>
|
|
237
|
+
typeof tag === 'string' ? { text: tag, variant: 'default' } : tag,
|
|
238
|
+
)
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
hasFooterContent() {
|
|
242
|
+
return this.footerLinks.length > 0 || this.tags.length > 0
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
activeStyles() {
|
|
246
|
+
if (!this.active) return {}
|
|
247
|
+
const variantMap = {
|
|
248
|
+
success: 'var(--color-success)',
|
|
249
|
+
primary: 'var(--color-primary-element)',
|
|
250
|
+
warning: 'var(--color-warning)',
|
|
251
|
+
error: 'var(--color-error)',
|
|
252
|
+
info: 'var(--color-info)',
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
'--cn-card-active-border': variantMap[this.activeVariant] || variantMap.success,
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
mounted() {
|
|
261
|
+
this.checkTitleEllipsis()
|
|
262
|
+
this._resizeObserver = new ResizeObserver(() => this.checkTitleEllipsis())
|
|
263
|
+
this._resizeObserver.observe(this.$el)
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
beforeDestroy() {
|
|
267
|
+
if (this._resizeObserver) {
|
|
268
|
+
this._resizeObserver.disconnect()
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
methods: {
|
|
273
|
+
onClick(event) {
|
|
274
|
+
if (this.clickable) {
|
|
275
|
+
this.$emit('click', event)
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
checkTitleEllipsis() {
|
|
280
|
+
const el = this.$refs.titleText
|
|
281
|
+
this.isTitleEllipsized = el ? el.scrollWidth > el.clientWidth : false
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
}
|
|
285
|
+
</script>
|
|
286
|
+
|
|
287
|
+
<style scoped lang="scss">
|
|
288
|
+
.cn-card {
|
|
289
|
+
padding: 16px;
|
|
290
|
+
border: 1px solid var(--color-border);
|
|
291
|
+
border-radius: var(--border-radius-large);
|
|
292
|
+
background: var(--color-main-background);
|
|
293
|
+
height: 100%;
|
|
294
|
+
display: flex;
|
|
295
|
+
flex-direction: column;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.cn-card--active {
|
|
299
|
+
border: 2px solid var(--cn-card-active-border);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.cn-card--clickable {
|
|
303
|
+
cursor: pointer;
|
|
304
|
+
transition: box-shadow 0.2s ease, border-color 0.2s ease;
|
|
305
|
+
|
|
306
|
+
&:hover {
|
|
307
|
+
border-color: var(--color-primary-element);
|
|
308
|
+
box-shadow: 0 2px 8px var(--color-box-shadow);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.cn-card__header {
|
|
313
|
+
display: grid;
|
|
314
|
+
grid-template-columns: 1fr auto;
|
|
315
|
+
grid-template-rows: auto auto;
|
|
316
|
+
align-items: center;
|
|
317
|
+
border-bottom: 1px solid var(--color-border);
|
|
318
|
+
padding-block-end: 1rem;
|
|
319
|
+
margin-block-end: 0.5rem;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.cn-card__title {
|
|
323
|
+
display: flex;
|
|
324
|
+
align-items: center;
|
|
325
|
+
gap: 6px;
|
|
326
|
+
font-size: 16px;
|
|
327
|
+
margin: 0;
|
|
328
|
+
min-width: 0;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.cn-card__title-text {
|
|
332
|
+
overflow: hidden;
|
|
333
|
+
text-overflow: ellipsis;
|
|
334
|
+
white-space: nowrap;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.cn-card__labels {
|
|
338
|
+
grid-column: 1 / -1;
|
|
339
|
+
display: flex;
|
|
340
|
+
gap: 4px;
|
|
341
|
+
flex-wrap: wrap;
|
|
342
|
+
margin-top: 6px;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.cn-card__actions {
|
|
346
|
+
flex-shrink: 0;
|
|
347
|
+
margin-inline-start: 0.25rem;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.cn-card__body {
|
|
351
|
+
flex-grow: 1;
|
|
352
|
+
display: flex;
|
|
353
|
+
flex-direction: column;
|
|
354
|
+
justify-content: space-between;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.cn-card__description {
|
|
358
|
+
color: var(--color-text-lighter);
|
|
359
|
+
margin-bottom: 12px;
|
|
360
|
+
word-wrap: break-word;
|
|
361
|
+
overflow-wrap: break-word;
|
|
362
|
+
display: -webkit-box;
|
|
363
|
+
-webkit-box-orient: vertical;
|
|
364
|
+
overflow: hidden;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
.cn-card__content {
|
|
368
|
+
margin-bottom: 12px;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.cn-card__stats {
|
|
372
|
+
display: flex;
|
|
373
|
+
flex-direction: column;
|
|
374
|
+
gap: 4px;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.cn-card__stat {
|
|
378
|
+
display: flex;
|
|
379
|
+
justify-content: space-between;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.cn-card__stat-label {
|
|
383
|
+
color: var(--color-text-lighter);
|
|
384
|
+
font-size: 12px;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.cn-card__stat-value {
|
|
388
|
+
font-weight: 600;
|
|
389
|
+
font-size: 12px;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.cn-card__footer {
|
|
393
|
+
display: flex;
|
|
394
|
+
flex-wrap: wrap;
|
|
395
|
+
gap: 8px;
|
|
396
|
+
align-items: center;
|
|
397
|
+
padding-top: 8px;
|
|
398
|
+
margin-top: 8px;
|
|
399
|
+
border-top: 1px solid var(--color-border);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.cn-card__footer-link {
|
|
403
|
+
display: inline-flex;
|
|
404
|
+
align-items: center;
|
|
405
|
+
gap: 4px;
|
|
406
|
+
font-size: 0.85em;
|
|
407
|
+
color: var(--color-primary-element);
|
|
408
|
+
text-decoration: none;
|
|
409
|
+
transition: color 0.2s;
|
|
410
|
+
|
|
411
|
+
&:hover {
|
|
412
|
+
text-decoration: underline;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
</style>
|