@conduction/nextcloud-vue 0.1.0-beta.4 → 0.1.0-beta.5

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 (68) hide show
  1. package/dist/nextcloud-vue.cjs +67614 -0
  2. package/dist/nextcloud-vue.cjs.js +9554 -8980
  3. package/dist/nextcloud-vue.cjs.js.map +1 -1
  4. package/dist/nextcloud-vue.cjs.map +1 -0
  5. package/dist/nextcloud-vue.css +1231 -1231
  6. package/dist/nextcloud-vue.esm.js +9554 -8980
  7. package/dist/nextcloud-vue.esm.js.map +1 -1
  8. package/package.json +11 -4
  9. package/src/components/CnActionsBar/CnActionsBar.vue +235 -235
  10. package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +579 -579
  11. package/src/components/CnAdvancedFormDialog/CnDataTab.vue +217 -217
  12. package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +121 -121
  13. package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +418 -418
  14. package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +247 -247
  15. package/src/components/CnCardGrid/CnCardGrid.vue +152 -152
  16. package/src/components/CnCellRenderer/CnCellRenderer.vue +132 -132
  17. package/src/components/CnChartWidget/CnChartWidget.vue +320 -320
  18. package/src/components/CnConfigurationCard/CnConfigurationCard.vue +77 -77
  19. package/src/components/CnCopyDialog/CnCopyDialog.vue +250 -250
  20. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +225 -225
  21. package/src/components/CnDashboardPage/CnDashboardPage.vue +390 -390
  22. package/src/components/CnDataTable/CnDataTable.vue +349 -349
  23. package/src/components/CnDeleteDialog/CnDeleteDialog.vue +170 -170
  24. package/src/components/CnDetailCard/CnDetailCard.vue +214 -214
  25. package/src/components/CnDetailPage/CnDetailPage.vue +285 -281
  26. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +231 -231
  27. package/src/components/CnFilterBar/CnFilterBar.vue +152 -152
  28. package/src/components/CnFormDialog/CnFormDialog.vue +302 -11
  29. package/src/components/CnIcon/CnIcon.vue +89 -89
  30. package/src/components/CnIndexPage/CnIndexPage.vue +884 -874
  31. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +503 -503
  32. package/src/components/CnItemCard/CnItemCard.vue +132 -132
  33. package/src/components/CnKpiGrid/CnKpiGrid.vue +89 -89
  34. package/src/components/CnMassActionBar/CnMassActionBar.vue +160 -160
  35. package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +320 -320
  36. package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +238 -238
  37. package/src/components/CnMassExportDialog/CnMassExportDialog.vue +190 -190
  38. package/src/components/CnMassImportDialog/CnMassImportDialog.vue +491 -491
  39. package/src/components/CnNoteCard/CnNoteCard.vue +149 -149
  40. package/src/components/CnNotesCard/CnNotesCard.vue +413 -413
  41. package/src/components/CnObjectCard/CnObjectCard.vue +292 -292
  42. package/src/components/CnObjectSidebar/CnObjectSidebar.vue +876 -876
  43. package/src/components/CnPageHeader/CnPageHeader.vue +57 -57
  44. package/src/components/CnPagination/CnPagination.vue +252 -252
  45. package/src/components/CnRegisterMapping/CnRegisterMapping.vue +792 -792
  46. package/src/components/CnRowActions/CnRowActions.vue +95 -73
  47. package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +226 -226
  48. package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +787 -787
  49. package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +305 -305
  50. package/src/components/CnSchemaFormDialog/CnSchemaPropertyActions.vue +1398 -1398
  51. package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +236 -236
  52. package/src/components/CnSettingsCard/CnSettingsCard.vue +92 -92
  53. package/src/components/CnSettingsSection/CnSettingsSection.vue +266 -266
  54. package/src/components/CnStatsBlock/CnStatsBlock.vue +420 -420
  55. package/src/components/CnStatusBadge/CnStatusBadge.vue +77 -77
  56. package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +540 -540
  57. package/src/components/CnTasksCard/CnTasksCard.vue +373 -373
  58. package/src/components/CnTileWidget/CnTileWidget.vue +159 -159
  59. package/src/components/CnTimelineStages/CnTimelineStages.vue +292 -292
  60. package/src/components/CnUserActionMenu/CnUserActionMenu.vue +435 -435
  61. package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +312 -312
  62. package/src/components/CnWidgetRenderer/CnWidgetRenderer.vue +180 -180
  63. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +211 -211
  64. package/src/index.js +1 -1
  65. package/src/types/notification.d.ts +13 -13
  66. package/src/types/organisation.d.ts +15 -15
  67. package/src/types/schema.d.ts +13 -13
  68. package/src/types/task.d.ts +6 -6
