@conduction/nextcloud-vue 0.1.0-beta.1

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 (93) hide show
  1. package/dist/nextcloud-vue.cjs.js +10710 -0
  2. package/dist/nextcloud-vue.cjs.js.map +1 -0
  3. package/dist/nextcloud-vue.css +803 -0
  4. package/dist/nextcloud-vue.esm.js +10665 -0
  5. package/dist/nextcloud-vue.esm.js.map +1 -0
  6. package/package.json +63 -0
  7. package/src/components/CnCardGrid/CnCardGrid.vue +152 -0
  8. package/src/components/CnCardGrid/index.js +1 -0
  9. package/src/components/CnCellRenderer/CnCellRenderer.vue +132 -0
  10. package/src/components/CnCellRenderer/index.js +1 -0
  11. package/src/components/CnConfigurationCard/CnConfigurationCard.vue +77 -0
  12. package/src/components/CnConfigurationCard/index.js +1 -0
  13. package/src/components/CnDataTable/CnDataTable.vue +354 -0
  14. package/src/components/CnDataTable/index.js +1 -0
  15. package/src/components/CnDetailViewLayout/CnDetailViewLayout.vue +88 -0
  16. package/src/components/CnDetailViewLayout/index.js +1 -0
  17. package/src/components/CnEmptyState/CnEmptyState.vue +78 -0
  18. package/src/components/CnEmptyState/index.js +1 -0
  19. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +223 -0
  20. package/src/components/CnFacetSidebar/index.js +1 -0
  21. package/src/components/CnFilterBar/CnFilterBar.vue +152 -0
  22. package/src/components/CnFilterBar/index.js +1 -0
  23. package/src/components/CnIndexPage/CnIndexPage.vue +682 -0
  24. package/src/components/CnIndexPage/index.js +1 -0
  25. package/src/components/CnKpiGrid/CnKpiGrid.vue +89 -0
  26. package/src/components/CnKpiGrid/index.js +1 -0
  27. package/src/components/CnListViewLayout/CnListViewLayout.vue +80 -0
  28. package/src/components/CnListViewLayout/index.js +1 -0
  29. package/src/components/CnMassActionBar/CnMassActionBar.vue +160 -0
  30. package/src/components/CnMassActionBar/index.js +1 -0
  31. package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +320 -0
  32. package/src/components/CnMassCopyDialog/index.js +1 -0
  33. package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +238 -0
  34. package/src/components/CnMassDeleteDialog/index.js +1 -0
  35. package/src/components/CnMassExportDialog/CnMassExportDialog.vue +190 -0
  36. package/src/components/CnMassExportDialog/index.js +1 -0
  37. package/src/components/CnMassImportDialog/CnMassImportDialog.vue +491 -0
  38. package/src/components/CnMassImportDialog/index.js +1 -0
  39. package/src/components/CnObjectCard/CnObjectCard.vue +292 -0
  40. package/src/components/CnObjectCard/index.js +1 -0
  41. package/src/components/CnPagination/CnPagination.vue +252 -0
  42. package/src/components/CnPagination/index.js +1 -0
  43. package/src/components/CnRowActions/CnRowActions.vue +73 -0
  44. package/src/components/CnRowActions/index.js +1 -0
  45. package/src/components/CnSettingsCard/CnSettingsCard.vue +92 -0
  46. package/src/components/CnSettingsCard/index.js +1 -0
  47. package/src/components/CnSettingsSection/CnSettingsSection.vue +266 -0
  48. package/src/components/CnSettingsSection/index.js +1 -0
  49. package/src/components/CnStatsBlock/CnStatsBlock.vue +366 -0
  50. package/src/components/CnStatsBlock/index.js +1 -0
  51. package/src/components/CnStatusBadge/CnStatusBadge.vue +77 -0
  52. package/src/components/CnStatusBadge/index.js +1 -0
  53. package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +312 -0
  54. package/src/components/CnVersionInfoCard/index.js +1 -0
  55. package/src/components/CnViewModeToggle/CnViewModeToggle.vue +77 -0
  56. package/src/components/CnViewModeToggle/index.js +1 -0
  57. package/src/components/index.js +25 -0
  58. package/src/composables/index.js +3 -0
  59. package/src/composables/useDetailView.js +132 -0
  60. package/src/composables/useListView.js +153 -0
  61. package/src/composables/useSubResource.js +142 -0
  62. package/src/css/badge.css +51 -0
  63. package/src/css/card.css +128 -0
  64. package/src/css/detail.css +68 -0
  65. package/src/css/index.css +8 -0
  66. package/src/css/layout.css +90 -0
  67. package/src/css/pagination.css +72 -0
  68. package/src/css/table.css +143 -0
  69. package/src/css/utilities.css +46 -0
  70. package/src/index.js +50 -0
  71. package/src/store/createSubResourcePlugin.js +135 -0
  72. package/src/store/index.js +3 -0
  73. package/src/store/plugins/auditTrails.js +17 -0
  74. package/src/store/plugins/files.js +186 -0
  75. package/src/store/plugins/index.js +4 -0
  76. package/src/store/plugins/lifecycle.js +180 -0
  77. package/src/store/plugins/relations.js +68 -0
  78. package/src/store/useObjectStore.js +625 -0
  79. package/src/types/auditTrail.d.ts +32 -0
  80. package/src/types/file.d.ts +23 -0
  81. package/src/types/index.d.ts +35 -0
  82. package/src/types/notification.d.ts +36 -0
  83. package/src/types/object.d.ts +40 -0
  84. package/src/types/organisation.d.ts +41 -0
  85. package/src/types/register.d.ts +25 -0
  86. package/src/types/schema.d.ts +39 -0
  87. package/src/types/shared.d.ts +79 -0
  88. package/src/types/source.d.ts +14 -0
  89. package/src/types/task.d.ts +31 -0
  90. package/src/utils/errors.js +96 -0
  91. package/src/utils/headers.js +44 -0
  92. package/src/utils/index.js +3 -0
  93. package/src/utils/schema.js +287 -0
