@conduction/nextcloud-vue 0.1.0-beta.6 → 0.1.0-beta.7

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/dist/nextcloud-vue.cjs.js +13606 -1918
  2. package/dist/nextcloud-vue.cjs.js.map +1 -1
  3. package/dist/nextcloud-vue.css +1238 -270
  4. package/dist/nextcloud-vue.esm.js +13548 -1880
  5. package/dist/nextcloud-vue.esm.js.map +1 -1
  6. package/package.json +9 -4
  7. package/src/components/CnActionsBar/CnActionsBar.vue +6 -1
  8. package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +1 -11
  9. package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +5 -1
  10. package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +1 -1
  11. package/src/components/CnCard/CnCard.vue +415 -0
  12. package/src/components/CnCard/index.js +1 -0
  13. package/src/components/CnCardGrid/CnCardGrid.vue +20 -20
  14. package/src/components/CnChartWidget/CnChartWidget.vue +3 -1
  15. package/src/components/CnCopyDialog/CnCopyDialog.vue +7 -1
  16. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +4 -0
  17. package/src/components/CnDashboardPage/CnDashboardPage.vue +2 -0
  18. package/src/components/CnDataTable/CnDataTable.vue +6 -2
  19. package/src/components/CnDeleteDialog/CnDeleteDialog.vue +7 -1
  20. package/src/components/CnDetailCard/CnDetailCard.vue +12 -1
  21. package/src/components/CnDetailGrid/CnDetailGrid.vue +254 -0
  22. package/src/components/CnDetailGrid/index.js +1 -0
  23. package/src/components/CnDetailPage/CnDetailPage.vue +157 -11
  24. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +3 -1
  25. package/src/components/CnFormDialog/CnFormDialog.vue +934 -920
  26. package/src/components/CnIcon/CnIcon.vue +1 -1
  27. package/src/components/CnIndexPage/CnIndexPage.vue +51 -9
  28. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +37 -9
  29. package/src/components/CnInfoWidget/CnInfoWidget.vue +219 -0
  30. package/src/components/CnInfoWidget/index.js +1 -0
  31. package/src/components/CnJsonViewer/CnJsonViewer.vue +283 -0
  32. package/src/components/CnJsonViewer/index.js +1 -0
  33. package/src/components/CnKpiGrid/CnKpiGrid.vue +5 -1
  34. package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +7 -1
  35. package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +7 -1
  36. package/src/components/CnMassExportDialog/CnMassExportDialog.vue +1 -1
  37. package/src/components/CnMassImportDialog/CnMassImportDialog.vue +1 -1
  38. package/src/components/CnObjectCard/CnObjectCard.vue +1 -1
  39. package/src/components/CnObjectSidebar/CnAuditTrailTab.vue +368 -0
  40. package/src/components/CnObjectSidebar/CnFilesTab.vue +286 -0
  41. package/src/components/CnObjectSidebar/CnNotesTab.vue +249 -0
  42. package/src/components/CnObjectSidebar/CnObjectSidebar.vue +45 -668
  43. package/src/components/CnObjectSidebar/CnTagsTab.vue +258 -0
  44. package/src/components/CnObjectSidebar/CnTasksTab.vue +482 -0
  45. package/src/components/CnObjectSidebar/index.js +5 -0
  46. package/src/components/CnProgressBar/CnProgressBar.vue +262 -0
  47. package/src/components/CnProgressBar/index.js +1 -0
  48. package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +1 -1
  49. package/src/components/CnStatsBlock/CnStatsBlock.vue +27 -11
  50. package/src/components/CnStatsPanel/CnStatsPanel.vue +320 -0
  51. package/src/components/CnStatsPanel/index.js +1 -0
  52. package/src/components/CnStatusBadge/CnStatusBadge.vue +15 -2
  53. package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +5 -1
  54. package/src/components/CnTableWidget/CnTableWidget.vue +332 -0
  55. package/src/components/CnTableWidget/index.js +1 -0
  56. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +36 -1
  57. package/src/components/index.js +11 -0
  58. package/src/composables/useDashboardView.js +58 -12
  59. package/src/composables/useDetailView.js +3 -2
  60. package/src/composables/useListView.js +7 -6
  61. package/src/composables/useSubResource.js +3 -3
  62. package/src/css/badge.css +32 -0
  63. package/src/css/card.css +1 -0
  64. package/src/css/detail-page.css +74 -7
  65. package/src/index.js +16 -0
  66. package/src/mixins/gridLayout.js +118 -0
  67. package/src/store/createCrudStore.js +360 -0
  68. package/src/store/createSubResourcePlugin.js +5 -15
  69. package/src/store/index.js +1 -0
  70. package/src/store/plugins/auditTrails.js +346 -6
  71. package/src/store/plugins/lifecycle.js +4 -4
  72. package/src/store/plugins/registerMapping.js +18 -8
  73. package/src/store/plugins/relations.js +1 -1
  74. package/src/store/plugins/search.js +21 -8
  75. package/src/store/useObjectStore.js +30 -36
  76. package/src/utils/getTheme.js +9 -0
  77. package/src/utils/headers.js +13 -3
  78. package/src/utils/index.js +1 -0
  79. package/src/utils/schema.js +3 -3
  80. package/src/utils/widgetVisibility.js +162 -0
  81. package/src/components/CnObjectCard/eslint-setup.md +0 -235
  82. package/src/components/CnObjectCard/package.json-or.json +0 -132