@@ -89,18 +89,32 @@
89
89
  </span>
90
90
  </div>
91
91
 
92
- <!-- Select (enum) -->
92
+ <!-- Select (enum, supports async function) -->
93
93
  <div v-else-if="field.widget === 'select'" class="cn-form-dialog__select-wrapper">
94
94
  <label :for="'cn-form-' + field.key" class="cn-form-dialog__label">
95
95
  {{ field.label }}{{ field.required ? ' *' : '' }}
96
96
  </label>
97
97
  <NcSelect
98
98
  :input-id="'cn-form-' + field.key"
99
- :options="getEnumOptions(field)"
100
- :value="getSelectedEnumOption(field)"
99
+ :options="getEffectiveOptions(field)"
100
+ :value="getEffectiveSelectedOption(field)"
101
101
  :clearable="!field.required"
102
102
  :disabled="field.readOnly"
103
- @input="onSelectChange(field.key, $event)" />
103
+ :loading="isFieldLoading(field)"
104
+ :filterable="!isAsyncEnum(field)"
105
+ @input="onEffectiveSelectChange(field, $event)"
106
+ @search="isAsyncEnum(field) ? onAsyncSearch(field, $event) : undefined">
107
+ <template
108
+ v-if="$scopedSlots['field-' + field.key + '-option']"
109
+ #option="optionProps">
110
+ <slot :name="'field-' + field.key + '-option'" v-bind="optionProps" />
111
+ </template>
112
+ <template
113
+ v-if="$scopedSlots['field-' + field.key + '-selected-option']"
114
+ #selected-option="optionProps">
115
+ <slot :name="'field-' + field.key + '-selected-option'" v-bind="optionProps" />
116
+ </template>
117
+ </NcSelect>
104
118
  <span
105
119
  v-if="errors[field.key] || field.description"
106
120
  class="cn-form-dialog__helper"
@@ -109,19 +123,33 @@
109
123
  </span>
110
124
  </div>
111
125
 
112
- <!-- Multiselect (array with enum items) -->
126
+ <!-- Multiselect (array with enum items, supports async function) -->
113
127
  <div v-else-if="field.widget === 'multiselect'" class="cn-form-dialog__select-wrapper">
114
128
  <label :for="'cn-form-' + field.key" class="cn-form-dialog__label">
115
129
  {{ field.label }}{{ field.required ? ' *' : '' }}
116
130
  </label>
117
131
  <NcSelect
118
132
  :input-id="'cn-form-' + field.key"
119
- :options="getArrayEnumOptions(field)"
120
- :value="getSelectedArrayOptions(field)"
133
+ :options="getEffectiveArrayOptions(field)"
134
+ :value="getEffectiveSelectedArrayOptions(field)"
121
135
  :multiple="true"
122
136
  :clearable="true"
123
137
  :disabled="field.readOnly"
124
- @input="onMultiSelectChange(field.key, $event)" />
138
+ :loading="isFieldLoading(field)"
139
+ :filterable="!isAsyncItemsEnum(field)"
140
+ @input="onEffectiveMultiSelectChange(field, $event)"
141
+ @search="isAsyncItemsEnum(field) ? onAsyncSearch(field, $event) : undefined">
142
+ <template
143
+ v-if="$scopedSlots['field-' + field.key + '-option']"
144
+ #option="optionProps">
145
+ <slot :name="'field-' + field.key + '-option'" v-bind="optionProps" />
146
+ </template>
147
+ <template
148
+ v-if="$scopedSlots['field-' + field.key + '-selected-option']"
149
+ #selected-option="optionProps">
150
+ <slot :name="'field-' + field.key + '-selected-option'" v-bind="optionProps" />
151
+ </template>
152
+ </NcSelect>
125
153
  <span
