@conduction/nextcloud-vue 0.1.0-beta.1 → 0.1.0-beta.11

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 (208) hide show
  1. package/README.md +226 -0
  2. package/css/index.css +5 -0
  3. package/dist/nextcloud-vue.cjs.js +79416 -7715
  4. package/dist/nextcloud-vue.cjs.js.map +1 -1
  5. package/dist/nextcloud-vue.css +3583 -504
  6. package/dist/nextcloud-vue.esm.js +79343 -7692
  7. package/dist/nextcloud-vue.esm.js.map +1 -1
  8. package/l10n/en.json +164 -0
  9. package/l10n/nl.json +164 -0
  10. package/package.json +104 -63
  11. package/src/components/CnActionsBar/CnActionsBar.vue +254 -0
  12. package/src/components/CnActionsBar/index.js +1 -0
  13. package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +570 -0
  14. package/src/components/CnAdvancedFormDialog/CnDataTab.vue +217 -0
  15. package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +121 -0
  16. package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +422 -0
  17. package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +247 -0
  18. package/src/components/CnAdvancedFormDialog/index.js +1 -0
  19. package/src/components/CnCard/CnCard.vue +415 -0
  20. package/src/components/CnCard/index.js +1 -0
  21. package/src/components/CnCardGrid/CnCardGrid.vue +156 -152
  22. package/src/components/CnCardGrid/index.js +1 -1
  23. package/src/components/CnCellRenderer/CnCellRenderer.vue +132 -132
  24. package/src/components/CnCellRenderer/index.js +1 -1
  25. package/src/components/CnChartWidget/CnChartWidget.vue +346 -0
  26. package/src/components/CnChartWidget/index.js +1 -0
  27. package/src/components/CnConfigurationCard/CnConfigurationCard.vue +77 -77
  28. package/src/components/CnConfigurationCard/index.js +1 -1
  29. package/src/components/CnContextMenu/CnContextMenu.vue +142 -0
  30. package/src/components/CnContextMenu/index.js +1 -0
  31. package/src/components/CnCopyDialog/CnCopyDialog.vue +266 -0
  32. package/src/components/CnCopyDialog/index.js +1 -0
  33. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +229 -0
  34. package/src/components/CnDashboardGrid/index.js +1 -0
  35. package/src/components/CnDashboardPage/CnDashboardPage.vue +397 -0
  36. package/src/components/CnDashboardPage/index.js +1 -0
  37. package/src/components/CnDataTable/CnDataTable.vue +362 -354
  38. package/src/components/CnDataTable/index.js +1 -1
  39. package/src/components/CnDeleteDialog/CnDeleteDialog.vue +177 -0
  40. package/src/components/CnDeleteDialog/index.js +1 -0
  41. package/src/components/CnDetailCard/CnDetailCard.vue +225 -0
  42. package/src/components/CnDetailCard/index.js +1 -0
  43. package/src/components/CnDetailGrid/CnDetailGrid.vue +256 -0
  44. package/src/components/CnDetailGrid/index.js +1 -0
  45. package/src/components/CnDetailPage/CnDetailPage.vue +432 -0
  46. package/src/components/CnDetailPage/index.js +1 -0
  47. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +234 -223
  48. package/src/components/CnFacetSidebar/index.js +1 -1
  49. package/src/components/CnFilterBar/CnFilterBar.vue +153 -152
  50. package/src/components/CnFilterBar/index.js +1 -1
  51. package/src/components/CnFormDialog/CnFormDialog.vue +1047 -0
  52. package/src/components/CnFormDialog/index.js +1 -0
  53. package/src/components/CnIcon/CnIcon.vue +89 -0
  54. package/src/components/CnIcon/index.js +1 -0
  55. package/src/components/CnIndexPage/CnIndexPage.vue +980 -682
  56. package/src/components/CnIndexPage/index.js +1 -1
  57. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +536 -0
  58. package/src/components/CnIndexSidebar/index.js +1 -0
  59. package/src/components/CnInfoWidget/CnInfoWidget.vue +219 -0
  60. package/src/components/CnInfoWidget/index.js +1 -0
  61. package/src/components/CnItemCard/CnItemCard.vue +134 -0
  62. package/src/components/CnItemCard/index.js +1 -0
  63. package/src/components/CnJsonViewer/CnJsonViewer.vue +312 -0
  64. package/src/components/CnJsonViewer/index.js +1 -0
  65. package/src/components/CnKpiGrid/CnKpiGrid.vue +93 -89
  66. package/src/components/CnKpiGrid/index.js +1 -1
  67. package/src/components/CnMassActionBar/CnMassActionBar.vue +161 -160
  68. package/src/components/CnMassActionBar/index.js +1 -1
  69. package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +327 -320
  70. package/src/components/CnMassCopyDialog/index.js +1 -1
  71. package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +245 -238
  72. package/src/components/CnMassDeleteDialog/index.js +1 -1
  73. package/src/components/CnMassExportDialog/CnMassExportDialog.vue +191 -190
  74. package/src/components/CnMassExportDialog/index.js +1 -1
  75. package/src/components/CnMassImportDialog/CnMassImportDialog.vue +494 -491
  76. package/src/components/CnMassImportDialog/index.js +1 -1
  77. package/src/components/CnNoteCard/CnNoteCard.vue +149 -0
  78. package/src/components/CnNoteCard/index.js +1 -0
  79. package/src/components/CnNotesCard/CnNotesCard.vue +416 -0
  80. package/src/components/CnNotesCard/index.js +1 -0
  81. package/src/components/CnObjectCard/CnObjectCard.vue +294 -292
  82. package/src/components/CnObjectCard/index.js +1 -1
  83. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +854 -0
  84. package/src/components/CnObjectDataWidget/index.js +1 -0
  85. package/src/components/CnObjectMetadataWidget/CnObjectMetadataWidget.vue +289 -0
  86. package/src/components/CnObjectMetadataWidget/index.js +1 -0
  87. package/src/components/CnObjectSidebar/CnAuditTrailTab.vue +369 -0
  88. package/src/components/CnObjectSidebar/CnFilesTab.vue +287 -0
  89. package/src/components/CnObjectSidebar/CnNotesTab.vue +250 -0
  90. package/src/components/CnObjectSidebar/CnObjectSidebar.vue +255 -0
  91. package/src/components/CnObjectSidebar/CnTagsTab.vue +259 -0
  92. package/src/components/CnObjectSidebar/CnTasksTab.vue +483 -0
  93. package/src/components/CnObjectSidebar/index.js +6 -0
  94. package/src/components/CnPageHeader/CnPageHeader.vue +61 -0
  95. package/src/components/CnPageHeader/index.js +1 -0
  96. package/src/components/CnPagination/CnPagination.vue +253 -252
  97. package/src/components/CnPagination/index.js +1 -1
  98. package/src/components/CnProgressBar/CnProgressBar.vue +262 -0
  99. package/src/components/CnProgressBar/index.js +1 -0
  100. package/src/components/CnRegisterMapping/CnRegisterMapping.vue +793 -0
  101. package/src/components/CnRegisterMapping/index.js +1 -0
  102. package/src/components/CnRowActions/CnRowActions.vue +95 -73
  103. package/src/components/CnRowActions/index.js +1 -1
  104. package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +226 -0
  105. package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +788 -0
  106. package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +305 -0
  107. package/src/components/CnSchemaFormDialog/CnSchemaPropertyActions.vue +1398 -0
  108. package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +236 -0
  109. package/src/components/CnSchemaFormDialog/index.js +1 -0
  110. package/src/components/CnSettingsCard/CnSettingsCard.vue +92 -92
  111. package/src/components/CnSettingsCard/index.js +1 -1
  112. package/src/components/CnSettingsSection/CnSettingsSection.vue +267 -266
  113. package/src/components/CnSettingsSection/index.js +1 -1
  114. package/src/components/CnStatsBlock/CnStatsBlock.vue +437 -366
  115. package/src/components/CnStatsBlock/index.js +1 -1
  116. package/src/components/CnStatsPanel/CnStatsPanel.vue +321 -0
  117. package/src/components/CnStatsPanel/index.js +1 -0
  118. package/src/components/CnStatusBadge/CnStatusBadge.vue +90 -77
  119. package/src/components/CnStatusBadge/index.js +1 -1
  120. package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +545 -0
  121. package/src/components/CnTabbedFormDialog/index.js +1 -0
  122. package/src/components/CnTableWidget/CnTableWidget.vue +333 -0
  123. package/src/components/CnTableWidget/index.js +1 -0
  124. package/src/components/CnTasksCard/CnTasksCard.vue +374 -0
  125. package/src/components/CnTasksCard/index.js +1 -0
  126. package/src/components/CnTileWidget/CnTileWidget.vue +159 -0
  127. package/src/components/CnTileWidget/index.js +1 -0
  128. package/src/components/CnTimelineStages/CnTimelineStages.vue +294 -0
  129. package/src/components/CnTimelineStages/index.js +1 -0
  130. package/src/components/CnUserActionMenu/CnUserActionMenu.vue +436 -0
  131. package/src/components/CnUserActionMenu/index.js +1 -0
  132. package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +313 -312
  133. package/src/components/CnVersionInfoCard/index.js +1 -1
  134. package/src/components/CnWidgetRenderer/CnWidgetRenderer.vue +180 -0
  135. package/src/components/CnWidgetRenderer/index.js +1 -0
  136. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +248 -0
  137. package/src/components/CnWidgetWrapper/index.js +1 -0
  138. package/src/components/index.js +57 -25
  139. package/src/composables/index.js +5 -3
  140. package/src/composables/useContextMenu.js +126 -0
  141. package/src/composables/useDashboardView.js +286 -0
  142. package/src/composables/useDetailView.js +290 -132
  143. package/src/composables/useListView.js +364 -153
  144. package/src/composables/useSubResource.js +142 -142
  145. package/src/constants/metadata.js +30 -0
  146. package/src/css/CnSchemaFormDialog.css +546 -0
  147. package/src/css/__sample_nextcloud_tokens.css +110 -0
  148. package/src/css/actions-bar.css +54 -0
  149. package/src/css/badge.css +83 -51
  150. package/src/css/card.css +129 -128
  151. package/src/css/context-menu.css +20 -0
  152. package/src/css/dashboard.css +70 -0
  153. package/src/css/detail-page.css +235 -0
  154. package/src/css/detail.css +68 -68
  155. package/src/css/index-page.css +44 -0
  156. package/src/css/index-sidebar.css +193 -0
  157. package/src/css/index.css +17 -8
  158. package/src/css/layout.css +90 -90
  159. package/src/css/page-header.css +35 -0
  160. package/src/css/pagination.css +72 -72
  161. package/src/css/table.css +142 -143
  162. package/src/css/timeline-stages.css +220 -0
  163. package/src/css/utilities.css +46 -46
  164. package/src/index.js +95 -50
  165. package/src/l10n/index.js +12 -0
  166. package/src/mixins/gridLayout.js +118 -0
  167. package/src/store/createCrudStore.d.ts +350 -0
  168. package/src/store/createCrudStore.js +413 -0
  169. package/src/store/createSubResourcePlugin.js +125 -135
  170. package/src/store/index.js +4 -3
  171. package/src/store/pluginMerge.js +55 -0
  172. package/src/store/plugins/auditTrails.js +357 -17
  173. package/src/store/plugins/files.js +250 -186
  174. package/src/store/plugins/index.js +8 -4
  175. package/src/store/plugins/lifecycle.js +180 -180
  176. package/src/store/plugins/logs.d.ts +22 -0
  177. package/src/store/plugins/logs.js +172 -0
  178. package/src/store/plugins/registerMapping.js +195 -0
  179. package/src/store/plugins/relations.js +68 -68
  180. package/src/store/plugins/search.js +385 -0
  181. package/src/store/plugins/selection.js +104 -0
  182. package/src/store/useObjectStore.js +793 -625
  183. package/src/types/auditTrail.d.ts +32 -32
  184. package/src/types/file.d.ts +23 -23
  185. package/src/types/index.d.ts +67 -35
  186. package/src/types/notification.d.ts +36 -36
  187. package/src/types/object.d.ts +40 -40
  188. package/src/types/organisation.d.ts +41 -41
  189. package/src/types/register.d.ts +25 -25
  190. package/src/types/schema.d.ts +39 -39
  191. package/src/types/shared.d.ts +79 -79
  192. package/src/types/source.d.ts +14 -14
  193. package/src/types/task.d.ts +31 -31
  194. package/src/utils/errors.js +96 -96
  195. package/src/utils/getTheme.js +9 -0
  196. package/src/utils/headers.js +80 -44
  197. package/src/utils/id.js +13 -0
  198. package/src/utils/index.js +4 -3
  199. package/src/utils/schema.js +423 -287
  200. package/src/utils/widgetVisibility.js +162 -0
  201. package/src/components/CnDetailViewLayout/CnDetailViewLayout.vue +0 -88
  202. package/src/components/CnDetailViewLayout/index.js +0 -1
  203. package/src/components/CnEmptyState/CnEmptyState.vue +0 -78
  204. package/src/components/CnEmptyState/index.js +0 -1
  205. package/src/components/CnListViewLayout/CnListViewLayout.vue +0 -80
  206. package/src/components/CnListViewLayout/index.js +0 -1
  207. package/src/components/CnViewModeToggle/CnViewModeToggle.vue +0 -77
  208. package/src/components/CnViewModeToggle/index.js +0 -1