@@ -0,0 +1,262 @@
1
+ <template>
2
+ <div class="cn-progress-bar">
3
+ <div class="cn-progress-bar__items">
4
+ <div
5
+ v-for="(item, index) in items"
6
+ :key="item.key || index"
7
+ class="cn-progress-bar__item">
8
+ <div class="cn-progress-bar__label">
9
+ <span
10
+ class="cn-progress-bar__name"
11
+ :class="{ 'cn-progress-bar__name--tooltip': item.tooltip }"
12
+ :title="item.tooltip || undefined">
13
+ <slot :name="'label-' + (item.key || index)" :item="item">
14
+ {{ item.label }}
15
+ </slot>
16
+ </span>
17
+ <span class="cn-progress-bar__value">
18
+ <slot :name="'value-' + (item.key || index)" :item="item" :percentage="getPercentage(item)">
19
+ {{ formatValue(item) }}
20
+ </slot>
21
+ </span>
22
+ </div>
23
+ <div
24
+ class="cn-progress-bar__track"
25
+ :class="{ 'cn-progress-bar__track--rounded': rounded }"
26
+ :style="{ height: barHeight + 'px' }">
27
+ <div
28
+ class="cn-progress-bar__fill"
29
+ :class="[
30
+ 'cn-progress-bar__fill--' + resolveVariant(item),
31
+ { 'cn-progress-bar__fill--rounded': rounded },
32
+ ]"
33
+ :style="{ width: getPercentage(item) + '%' }" />
34
+ </div>
35
+ </div>
36
+ </div>
37
+ </div>
38
+ </template>
39
+
40
+ <script>
41
+ /**
42
+ * CnProgressBar — Labeled horizontal progress bars with variant colors.
43
+ *
44
+ * Renders a list of items as labeled progress bars. Each item has a label, a count,
45
+ * and a percentage-width fill bar. Percentages are calculated automatically from the
46
+ * items' count values relative to the total (sum of all counts), or can be provided
47
+ * explicitly via `item.percentage`.
48
+ *
49
+ * Suitable for distribution visualizations (e.g., query complexity, action breakdown,
50
+ * status distribution).
51
+ *
52
+ * @example Basic usage
53
+ * <CnProgressBar :items="[
54
+ * { key: 'simple', label: 'Simple', count: 50, variant: 'success' },
55
+ * { key: 'medium', label: 'Medium', count: 30, variant: 'warning' },
56
+ * { key: 'complex', label: 'Complex', count: 20, variant: 'error' },
57
+ * ]" />
58
+ *
59
+ * @example With explicit percentages
60
+ * <CnProgressBar :items="[
61
+ * { key: 'cpu', label: 'CPU', percentage: 72, variant: 'warning' },
62
+ * { key: 'mem', label: 'Memory', percentage: 45, variant: 'success' },
63
+ * ]" show-percentage />
64
+ *
65
+ * @example With tooltips
66
+ * <CnProgressBar :items="[
67
+ * { key: 'simple', label: 'Simple', count: 50, variant: 'success', tooltip: 'Basic text searches' },
68
+ * ]" />
69
+ */
70
+ export default {
71
+ name: 'CnProgressBar',
72
+
73
+ props: {
74
+ /**
75
+ * Array of progress bar items.
76
+ *
77
+ * Each item: `{ key?, label, count?, percentage?, variant?, tooltip? }`
78
+ *
79
+ * - `key` — Unique identifier (optional, falls back to index)
80
+ * - `label` — Display label
81
+ * - `count` — Numeric value (used to calculate percentage from total if `percentage` not given)
82
+ * - `total` — Per-item total for percentage calculation (e.g. count=50, total=500 → 10%). Defaults to sum of all item counts.
83
+ * - `percentage` — Explicit percentage (0-100), overrides both count and total
84
+ * - `variant` — Color variant: a string ('default'|'primary'|'success'|'warning'|'error'|'info') or a function `({ item, count, total, percentage }) => string` for dynamic resolution
85
+ * - `tooltip` — Hover tooltip text for the label
86
+ */
87
+ items: {
88
+ type: Array,
89
+ default: () => [],
90
+ },
91
+
92
+ /**
93
+ * Default color variant for all bars. Individual items can override via `item.variant`.
94
+ * One of: 'default', 'primary', 'success', 'warning', 'error', 'info'
95
+ */
96
+ variant: {
97
+ type: String,
98
+ default: 'primary',
99
+ validator: (v) => ['default', 'primary', 'success', 'warning', 'error', 'info'].includes(v),
100
+ },
101
+
102
+ /** Height of the progress bar track in pixels */
103
+ barHeight: {
104
+ type: Number,
105
+ default: 8,
106
+ },
107
+
108
+ /** Whether to use rounded corners on the track and fill */
109
+ rounded: {
110
+ type: Boolean,
111
+ default: true,
112
+ },
113
+
114
+ /** Whether to show the percentage value instead of the count */
115
+ showPercentage: {
116
+ type: Boolean,
117
+ default: false,
118
+ },
119
+ },
120
+
121
+ computed: {
122
+ /**
123
+ * Total count across all items (for percentage calculation).
124
+ * @return {number}
125
+ */
126
+ totalCount() {
127
+ return this.items.reduce((sum, item) => sum + (item.count || 0), 0)
128
+ },
129
+ },
130
+
131
+ methods: {
132
+ /**
133
+ * Get the percentage for an item.
134
+ * Uses item.percentage if provided, otherwise calculates from count/total.
135
+ * @param {object} item - The progress bar item
136
+ * @return {number} Percentage (0-100)
137
+ */
138
+ /**
139
+ * Resolve the variant for an item. Supports string or function.
140
+ * @param {object} item - The progress bar item
141
+ * @return {string} Resolved variant name
142
+ */
143
+ resolveVariant(item) {
144
+ const itemVariant = item.variant
145
+ if (typeof itemVariant === 'function') {
146
+ return itemVariant({
147
+ item,
148
+ count: item.count ?? 0,
149
+ total: item.total ?? this.totalCount,
150
+ percentage: this.getPercentage(item),
151
+ }) || this.variant
152
+ }
153
+ return itemVariant || this.variant
154
+ },
155
+
156
+ getPercentage(item) {
157
+ if (item.percentage !== undefined && item.percentage !== null) {
158
+ return Math.min(100, Math.max(0, item.percentage))
159
+ }
160
+ const total = (item.total !== undefined && item.total !== null) ? item.total : this.totalCount
161
+ if (total === 0) return 0
162
+ return Math.round(((item.count || 0) / total) * 100)
163
+ },
164
+
165
+ /**
166
+ * Format the display value for an item.
167
+ * @param {object} item - The progress bar item
168
+ * @return {string} Formatted value
169
+ */
170
+ formatValue(item) {
171
+ if (this.showPercentage) {
172
+ return `${this.getPercentage(item)}%`
173
+ }
174
+ return String(item.count ?? 0)
175
+ },
176
+ },
177
+ }
178
+ </script>
179
+
180
+ <style scoped>
181
+ .cn-progress-bar__items {
182
+ display: flex;
183
+ flex-direction: column;
184
+ gap: 8px;
185
+ }
186
+
187
+ .cn-progress-bar__item {
188
+ display: flex;
189
+ flex-direction: column;
190
+ gap: 4px;
191
+ }
192
+
193
+ .cn-progress-bar__label {
194
+ display: flex;
195
+ justify-content: space-between;
196
+ align-items: center;
197
+ font-size: 0.9rem;
198
+ }
199
+
200
+ .cn-progress-bar__name {
201
+ color: var(--color-main-text);
202
+ }
203
+
204
+ .cn-progress-bar__name--tooltip {
205
+ cursor: help;
206
+ text-decoration: underline;
207
+ text-decoration-style: dotted;
208
+ text-underline-offset: 2px;
209
+ }
210
+
211
+ .cn-progress-bar__name--tooltip:hover {
212
+ text-decoration-style: solid;
213
+ }
214
+
215
+ .cn-progress-bar__value {
216
+ color: var(--color-text-maxcontrast);
217
+ font-variant-numeric: tabular-nums;
218
+ }
219
+
220
+ .cn-progress-bar__track {
221
+ background: var(--color-background-dark, var(--color-background-darker));
222
+ overflow: hidden;
223
+ }
224
+
225
+ .cn-progress-bar__track--rounded {
226
+ border-radius: 4px;
227
+ }
228
+
229
+ .cn-progress-bar__fill {
230
+ height: 100%;
231
+ transition: width 0.3s ease;
232
+ }
233
+
234
+ .cn-progress-bar__fill--rounded {
235
+ border-radius: 4px;
236
+ }
237
+
238
+ /* Variant colors */
239
+ .cn-progress-bar__fill--default {
240
+ background: var(--color-main-text);
241
+ }
242
+
243
+ .cn-progress-bar__fill--primary {
244
+ background: var(--color-primary-element);
245
+ }
246
+
247
+ .cn-progress-bar__fill--success {
248
+ background: var(--color-success);
249
+ }
250
+
251
+ .cn-progress-bar__fill--warning {
252
+ background: var(--color-warning);
253
+ }
254
+
255
+ .cn-progress-bar__fill--error {
256
+ background: var(--color-error);
257
+ }
258
+
259
+ .cn-progress-bar__fill--info {
260
+ background: var(--color-info, #0082c9);
261
+ }
262
+ </style>
@@ -0,0 +1 @@
1
+ export { default as CnProgressBar } from './CnProgressBar.vue'
@@ -179,7 +179,7 @@ export default {
179
179
  sortedProperties() {
180
180
  const properties = this.schema.properties || {}
181
181
  return Object.entries(properties)
182
- .sort(([keyA, propA], [keyB, propB]) => {
182
+ .sort(([, propA], [, propB]) => {
183
183
  const orderA = propA.order || 0
184
184
  const orderB = propB.order || 0
185
185
  if (orderA > 0 && orderB > 0) {
@@ -259,16 +259,22 @@ export default {
259
259
  color: inherit;
260
260
  border: 2px solid transparent;
261
261
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
262
+ height: 100%;
263
+ width: 100%;
264
+ box-sizing: border-box;
265
+ overflow: hidden;
266
+ min-width: 0;
262
267
  }
263
268
 
264
269
  .cn-stats-block--horizontal {
265
270
  flex-direction: row;
266
- align-items: flex-start;
267
- gap: 16px;
271
+ align-items: center;
272
+ gap: 12px;
268
273
  }
269
274
 
270
275
  .cn-stats-block--horizontal .cn-stats-block__content {
271
276
  text-align: left;
277
+ min-width: 0;
272
278
  }
273
279
 
274
280
  .cn-stats-block--horizontal .cn-stats-block__count {
@@ -305,6 +311,8 @@ export default {
305
311
 
306
312
  .cn-stats-block--horizontal .cn-stats-block__icon {
307
313
  margin-bottom: 0;
314
+ width: 36px;
315
+ height: 36px;
308
316
  }
309
317
 
310
318
  .cn-stats-block__icon--primary {
@@ -314,17 +322,17 @@ export default {
314
322
 
315
323
  .cn-stats-block__icon--success {
316
324
  background: rgba(70, 186, 97, 0.1);
317
- color: var(--color-success);
325
+ color: var(--color-element-success, var(--color-success));
318
326
  }
319
327
 
320
328
  .cn-stats-block__icon--warning {
321
329
  background: rgba(232, 163, 24, 0.1);
322
- color: var(--color-warning);
330
+ color: var(--color-element-warning, var(--color-warning));
323
331
  }
324
332
 
325
333
  .cn-stats-block__icon--error {
326
334
  background: rgba(224, 36, 36, 0.1);
327
- color: var(--color-error);
335
+ color: var(--color-element-error, var(--color-error));
328
336
  }
329
337
 
330
338
  /* Content */
@@ -336,34 +344,42 @@ export default {
336
344
 
337
345
  .cn-stats-block__header h4 {
338
346
  margin-top: 0;
339
- margin-bottom: 0.5rem;
347
+ margin-bottom: 0.25rem;
340
348
  color: var(--color-main-text);
341
349
  font-size: 14px;
342
350
  font-weight: 600;
351
+ white-space: nowrap;
352
+ overflow: hidden;
353
+ text-overflow: ellipsis;
343
354
  }
344
355
 
345
356
  .cn-stats-block__count {
346
357
  display: flex;
347
358
  align-items: baseline;
348
359
  justify-content: center;
349
- gap: 0.5rem;
360
+ gap: 0.25rem;
350
361
  font-size: 1.2rem;
351
- margin-bottom: 0.5rem;
362
+ margin-bottom: 0.25rem;
363
+ white-space: nowrap;
364
+ overflow: hidden;
352
365
  }
353
366
 
354
367
  .cn-stats-block__count-value {
355
368
  font-size: 2rem;
356
369
  font-weight: bold;
357
370
  color: var(--color-primary-element);
371
+ flex-shrink: 0;
358
372
  }
359
373
 
360
374
  .cn-stats-block--primary .cn-stats-block__count-value { color: var(--color-primary-element); }
361
- .cn-stats-block--success .cn-stats-block__count-value { color: var(--color-success); }
362
- .cn-stats-block--warning .cn-stats-block__count-value { color: var(--color-warning); }
363
- .cn-stats-block--error .cn-stats-block__count-value { color: var(--color-error); }
375
+ .cn-stats-block--success .cn-stats-block__count-value { color: var(--color-element-success, var(--color-success)); }
376
+ .cn-stats-block--warning .cn-stats-block__count-value { color: var(--color-element-warning, var(--color-warning)); }
377
+ .cn-stats-block--error .cn-stats-block__count-value { color: var(--color-element-error, var(--color-error)); }
364
378
 
365
379
  .cn-stats-block__count-label {
366
380
  color: var(--color-text-maxcontrast);
381
+ overflow: hidden;
382
+ text-overflow: ellipsis;
367
383
  }
368
384
 
369
385
  .cn-stats-block__loading {
@@ -0,0 +1,320 @@
1
+ <template>
2
+ <div class="cn-stats-panel">
3
+ <!-- Header slot for filters/selectors -->
4
+ <div v-if="$slots.header" class="cn-stats-panel__header">
5
+ <slot name="header" />
6
+ </div>
7
+
8
+ <!-- Global loading state -->
9
+ <div v-if="loading" class="cn-stats-panel__loading">
10
+ <NcLoadingIcon :size="20" />
11
+ <span>{{ loadingLabel }}</span>
12
+ </div>
13
+
14
+ <!-- Sections -->
15
+ <template v-else>
16
+ <div
17
+ v-for="section in sections"
18
+ :key="section.id"
19
+ class="cn-stats-panel__section">
20
+ <!-- Section title -->
21
+ <h4 v-if="section.title" class="cn-stats-panel__section-title">
22
+ {{ section.title }}
23
+ </h4>
24
+
25
+ <!-- Section-level loading -->
26
+ <div v-if="section.loading" class="cn-stats-panel__loading">
27
+ <NcLoadingIcon :size="20" />
28
+ <span>{{ loadingLabel }}</span>
29
+ </div>
30
+
31
+ <!-- Empty section -->
32
+ <div v-else-if="!section.items || !section.items.length" class="cn-stats-panel__empty">
33
+ {{ section.emptyLabel || emptyLabel }}
34
+ </div>
35
+
36
+ <!-- Stats section -->
37
+ <template v-else-if="section.type === 'stats'">
38
+ <slot :name="'section-' + section.id" :section="section">
39
+ <!-- Stack layout -->
40
+ <div v-if="section.layout === 'stack'" class="cn-stats-panel__stack">
41
+ <CnStatsBlock
42
+ v-for="(item, index) in section.items"
43
+ :key="index"
44
+ :title="item.title"
45
+ :count="item.count"
46
+ :count-label="item.countLabel"
47
+ :variant="item.variant || 'default'"
48
+ :icon="isComponentIcon(item.icon) ? item.icon : null"
49
+ :icon-size="item.iconSize || 24"
50
+ :horizontal="item.horizontal !== undefined ? item.horizontal : true"
51
+ :show-zero-count="item.showZeroCount !== undefined ? item.showZeroCount : true"
52
+ :breakdown="item.breakdown || null"
53
+ :route="item.route || null"
54
+ :clickable="item.clickable || false"
55
+ :loading="item.loading || false"
56
+ @click="$emit('stat-click', { section: section.id, item, index })">
57
+ <template v-if="typeof item.icon === 'string'" #icon>
58
+ <CnIcon :name="item.icon" :size="item.iconSize || 24" />
59
+ </template>
60
+ </CnStatsBlock>
61
+ </div>
62
+
63
+ <!-- Grid layout -->
64
+ <CnKpiGrid
65
+ v-else-if="section.layout === 'grid'"
66
+ grid-class="remove-margin"
67
+ :columns="section.columns || 2">
68
+ <CnStatsBlock
69
+ v-for="(item, index) in section.items"
70
+ :key="index"
71
+ :title="item.title"
72
+ :count="item.count"
73
+ :count-label="item.countLabel"
74
+ :variant="item.variant || 'default'"
75
+ :icon="isComponentIcon(item.icon) ? item.icon : null"
76
+ :icon-size="item.iconSize || 24"
77
+ :horizontal="item.horizontal !== undefined ? item.horizontal : false"
78
+ :show-zero-count="item.showZeroCount !== undefined ? item.showZeroCount : true"
79
+ :breakdown="item.breakdown || null"
80
+ :route="item.route || null"
81
+ :clickable="item.clickable || false"
82
+ :loading="item.loading || false"
83
+ @click="$emit('stat-click', { section: section.id, item, index })">
84
+ <template v-if="typeof item.icon === 'string'" #icon>
85
+ <CnIcon :name="item.icon" :size="item.iconSize || 24" />
86
+ </template>
87
+ </CnStatsBlock>
88
+ </CnKpiGrid>
89
+ </slot>
90
+ </template>
91
+
92
+ <!-- Progress section -->
93
+ <template v-else-if="section.type === 'progress'">
94
+ <slot :name="'section-' + section.id" :section="section">
95
+ <CnProgressBar
96
+ :items="section.items"
97
+ :variant="section.variant || 'primary'"
98
+ :bar-height="section.barHeight || 8"
99
+ :rounded="section.rounded !== undefined ? section.rounded : true"
100
+ :show-percentage="section.showPercentage || false" />
101
+ </slot>
102
+ </template>
103
+
104
+ <!-- List section -->
105
+ <template v-else-if="section.type === 'list'">
106
+ <slot :name="'section-' + section.id" :section="section">
107
+ <div class="cn-stats-panel__list">
108
+ <NcListItem
109
+ v-for="item in section.items"
110
+ :key="item.key"
111
+ :name="item.name"
112
+ :bold="item.bold || false"
113
+ @click="$emit('list-click', { section: section.id, item })">
114
+ <template #icon>
115
+ <slot :name="'item-icon-' + section.id" :item="item">
116
+ <CnIcon
117
+ v-if="typeof item.icon === 'string'"
118
+ :name="item.icon"
119
+ :size="item.iconSize || 32" />
120
+ <component
121
+ :is="item.icon"
122
+ v-else-if="item.icon"
123
+ :size="item.iconSize || 32" />
124
+ </slot>
125
+ </template>
126
+ <template #subname>
127
+ <slot :name="'item-subname-' + section.id" :item="item">
128
+ {{ item.subname }}
129
+ </slot>
130
+ </template>
131
+ </NcListItem>
132
+ </div>
133
+ </slot>
134
+ </template>
135
+ </div>
136
+ </template>
137
+
138
+ <!-- Footer slot -->
139
+ <div v-if="$slots.footer" class="cn-stats-panel__footer">
140
+ <slot name="footer" />
141
+ </div>
142
+ </div>
143
+ </template>
144
+
145
+ <script>
146
+ import { NcLoadingIcon, NcListItem } from '@nextcloud/vue'
147
+ import { CnStatsBlock } from '../CnStatsBlock/index.js'
148
+ import { CnKpiGrid } from '../CnKpiGrid/index.js'
149
+ import { CnIcon } from '../CnIcon/index.js'
150
+ import { CnProgressBar } from '../CnProgressBar/index.js'
151
+
152
+ /**
153
+ * CnStatsPanel — Configurable statistics panel with sections of stat blocks and list items.
154
+ *
155
+ * Renders statistics content from a declarative sections array. Each section can be
156
+ * either a 'stats' section (renders CnStatsBlocks in stack or grid layout) or a
157
+ * 'list' section (renders NcListItems). Suitable for sidebar tabs, dashboard widgets,
158
+ * or any panel that displays statistics.
159
+ *
160
+ * @example Stats stack (vertical)
161
+ * <CnStatsPanel :sections="[{
162
+ * type: 'stats',
163
+ * id: 'totals',
164
+ * title: 'System Totals',
165
+ * layout: 'stack',
166
+ * items: [
167
+ * { title: 'Objects', count: 42, countLabel: 'objects', variant: 'primary', icon: PackageIcon },
168
+ * { title: 'Files', count: 128, countLabel: 'files', icon: FileIcon },
169
+ * ],
170
+ * }]" />
171
+ *
172
+ * @example Stats grid (2-column)
173
+ * <CnStatsPanel :sections="[{
174
+ * type: 'stats',
175
+ * id: 'operations',
176
+ * title: 'Operations',
177
+ * layout: 'grid',
178
+ * columns: 2,
179
+ * items: [
180
+ * { title: 'Create', count: 10, countLabel: 'ops', variant: 'success', icon: PlusIcon },
181
+ * { title: 'Delete', count: 3, countLabel: 'ops', variant: 'error', icon: DeleteIcon },
182
+ * ],
183
+ * }]" />
184
+ *
185
+ * @example List section
186
+ * <CnStatsPanel :sections="[{
187
+ * type: 'list',
188
+ * id: 'topObjects',
189
+ * title: 'Most Active',
190
+ * items: [
191
+ * { key: '1', name: 'Object A', subname: '42 entries', icon: CogIcon },
192
+ * ],
193
+ * }]" />
194
+ *
195
+ * @example With header slot for filters
196
+ * <CnStatsPanel :sections="sections">
197
+ * <template #header>
198
+ * <NcSelect v-bind="registerOptions" />
199
+ * </template>
200
+ * </CnStatsPanel>
201
+ */
202
+ export default {
203
+ name: 'CnStatsPanel',
204
+
205
+ components: {
206
+ NcLoadingIcon,
207
+ NcListItem,
208
+ CnStatsBlock,
209
+ CnKpiGrid,
210
+ CnIcon,
211
+ CnProgressBar,
212
+ },
213
+
214
+ props: {
215
+ /**
216
+ * Array of section definitions to render.
217
+ * Each section has a `type` of 'stats', 'list', or 'progress'.
218
+ *
219
+ * Stats sections: `{ type: 'stats', id, title, layout: 'stack'|'grid', columns?, loading?, items: StatItem[] }`
220
+ * List sections: `{ type: 'list', id, title, loading?, items: ListItem[] }`
221
+ * Progress sections: `{ type: 'progress', id, title, variant?, barHeight?, rounded?, showPercentage?, loading?, items: ProgressItem[] }`
222
+ *
223
+ * StatItem: `{ title, count, countLabel, variant?, icon?, iconSize?, horizontal?, showZeroCount?, breakdown?, route?, clickable?, loading? }`
224
+ * ListItem: `{ key, name, subname?, bold?, icon?, iconSize? }`
225
+ * ProgressItem: `{ key?, label, count?, percentage?, variant?, tooltip? }`
226
+ */
227
+ sections: {
228
+ type: Array,
229
+ default: () => [],
230
+ },
231
+
232
+ /** Whether the entire panel is in a loading state */
233
+ loading: {
234
+ type: Boolean,
235
+ default: false,
236
+ },
237
+
238
+ /** Label shown during loading state */
239
+ loadingLabel: {
240
+ type: String,
241
+ default: 'Loading...',
242
+ },
243
+
244
+ /** Default text shown when a section has no items. Can be overridden per section via `section.emptyLabel`. */
245
+ emptyLabel: {
246
+ type: String,
247
+ default: 'No data available',
248
+ },
249
+ },
250
+
251
+ emits: ['stat-click', 'list-click', 'progress-click'],
252
+
253
+ methods: {
254
+ /**
255
+ * Check if an icon value is a component reference (not a string name).
256
+ * @param {*} icon - Icon value to check
257
+ * @return {boolean}
258
+ */
259
+ isComponentIcon(icon) {
260
+ return icon != null && typeof icon !== 'string'
261
+ },
262
+ },
263
+ }
264
+ </script>
265
+
266
+ <style scoped>
267
+ .cn-stats-panel__section {
268
+ padding: 12px 0;
269
+ border-bottom: 1px solid var(--color-border);
270
+ }
271
+
272
+ .remove-margin {
273
+ margin: 0;
274
+ }
275
+
276
+ .cn-stats-panel__section:last-child {
277
+ border-bottom: none;
278
+ }
279
+
280
+ .cn-stats-panel__section-title {
281
+ color: var(--color-text-maxcontrast);
282
+ font-size: 14px;
283
+ font-weight: bold;
284
+ padding: 0 16px;
285
+ margin: 0 0 12px 0;
286
+ }
287
+
288
+ .cn-stats-panel__stack {
289
+ display: flex;
290
+ flex-direction: column;
291
+ gap: 12px;
292
+ }
293
+
294
+ .cn-stats-panel__loading {
295
+ display: flex;
296
+ align-items: center;
297
+ gap: 8px;
298
+ padding: 0 16px;
299
+ color: var(--color-text-maxcontrast);
300
+ }
301
+
302
+ .cn-stats-panel__empty {
303
+ padding: 0 16px;
304
+ color: var(--color-text-maxcontrast);
305
+ font-style: italic;
306
+ }
307
+
308
+ .cn-stats-panel__header {
309
+ padding-bottom: 12px;
310
+ border-bottom: 1px solid var(--color-border);
311
+ }
312
+
313
+ .cn-stats-panel__footer {
314
+ padding-top: 12px;
315
+ }
316
+
317
+ .cn-stats-panel__list {
318
+ margin-top: 4px;
319
+ }
320
+ </style>
@@ -0,0 +1 @@
1
+ export { default as CnStatsPanel } from './CnStatsPanel.vue'