126
154
  v-if="errors[field.key] || field.description"
127
155
  class="cn-form-dialog__helper"
@@ -130,19 +158,35 @@
130
158
  </span>
131
159
  </div>
132
160
 
133
- <!-- Tags (array, freeform) -->
161
+ <!-- Tags (array, freeform, supports async suggestions) -->
134
162
  <div v-else-if="field.widget === 'tags'" class="cn-form-dialog__select-wrapper">
135
163
  <label :for="'cn-form-' + field.key" class="cn-form-dialog__label">
136
164
  {{ field.label }}{{ field.required ? ' *' : '' }}
137
165
  </label>
166
+ <!-- TODO: restore `:options` to `asyncState[field.key]?.options` once on Vue 3 (buble doesn't support optional chaining) -->
138
167
  <NcSelect
139
168
  :input-id="'cn-form-' + field.key"
140
169
  :value="formData[field.key] || []"
170
+ :options="isFieldAsync(field) ? ((asyncState[field.key] && asyncState[field.key].options) || []) : []"
141
171
  :multiple="true"
142
172
  :taggable="true"
143
173
  :clearable="true"
144
174
  :disabled="field.readOnly"
145
- @input="updateField(field.key, $event)" />
175
+ :loading="isFieldLoading(field)"
176
+ :filterable="!isFieldAsync(field)"
177
+ @input="updateField(field.key, $event)"
178
+ @search="isFieldAsync(field) ? onAsyncSearch(field, $event) : undefined">
179
+ <template
180
+ v-if="$scopedSlots['field-' + field.key + '-option']"
181
+ #option="optionProps">
182
+ <slot :name="'field-' + field.key + '-option'" v-bind="optionProps" />
183
+ </template>
184
+ <template
185
+ v-if="$scopedSlots['field-' + field.key + '-selected-option']"
186
+ #selected-option="optionProps">
187
+ <slot :name="'field-' + field.key + '-selected-option'" v-bind="optionProps" />
188
+ </template>
189
+ </NcSelect>
146
190
  <span
147
191
  v-if="errors[field.key] || field.description"
148
192
  class="cn-form-dialog__helper"
@@ -235,8 +279,24 @@ import { fieldsFromSchema } from '../../utils/schema.js'
235
279
  *
236
280
  * - `#form` — Replace the entire form content
237
281
  * - `#field-{key}` — Replace a single auto-generated field
282
+ * - `#field-{key}-option` — Customize dropdown option rendering for a select/multiselect/tags field
283
+ * - `#field-{key}-selected-option` — Customize selected option display for a select/multiselect/tags field
238
284
  * - `#before-fields` / `#after-fields` — Inject content around fields
239
285
  *
286
+ * ## Async select support
287
+ *
288
+ * Select, multiselect, and tags fields support async options by setting `field.enum`
289
+ * (or `field.items.enum` for multiselect) to an async function instead of a static array:
290
+ *
291
+ * ```js
292
+ * { key: 'org', widget: 'select', enum: async (query) => fetchOrgs(query) }
293
+ * ```
294
+ *
295
+ * The function receives the search query and must return an array of option objects
296
+ * (each must have a `label` property for default display). Options are loaded on mount
297
+ * (with empty query) and on each search input (debounced, default 300ms, configurable
298
+ * via `field.debounce`). Async selects store the full option object in formData.
299
+ *
240
300
  * The dialog does NOT perform the save itself — it emits a `confirm` event
241
301
  * with the form data. The parent performs the actual API call and calls
242
302
  * `setResult()` via a ref.
@@ -266,6 +326,27 @@ import { fieldsFromSchema } from '../../utils/schema.js'
266
326
  * this.$refs.formDialog.setResult({ error: e.message })
267
327
  * }