@@ -1,366 +1,437 @@
1
- <template>
2
- <component
3
- :is="clickable ? 'a' : 'div'"
4
- class="cn-stats-block"
5
- :class="rootClasses"
6
- v-bind="clickable ? { href: '#', role: 'button', tabindex: '0' } : {}"
7
- @click="onClick">
8
- <!-- Icon -->
9
- <div v-if="hasIcon" class="cn-stats-block__icon" :class="iconClasses">
10
- <slot name="icon">
11
- <component :is="icon" v-if="icon" :size="iconSize" />
12
- </slot>
13
- </div>
14
-
15
- <!-- Content -->
16
- <div class="cn-stats-block__content">
17
- <div class="cn-stats-block__header">
18
- <h4>{{ title || 'Objects' }}</h4>
19
- </div>
20
-
21
- <div v-if="count > 0" class="cn-stats-block__count">
22
- <span class="cn-stats-block__count-value">{{ formattedCount }}</span>
23
- <span class="cn-stats-block__count-label">{{ countLabel }}</span>
24
- </div>
25
- <div v-else-if="loading" class="cn-stats-block__loading">
26
- <NcLoadingIcon :size="16" />
27
- {{ loadingLabel }}
28
- </div>
29
- <div v-else class="cn-stats-block__empty">
30
- {{ emptyLabel }}
31
- </div>
32
-
33
- <!-- Breakdown details -->
34
- <div v-if="breakdown && count > 0" class="cn-stats-block__breakdown">
35
- <div
36
- v-for="(value, key) in breakdown"
37
- :key="key"
38
- class="cn-stats-block__breakdown-item">
39
- <span class="cn-stats-block__breakdown-label">{{ formatBreakdownLabel(key) }}</span>
40
- <span
41
- class="cn-stats-block__breakdown-value"
42
- :class="'cn-stats-block__breakdown-value--' + key">
43
- {{ value }}
44
- </span>
45
- </div>
46
- </div>
47
- </div>
48
- </component>
49
- </template>
50
-
51
- <script>
52
- import { NcLoadingIcon } from '@nextcloud/vue'
53
-
54
- /**
55
- * CnStatsBlock — Statistics display card with icon, count, and optional breakdown.
56
- *
57
- * Supports vertical (default) and horizontal layouts, color variants, icons,
58
- * and clickable state. Use in a CnKpiGrid for responsive dashboard layouts.
59
- *
60
- * @example Basic vertical (default)
61
- * <CnStatsBlock title="Cases" :count="42" count-label="open cases" />
62
- *
63
- * @example Horizontal with icon and variant
64
- * <CnStatsBlock
65
- * title="Open Cases"
66
- * :count="42"
67
- * :icon="BriefcaseOutline"
68
- * variant="primary"
69
- * horizontal
70
- * clickable
71
- * @click="goToCases" />
72
- *
73
- * @example With breakdown
74
- * <CnStatsBlock
75
- * title="Cases"
76
- * :count="42"
77
- * :breakdown="{ total: 100, invalid: 3, deleted: 5, published: 92 }" />
78
- *
79
- * @example Custom icon slot
80
- * <CnStatsBlock title="Files" :count="128">
81
- * <template #icon>
82
- * <FileDocumentOutline :size="24" />
83
- * </template>
84
- * </CnStatsBlock>
85
- */
86
- export default {
87
- name: 'CnStatsBlock',
88
-
89
- components: {
90
- NcLoadingIcon,
91
- },
92
-
93
- props: {
94
- /** Block title */
95
- title: {
96
- type: String,
97
- default: '',
98
- },
99
- /** The main count number to display prominently */
100
- count: {
101
- type: Number,
102
- default: 0,
103
- },
104
- /** Label displayed next to the count */
105
- countLabel: {
106
- type: String,
107
- default: 'objects',
108
- },
109
- /** Detailed breakdown object (key-value pairs) */
110
- breakdown: {
111
- type: Object,
112
- default: null,
113
- },
114
- /** Whether data is currently loading */
115
- loading: {
116
- type: Boolean,
117
- default: false,
118
- },
119
- /** Text shown while loading */
120
- loadingLabel: {
121
- type: String,
122
- default: 'Loading...',
123
- },
124
- /** Text shown when count is 0 */
125
- emptyLabel: {
126
- type: String,
127
- default: 'No items found',
128
- },
129
- /** Icon component (e.g., imported MDI icon) */
130
- icon: {
131
- type: [Object, Function],
132
- default: null,
133
- },
134
- /** Icon size in pixels */
135
- iconSize: {
136
- type: Number,
137
- default: 24,
138
- },
139
- /** Color variant: 'default', 'primary', 'success', 'warning', 'error' */
140
- variant: {
141
- type: String,
142
- default: 'default',
143
- validator: (v) => ['default', 'primary', 'success', 'warning', 'error'].includes(v),
144
- },
145
- /** Use horizontal layout (icon left, content right) */
146
- horizontal: {
147
- type: Boolean,
148
- default: false,
149
- },
150
- /** Whether the card is clickable */
151
- clickable: {
152
- type: Boolean,
153
- default: false,
154
- },
155
- },
156
-
157
- computed: {
158
- hasIcon() {
159
- return this.icon !== null || this.$scopedSlots.icon || this.$slots.icon
160
- },
161
-
162
- formattedCount() {
163
- return this.count.toLocaleString()
164
- },
165
-
166
- rootClasses() {
167
- return {
168
- 'cn-stats-block--horizontal': this.horizontal,
169
- 'cn-stats-block--clickable': this.clickable,
170
- [`cn-stats-block--${this.variant}`]: this.variant !== 'default',
171
- }
172
- },
173
-
174
- iconClasses() {
175
- return {
176
- [`cn-stats-block__icon--${this.variant}`]: this.variant !== 'default',
177
- }
178
- },
179
- },
180
-
181
- methods: {
182
- formatBreakdownLabel(key) {
183
- return key.charAt(0).toUpperCase() + key.slice(1) + ':'
184
- },
185
-
186
- onClick(event) {
187
- if (this.clickable) {
188
- event.preventDefault()
189
- this.$emit('click', event)
190
- }
191
- },
192
- },
193
- }
194
- </script>
195
-
196
- <style scoped>
197
- .cn-stats-block {
198
- background: var(--color-background-hover);
199
- border-radius: var(--border-radius-large, 10px);
200
- padding: 1rem;
201
- display: flex;
202
- flex-direction: column;
203
- align-items: center;
204
- text-decoration: none;
205
- color: inherit;
206
- border: 2px solid transparent;
207
- transition: border-color 0.15s ease, box-shadow 0.15s ease;
208
- }
209
-
210
- .cn-stats-block--horizontal {
211
- flex-direction: row;
212
- align-items: flex-start;
213
- gap: 16px;
214
- }
215
-
216
- .cn-stats-block--horizontal .cn-stats-block__content {
217
- text-align: left;
218
- }
219
-
220
- .cn-stats-block--horizontal .cn-stats-block__count {
221
- justify-content: flex-start;
222
- }
223
-
224
- .cn-stats-block--clickable {
225
- cursor: pointer;
226
- }
227
-
228
- .cn-stats-block--clickable:hover {
229
- border-color: var(--color-primary-element);
230
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
231
- }
232
-
233
- .cn-stats-block--clickable:focus-visible {
234
- outline: 2px solid var(--color-primary-element);
235
- outline-offset: 2px;
236
- }
237
-
238
- /* Icon */
239
- .cn-stats-block__icon {
240
- display: flex;
241
- align-items: center;
242
- justify-content: center;
243
- width: 44px;
244
- height: 44px;
245
- border-radius: 50%;
246
- background: var(--color-primary-element-light, rgba(0, 130, 201, 0.1));
247
- color: var(--color-primary-element);
248
- flex-shrink: 0;
249
- margin-bottom: 8px;
250
- }
251
-
252
- .cn-stats-block--horizontal .cn-stats-block__icon {
253
- margin-bottom: 0;
254
- }
255
-
256
- .cn-stats-block__icon--primary {
257
- background: var(--color-primary-element-light, rgba(0, 130, 201, 0.1));
258
- color: var(--color-primary-element);
259
- }
260
-
261
- .cn-stats-block__icon--success {
262
- background: rgba(70, 186, 97, 0.1);
263
- color: var(--color-success);
264
- }
265
-
266
- .cn-stats-block__icon--warning {
267
- background: rgba(232, 163, 24, 0.1);
268
- color: var(--color-warning);
269
- }
270
-
271
- .cn-stats-block__icon--error {
272
- background: rgba(224, 36, 36, 0.1);
273
- color: var(--color-error);
274
- }
275
-
276
- /* Content */
277
- .cn-stats-block__content {
278
- flex: 1;
279
- min-width: 0;
280
- text-align: center;
281
- }
282
-
283
- .cn-stats-block__header h4 {
284
- margin-top: 0;
285
- margin-bottom: 0.5rem;
286
- color: var(--color-main-text);
287
- font-size: 14px;
288
- font-weight: 600;
289
- }
290
-
291
- .cn-stats-block__count {
292
- display: flex;
293
- align-items: baseline;
294
- justify-content: center;
295
- gap: 0.5rem;
296
- font-size: 1.2rem;
297
- margin-bottom: 0.5rem;
298
- }
299
-
300
- .cn-stats-block__count-value {
301
- font-size: 2rem;
302
- font-weight: bold;
303
- color: var(--color-primary-element);
304
- }
305
-
306
- .cn-stats-block--primary .cn-stats-block__count-value { color: var(--color-primary-element); }
307
- .cn-stats-block--success .cn-stats-block__count-value { color: var(--color-success); }
308
- .cn-stats-block--warning .cn-stats-block__count-value { color: var(--color-warning); }
309
- .cn-stats-block--error .cn-stats-block__count-value { color: var(--color-error); }
310
-
311
- .cn-stats-block__count-label {
312
- color: var(--color-text-maxcontrast);
313
- }
314
-
315
- .cn-stats-block__loading {
316
- display: flex;
317
- align-items: center;
318
- justify-content: center;
319
- gap: 0.5rem;
320
- color: var(--color-text-maxcontrast);
321
- margin-bottom: 0.5rem;
322
- }
323
-
324
- .cn-stats-block__empty {
325
- text-align: center;
326
- color: var(--color-text-maxcontrast);
327
- font-style: italic;
328
- margin-bottom: 0.5rem;
329
- }
330
-
331
- .cn-stats-block__breakdown {
332
- margin-top: 0.5rem;
333
- padding: 0.75rem;
334
- background: var(--color-background-hover);
335
- border-radius: var(--border-radius);
336
- border: 1px solid var(--color-border);
337
- width: 100%;
338
- }
339
-
340
- .cn-stats-block__breakdown-item {
341
- display: flex;
342
- justify-content: space-between;
343
- align-items: center;
344
- margin-bottom: 0.5rem;
345
- }
346
-
347
- .cn-stats-block__breakdown-item:last-child {
348
- margin-bottom: 0;
349
- }
350
-
351
- .cn-stats-block__breakdown-label {
352
- font-weight: 500;
353
- color: var(--color-main-text);
354
- }
355
-
356
- .cn-stats-block__breakdown-value {
357
- font-weight: 600;
358
- padding: 0.25rem 0.5rem;
359
- border-radius: var(--border-radius);
360
- background: var(--color-background-hover);
361
- }
362
-
363
- .cn-stats-block__breakdown-value--invalid { color: var(--color-warning); }
364
- .cn-stats-block__breakdown-value--deleted { color: var(--color-error); }
365
- .cn-stats-block__breakdown-value--published { color: var(--color-success); }
366
- </style>
1
+ <template>
2
+ <component
3
+ :is="componentTag"
4
+ class="cn-stats-block"
5
+ :class="rootClasses"
6
+ v-bind="componentAttrs"
7
+ @click="onClick">
8
+ <!-- Icon -->
9
+ <div v-if="hasIcon" class="cn-stats-block__icon" :class="iconClasses">
10
+ <slot name="icon">
11
+ <component :is="icon" v-if="icon" :size="iconSize" />
12
+ </slot>
13
+ </div>
14
+
15
+ <!-- Content -->
16
+ <div class="cn-stats-block__content">
17
+ <div class="cn-stats-block__header">
18
+ <h4>{{ title || t('nextcloud-vue', 'Objects') }}</h4>
19
+ </div>
20
+
21
+ <div v-if="count > 0 || (showZeroCount && count === 0)" class="cn-stats-block__count">
22
+ <span class="cn-stats-block__count-value">{{ formattedCount }}</span>
23
+ <span class="cn-stats-block__count-label">{{ countLabel }}</span>
24
+ </div>
25
+ <div v-else-if="loading" class="cn-stats-block__loading">
26
+ <NcLoadingIcon :size="16" />
27
+ {{ loadingLabel }}
28
+ </div>
29
+ <div v-else class="cn-stats-block__empty">
30
+ {{ emptyLabel }}
31
+ </div>
32
+
33
+ <!-- Breakdown details -->
34
+ <div v-if="breakdown && (count > 0 || showZeroCount)" class="cn-stats-block__breakdown">
35
+ <div
36
+ v-for="(value, key) in breakdown"
37
+ :key="key"
38
+ class="cn-stats-block__breakdown-item">
39
+ <span class="cn-stats-block__breakdown-label">{{ formatBreakdownLabel(key) }}</span>
40
+ <span
41
+ class="cn-stats-block__breakdown-value"
42
+ :class="'cn-stats-block__breakdown-value--' + key">
43
+ {{ value }}
44
+ </span>
45
+ </div>
46
+ </div>
47
+ </div>
48
+ </component>
49
+ </template>
50
+
51
+ <script>
52
+ import { translate as t } from '@nextcloud/l10n'
53
+ import { NcLoadingIcon } from '@nextcloud/vue'
54
+
55
+ /**
56
+ * CnStatsBlock — Statistics display card with icon, count, and optional breakdown.
57
+ *
58
+ * Supports vertical (default) and horizontal layouts, color variants, icons,
59
+ * and clickable state. Use in a CnKpiGrid for responsive dashboard layouts.
60
+ *
61
+ * @example Basic vertical (default)
62
+ * <CnStatsBlock title="Cases" :count="42" count-label="open cases" />
63
+ *
64
+ * @example Horizontal with icon and variant
65
+ * <CnStatsBlock
66
+ * title="Open Cases"
67
+ * :count="42"
68
+ * :icon="BriefcaseOutline"
69
+ * variant="primary"
70
+ * horizontal
71
+ * clickable
72
+ * @click="goToCases" />
73
+ *
74
+ * @example With route-based navigation (renders as <router-link>)
75
+ * <CnStatsBlock
76
+ * title="Open Cases"
77
+ * :count="42"
78
+ * :icon="BriefcaseOutline"
79
+ * variant="primary"
80
+ * :route="{ name: 'Cases', query: { status: 'open' } }" />
81
+ *
82
+ * @example With breakdown
83
+ * <CnStatsBlock
84
+ * title="Cases"
85
+ * :count="42"
86
+ * :breakdown="{ total: 100, invalid: 3, deleted: 5, published: 92 }" />
87
+ *
88
+ * @example Custom icon slot
89
+ * <CnStatsBlock title="Files" :count="128">
90
+ * <template #icon>
91
+ * <FileDocumentOutline :size="24" />
92
+ * </template>
93
+ * </CnStatsBlock>
94
+ */
95
+ export default {
96
+ name: 'CnStatsBlock',
97
+
98
+ components: {
99
+ NcLoadingIcon,
100
+ },
101
+
102
+ props: {
103
+ /** Block title */
104
+ title: {
105
+ type: String,
106
+ default: '',
107
+ },
108
+ /** The main count number to display prominently */
109
+ count: {
110
+ type: Number,
111
+ default: 0,
112
+ },
113
+ /** Label displayed next to the count */
114
+ countLabel: {
115
+ type: String,
116
+ default: () => t('nextcloud-vue', 'objects'),
117
+ },
118
+ /** Detailed breakdown object (key-value pairs) */
119
+ breakdown: {
120
+ type: Object,
121
+ default: null,
122
+ },
123
+ /** Whether data is currently loading */
124
+ loading: {
125
+ type: Boolean,
126
+ default: false,
127
+ },
128
+ /** Text shown while loading */
129
+ loadingLabel: {
130
+ type: String,
131
+ default: () => t('nextcloud-vue', 'Loading...'),
132
+ },
133
+ /** Text shown when count is 0 */
134
+ emptyLabel: {
135
+ type: String,
136
+ default: () => t('nextcloud-vue', 'No items found'),
137
+ },
138
+ /** Icon component (e.g., imported MDI icon) */
139
+ icon: {
140
+ type: [Object, Function],
141
+ default: null,
142
+ },
143
+ /** Icon size in pixels */
144
+ iconSize: {
145
+ type: Number,
146
+ default: 24,
147
+ },
148
+ /** Color variant: 'default', 'primary', 'success', 'warning', 'error' */
149
+ variant: {
150
+ type: String,
151
+ default: 'default',
152
+ validator: (v) => ['default', 'primary', 'success', 'warning', 'error'].includes(v),
153
+ },
154
+ /** Use horizontal layout (icon left, content right) */
155
+ horizontal: {
156
+ type: Boolean,
157
+ default: false,
158
+ },
159
+ /** Whether the card is clickable */
160
+ clickable: {
161
+ type: Boolean,
162
+ default: false,
163
+ },
164
+ /** Whether to display 0 as a count value instead of the empty label */
165
+ showZeroCount: {
166
+ type: Boolean,
167
+ default: false,
168
+ },
169
+ /**
170
+ * Vue Router location object for declarative navigation.
171
+ * When set, the card renders as a <router-link> and clickable styles are implied.
172
+ * @example { name: 'Cases', query: { status: 'open' } }
173
+ * @example { path: '/catalogi' }
174
+ */
175
+ route: {
176
+ type: Object,
177
+ default: null,
178
+ },
179
+ },
180
+
181
+ computed: {
182
+ hasIcon() {
183
+ return this.icon !== null || this.$scopedSlots.icon || this.$slots.icon
184
+ },
185
+
186
+ formattedCount() {
187
+ return this.count.toLocaleString()
188
+ },
189
+
190
+ /**
191
+ * Whether the card is interactive (clickable or has a route).
192
+ * Used for applying hover/focus styles.
193
+ */
194
+ isInteractive() {
195
+ return !!this.route || this.clickable
196
+ },
197
+
198
+ /**
199
+ * Determines which HTML element or component to render.
200
+ * - route set → 'router-link' (SPA navigation)
201
+ * - clickable → 'a' (app handles click via event)
202
+ * - neither → 'div' (static display)
203
+ */
204
+ componentTag() {
205
+ if (this.route) return 'router-link'
206
+ if (this.clickable) return 'a'
207
+ return 'div'
208
+ },
209
+
210
+ /**
211
+ * Dynamic attributes for the root element based on rendering mode.
212
+ */
213
+ componentAttrs() {
214
+ if (this.route) return { to: this.route, tabindex: '0' }
215
+ if (this.clickable) return { href: '#', role: 'button', tabindex: '0' }
216
+ return {}
217
+ },
218
+
219
+ rootClasses() {
220
+ return {
221
+ 'cn-stats-block--horizontal': this.horizontal,
222
+ 'cn-stats-block--clickable': this.isInteractive,
223
+ [`cn-stats-block--${this.variant}`]: this.variant !== 'default',
224
+ }
225
+ },
226
+
227
+ iconClasses() {
228
+ return {
229
+ [`cn-stats-block__icon--${this.variant}`]: this.variant !== 'default',
230
+ }
231
+ },
232
+ },
233
+
234
+ methods: {
235
+ formatBreakdownLabel(key) {
236
+ return key.charAt(0).toUpperCase() + key.slice(1) + ':'
237
+ },
238
+
239
+ onClick(event) {
240
+ // When route is set, router-link handles navigation — do not emit click
241
+ if (this.route) return
242
+ if (this.clickable) {
243
+ event.preventDefault()
244
+ this.$emit('click', event)
245
+ }
246
+ },
247
+ },
248
+ }
249
+ </script>
250
+
251
+ <style scoped>
252
+ .cn-stats-block {
253
+ background: var(--color-background-hover);
254
+ border-radius: var(--border-radius-large, 10px);
255
+ padding: 1rem;
256
+ display: flex;
257
+ flex-direction: column;
258
+ align-items: center;
259
+ text-decoration: none;
260
+ color: inherit;
261
+ border: 2px solid transparent;
262
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
263
+ height: 100%;
264
+ width: 100%;
265
+ box-sizing: border-box;
266
+ overflow: hidden;
267
+ min-width: 0;
268
+ }
269
+
270
+ .cn-stats-block--horizontal {
271
+ flex-direction: row;
272
+ align-items: center;
273
+ gap: 12px;
274
+ }
275
+
276
+ .cn-stats-block--horizontal .cn-stats-block__content {
277
+ text-align: left;
278
+ min-width: 0;
279
+ }
280
+
281
+ .cn-stats-block--horizontal .cn-stats-block__count {
282
+ justify-content: flex-start;
283
+ }
284
+
285
+ .cn-stats-block--clickable {
286
+ cursor: pointer;
287
+ }
288
+
289
+ .cn-stats-block--clickable:hover {
290
+ border-color: var(--color-primary-element);
291
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
292
+ }
293
+
294
+ .cn-stats-block--clickable:focus-visible {
295
+ outline: 2px solid var(--color-primary-element);
296
+ outline-offset: 2px;
297
+ }
298
+
299
+ /* Icon */
300
+ .cn-stats-block__icon {
301
+ display: flex;
302
+ align-items: center;
303
+ justify-content: center;
304
+ width: 44px;
305
+ height: 44px;
306
+ border-radius: 50%;
307
+ background: var(--color-primary-element-light, rgba(0, 130, 201, 0.1));
308
+ color: var(--color-primary-element);
309
+ flex-shrink: 0;
310
+ margin-bottom: 8px;
311
+ }
312
+
313
+ .cn-stats-block--horizontal .cn-stats-block__icon {
314
+ margin-bottom: 0;
315
+ width: 36px;
316
+ height: 36px;
317
+ }
318
+
319
+ .cn-stats-block__icon--primary {
320
+ background: var(--color-primary-element-light, rgba(0, 130, 201, 0.1));
321
+ color: var(--color-primary-element);
322
+ }
323
+
324
+ .cn-stats-block__icon--success {
325
+ background: rgba(70, 186, 97, 0.1);
326
+ color: var(--color-element-success, var(--color-success));
327
+ }
328
+
329
+ .cn-stats-block__icon--warning {
330
+ background: rgba(232, 163, 24, 0.1);
331
+ color: var(--color-element-warning, var(--color-warning));
332
+ }
333
+
334
+ .cn-stats-block__icon--error {
335
+ background: rgba(224, 36, 36, 0.1);
336
+ color: var(--color-element-error, var(--color-error));
337
+ }
338
+
339
+ /* Content */
340
+ .cn-stats-block__content {
341
+ flex: 1;
342
+ min-width: 0;
343
+ text-align: center;
344
+ }
345
+
346
+ .cn-stats-block__header h4 {
347
+ margin-top: 0;
348
+ margin-bottom: 0.25rem;
349
+ color: var(--color-main-text);
350
+ font-size: 14px;
351
+ font-weight: 600;
352
+ white-space: nowrap;
353
+ overflow: hidden;
354
+ text-overflow: ellipsis;
355
+ }
356
+
357
+ .cn-stats-block__count {
358
+ display: flex;
359
+ align-items: baseline;
360
+ justify-content: center;
361
+ gap: 0.25rem;
362
+ font-size: 1.2rem;
363
+ margin-bottom: 0.25rem;
364
+ white-space: nowrap;
365
+ overflow: hidden;
366
+ }
367
+
368
+ .cn-stats-block__count-value {
369
+ font-size: 2rem;
370
+ font-weight: bold;
371
+ color: var(--color-primary-element);
372
+ flex-shrink: 0;
373
+ }
374
+
375
+ .cn-stats-block--primary .cn-stats-block__count-value { color: var(--color-primary-element); }
376
+ .cn-stats-block--success .cn-stats-block__count-value { color: var(--color-element-success, var(--color-success)); }
377
+ .cn-stats-block--warning .cn-stats-block__count-value { color: var(--color-element-warning, var(--color-warning)); }
378
+ .cn-stats-block--error .cn-stats-block__count-value { color: var(--color-element-error, var(--color-error)); }
379
+
380
+ .cn-stats-block__count-label {
381
+ color: var(--color-text-maxcontrast);
382
+ overflow: hidden;
383
+ text-overflow: ellipsis;
384
+ }
385
+
386
+ .cn-stats-block__loading {
387
+ display: flex;
388
+ align-items: center;
389
+ justify-content: center;
390
+ gap: 0.5rem;
391
+ color: var(--color-text-maxcontrast);
392
+ margin-bottom: 0.5rem;
393
+ }
394
+
395
+ .cn-stats-block__empty {
396
+ text-align: center;
397
+ color: var(--color-text-maxcontrast);
398
+ font-style: italic;
399
+ margin-bottom: 0.5rem;
400
+ }
401
+
402
+ .cn-stats-block__breakdown {
403
+ margin-top: 0.5rem;
404
+ padding: 0.75rem;
405
+ background: var(--color-background-hover);
406
+ border-radius: var(--border-radius);
407
+ border: 1px solid var(--color-border);
408
+ width: 100%;
409
+ }
410
+
411
+ .cn-stats-block__breakdown-item {
412
+ display: flex;
413
+ justify-content: space-between;
414
+ align-items: center;
415
+ margin-bottom: 0.5rem;
416
+ }
417
+
418
+ .cn-stats-block__breakdown-item:last-child {
419
+ margin-bottom: 0;
420
+ }
421
+
422
+ .cn-stats-block__breakdown-label {
423
+ font-weight: 500;
424
+ color: var(--color-main-text);
425
+ }
426
+
427
+ .cn-stats-block__breakdown-value {
428
+ font-weight: 600;
429
+ padding: 0.25rem 0.5rem;
430
+ border-radius: var(--border-radius);
431
+ background: var(--color-background-hover);
432
+ }
433
+
434
+ .cn-stats-block__breakdown-value--invalid { color: var(--color-element-warning); }
435
+ .cn-stats-block__breakdown-value--deleted { color: var(--color-element-error); }
436
+ .cn-stats-block__breakdown-value--published { color: var(--color-element-success); }
437
+ </style>