@@ -0,0 +1,292 @@
1
+ <template>
2
+ <div
3
+ class="cn-object-card"
4
+ :class="{ 'cn-object-card--selected': selected }"
5
+ @click="$emit('click', object)">
6
+ <!-- Selection checkbox -->
7
+ <div v-if="selectable" class="cn-object-card__checkbox" @click.stop>
8
+ <NcCheckboxRadioSwitch
9
+ :checked="selected"
10
+ @update:checked="$emit('select', object)" />
11
+ </div>
12
+
13
+ <!-- Card content -->
14
+ <div class="cn-object-card__content">
15
+ <!-- Header: image + title -->
16
+ <div class="cn-object-card__header">
17
+ <img
18
+ v-if="imageUrl"
19
+ :src="imageUrl"
20
+ :alt="title"
21
+ class="cn-object-card__image">
22
+
23
+ <div class="cn-object-card__title-area">
24
+ <h3 class="cn-object-card__title">{{ title }}</h3>
25
+ <p v-if="description" class="cn-object-card__description">
26
+ {{ truncatedDescription }}
27
+ </p>
28
+ </div>
29
+ </div>
30
+
31
+ <!-- Badges slot -->
32
+ <div v-if="$scopedSlots.badges" class="cn-object-card__badges">
33
+ <slot name="badges" :object="object" />
34
+ </div>
35
+
36
+ <!-- Metadata: visible properties as label:value pairs -->
37
+ <div v-if="metadataFields.length > 0" class="cn-object-card__metadata">
38
+ <slot name="metadata" :object="object" :fields="metadataFields">
39
+ <div
40
+ v-for="field in metadataFields"
41
+ :key="field.key"
42
+ class="cn-object-card__meta-item">
43
+ <span class="cn-object-card__meta-label">{{ field.label }}</span>
44
+ <CnCellRenderer
45
+ :value="field.value"
46
+ :property="field.property"
47
+ :truncate="60" />
48
+ </div>
49
+ </slot>
50
+ </div>
51
+ </div>
52
+
53
+ <!-- Actions slot -->
54
+ <div v-if="$scopedSlots.actions" class="cn-object-card__actions" @click.stop>
55
+ <slot name="actions" :object="object" />
56
+ </div>
57
+ </div>
58
+ </template>
59
+
60
+ <script>
61
+ import { NcCheckboxRadioSwitch } from '@nextcloud/vue'
62
+ import { CnCellRenderer } from '../CnCellRenderer/index.js'
63
+ import { formatValue } from '../../utils/schema.js'
64
+
65
+ /**
66
+ * CnObjectCard — Schema-configuration-driven card for object display.
67
+ *
68
+ * Uses `schema.configuration` to determine which fields map to the card title,
69
+ * description, and image. Remaining visible properties are shown as metadata.
70
+ *
71
+ * @example
72
+ * <CnObjectCard :object="publication" :schema="pubSchema">
73
+ * <template #actions="{ object }">
74
+ * <NcActions><NcActionButton @click="edit(object)">Edit</NcActionButton></NcActions>
75
+ * </template>
76
+ * </CnObjectCard>
77
+ */
78
+ export default {
79
+ name: 'CnObjectCard',
80
+
81
+ components: {
82
+ NcCheckboxRadioSwitch,
83
+ CnCellRenderer,
84
+ },
85
+
86
+ props: {
87
+ /** The object data */
88
+ object: {
89
+ type: Object,
90
+ required: true,
91
+ },
92
+ /** Schema definition with properties and configuration */
93
+ schema: {
94
+ type: Object,
95
+ required: true,
96
+ },
97
+ /** Whether this card is selected */
98
+ selected: {
99
+ type: Boolean,
100
+ default: false,
101
+ },
102
+ /** Whether to show selection checkbox */
103
+ selectable: {
104
+ type: Boolean,
105
+ default: false,
106
+ },
107
+ /** Maximum number of metadata fields to show */
108
+ maxMetadata: {
109
+ type: Number,
110
+ default: 4,
111
+ },
112
+ },
113
+
114
+ computed: {
115
+ config() {
116
+ return this.schema?.configuration || {}
117
+ },
118
+
119
+ title() {
120
+ const field = this.config.objectNameField
121
+ if (field && this.object[field]) {
122
+ return String(this.object[field])
123
+ }
124
+ return this.object.title || this.object.name || this.object.id || '—'
125
+ },
126
+
127
+ description() {
128
+ const field = this.config.objectDescriptionField
129
+ if (field && this.object[field]) {
130
+ return String(this.object[field])
131
+ }
132
+ return null
133
+ },
134
+
135
+ truncatedDescription() {
136
+ if (!this.description) return null
137
+ if (this.description.length > 120) {
138
+ return this.description.substring(0, 120) + '...'
139
+ }
140
+ return this.description
141
+ },
142
+
143
+ imageUrl() {
144
+ const field = this.config.objectImageField
145
+ if (field && this.object[field]) {
146
+ return this.object[field]
147
+ }
148
+ return null
149
+ },
150
+
151
+ /** Fields excluded from metadata (already shown as title/desc/image) */
152
+ configFields() {
153
+ return [
154
+ this.config.objectNameField,
155
+ this.config.objectDescriptionField,
156
+ this.config.objectSummaryField,
157
+ this.config.objectImageField,
158
+ ].filter(Boolean)
159
+ },
160
+
161
+ /** Remaining visible properties for the metadata section */
162
+ metadataFields() {
163
+ if (!this.schema?.properties) return []
164
+
165
+ return Object.entries(this.schema.properties)
166
+ .filter(([key, prop]) => {
167
+ if (this.configFields.includes(key)) return false
168
+ if (prop.visible === false) return false
169
+ if (prop.type === 'object') return false
170
+ if (prop.format === 'markdown') return false
171
+ return true
172
+ })
173
+ .sort(([, a], [, b]) => {
174
+ const orderA = typeof a.order === 'number' ? a.order : Infinity
175
+ const orderB = typeof b.order === 'number' ? b.order : Infinity
176
+ return orderA - orderB
177
+ })
178
+ .slice(0, this.maxMetadata)
179
+ .map(([key, prop]) => ({
180
+ key,
181
+ label: prop.title || key,
182
+ value: this.object[key],
183
+ property: prop,
184
+ }))
185
+ },
186
+ },
187
+
188
+ methods: {
189
+ formatValue,
190
+ },
191
+ }
192
+ </script>
193
+
194
+ <style scoped>
195
+ .cn-object-card {
196
+ display: flex;
197
+ gap: 12px;
198
+ padding: 16px;
199
+ background: var(--color-main-background);
200
+ border: 1px solid var(--color-border);
201
+ border-radius: var(--border-radius-large, 10px);
202
+ cursor: pointer;
203
+ transition: box-shadow 0.2s ease, border-color 0.2s ease;
204
+ }
205
+
206
+ .cn-object-card:hover {
207
+ border-color: var(--color-primary-element);
208
+ box-shadow: 0 2px 8px var(--color-box-shadow);
209
+ }
210
+
211
+ .cn-object-card--selected {
212
+ border-color: var(--color-primary-element);
213
+ background: var(--color-primary-element-light);
214
+ }
215
+
216
+ .cn-object-card__checkbox {
217
+ flex-shrink: 0;
218
+ padding-top: 2px;
219
+ }
220
+
221
+ .cn-object-card__content {
222
+ flex: 1;
223
+ min-width: 0;
224
+ }
225
+
226
+ .cn-object-card__header {
227
+ display: flex;
228
+ gap: 12px;
229
+ align-items: flex-start;
230
+ }
231
+
232
+ .cn-object-card__image {
233
+ width: 48px;
234
+ height: 48px;
235
+ border-radius: var(--border-radius);
236
+ object-fit: cover;
237
+ flex-shrink: 0;
238
+ }
239
+
240
+ .cn-object-card__title-area {
241
+ flex: 1;
242
+ min-width: 0;
243
+ }
244
+
245
+ .cn-object-card__title {
246
+ margin: 0;
247
+ font-size: 16px;
248
+ font-weight: 600;
249
+ line-height: 1.3;
250
+ overflow: hidden;
251
+ text-overflow: ellipsis;
252
+ white-space: nowrap;
253
+ }
254
+
255
+ .cn-object-card__description {
256
+ margin: 4px 0 0;
257
+ font-size: 13px;
258
+ color: var(--color-text-maxcontrast);
259
+ line-height: 1.4;
260
+ }
261
+
262
+ .cn-object-card__badges {
263
+ margin-top: 8px;
264
+ }
265
+
266
+ .cn-object-card__metadata {
267
+ display: grid;
268
+ grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
269
+ gap: 8px;
270
+ margin-top: 12px;
271
+ padding-top: 12px;
272
+ border-top: 1px solid var(--color-border);
273
+ }
274
+
275
+ .cn-object-card__meta-item {
276
+ display: flex;
277
+ flex-direction: column;
278
+ gap: 2px;
279
+ }
280
+
281
+ .cn-object-card__meta-label {
282
+ font-size: 11px;
283
+ font-weight: 500;
284
+ color: var(--color-text-maxcontrast);
285
+ text-transform: uppercase;
286
+ letter-spacing: 0.3px;
287
+ }
288
+
289
+ .cn-object-card__actions {
290
+ flex-shrink: 0;
291
+ }
292
+ </style>
@@ -0,0 +1 @@
1
+ export { default as CnObjectCard } from './CnObjectCard.vue'
@@ -0,0 +1,252 @@
1
+ <template>
2
+ <div v-if="totalPages > 1 || totalItems > minItemsToShow" class="cn-pagination">
3
+ <!-- Page info -->
4
+ <div class="cn-pagination__info">
5
+ <span class="cn-pagination__page-info">
6
+ {{ pageInfoText }}
7
+ </span>
8
+ </div>
9
+
10
+ <!-- Page navigation -->
11
+ <div v-if="totalPages > 1" class="cn-pagination__nav">
12
+ <NcButton
13
+ :disabled="currentPage === 1"
14
+ @click="changePage(1)">
15
+ {{ firstLabel }}
16
+ </NcButton>
17
+
18
+ <NcButton
19
+ :disabled="currentPage === 1"
20
+ @click="changePage(currentPage - 1)">
21
+ {{ previousLabel }}
22
+ </NcButton>
23
+
24
+ <div class="cn-pagination__numbers">
25
+ <template v-for="page in visiblePages">
26
+ <span v-if="page === '...'" :key="'ellipsis-' + page" class="cn-pagination__ellipsis">...</span>
27
+ <NcButton
28
+ v-else
29
+ :key="page"
30
+ :type="page === currentPage ? 'primary' : 'secondary'"
31
+ :disabled="page === currentPage"
32
+ @click="changePage(page)">
33
+ {{ page }}
34
+ </NcButton>
35
+ </template>
36
+ </div>
37
+
38
+ <NcButton
39
+ :disabled="currentPage === totalPages"
40
+ @click="changePage(currentPage + 1)">
41
+ {{ nextLabel }}
42
+ </NcButton>
43
+
44
+ <NcButton
45
+ :disabled="currentPage === totalPages"
46
+ @click="changePage(totalPages)">
47
+ {{ lastLabel }}
48
+ </NcButton>
49
+ </div>
50
+
51
+ <!-- Page size selector -->
52
+ <div class="cn-pagination__page-size">
53
+ <label :for="pageSizeId">{{ itemsPerPageLabel }}</label>
54
+ <NcSelect
55
+ :input-id="pageSizeId"
56
+ class="cn-pagination__page-size-select"
57
+ :value="currentPageSizeOption"
58
+ :options="pageSizeOptions"
59
+ :clearable="false"
60
+ :input-label="itemsPerPageLabel"
61
+ @option:selected="changePageSize" />
62
+ </div>
63
+ </div>
64
+ </template>
65
+
66
+ <script>
67
+ import { NcButton, NcSelect } from '@nextcloud/vue'
68
+
69
+ /**
70
+ * CnPagination — Full pagination with page numbers, navigation, and page size selector.
71
+ *
72
+ * Extracted from OpenRegister's PaginationComponent. Zero store dependencies.
73
+ * Supports First/Previous/Next/Last buttons, smart page number display with
74
+ * ellipsis, and configurable page size.
75
+ *
76
+ * NL Design tokens used:
77
+ * - Inherits from cn-pagination CSS class (see css/pagination.css)
78
+ *
79
+ * @example
80
+ * <CnPagination
81
+ * :current-page="page"
82
+ * :total-pages="totalPages"
83
+ * :total-items="totalItems"
84
+ * :current-page-size="limit"
85
+ * @page-changed="onPageChange"
86
+ * @page-size-changed="onPageSizeChange" />
87
+ */
88
+ export default {
89
+ name: 'CnPagination',
90
+
91
+ components: {
92
+ NcButton,
93
+ NcSelect,
94
+ },
95
+
96
+ props: {
97
+ /** Current page number (1-based) */
98
+ currentPage: {
99
+ type: Number,
100
+ default: 1,
101
+ },
102
+ /** Total number of pages */
103
+ totalPages: {
104
+ type: Number,
105
+ default: 1,
106
+ },
107
+ /** Total number of items across all pages */
108
+ totalItems: {
109
+ type: Number,
110
+ default: 0,
111
+ },
112
+ /** Current items per page */
113
+ currentPageSize: {
114
+ type: Number,
115
+ default: 20,
116
+ },
117
+ /** Available page size options */
118
+ pageSizeOptions: {
119
+ type: Array,
120
+ default: () => [
121
+ { value: 10, label: '10' },
122
+ { value: 20, label: '20' },
123
+ { value: 50, label: '50' },
124
+ { value: 100, label: '100' },
125
+ { value: 250, label: '250' },
126
+ { value: 500, label: '500' },
127
+ { value: 1000, label: '1000' },
128
+ ],
129
+ },
130
+ /** Minimum items before pagination is shown */
131
+ minItemsToShow: {
132
+ type: Number,
133
+ default: 10,
134
+ },
135
+ /** Label for "First" button */
136
+ firstLabel: {
137
+ type: String,
138
+ default: 'First',
139
+ },
140
+ /** Label for "Previous" button */
141
+ previousLabel: {
142
+ type: String,
143
+ default: 'Previous',
144
+ },
145
+ /** Label for "Next" button */
146
+ nextLabel: {
147
+ type: String,
148
+ default: 'Next',
149
+ },
150
+ /** Label for "Last" button */
151
+ lastLabel: {
152
+ type: String,
153
+ default: 'Last',
154
+ },
155
+ /** Label for "Items per page:" */
156
+ itemsPerPageLabel: {
157
+ type: String,
158
+ default: 'Items per page:',
159
+ },
160
+ /**
161
+ * Page info format string. Use {current} and {total} as placeholders.
162
+ * @example "Page {current} of {total}"
163
+ */
164
+ pageInfoFormat: {
165
+ type: String,
166
+ default: 'Page {current} of {total}',
167
+ },
168
+ },
169
+
170
+ computed: {
171
+ pageSizeId() {
172
+ return 'cn-page-size-' + this._uid
173
+ },
174
+
175
+ currentPageSizeOption() {
176
+ return this.pageSizeOptions.find(
177
+ (option) => option.value === this.currentPageSize,
178
+ ) || this.pageSizeOptions[1]
179
+ },
180
+
181
+ pageInfoText() {
182
+ return this.pageInfoFormat
183
+ .replace('{current}', this.currentPage)
184
+ .replace('{total}', this.totalPages)
185
+ },
186
+
187
+ /**
188
+ * Calculate visible page numbers with ellipsis for large page counts.
189
+ * Shows up to 7 page numbers at a time.
190
+ */
191
+ visiblePages() {
192
+ const current = this.currentPage
193
+ const total = this.totalPages
194
+ const pages = []
195
+
196
+ if (total <= 7) {
197
+ for (let i = 1; i <= total; i++) {
198
+ pages.push(i)
199
+ }
200
+ } else {
201
+ pages.push(1)
202
+
203
+ if (current <= 4) {
204
+ for (let i = 2; i <= 5; i++) {
205
+ pages.push(i)
206
+ }
207
+ pages.push('...')
208
+ pages.push(total)
209
+ } else if (current >= total - 3) {
210
+ pages.push('...')
211
+ for (let i = total - 4; i <= total; i++) {
212
+ pages.push(i)
213
+ }
214
+ } else {
215
+ pages.push('...')
216
+ for (let i = current - 1; i <= current + 1; i++) {
217
+ pages.push(i)
218
+ }
219
+ pages.push('...')
220
+ pages.push(total)
221
+ }
222
+ }
223
+
224
+ return pages
225
+ },
226
+ },
227
+
228
+ methods: {
229
+ /**
230
+ * Navigate to a specific page.
231
+ * @param {number} page Target page number
232
+ */
233
+ changePage(page) {
234
+ if (page !== this.currentPage && page >= 1 && page <= this.totalPages) {
235
+ /** @event page-changed Emitted when page changes. Payload: new page number. */
236
+ this.$emit('page-changed', page)
237
+ }
238
+ },
239
+
240
+ /**
241
+ * Change the page size.
242
+ * @param {object} option Selected page size option { value, label }
243
+ */
244
+ changePageSize(option) {
245
+ if (option.value !== this.currentPageSize) {
246
+ /** @event page-size-changed Emitted when page size changes. Payload: new page size. */
247
+ this.$emit('page-size-changed', option.value)
248
+ }
249
+ },
250
+ },
251
+ }
252
+ </script>
@@ -0,0 +1 @@
1
+ export { default as CnPagination } from './CnPagination.vue'
@@ -0,0 +1,73 @@
1
+ <template>
2
+ <NcActions :force-menu="actions.length > 3">
3
+ <NcActionButton
4
+ v-for="action in actions"
5
+ :key="action.label"
6
+ :disabled="action.disabled"
7
+ :class="{ 'cn-row-action--destructive': action.destructive }"
8
+ @click="onAction(action)">
9
+ <template v-if="action.icon" #icon>
10
+ <component :is="action.icon" :size="20" />
11
+ </template>
12
+ {{ action.label }}
13
+ </NcActionButton>
14
+ </NcActions>
15
+ </template>
16
+
17
+ <script>
18
+ import { NcActions, NcActionButton } from '@nextcloud/vue'
19
+
20
+ /**
21
+ * CnRowActions — Action menu wrapper for table rows and cards.
22
+ *
23
+ * Wraps NcActions + NcActionButton for consistent row/card action menus.
24
+ * Actions are defined as an array of objects with label, icon, handler, etc.
25
+ *
26
+ * @example
27
+ * <CnRowActions
28
+ * :actions="[
29
+ * { label: 'Edit', icon: PencilIcon, handler: (row) => editRow(row) },
30
+ * { label: 'Delete', icon: TrashIcon, handler: (row) => deleteRow(row), destructive: true },
31
+ * ]"
32
+ * :row="row" />
33
+ */
34
+ export default {
35
+ name: 'CnRowActions',
36
+
37
+ components: {
38
+ NcActions,
39
+ NcActionButton,
40
+ },
41
+
42
+ props: {
43
+ /**
44
+ * Action definitions.
45
+ * @type {Array<{label: string, icon?: Component, handler: Function, disabled?: boolean, destructive?: boolean}>}
46
+ */
47
+ actions: {
48
+ type: Array,
49
+ default: () => [],
50
+ },
51
+ /** The row/object data (passed to action handlers) */
52
+ row: {
53
+ type: Object,
54
+ default: null,
55
+ },
56
+ },
57
+
58
+ methods: {
59
+ onAction(action) {
60
+ if (action.handler && typeof action.handler === 'function') {
61
+ action.handler(this.row)
62
+ }
63
+ this.$emit('action', { action: action.label, row: this.row })
64
+ },
65
+ },
66
+ }
67
+ </script>
68
+
69
+ <style scoped>
70
+ .cn-row-action--destructive {
71
+ color: var(--color-error) !important;
72
+ }
73
+ </style>
@@ -0,0 +1 @@
1
+ export { default as CnRowActions } from './CnRowActions.vue'
@@ -0,0 +1,92 @@
1
+ <template>
2
+ <div class="cn-settings-card" :class="{ 'cn-settings-card--collapsible': collapsible }">
3
+ <h4
4
+ v-if="title"
5
+ :class="{ 'cn-settings-card__header--clickable': collapsible }"
6
+ @click="collapsible ? toggleCollapsed() : null">
7
+ <span>{{ icon }} {{ title }}</span>
8
+ <ChevronDown
9
+ v-if="collapsible && !isCollapsed"
10
+ :size="20"
11
+ class="cn-settings-card__chevron" />
12
+ <ChevronUp
13
+ v-if="collapsible && isCollapsed"
14
+ :size="20"
15
+ class="cn-settings-card__chevron" />
16
+ </h4>
17
+
18
+ <transition v-if="collapsible" name="cn-slide-fade">
19
+ <div v-show="!isCollapsed" class="cn-settings-card__content">
20
+ <slot />
21
+ </div>
22
+ </transition>
23
+
24
+ <div v-else>
25
+ <slot />
26
+ </div>
27
+ </div>
28
+ </template>
29
+
30
+ <script>
31
+ import ChevronDown from 'vue-material-design-icons/ChevronDown.vue'
32
+ import ChevronUp from 'vue-material-design-icons/ChevronUp.vue'
33
+
34
+ /**
35
+ * CnSettingsCard — Collapsible card for settings and configuration sections.
36
+ *
37
+ * Extracted from OpenRegister's SettingsCard. Provides a titled card with
38
+ * optional collapse/expand animation.
39
+ *
40
+ * @example
41
+ * <CnSettingsCard title="Database Settings" icon="🗄️" collapsible>
42
+ * <p>Content here</p>
43
+ * </CnSettingsCard>
44
+ */
45
+ export default {
46
+ name: 'CnSettingsCard',
47
+
48
+ components: {
49
+ ChevronDown,
50
+ ChevronUp,
51
+ },
52
+
53
+ props: {
54
+ /** Card title text */
55
+ title: {
56
+ type: String,
57
+ default: '',
58
+ },
59
+ /** Icon emoji or text displayed before the title */
60
+ icon: {
61
+ type: String,
62
+ default: '',
63
+ },
64
+ /** Whether the card can be collapsed */
65
+ collapsible: {
66
+ type: Boolean,
67
+ default: false,
68
+ },
69
+ /** Whether the card starts collapsed (only applies when collapsible) */
70
+ defaultCollapsed: {
71
+ type: Boolean,
72
+ default: false,
73
+ },
74
+ },
75
+
76
+ data() {
77
+ return {
78
+ isCollapsed: this.defaultCollapsed,
79
+ }
80
+ },
81
+
82
+ methods: {
83
+ toggleCollapsed() {
84
+ if (this.collapsible) {
85
+ this.isCollapsed = !this.isCollapsed
86
+ /** @event toggle Emitted when collapse state changes. Payload: isCollapsed boolean. */
87
+ this.$emit('toggle', this.isCollapsed)
88
+ }
89
+ },
90
+ },
91
+ }
92
+ </script>