268
328
  * }
329
+ *
330
+ * @example <caption>Async select with custom option rendering</caption>
331
+ * <CnFormDialog :fields="[{
332
+ * key: 'organisation',
333
+ * widget: 'select',
334
+ * label: 'Organisation',
335
+ * required: true,
336
+ * enum: async (query) => {
337
+ * const results = await store.search(query)
338
+ * return results.map(o => ({ label: o.name, id: o.uuid, ...o }))
339
+ * },
340
+ * debounce: 500,
341
+ * }]" @confirm="onConfirm">
342
+ * <template #field-organisation-option="{ name, description }">
343
+ * <strong>{{ name }}</strong>
344
+ * <p>{{ description }}</p>
345
+ * </template>
346
+ * <template #field-organisation-selected-option="{ name }">
347
+ * {{ name }}
348
+ * </template>
349
+ * </CnFormDialog>
269
350
  */
270
351
  export default {
271
352
  name: 'CnFormDialog',
@@ -349,6 +430,8 @@ export default {
349
430
  loading: false,
350
431
  result: null,
351
432
  closeTimeout: null,
433
+ /** Per-field async state: { [fieldKey]: { options: [], loading: false, searchTimeout: null } } */
434
+ asyncState: {},
352
435
  }
353
436
  },
354
437
 
@@ -400,6 +483,13 @@ export default {
400
483
  },
401
484
  },
402
485
 
486
+ beforeDestroy() {
487
+ for (const state of Object.values(this.asyncState)) {
488
+ if (state.searchTimeout) clearTimeout(state.searchTimeout)
489
+ }
490
+ if (this.closeTimeout) clearTimeout(this.closeTimeout)
491
+ },
492
+
403
493
  methods: {
404
494
  initFormData(item) {
405
495
  if (item) {
@@ -422,6 +512,7 @@ export default {
422
512
  this.formData = data
423
513
  }
424
514
  this.errors = {}
515
+ this.initAsyncFields()
425
516
  },
426
517
 
427
518
  updateField(key, value) {
@@ -468,6 +559,205 @@ export default {
468
559
  this.updateField(key, (options || []).map((o) => o.id))
469
560
  },
470
561
 
562
+ /**
563
+ * Check if a field has an async enum (function instead of static array).
564
+ *
565
+ * @param {object} field The field definition
566
+ * @return {boolean}
567
+ */
568
+ isAsyncEnum(field) {
569
+ return typeof field.enum === 'function'
570
+ },
571
+
572
+ /**
573
+ * Check if an array field has an async items enum.
574
+ *
575
+ * @param {object} field The field definition
576
+ * @return {boolean}
577
+ */
578
+ isAsyncItemsEnum(field) {
579
+ return !!(field.items && typeof field.items.enum === 'function')
580
+ },
581
+
582
+ /**
583
+ * Initialize async state for all async fields and trigger initial load.
584
+ */
585
+ initAsyncFields() {
586
+ // Clean up existing timeouts
587
+ for (const state of Object.values(this.asyncState)) {
588
+ if (state.searchTimeout) clearTimeout(state.searchTimeout)
589
+ }
590
+
591
+ const newState = {}
592
+ for (const field of this.resolvedFields) {
593
+ if (this.isAsyncEnum(field) || this.isAsyncItemsEnum(field)) {
594
+ newState[field.key] = { options: [], loading: false, searchTimeout: null }
595
+ }
596
+ }
597
+ this.asyncState = newState
598
+
599
+ // Trigger initial load for each async field
600
+ this.$nextTick(() => {
601
+ for (const field of this.resolvedFields) {
602
+ if (this.isAsyncEnum(field) || this.isAsyncItemsEnum(field)) {
603
+ this.loadAsyncOptions(field, '')
604
+ }
605
+ }
606
+ })
607
+ },
608
+
609
+ /**
610
+ * Load async options for a field by calling its enum function.
611
+ *
612
+ * @param {object} field The field definition
613
+ * @param {string} query Search query
614
+ */
615
+ async loadAsyncOptions(field, query) {
616
+ const state = this.asyncState[field.key]
617
+ if (!state) return
618
+
619
+ this.$set(state, 'loading', true)
620
+
621
+ try {
622
+ const enumFn = this.isAsyncEnum(field) ? field.enum : field.items.enum
623
+ const results = await enumFn(query)
624
+ this.$set(state, 'options', Array.isArray(results) ? results : [])
625
+ } catch (err) {
626
+ console.error(`CnFormDialog: async enum error for field "${field.key}":`, err)
627
+ this.$set(state, 'options', [])
628
+ } finally {
629
+ this.$set(state, 'loading', false)
630
+ }
631
+ },
632
+
633
+ /**
634
+ * Handle search input on an async select with debounce.
635
+ *
636
+ * @param {object} field The field definition
637
+ * @param {string} query Search query
638
+ */
639
+ onAsyncSearch(field, query) {
640
+ const state = this.asyncState[field.key]
641
+ if (!state) return
642
+
643
+ if (state.searchTimeout) {
644
+ clearTimeout(state.searchTimeout)
645
+ }
646
+
647
+ const debounceMs = field.debounce || 300
648
+
649
+ state.searchTimeout = setTimeout(() => {
650
+ this.loadAsyncOptions(field, query || '')
651
+ }, debounceMs)
652
+ },
653
+
654
+ /**
655
+ * Get the effective options for a select field (async or static).
656
+ *
657
+ * @param {object} field The field definition
658
+ * @return {Array}
659
+ */
660
+ getEffectiveOptions(field) {
661
+ if (this.isAsyncEnum(field)) {
662
+ // TODO: restore to `this.asyncState[field.key]?.options || []` once on Vue 3 (buble doesn't support optional chaining)
663
+ return (this.asyncState[field.key] && this.asyncState[field.key].options) || []
664
+ }
665
+ return this.getEnumOptions(field)
666
+ },
667
+
668
+ /**
669
+ * Get the effective selected value for a select field (async or static).
670
+ *
671
+ * @param {object} field The field definition
672
+ * @return {object|null}
673
+ */
674
+ getEffectiveSelectedOption(field) {
675
+ if (this.isAsyncEnum(field)) {
676
+ // For async fields, formData stores the full option object
677
+ return this.formData[field.key] || null
678
+ }
679
+ return this.getSelectedEnumOption(field)
680
+ },
681
+
682
+ /**
683
+ * Handle select change for both async and static fields.
684
+ *
685
+ * @param {object} field The field definition
686
+ * @param {object|null} option The selected option
687
+ */
688
+ onEffectiveSelectChange(field, option) {
689
+ if (this.isAsyncEnum(field)) {
690
+ // Store full option object for async selects
691
+ this.updateField(field.key, option || null)
692
+ } else {
693
+ this.onSelectChange(field.key, option)
694
+ }
695
+ },
696
+
697
+ /**
698
+ * Get effective options for a multiselect field (async or static).
699
+ *
700
+ * @param {object} field The field definition
701
+ * @return {Array}
702
+ */
703
+ getEffectiveArrayOptions(field) {
704
+ if (this.isAsyncItemsEnum(field)) {
705
+ // TODO: restore to `this.asyncState[field.key]?.options || []` once on Vue 3 (buble doesn't support optional chaining)
706
+ return (this.asyncState[field.key] && this.asyncState[field.key].options) || []
707
+ }
708
+ return this.getArrayEnumOptions(field)
709
+ },
710
+
711
+ /**
712
+ * Get effective selected values for a multiselect field (async or static).
713
+ *
714
+ * @param {object} field The field definition
715
+ * @return {Array}
716
+ */
717
+ getEffectiveSelectedArrayOptions(field) {
718
+ if (this.isAsyncItemsEnum(field)) {
719
+ // For async fields, formData stores array of full option objects
720
+ return this.formData[field.key] || []
721
+ }
722
+ return this.getSelectedArrayOptions(field)
723
+ },
724
+
725
+ /**
726
+ * Handle multiselect change for both async and static fields.
727
+ *
728
+ * @param {object} field The field definition
729
+ * @param {Array} options The selected options
730
+ */
731
+ onEffectiveMultiSelectChange(field, options) {
732
+ if (this.isAsyncItemsEnum(field)) {
733
+ // Store full option objects for async multiselect
734
+ this.updateField(field.key, options || [])
735
+ } else {
736
+ this.onMultiSelectChange(field.key, options)
737
+ }
738
+ },
739
+
740
+ /**
741
+ * Whether a field's async options are currently loading.
742
+ *
743
+ * @param {object} field The field definition
744
+ * @return {boolean}
745
+ */
746
+ isFieldLoading(field) {
747
+ // TODO: restore to `this.asyncState[field.key]?.loading || false` once on Vue 3 (buble doesn't support optional chaining)
748
+ return (this.asyncState[field.key] && this.asyncState[field.key].loading) || false
749
+ },
750
+
751
+ /**
752
+ * Whether a field has any async behavior (enum or items.enum is a function).
753
+ *
754
+ * @param {object} field The field definition
755
+ * @return {boolean}
756
+ */
757
+ isFieldAsync(field) {
758
+ return this.isAsyncEnum(field) || this.isAsyncItemsEnum(field)
759
+ },
760
+
471
761
  /**
472
762
  * Run client-side validation on all form fields.
473
763
  * Checks required, minLength, maxLength, pattern, minimum, maximum.
@@ -508,7 +798,8 @@ export default {
508
798
  if (!new RegExp(v.pattern).test(value)) {
509
799
  newErrors[field.key] = 'Invalid format.'
510
800
  }
511
- } catch {
801
+ // TODO: restore to `catch {` (optional catch binding) once on Vue 3 (buble doesn't support it)
802
+ } catch (e) {
512
803
  // Ignore invalid regex patterns
513
804
  }
514
805
  }
@@ -1,89 +1,89 @@
1
- <template>
2
- <component :is="resolvedComponent" :size="size" />
3
- </template>
4
-
5
- <script>
6
- import HelpCircleOutline from 'vue-material-design-icons/HelpCircleOutline.vue'
7
-
8
- /**
9
- * Mutable icon registry.
10
- *
11
- * Pre-populated with HelpCircleOutline (the default fallback).
12
- * Apps extend this at boot via registerIcons() — import only the
13
- * icons you need, keeping bundles small.
14
- *
15
- * @example
16
- * import { registerIcons } from '@conduction/nextcloud-vue'
17
- * import Sword from 'vue-material-design-icons/Sword.vue'
18
- * registerIcons({ Sword })
19
- */
20
- const _registry = {
21
- HelpCircleOutline,
22
- }
23
-
24
- /**
25
- * Register one or more MDI icon components for use with CnIcon.
26
- *
27
- * Call this in your app's main.js before mounting the Vue instance.
28
- * Each key must be the PascalCase icon name matching the
29
- * vue-material-design-icons file name (e.g. "Sword" for Sword.vue).
30
- *
31
- * @param {Record<string, import('vue').Component>} icons
32
- *
33
- * @example
34
- * import { registerIcons } from '@conduction/nextcloud-vue'
35
- * import Sword from 'vue-material-design-icons/Sword.vue'
36
- * import MagicStaff from 'vue-material-design-icons/MagicStaff.vue'
37
- * registerIcons({ Sword, MagicStaff })
38
- */
39
- export function registerIcons(icons) {
40
- Object.assign(_registry, icons)
41
- }
42
-
43
- /**
44
- * Read-only reference to the current icon registry.
45
- * Useful for checking which icons are available.
46
- *
47
- * @type {Record<string, import('vue').Component>}
48
- */
49
- export const ICON_MAP = _registry
50
-
51
- /**
52
- * CnIcon — Renders a Material Design Icon by PascalCase name.
53
- *
54
- * Looks up the name in the shared registry. If not found, renders
55
- * the fallback icon (HelpCircleOutline by default).
56
- *
57
- * @example
58
- * <CnIcon name="AccountGroup" :size="24" />
59
- *
60
- * @see https://pictogrammers.com/library/mdi/
61
- */
62
- export default {
63
- name: 'CnIcon',
64
-
65
- props: {
66
- /** MDI icon name in PascalCase (e.g. "AccountGroup") */
67
- name: {
68
- type: String,
69
- required: true,
70
- },
71
- /** Icon pixel size */
72
- size: {
73
- type: Number,
74
- default: 20,
75
- },
76
- /** Fallback icon name if `name` is not found in the registry */
77
- fallback: {
78
- type: String,
79
- default: 'HelpCircleOutline',
80
- },
81
- },
82
-
83
- computed: {
84
- resolvedComponent() {
85
- return _registry[this.name] || _registry[this.fallback] || HelpCircleOutline
86
- },
87
- },
88
- }
89
- </script>
1
+ <template>
2
+ <component :is="resolvedComponent" :size="size" />
3
+ </template>
4
+
5
+ <script>
6
+ import HelpCircleOutline from 'vue-material-design-icons/HelpCircleOutline.vue'
7
+
8
+ /**
9
+ * Mutable icon registry.
10
+ *
11
+ * Pre-populated with HelpCircleOutline (the default fallback).
12
+ * Apps extend this at boot via registerIcons() — import only the
13
+ * icons you need, keeping bundles small.
14
+ *
15
+ * @example
16
+ * import { registerIcons } from '@conduction/nextcloud-vue'
17
+ * import Sword from 'vue-material-design-icons/Sword.vue'
18
+ * registerIcons({ Sword })
19
+ */
20
+ const _registry = {
21
+ HelpCircleOutline,
22
+ }
23
+
24
+ /**
25
+ * Register one or more MDI icon components for use with CnIcon.
26
+ *
27
+ * Call this in your app's main.js before mounting the Vue instance.
28
+ * Each key must be the PascalCase icon name matching the
29
+ * vue-material-design-icons file name (e.g. "Sword" for Sword.vue).
30
+ *
31
+ * @param {Record<string, import('vue').Component>} icons
32
+ *
33
+ * @example
34
+ * import { registerIcons } from '@conduction/nextcloud-vue'
35
+ * import Sword from 'vue-material-design-icons/Sword.vue'
36
+ * import MagicStaff from 'vue-material-design-icons/MagicStaff.vue'
37
+ * registerIcons({ Sword, MagicStaff })
38
+ */
39
+ export function registerIcons(icons) {
40
+ Object.assign(_registry, icons)
41
+ }
42
+
43
+ /**
44
+ * Read-only reference to the current icon registry.
45
+ * Useful for checking which icons are available.
46
+ *
47
+ * @type {Record<string, import('vue').Component>}
48
+ */
49
+ export const ICON_MAP = _registry
50
+
51
+ /**
52
+ * CnIcon — Renders a Material Design Icon by PascalCase name.
53
+ *
54
+ * Looks up the name in the shared registry. If not found, renders
55
+ * the fallback icon (HelpCircleOutline by default).
56
+ *
57
+ * @example
58
+ * <CnIcon name="AccountGroup" :size="24" />
59
+ *
60
+ * @see https://pictogrammers.com/library/mdi/
61
+ */
62
+ export default {
63
+ name: 'CnIcon',
64
+
65
+ props: {
66
+ /** MDI icon name in PascalCase (e.g. "AccountGroup") */
67
+ name: {
68
+ type: String,
69
+ required: true,
70
+ },
71
+ /** Icon pixel size */
72
+ size: {
73
+ type: Number,
74
+ default: 20,
75
+ },
76
+ /** Fallback icon name if `name` is not found in the registry */
77
+ fallback: {
78
+ type: String,
79
+ default: 'HelpCircleOutline',
80
+ },
81
+ },
82
+
83
+ computed: {
84
+ resolvedComponent() {
85
+ return _registry[this.name] || _registry[this.fallback] || HelpCircleOutline
86
+ },
87
+ },
88
+ }
89
+ </script>