@conduction/nextcloud-vue 2.56.0-beta.25 → 2.56.0-beta.26

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 (67) hide show
  1. package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js +6 -0
  2. package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js.map +1 -1
  3. package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js +3 -2
  4. package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js.map +1 -1
  5. package/dist/esm/components/CnCredentials/CnCredentials.vue2.js +11 -10
  6. package/dist/esm/components/CnCredentials/CnCredentials.vue2.js.map +1 -1
  7. package/dist/esm/components/CnCredentials/CnCredentials.vue3.js.map +1 -1
  8. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +8 -5
  9. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  10. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  11. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +144 -6
  12. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  13. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +12 -11
  14. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  15. package/dist/esm/components/CnIndexPage/selfModeActions.js +90 -14
  16. package/dist/esm/components/CnIndexPage/selfModeActions.js.map +1 -1
  17. package/dist/esm/components/CnIndexPage/useSelfFetchList.js +15 -3
  18. package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
  19. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js +26 -0
  20. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js.map +1 -1
  21. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js +26 -9
  22. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js.map +1 -1
  23. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +19 -3
  24. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  25. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  26. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js +153 -38
  27. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js.map +1 -1
  28. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue3.js +74 -56
  29. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue3.js.map +1 -1
  30. package/dist/esm/composables/useObjectLock.js +41 -10
  31. package/dist/esm/composables/useObjectLock.js.map +1 -1
  32. package/dist/esm/index.js +1 -0
  33. package/dist/esm/index.js.map +1 -1
  34. package/dist/esm/l10n/en.json.js +7 -1
  35. package/dist/esm/l10n/en.json.js.map +1 -1
  36. package/dist/esm/l10n/nl.json.js +8 -2
  37. package/dist/esm/l10n/nl.json.js.map +1 -1
  38. package/dist/esm/nextcloud-vue.css +88 -1
  39. package/dist/esm/store/useObjectStore.js +37 -9
  40. package/dist/esm/store/useObjectStore.js.map +1 -1
  41. package/dist/esm/utils/objectSignals.js +44 -0
  42. package/dist/esm/utils/objectSignals.js.map +1 -0
  43. package/dist/nextcloud-vue.cjs.js +871 -328
  44. package/dist/nextcloud-vue.cjs.js.map +2 -2
  45. package/dist/nextcloud-vue.css +88 -1
  46. package/l10n/en.json +7 -1
  47. package/l10n/nl.json +8 -2
  48. package/package.json +1 -1
  49. package/src/components/CnCardGrid/CnCardGrid.md +1 -0
  50. package/src/components/CnCardGrid/CnCardGrid.vue +7 -0
  51. package/src/components/CnCredentials/CnCredentials.vue +17 -16
  52. package/src/components/CnDetailPage/CnDetailPage.vue +8 -5
  53. package/src/components/CnIndexPage/CnIndexPage.md +15 -0
  54. package/src/components/CnIndexPage/CnIndexPage.vue +155 -16
  55. package/src/components/CnIndexPage/selfModeActions.js +89 -13
  56. package/src/components/CnIndexPage/useSelfFetchList.js +15 -3
  57. package/src/components/CnObjectCard/CnObjectCard.md +1 -0
  58. package/src/components/CnObjectCard/CnObjectCard.vue +89 -1
  59. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +19 -3
  60. package/src/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue +181 -38
  61. package/src/composables/__tests__/useObjectLock.endpoints.spec.js +83 -0
  62. package/src/composables/useObjectLock.js +41 -10
  63. package/src/css/look-board-index.css +35 -1
  64. package/src/index.js +1 -0
  65. package/src/store/useObjectStore.js +37 -9
  66. package/src/utils/index.js +1 -0
  67. package/src/utils/objectSignals.js +41 -0
@@ -125,6 +125,12 @@ var script = {
125
125
  default: null,
126
126
  },
127
127
 
128
+ /** `(object) => { variant, icon?, label? }` for each card's status accent. See CnObjectCard `accent`. */
129
+ accentOf: {
130
+ type: Function,
131
+ default: null,
132
+ },
133
+
128
134
  /** Text shown when there are no objects */
129
135
  emptyText: {
130
136
  type: String,
@@ -1 +1 @@
1
- {"version":3,"file":"CnCardGrid.vue2.js","sources":["../../../../src/components/CnCardGrid/CnCardGrid.vue"],"sourcesContent":["<template>\n\t<div class=\"cn-card-grid\">\n\t\t<!-- Loading state -->\n\t\t<div v-if=\"loading\" class=\"cn-card-grid__loading\">\n\t\t\t<NcLoadingIcon :size=\"32\" />\n\t\t</div>\n\n\t\t<!-- Empty state -->\n\t\t<div v-else-if=\"objects.length === 0\" class=\"cn-card-grid__empty\">\n\t\t\t<slot name=\"empty\">\n\t\t\t\t<CnEmptyContent :name=\"resolvedEmptyText\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<ViewGrid :size=\"64\" />\n\t\t\t\t\t</template>\n\t\t\t\t</CnEmptyContent>\n\t\t\t</slot>\n\t\t</div>\n\n\t\t<!-- Card grid -->\n\t\t<div v-else\n\t\t\tclass=\"cn-card-grid__grid\"\n\t\t\t:class=\"{ 'cn-card-grid__grid--board': isBoardLook }\"\n\t\t\t:style=\"gridStyle\">\n\t\t\t<div\n\t\t\t\tv-for=\"object in objects\"\n\t\t\t\t:key=\"object[rowKey]\"\n\t\t\t\tclass=\"cn-card-grid__item\">\n\t\t\t\t<slot\n\t\t\t\t\tname=\"card\"\n\t\t\t\t\t:object=\"object\"\n\t\t\t\t\t:selected=\"isSelected(object)\"\n\t\t\t\t\t:schema=\"schema\">\n\t\t\t\t\t<CnObjectCard\n\t\t\t\t\t\t:object=\"object\"\n\t\t\t\t\t\t:schema=\"schema\"\n\t\t\t\t\t\t:selectable=\"selectable\"\n\t\t\t\t\t\t:clickToView=\"clickToView\"\n\t\t\t\t\t\t:selected=\"isSelected(object)\"\n\t\t\t\t\t\t:cardFields=\"cardFields\"\n\t\t\t\t\t\t:status=\"statusOf ? statusOf(object) : null\"\n\t\t\t\t\t\t:leading=\"leadingOf ? leadingOf(object) : null\"\n\t\t\t\t\t\t:footerAction=\"footerActionOf ? footerActionOf(object) : null\"\n\t\t\t\t\t\tv-on=\"cardListeners(object)\">\n\t\t\t\t\t\t<template v-if=\"$slots['card-actions']\" #actions=\"{ object: obj }\">\n\t\t\t\t\t\t\t<slot name=\"card-actions\" :object=\"obj\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t<template v-if=\"$slots['card-badges']\" #badges=\"{ object: obj }\">\n\t\t\t\t\t\t\t<slot name=\"card-badges\" :object=\"obj\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</CnObjectCard>\n\t\t\t\t</slot>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport ViewGrid from 'vue-material-design-icons/ViewGrid.vue'\nimport CnEmptyContent from '../CnEmptyContent/CnEmptyContent.vue'\nimport { normalizeLook } from '../../composables/useLook.js'\nimport { CnObjectCard } from '../CnObjectCard/index.js'\n\n/**\n * CnCardGrid — Responsive grid container for CnObjectCard instances.\n *\n * Displays objects in a responsive CSS grid layout using schema-driven cards.\n * Supports selection, loading/empty states, and custom card rendering via slots.\n *\n * ```vue\n * <CnCardGrid\n * :objects=\"publications\"\n * :schema=\"pubSchema\"\n * :selectable=\"true\"\n * :selected-ids=\"selectedIds\"\n * @click=\"openPublication\"\n * @select=\"onSelect\">\n * <template #card-actions=\"{ object }\">\n * <NcActions><NcActionButton>Edit</NcActionButton></NcActions>\n * </template>\n * </CnCardGrid>\n * ```\n */\nexport default {\n\tname: 'CnCardGrid',\n\n\tcomponents: {\n\t\tNcLoadingIcon,\n\t\tCnEmptyContent,\n\t\tViewGrid,\n\t\tCnObjectCard,\n\t},\n\n\tinject: {\n\t\t/**\n\t\t * Host translate function provided by CnAppRoot as\n\t\t * `cnTranslate: this.translate` (bound to the host app's id). The\n\t\t * manifest-authored empty-state copy is run through it. Defaults to\n\t\t * an identity function so an untranslated key renders as itself.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t\t/** The look CnAppRoot provides; `board` lays the cards out on the board track. */\n\t\tcnLook: { default: 'nextcloud' },\n\t},\n\n\tprops: {\n\t\t/** Array of objects to display as cards */\n\t\tobjects: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Schema definition (passed through to CnObjectCard) */\n\t\tschema: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Whether data is loading */\n\t\tloading: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Whether cards can be selected */\n\t\tselectable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * When true, a body click on a selectable card emits `click`\n\t\t * (navigation) and selection happens via the checkbox only — the\n\t\t * card-grid counterpart of CnIndexPage's `rowClickToView`.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tclickToView: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Array of currently selected object IDs */\n\t\tselectedIds: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Property name used as unique identifier */\n\t\trowKey: {\n\t\t\ttype: String,\n\t\t\tdefault: 'id',\n\t\t},\n\n\t\t/**\n\t\t * The property keys each card's facts list shows (manifest key\n\t\t * `config.cardFields`), passed through to CnObjectCard.\n\t\t *\n\t\t * @type {Array<string>}\n\t\t */\n\t\tcardFields: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Board look: `(object) => status` for each card's status pill. See CnObjectCard `status`. */\n\t\tstatusOf: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Board look: `(object) => { initials | icon }` for each card's leading element. See CnObjectCard `leading`. */\n\t\tleadingOf: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Board look: `(object) => { label, meta? }` for each card's footer action. See CnObjectCard `footerAction`. */\n\t\tfooterActionOf: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Text shown when there are no objects */\n\t\temptyText: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'No items found'),\n\t\t},\n\t},\n\n\temits: ['click', 'select', 'aux-click'],\n\n\tcomputed: {\n\t\t/**\n\t\t * Whether the cards lie on the board look's track.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tisBoardLook() {\n\t\t\treturn normalizeLook(this.cnLook) === 'board'\n\t\t},\n\n\t\t/**\n\t\t * Under the board look the grid is `minmax(var(--cn-card-grid-min,\n\t\t * 260px), 1fr)` with a `var(--cn-card-grid-gap, 16px)` gap, theme\n\t\t * hooks for both; without it the style is empty and the stylesheet's\n\t\t * 320px track stands.\n\t\t *\n\t\t * @spec openspec/changes/screens-card-parity/specs/card-board-look/spec.md#requirement-the-card-grid-takes-the-board-track\n\t\t * @return {object}\n\t\t */\n\t\tgridStyle() {\n\t\t\tif (!this.isBoardLook) {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tgridTemplateColumns: 'repeat(auto-fill, minmax(var(--cn-card-grid-min, 260px), 1fr))',\n\t\t\t\tgap: 'var(--cn-card-grid-gap, 16px)',\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The empty-state copy run through the host translate function.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedEmptyText() {\n\t\t\tconst fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k\n\t\t\treturn this.emptyText ? fn(this.emptyText) : this.emptyText\n\t\t},\n\t},\n\n\tmethods: {\n\t\tisSelected(object) {\n\t\t\treturn this.selectedIds.includes(object[this.rowKey])\n\t\t},\n\n\t\t/**\n\t\t * Listeners bound on each CnObjectCard. Selectable cards select via\n\t\t * `@select` only; the `@click` (navigation) listener is bound for\n\t\t * non-selectable cards — otherwise CnObjectCard's deprecated\n\t\t * click-to-select path fires (a body click would both select AND\n\t\t * navigate, unlike a table row which only selects) — and for\n\t\t * `clickToView` cards, where a body click navigates and the checkbox\n\t\t * is the only selection surface.\n\t\t *\n\t\t * @param {object} object The card's object.\n\t\t * @return {object} Event listeners for the card.\n\t\t */\n\t\tcardListeners(object) {\n\t\t\tconst listeners = { select: () => this.toggleSelect(object) }\n\t\t\tif (!this.selectable || this.clickToView) {\n\t\t\t\tlisteners.click = (_card, event) => this.$emit('click', object, event)\n\t\t\t\t// A middle click, kept off `click` so a navigating listener stays put.\n\t\t\t\tlisteners['aux-click'] = (_card, event) => {\n\t\t\t\t\t/**\n\t\t\t\t\t * @event aux-click Emitted when a navigating card is middle-clicked, for opening it in a new tab (see `openRowTarget`). Payload: `(object, event)` — the card's object and the native auxclick event.\n\t\t\t\t\t * @type {object} The card's object.\n\t\t\t\t\t */\n\t\t\t\t\tthis.$emit('aux-click', object, event)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn listeners\n\t\t},\n\n\t\ttoggleSelect(object) {\n\t\t\tconst id = object[this.rowKey]\n\t\t\tconst newIds = this.isSelected(object)\n\t\t\t\t? this.selectedIds.filter((i) => i !== id)\n\t\t\t\t: [...this.selectedIds, id]\n\t\t\tthis.$emit('select', newIds)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-card-grid__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(320px, 1fr));\n\tgap: 16px;\n}\n\n/* A card slot whose component renders nothing (e.g. a custom card that hides\n itself with `v-if` for a client-side filter) leaves an empty grid item that\n would still occupy a grid track and a row gap — pushing the visible cards\n down with a large blank gap above them. Collapse those empty cells so the\n remaining cards start at the top. A comment-only node (`v-if` false) still\n matches `:empty` per the CSS spec, so this catches the v-if-hidden case. */\n.cn-card-grid__item:empty {\n\tdisplay: none;\n}\n\n.cn-card-grid__loading {\n\tdisplay: flex;\n\tjustify-content: center;\n\tpadding: 40px;\n}\n\n.cn-card-grid__empty {\n\tpadding: 40px 20px;\n\ttext-align: center;\n}\n</style>\n"],"names":["CnEmptyContent","CnObjectCard","t"],"mappings":"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"}
1
+ {"version":3,"file":"CnCardGrid.vue2.js","sources":["../../../../src/components/CnCardGrid/CnCardGrid.vue"],"sourcesContent":["<template>\n\t<div class=\"cn-card-grid\">\n\t\t<!-- Loading state -->\n\t\t<div v-if=\"loading\" class=\"cn-card-grid__loading\">\n\t\t\t<NcLoadingIcon :size=\"32\" />\n\t\t</div>\n\n\t\t<!-- Empty state -->\n\t\t<div v-else-if=\"objects.length === 0\" class=\"cn-card-grid__empty\">\n\t\t\t<slot name=\"empty\">\n\t\t\t\t<CnEmptyContent :name=\"resolvedEmptyText\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<ViewGrid :size=\"64\" />\n\t\t\t\t\t</template>\n\t\t\t\t</CnEmptyContent>\n\t\t\t</slot>\n\t\t</div>\n\n\t\t<!-- Card grid -->\n\t\t<div v-else\n\t\t\tclass=\"cn-card-grid__grid\"\n\t\t\t:class=\"{ 'cn-card-grid__grid--board': isBoardLook }\"\n\t\t\t:style=\"gridStyle\">\n\t\t\t<div\n\t\t\t\tv-for=\"object in objects\"\n\t\t\t\t:key=\"object[rowKey]\"\n\t\t\t\tclass=\"cn-card-grid__item\">\n\t\t\t\t<slot\n\t\t\t\t\tname=\"card\"\n\t\t\t\t\t:object=\"object\"\n\t\t\t\t\t:selected=\"isSelected(object)\"\n\t\t\t\t\t:schema=\"schema\">\n\t\t\t\t\t<CnObjectCard\n\t\t\t\t\t\t:object=\"object\"\n\t\t\t\t\t\t:schema=\"schema\"\n\t\t\t\t\t\t:selectable=\"selectable\"\n\t\t\t\t\t\t:clickToView=\"clickToView\"\n\t\t\t\t\t\t:selected=\"isSelected(object)\"\n\t\t\t\t\t\t:cardFields=\"cardFields\"\n\t\t\t\t\t\t:status=\"statusOf ? statusOf(object) : null\"\n\t\t\t\t\t\t:leading=\"leadingOf ? leadingOf(object) : null\"\n\t\t\t\t\t\t:footerAction=\"footerActionOf ? footerActionOf(object) : null\"\n\t\t\t\t\t\t:accent=\"accentOf ? accentOf(object) : null\"\n\t\t\t\t\t\tv-on=\"cardListeners(object)\">\n\t\t\t\t\t\t<template v-if=\"$slots['card-actions']\" #actions=\"{ object: obj }\">\n\t\t\t\t\t\t\t<slot name=\"card-actions\" :object=\"obj\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t<template v-if=\"$slots['card-badges']\" #badges=\"{ object: obj }\">\n\t\t\t\t\t\t\t<slot name=\"card-badges\" :object=\"obj\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</CnObjectCard>\n\t\t\t\t</slot>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport ViewGrid from 'vue-material-design-icons/ViewGrid.vue'\nimport CnEmptyContent from '../CnEmptyContent/CnEmptyContent.vue'\nimport { normalizeLook } from '../../composables/useLook.js'\nimport { CnObjectCard } from '../CnObjectCard/index.js'\n\n/**\n * CnCardGrid — Responsive grid container for CnObjectCard instances.\n *\n * Displays objects in a responsive CSS grid layout using schema-driven cards.\n * Supports selection, loading/empty states, and custom card rendering via slots.\n *\n * ```vue\n * <CnCardGrid\n * :objects=\"publications\"\n * :schema=\"pubSchema\"\n * :selectable=\"true\"\n * :selected-ids=\"selectedIds\"\n * @click=\"openPublication\"\n * @select=\"onSelect\">\n * <template #card-actions=\"{ object }\">\n * <NcActions><NcActionButton>Edit</NcActionButton></NcActions>\n * </template>\n * </CnCardGrid>\n * ```\n */\nexport default {\n\tname: 'CnCardGrid',\n\n\tcomponents: {\n\t\tNcLoadingIcon,\n\t\tCnEmptyContent,\n\t\tViewGrid,\n\t\tCnObjectCard,\n\t},\n\n\tinject: {\n\t\t/**\n\t\t * Host translate function provided by CnAppRoot as\n\t\t * `cnTranslate: this.translate` (bound to the host app's id). The\n\t\t * manifest-authored empty-state copy is run through it. Defaults to\n\t\t * an identity function so an untranslated key renders as itself.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t\t/** The look CnAppRoot provides; `board` lays the cards out on the board track. */\n\t\tcnLook: { default: 'nextcloud' },\n\t},\n\n\tprops: {\n\t\t/** Array of objects to display as cards */\n\t\tobjects: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Schema definition (passed through to CnObjectCard) */\n\t\tschema: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Whether data is loading */\n\t\tloading: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Whether cards can be selected */\n\t\tselectable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * When true, a body click on a selectable card emits `click`\n\t\t * (navigation) and selection happens via the checkbox only — the\n\t\t * card-grid counterpart of CnIndexPage's `rowClickToView`.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tclickToView: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Array of currently selected object IDs */\n\t\tselectedIds: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Property name used as unique identifier */\n\t\trowKey: {\n\t\t\ttype: String,\n\t\t\tdefault: 'id',\n\t\t},\n\n\t\t/**\n\t\t * The property keys each card's facts list shows (manifest key\n\t\t * `config.cardFields`), passed through to CnObjectCard.\n\t\t *\n\t\t * @type {Array<string>}\n\t\t */\n\t\tcardFields: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Board look: `(object) => status` for each card's status pill. See CnObjectCard `status`. */\n\t\tstatusOf: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Board look: `(object) => { initials | icon }` for each card's leading element. See CnObjectCard `leading`. */\n\t\tleadingOf: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Board look: `(object) => { label, meta? }` for each card's footer action. See CnObjectCard `footerAction`. */\n\t\tfooterActionOf: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** `(object) => { variant, icon?, label? }` for each card's status accent. See CnObjectCard `accent`. */\n\t\taccentOf: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Text shown when there are no objects */\n\t\temptyText: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'No items found'),\n\t\t},\n\t},\n\n\temits: ['click', 'select', 'aux-click'],\n\n\tcomputed: {\n\t\t/**\n\t\t * Whether the cards lie on the board look's track.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tisBoardLook() {\n\t\t\treturn normalizeLook(this.cnLook) === 'board'\n\t\t},\n\n\t\t/**\n\t\t * Under the board look the grid is `minmax(var(--cn-card-grid-min,\n\t\t * 260px), 1fr)` with a `var(--cn-card-grid-gap, 16px)` gap, theme\n\t\t * hooks for both; without it the style is empty and the stylesheet's\n\t\t * 320px track stands.\n\t\t *\n\t\t * @spec openspec/changes/screens-card-parity/specs/card-board-look/spec.md#requirement-the-card-grid-takes-the-board-track\n\t\t * @return {object}\n\t\t */\n\t\tgridStyle() {\n\t\t\tif (!this.isBoardLook) {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tgridTemplateColumns: 'repeat(auto-fill, minmax(var(--cn-card-grid-min, 260px), 1fr))',\n\t\t\t\tgap: 'var(--cn-card-grid-gap, 16px)',\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The empty-state copy run through the host translate function.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedEmptyText() {\n\t\t\tconst fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k\n\t\t\treturn this.emptyText ? fn(this.emptyText) : this.emptyText\n\t\t},\n\t},\n\n\tmethods: {\n\t\tisSelected(object) {\n\t\t\treturn this.selectedIds.includes(object[this.rowKey])\n\t\t},\n\n\t\t/**\n\t\t * Listeners bound on each CnObjectCard. Selectable cards select via\n\t\t * `@select` only; the `@click` (navigation) listener is bound for\n\t\t * non-selectable cards — otherwise CnObjectCard's deprecated\n\t\t * click-to-select path fires (a body click would both select AND\n\t\t * navigate, unlike a table row which only selects) — and for\n\t\t * `clickToView` cards, where a body click navigates and the checkbox\n\t\t * is the only selection surface.\n\t\t *\n\t\t * @param {object} object The card's object.\n\t\t * @return {object} Event listeners for the card.\n\t\t */\n\t\tcardListeners(object) {\n\t\t\tconst listeners = { select: () => this.toggleSelect(object) }\n\t\t\tif (!this.selectable || this.clickToView) {\n\t\t\t\tlisteners.click = (_card, event) => this.$emit('click', object, event)\n\t\t\t\t// A middle click, kept off `click` so a navigating listener stays put.\n\t\t\t\tlisteners['aux-click'] = (_card, event) => {\n\t\t\t\t\t/**\n\t\t\t\t\t * @event aux-click Emitted when a navigating card is middle-clicked, for opening it in a new tab (see `openRowTarget`). Payload: `(object, event)` — the card's object and the native auxclick event.\n\t\t\t\t\t * @type {object} The card's object.\n\t\t\t\t\t */\n\t\t\t\t\tthis.$emit('aux-click', object, event)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn listeners\n\t\t},\n\n\t\ttoggleSelect(object) {\n\t\t\tconst id = object[this.rowKey]\n\t\t\tconst newIds = this.isSelected(object)\n\t\t\t\t? this.selectedIds.filter((i) => i !== id)\n\t\t\t\t: [...this.selectedIds, id]\n\t\t\tthis.$emit('select', newIds)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-card-grid__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(320px, 1fr));\n\tgap: 16px;\n}\n\n/* A card slot whose component renders nothing (e.g. a custom card that hides\n itself with `v-if` for a client-side filter) leaves an empty grid item that\n would still occupy a grid track and a row gap — pushing the visible cards\n down with a large blank gap above them. Collapse those empty cells so the\n remaining cards start at the top. A comment-only node (`v-if` false) still\n matches `:empty` per the CSS spec, so this catches the v-if-hidden case. */\n.cn-card-grid__item:empty {\n\tdisplay: none;\n}\n\n.cn-card-grid__loading {\n\tdisplay: flex;\n\tjustify-content: center;\n\tpadding: 40px;\n}\n\n.cn-card-grid__empty {\n\tpadding: 40px 20px;\n\ttext-align: center;\n}\n</style>\n"],"names":["CnEmptyContent","CnObjectCard","t"],"mappings":"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"}
@@ -56,7 +56,8 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
56
56
  cardFields: $props.cardFields,
57
57
  status: $props.statusOf ? $props.statusOf(object) : null,
58
58
  leading: $props.leadingOf ? $props.leadingOf(object) : null,
59
- footerAction: $props.footerActionOf ? $props.footerActionOf(object) : null
59
+ footerAction: $props.footerActionOf ? $props.footerActionOf(object) : null,
60
+ accent: $props.accentOf ? $props.accentOf(object) : null
60
61
  }, toHandlers($options.cardListeners(object))), createSlots({ _: 2 /* DYNAMIC */ }, [
61
62
  (_ctx.$slots['card-actions'])
62
63
  ? {
@@ -76,7 +77,7 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
76
77
  key: "1"
77
78
  }
78
79
  : undefined
79
- ]), 1040 /* FULL_PROPS, DYNAMIC_SLOTS */, ["object", "schema", "selectable", "clickToView", "selected", "cardFields", "status", "leading", "footerAction"])
80
+ ]), 1040 /* FULL_PROPS, DYNAMIC_SLOTS */, ["object", "schema", "selectable", "clickToView", "selected", "cardFields", "status", "leading", "footerAction", "accent"])
80
81
  ])
81
82
  ]))
82
83
  }), 128 /* KEYED_FRAGMENT */))
@@ -1 +1 @@
1
- {"version":3,"file":"CnCardGrid.vue3.js","sources":["../../../../src/components/CnCardGrid/CnCardGrid.vue?vue&type=template&id=1d1f4122&lang.js"],"sourcesContent":["<template>\n\t<div class=\"cn-card-grid\">\n\t\t<!-- Loading state -->\n\t\t<div v-if=\"loading\" class=\"cn-card-grid__loading\">\n\t\t\t<NcLoadingIcon :size=\"32\" />\n\t\t</div>\n\n\t\t<!-- Empty state -->\n\t\t<div v-else-if=\"objects.length === 0\" class=\"cn-card-grid__empty\">\n\t\t\t<slot name=\"empty\">\n\t\t\t\t<CnEmptyContent :name=\"resolvedEmptyText\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<ViewGrid :size=\"64\" />\n\t\t\t\t\t</template>\n\t\t\t\t</CnEmptyContent>\n\t\t\t</slot>\n\t\t</div>\n\n\t\t<!-- Card grid -->\n\t\t<div v-else\n\t\t\tclass=\"cn-card-grid__grid\"\n\t\t\t:class=\"{ 'cn-card-grid__grid--board': isBoardLook }\"\n\t\t\t:style=\"gridStyle\">\n\t\t\t<div\n\t\t\t\tv-for=\"object in objects\"\n\t\t\t\t:key=\"object[rowKey]\"\n\t\t\t\tclass=\"cn-card-grid__item\">\n\t\t\t\t<slot\n\t\t\t\t\tname=\"card\"\n\t\t\t\t\t:object=\"object\"\n\t\t\t\t\t:selected=\"isSelected(object)\"\n\t\t\t\t\t:schema=\"schema\">\n\t\t\t\t\t<CnObjectCard\n\t\t\t\t\t\t:object=\"object\"\n\t\t\t\t\t\t:schema=\"schema\"\n\t\t\t\t\t\t:selectable=\"selectable\"\n\t\t\t\t\t\t:clickToView=\"clickToView\"\n\t\t\t\t\t\t:selected=\"isSelected(object)\"\n\t\t\t\t\t\t:cardFields=\"cardFields\"\n\t\t\t\t\t\t:status=\"statusOf ? statusOf(object) : null\"\n\t\t\t\t\t\t:leading=\"leadingOf ? leadingOf(object) : null\"\n\t\t\t\t\t\t:footerAction=\"footerActionOf ? footerActionOf(object) : null\"\n\t\t\t\t\t\tv-on=\"cardListeners(object)\">\n\t\t\t\t\t\t<template v-if=\"$slots['card-actions']\" #actions=\"{ object: obj }\">\n\t\t\t\t\t\t\t<slot name=\"card-actions\" :object=\"obj\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t<template v-if=\"$slots['card-badges']\" #badges=\"{ object: obj }\">\n\t\t\t\t\t\t\t<slot name=\"card-badges\" :object=\"obj\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</CnObjectCard>\n\t\t\t\t</slot>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport ViewGrid from 'vue-material-design-icons/ViewGrid.vue'\nimport CnEmptyContent from '../CnEmptyContent/CnEmptyContent.vue'\nimport { normalizeLook } from '../../composables/useLook.js'\nimport { CnObjectCard } from '../CnObjectCard/index.js'\n\n/**\n * CnCardGrid — Responsive grid container for CnObjectCard instances.\n *\n * Displays objects in a responsive CSS grid layout using schema-driven cards.\n * Supports selection, loading/empty states, and custom card rendering via slots.\n *\n * ```vue\n * <CnCardGrid\n * :objects=\"publications\"\n * :schema=\"pubSchema\"\n * :selectable=\"true\"\n * :selected-ids=\"selectedIds\"\n * @click=\"openPublication\"\n * @select=\"onSelect\">\n * <template #card-actions=\"{ object }\">\n * <NcActions><NcActionButton>Edit</NcActionButton></NcActions>\n * </template>\n * </CnCardGrid>\n * ```\n */\nexport default {\n\tname: 'CnCardGrid',\n\n\tcomponents: {\n\t\tNcLoadingIcon,\n\t\tCnEmptyContent,\n\t\tViewGrid,\n\t\tCnObjectCard,\n\t},\n\n\tinject: {\n\t\t/**\n\t\t * Host translate function provided by CnAppRoot as\n\t\t * `cnTranslate: this.translate` (bound to the host app's id). The\n\t\t * manifest-authored empty-state copy is run through it. Defaults to\n\t\t * an identity function so an untranslated key renders as itself.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t\t/** The look CnAppRoot provides; `board` lays the cards out on the board track. */\n\t\tcnLook: { default: 'nextcloud' },\n\t},\n\n\tprops: {\n\t\t/** Array of objects to display as cards */\n\t\tobjects: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Schema definition (passed through to CnObjectCard) */\n\t\tschema: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Whether data is loading */\n\t\tloading: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Whether cards can be selected */\n\t\tselectable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * When true, a body click on a selectable card emits `click`\n\t\t * (navigation) and selection happens via the checkbox only — the\n\t\t * card-grid counterpart of CnIndexPage's `rowClickToView`.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tclickToView: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Array of currently selected object IDs */\n\t\tselectedIds: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Property name used as unique identifier */\n\t\trowKey: {\n\t\t\ttype: String,\n\t\t\tdefault: 'id',\n\t\t},\n\n\t\t/**\n\t\t * The property keys each card's facts list shows (manifest key\n\t\t * `config.cardFields`), passed through to CnObjectCard.\n\t\t *\n\t\t * @type {Array<string>}\n\t\t */\n\t\tcardFields: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Board look: `(object) => status` for each card's status pill. See CnObjectCard `status`. */\n\t\tstatusOf: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Board look: `(object) => { initials | icon }` for each card's leading element. See CnObjectCard `leading`. */\n\t\tleadingOf: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Board look: `(object) => { label, meta? }` for each card's footer action. See CnObjectCard `footerAction`. */\n\t\tfooterActionOf: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Text shown when there are no objects */\n\t\temptyText: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'No items found'),\n\t\t},\n\t},\n\n\temits: ['click', 'select', 'aux-click'],\n\n\tcomputed: {\n\t\t/**\n\t\t * Whether the cards lie on the board look's track.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tisBoardLook() {\n\t\t\treturn normalizeLook(this.cnLook) === 'board'\n\t\t},\n\n\t\t/**\n\t\t * Under the board look the grid is `minmax(var(--cn-card-grid-min,\n\t\t * 260px), 1fr)` with a `var(--cn-card-grid-gap, 16px)` gap, theme\n\t\t * hooks for both; without it the style is empty and the stylesheet's\n\t\t * 320px track stands.\n\t\t *\n\t\t * @spec openspec/changes/screens-card-parity/specs/card-board-look/spec.md#requirement-the-card-grid-takes-the-board-track\n\t\t * @return {object}\n\t\t */\n\t\tgridStyle() {\n\t\t\tif (!this.isBoardLook) {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tgridTemplateColumns: 'repeat(auto-fill, minmax(var(--cn-card-grid-min, 260px), 1fr))',\n\t\t\t\tgap: 'var(--cn-card-grid-gap, 16px)',\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The empty-state copy run through the host translate function.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedEmptyText() {\n\t\t\tconst fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k\n\t\t\treturn this.emptyText ? fn(this.emptyText) : this.emptyText\n\t\t},\n\t},\n\n\tmethods: {\n\t\tisSelected(object) {\n\t\t\treturn this.selectedIds.includes(object[this.rowKey])\n\t\t},\n\n\t\t/**\n\t\t * Listeners bound on each CnObjectCard. Selectable cards select via\n\t\t * `@select` only; the `@click` (navigation) listener is bound for\n\t\t * non-selectable cards — otherwise CnObjectCard's deprecated\n\t\t * click-to-select path fires (a body click would both select AND\n\t\t * navigate, unlike a table row which only selects) — and for\n\t\t * `clickToView` cards, where a body click navigates and the checkbox\n\t\t * is the only selection surface.\n\t\t *\n\t\t * @param {object} object The card's object.\n\t\t * @return {object} Event listeners for the card.\n\t\t */\n\t\tcardListeners(object) {\n\t\t\tconst listeners = { select: () => this.toggleSelect(object) }\n\t\t\tif (!this.selectable || this.clickToView) {\n\t\t\t\tlisteners.click = (_card, event) => this.$emit('click', object, event)\n\t\t\t\t// A middle click, kept off `click` so a navigating listener stays put.\n\t\t\t\tlisteners['aux-click'] = (_card, event) => {\n\t\t\t\t\t/**\n\t\t\t\t\t * @event aux-click Emitted when a navigating card is middle-clicked, for opening it in a new tab (see `openRowTarget`). Payload: `(object, event)` — the card's object and the native auxclick event.\n\t\t\t\t\t * @type {object} The card's object.\n\t\t\t\t\t */\n\t\t\t\t\tthis.$emit('aux-click', object, event)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn listeners\n\t\t},\n\n\t\ttoggleSelect(object) {\n\t\t\tconst id = object[this.rowKey]\n\t\t\tconst newIds = this.isSelected(object)\n\t\t\t\t? this.selectedIds.filter((i) => i !== id)\n\t\t\t\t: [...this.selectedIds, id]\n\t\t\tthis.$emit('select', newIds)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-card-grid__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(320px, 1fr));\n\tgap: 16px;\n}\n\n/* A card slot whose component renders nothing (e.g. a custom card that hides\n itself with `v-if` for a client-side filter) leaves an empty grid item that\n would still occupy a grid track and a row gap — pushing the visible cards\n down with a large blank gap above them. Collapse those empty cells so the\n remaining cards start at the top. A comment-only node (`v-if` false) still\n matches `:empty` per the CSS spec, so this catches the v-if-hidden case. */\n.cn-card-grid__item:empty {\n\tdisplay: none;\n}\n\n.cn-card-grid__loading {\n\tdisplay: flex;\n\tjustify-content: center;\n\tpadding: 40px;\n}\n\n.cn-card-grid__empty {\n\tpadding: 40px 20px;\n\ttext-align: center;\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_createVNode","_renderSlot","_normalizeClass","_Fragment","_renderList","_mergeProps","_toHandlers","_createSlots","_withCtx"],"mappings":"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"}
1
+ {"version":3,"file":"CnCardGrid.vue3.js","sources":["../../../../src/components/CnCardGrid/CnCardGrid.vue?vue&type=template&id=1d1f4122&lang.js"],"sourcesContent":["<template>\n\t<div class=\"cn-card-grid\">\n\t\t<!-- Loading state -->\n\t\t<div v-if=\"loading\" class=\"cn-card-grid__loading\">\n\t\t\t<NcLoadingIcon :size=\"32\" />\n\t\t</div>\n\n\t\t<!-- Empty state -->\n\t\t<div v-else-if=\"objects.length === 0\" class=\"cn-card-grid__empty\">\n\t\t\t<slot name=\"empty\">\n\t\t\t\t<CnEmptyContent :name=\"resolvedEmptyText\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<ViewGrid :size=\"64\" />\n\t\t\t\t\t</template>\n\t\t\t\t</CnEmptyContent>\n\t\t\t</slot>\n\t\t</div>\n\n\t\t<!-- Card grid -->\n\t\t<div v-else\n\t\t\tclass=\"cn-card-grid__grid\"\n\t\t\t:class=\"{ 'cn-card-grid__grid--board': isBoardLook }\"\n\t\t\t:style=\"gridStyle\">\n\t\t\t<div\n\t\t\t\tv-for=\"object in objects\"\n\t\t\t\t:key=\"object[rowKey]\"\n\t\t\t\tclass=\"cn-card-grid__item\">\n\t\t\t\t<slot\n\t\t\t\t\tname=\"card\"\n\t\t\t\t\t:object=\"object\"\n\t\t\t\t\t:selected=\"isSelected(object)\"\n\t\t\t\t\t:schema=\"schema\">\n\t\t\t\t\t<CnObjectCard\n\t\t\t\t\t\t:object=\"object\"\n\t\t\t\t\t\t:schema=\"schema\"\n\t\t\t\t\t\t:selectable=\"selectable\"\n\t\t\t\t\t\t:clickToView=\"clickToView\"\n\t\t\t\t\t\t:selected=\"isSelected(object)\"\n\t\t\t\t\t\t:cardFields=\"cardFields\"\n\t\t\t\t\t\t:status=\"statusOf ? statusOf(object) : null\"\n\t\t\t\t\t\t:leading=\"leadingOf ? leadingOf(object) : null\"\n\t\t\t\t\t\t:footerAction=\"footerActionOf ? footerActionOf(object) : null\"\n\t\t\t\t\t\t:accent=\"accentOf ? accentOf(object) : null\"\n\t\t\t\t\t\tv-on=\"cardListeners(object)\">\n\t\t\t\t\t\t<template v-if=\"$slots['card-actions']\" #actions=\"{ object: obj }\">\n\t\t\t\t\t\t\t<slot name=\"card-actions\" :object=\"obj\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t<template v-if=\"$slots['card-badges']\" #badges=\"{ object: obj }\">\n\t\t\t\t\t\t\t<slot name=\"card-badges\" :object=\"obj\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</CnObjectCard>\n\t\t\t\t</slot>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport ViewGrid from 'vue-material-design-icons/ViewGrid.vue'\nimport CnEmptyContent from '../CnEmptyContent/CnEmptyContent.vue'\nimport { normalizeLook } from '../../composables/useLook.js'\nimport { CnObjectCard } from '../CnObjectCard/index.js'\n\n/**\n * CnCardGrid — Responsive grid container for CnObjectCard instances.\n *\n * Displays objects in a responsive CSS grid layout using schema-driven cards.\n * Supports selection, loading/empty states, and custom card rendering via slots.\n *\n * ```vue\n * <CnCardGrid\n * :objects=\"publications\"\n * :schema=\"pubSchema\"\n * :selectable=\"true\"\n * :selected-ids=\"selectedIds\"\n * @click=\"openPublication\"\n * @select=\"onSelect\">\n * <template #card-actions=\"{ object }\">\n * <NcActions><NcActionButton>Edit</NcActionButton></NcActions>\n * </template>\n * </CnCardGrid>\n * ```\n */\nexport default {\n\tname: 'CnCardGrid',\n\n\tcomponents: {\n\t\tNcLoadingIcon,\n\t\tCnEmptyContent,\n\t\tViewGrid,\n\t\tCnObjectCard,\n\t},\n\n\tinject: {\n\t\t/**\n\t\t * Host translate function provided by CnAppRoot as\n\t\t * `cnTranslate: this.translate` (bound to the host app's id). The\n\t\t * manifest-authored empty-state copy is run through it. Defaults to\n\t\t * an identity function so an untranslated key renders as itself.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t\t/** The look CnAppRoot provides; `board` lays the cards out on the board track. */\n\t\tcnLook: { default: 'nextcloud' },\n\t},\n\n\tprops: {\n\t\t/** Array of objects to display as cards */\n\t\tobjects: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Schema definition (passed through to CnObjectCard) */\n\t\tschema: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Whether data is loading */\n\t\tloading: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Whether cards can be selected */\n\t\tselectable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * When true, a body click on a selectable card emits `click`\n\t\t * (navigation) and selection happens via the checkbox only — the\n\t\t * card-grid counterpart of CnIndexPage's `rowClickToView`.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tclickToView: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Array of currently selected object IDs */\n\t\tselectedIds: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Property name used as unique identifier */\n\t\trowKey: {\n\t\t\ttype: String,\n\t\t\tdefault: 'id',\n\t\t},\n\n\t\t/**\n\t\t * The property keys each card's facts list shows (manifest key\n\t\t * `config.cardFields`), passed through to CnObjectCard.\n\t\t *\n\t\t * @type {Array<string>}\n\t\t */\n\t\tcardFields: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Board look: `(object) => status` for each card's status pill. See CnObjectCard `status`. */\n\t\tstatusOf: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Board look: `(object) => { initials | icon }` for each card's leading element. See CnObjectCard `leading`. */\n\t\tleadingOf: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Board look: `(object) => { label, meta? }` for each card's footer action. See CnObjectCard `footerAction`. */\n\t\tfooterActionOf: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** `(object) => { variant, icon?, label? }` for each card's status accent. See CnObjectCard `accent`. */\n\t\taccentOf: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Text shown when there are no objects */\n\t\temptyText: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'No items found'),\n\t\t},\n\t},\n\n\temits: ['click', 'select', 'aux-click'],\n\n\tcomputed: {\n\t\t/**\n\t\t * Whether the cards lie on the board look's track.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tisBoardLook() {\n\t\t\treturn normalizeLook(this.cnLook) === 'board'\n\t\t},\n\n\t\t/**\n\t\t * Under the board look the grid is `minmax(var(--cn-card-grid-min,\n\t\t * 260px), 1fr)` with a `var(--cn-card-grid-gap, 16px)` gap, theme\n\t\t * hooks for both; without it the style is empty and the stylesheet's\n\t\t * 320px track stands.\n\t\t *\n\t\t * @spec openspec/changes/screens-card-parity/specs/card-board-look/spec.md#requirement-the-card-grid-takes-the-board-track\n\t\t * @return {object}\n\t\t */\n\t\tgridStyle() {\n\t\t\tif (!this.isBoardLook) {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tgridTemplateColumns: 'repeat(auto-fill, minmax(var(--cn-card-grid-min, 260px), 1fr))',\n\t\t\t\tgap: 'var(--cn-card-grid-gap, 16px)',\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The empty-state copy run through the host translate function.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedEmptyText() {\n\t\t\tconst fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k\n\t\t\treturn this.emptyText ? fn(this.emptyText) : this.emptyText\n\t\t},\n\t},\n\n\tmethods: {\n\t\tisSelected(object) {\n\t\t\treturn this.selectedIds.includes(object[this.rowKey])\n\t\t},\n\n\t\t/**\n\t\t * Listeners bound on each CnObjectCard. Selectable cards select via\n\t\t * `@select` only; the `@click` (navigation) listener is bound for\n\t\t * non-selectable cards — otherwise CnObjectCard's deprecated\n\t\t * click-to-select path fires (a body click would both select AND\n\t\t * navigate, unlike a table row which only selects) — and for\n\t\t * `clickToView` cards, where a body click navigates and the checkbox\n\t\t * is the only selection surface.\n\t\t *\n\t\t * @param {object} object The card's object.\n\t\t * @return {object} Event listeners for the card.\n\t\t */\n\t\tcardListeners(object) {\n\t\t\tconst listeners = { select: () => this.toggleSelect(object) }\n\t\t\tif (!this.selectable || this.clickToView) {\n\t\t\t\tlisteners.click = (_card, event) => this.$emit('click', object, event)\n\t\t\t\t// A middle click, kept off `click` so a navigating listener stays put.\n\t\t\t\tlisteners['aux-click'] = (_card, event) => {\n\t\t\t\t\t/**\n\t\t\t\t\t * @event aux-click Emitted when a navigating card is middle-clicked, for opening it in a new tab (see `openRowTarget`). Payload: `(object, event)` — the card's object and the native auxclick event.\n\t\t\t\t\t * @type {object} The card's object.\n\t\t\t\t\t */\n\t\t\t\t\tthis.$emit('aux-click', object, event)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn listeners\n\t\t},\n\n\t\ttoggleSelect(object) {\n\t\t\tconst id = object[this.rowKey]\n\t\t\tconst newIds = this.isSelected(object)\n\t\t\t\t? this.selectedIds.filter((i) => i !== id)\n\t\t\t\t: [...this.selectedIds, id]\n\t\t\tthis.$emit('select', newIds)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-card-grid__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(320px, 1fr));\n\tgap: 16px;\n}\n\n/* A card slot whose component renders nothing (e.g. a custom card that hides\n itself with `v-if` for a client-side filter) leaves an empty grid item that\n would still occupy a grid track and a row gap — pushing the visible cards\n down with a large blank gap above them. Collapse those empty cells so the\n remaining cards start at the top. A comment-only node (`v-if` false) still\n matches `:empty` per the CSS spec, so this catches the v-if-hidden case. */\n.cn-card-grid__item:empty {\n\tdisplay: none;\n}\n\n.cn-card-grid__loading {\n\tdisplay: flex;\n\tjustify-content: center;\n\tpadding: 40px;\n}\n\n.cn-card-grid__empty {\n\tpadding: 40px 20px;\n\ttext-align: center;\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_createVNode","_renderSlot","_normalizeClass","_Fragment","_renderList","_mergeProps","_toHandlers","_createSlots","_withCtx"],"mappings":"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"}
@@ -12,7 +12,7 @@ const PROVIDERS_PATH = '/apps/openregister/api/credentials/providers';
12
12
  * Hardcoded per-provider presentation. Keyed by the OpenRegister catalogue
13
13
  * identifier. `colour` drives the tile; `setupHelp`/`setupUrl` explain how to
14
14
  * obtain the secret; `secretLabel` names the field. Providers not listed here
15
- * still work — they render a neutral tile from their identifier.
15
+ * still work, they render a neutral tile from their identifier.
16
16
  */
17
17
  const PROVIDER_META = {
18
18
  github: {
@@ -82,7 +82,7 @@ var script = {
82
82
  },
83
83
 
84
84
  /**
85
- * The current app's manifest `credentials[]` declarations — the
85
+ * The current app's manifest `credentials[]` declarations: the
86
86
  * providers this app can reach through the broker. Shape:
87
87
  * `[{ provider, reason, scopes }]`. Rendered read-only under the intro.
88
88
  *
@@ -147,15 +147,16 @@ var script = {
147
147
  },
148
148
 
149
149
  /**
150
- * Reframed intro copy per scope.
150
+ * Reframed intro copy per scope. No em-dashes, sentences under 16 words.
151
151
  *
152
+ * @spec openspec/changes/r4-object-lock-url-and-credentials-copy/specs/credentials-settings/spec.md
152
153
  * @return {string} Intro text.
153
154
  */
154
155
  introText() {
155
156
  if (this.scope === 'organisation') {
156
- return translate('nextcloud-vue', 'Organisation credentials let apps act on behalf of your organisation. The secret is stored in Keepiq — Nextcloud\'s native credential vault — never in the app. You choose which apps may use each credential.')
157
+ return translate('nextcloud-vue', 'Organisation credentials let apps act for your organisation. Keepiq, the credential vault of Nextcloud, keeps the secret. The app never holds it. You choose which apps may use each credential.')
157
158
  }
158
- return translate('nextcloud-vue', 'Apps sometimes need to act on your behalf against an external service. So they never hold your secrets, you give a secret to Nextcloud once and it is kept in Keepiq — a native, encrypted credential vault. Apps then make the call through Keepiq and never see the secret. You decide which apps may use each credential — share one across apps, or keep one per app.')
159
+ return translate('nextcloud-vue', 'Apps sometimes act for you on an external service. You give the secret to Nextcloud once, and Keepiq keeps it in an encrypted vault. Apps make the call through Keepiq and never see the secret. You choose which apps may use each credential. Share one across apps, or keep one per app.')
159
160
  },
160
161
 
161
162
  /**
@@ -181,7 +182,7 @@ var script = {
181
182
  const serverIds = this.providers.map((p) => p.identifier);
182
183
  let ids = serverIds.length ? serverIds : Object.keys(PROVIDER_META);
183
184
  // When the app declares which providers it uses (appCredentials),
184
- // only offer those — an app should not let you add a credential for
185
+ // only offer those, an app should not let you add a credential for
185
186
  // a provider it has no code path to use.
186
187
  if (this.supportedProviders.size > 0) {
187
188
  ids = ids.filter((id) => this.supportedProviders.has(id));
@@ -191,7 +192,7 @@ var script = {
191
192
 
192
193
  /**
193
194
  * Provider identifiers the app declares it uses, derived from
194
- * `appCredentials`. Empty when the app declares nothing — filtering is
195
+ * `appCredentials`. Empty when the app declares nothing, filtering is
195
196
  * then a no-op, so consumers that don't declare providers are unchanged.
196
197
  *
197
198
  * @return {Set<string>} Supported provider identifiers.
@@ -203,7 +204,7 @@ var script = {
203
204
  },
204
205
 
205
206
  /**
206
- * Stored credentials to display — filtered to the app's supported
207
+ * Stored credentials to display: filtered to the app's supported
207
208
  * providers when it declares any, so you can only see and authorise
208
209
  * credentials the app can actually use; otherwise all of them.
209
210
  *
@@ -246,7 +247,7 @@ var script = {
246
247
  },
247
248
 
248
249
  /**
249
- * Whether the add form can be submitted — name, provider and a secret
250
+ * Whether the add form can be submitted: name, provider and a secret
250
251
  * are required (the whole point is to store a secret).
251
252
  *
252
253
  * @return {boolean} True when submittable.
@@ -470,7 +471,7 @@ var script = {
470
471
  },
471
472
 
472
473
  /**
473
- * Human-readable provider title — hardcoded catalogue first, then the
474
+ * Human-readable provider title: hardcoded catalogue first, then the
474
475
  * server title, then the raw identifier.
475
476
  *
476
477
  * @param {string} identifier The provider identifier.
@@ -1 +1 @@
1
- {"version":3,"file":"CnCredentials.vue2.js","sources":["../../../../src/components/CnCredentials/CnCredentials.vue"],"sourcesContent":["<!--\n - SPDX-License-Identifier: EUPL-1.2\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n -\n - CnCredentials — the settings surface for the OpenRegister credential\n - broker. Apps occasionally need to act on your behalf against an external\n - service (GitHub, GitLab, …). Rather than hand the secret to the app, you\n - give it once to OpenRegister, which stores it in Keepiq — Nextcloud's\n - native, encrypted credential vault. Apps then make the outbound call\n - THROUGH OpenRegister and never see the secret itself. You decide which\n - apps may use each credential, so one credential can be shared across apps\n - or kept dedicated to one.\n -\n - Two scopes render from this one component:\n - • scope=\"personal\" (default) — the signed-in user's own\n - credentials, shown in an app's *personal* settings. From here you may\n - only add/remove *the app you are currently in* to/from a credential,\n - or add a new credential (the current app is auto-added). You never\n - manage the full app list of another app from here.\n - • scope=\"organisation\" — organisation-wide credentials, shown in an\n - app's *admin* settings. An admin manages the full allowed-app list.\n -\n - Talks to OpenRegister's credential endpoints (metadata only — a secret is\n - write-only and is NEVER returned or displayed):\n - GET /apps/openregister/api/credentials?scope={scope}\n - GET /apps/openregister/api/credentials/providers\n - POST /apps/openregister/api/credentials { name, provider, allowedApps?, secret?, scope? }\n - PUT /apps/openregister/api/credentials/{id} { name?, allowedApps?, secret? }\n - DELETE /apps/openregister/api/credentials/{id}\n -\n - Provider presentation (title, colour, how-to text + link, secret label) is\n - a hardcoded catalogue in this file (PROVIDER_META): new providers get a\n - richer card here without a server change; unknown providers fall back to a\n - neutral tile keyed off the identifier.\n -\n - Renders inside CnAppRoot's `#user-settings` (personal) or an admin surface\n - (organisation). Fails soft: a 404 (broker not installed) degrades to an\n - empty state with a friendly note rather than crashing.\n -->\n<template>\n\t<div class=\"cn-credentials\" :class=\"`cn-credentials--${scope}`\">\n\t\t<p class=\"cn-credentials__intro\">\n\t\t\t{{ introText }}\n\t\t\t<a v-if=\"resolvedVaultUrl\"\n\t\t\t\t:href=\"resolvedVaultUrl\"\n\t\t\t\tclass=\"cn-credentials__vault-link\"\n\t\t\t\ttarget=\"_self\">\n\t\t\t\t{{ t('nextcloud-vue', 'Learn about Keepiq') }} ↗\n\t\t\t</a>\n\t\t</p>\n\n\t\t<!-- What this app uses (the app's manifest credentials[] requirements) -->\n\t\t<section v-if=\"scope === 'personal'\" class=\"cn-credentials__section cn-credentials__supports\">\n\t\t\t<h4 class=\"cn-credentials__section-title\">\n\t\t\t\t{{ t('nextcloud-vue', 'What {app} uses', { app: appDisplayName }) }}\n\t\t\t</h4>\n\t\t\t<p v-if=\"appCredentials.length === 0\" class=\"cn-credentials__muted\">\n\t\t\t\t{{ t('nextcloud-vue', 'This app does not use any external credentials.') }}\n\t\t\t</p>\n\t\t\t<ul v-else class=\"cn-credentials__requests\">\n\t\t\t\t<li v-for=\"(req, index) in appCredentials\"\n\t\t\t\t\t:key=\"`req-${index}`\"\n\t\t\t\t\tclass=\"cn-credentials__request\">\n\t\t\t\t\t<span class=\"cn-credentials__tile-dot\" :style=\"dotStyle(req.provider)\">{{ providerInitial(req.provider) }}</span>\n\t\t\t\t\t<span class=\"cn-credentials__request-body\">\n\t\t\t\t\t\t<span class=\"cn-credentials__request-provider\">{{ providerTitle(req.provider) }}</span>\n\t\t\t\t\t\t<span v-if=\"req.reason\" class=\"cn-credentials__request-reason\">{{ req.reason }}</span>\n\t\t\t\t\t</span>\n\t\t\t\t</li>\n\t\t\t</ul>\n\t\t</section>\n\n\t\t<NcLoadingIcon v-if=\"loading\" :size=\"24\" />\n\n\t\t<template v-else>\n\t\t\t<NcNoteCard v-if=\"error\"\n\t\t\t\ttype=\"warning\"\n\t\t\t\tclass=\"cn-credentials__error\">\n\t\t\t\t{{ t('nextcloud-vue', 'Credentials are provided by OpenRegister and could not be loaded here.') }}\n\t\t\t</NcNoteCard>\n\n\t\t\t<!-- Your / the organisation's credentials -->\n\t\t\t<section class=\"cn-credentials__section\">\n\t\t\t\t<h4 class=\"cn-credentials__section-title\">\n\t\t\t\t\t{{ scope === 'organisation' ? t('nextcloud-vue', 'Organisation credentials') : t('nextcloud-vue', 'Your credentials') }}\n\t\t\t\t</h4>\n\t\t\t\t<p v-if=\"visibleCredentials.length === 0\" class=\"cn-credentials__muted\">\n\t\t\t\t\t{{ t('nextcloud-vue', 'No credentials stored yet.') }}\n\t\t\t\t</p>\n\t\t\t\t<ul v-else class=\"cn-credentials__list\">\n\t\t\t\t\t<li v-for=\"cred in visibleCredentials\"\n\t\t\t\t\t\t:key=\"cred.id\"\n\t\t\t\t\t\tclass=\"cn-credentials__item\">\n\t\t\t\t\t\t<div class=\"cn-credentials__item-head\">\n\t\t\t\t\t\t\t<span class=\"cn-credentials__tile-dot\" :style=\"dotStyle(cred.provider)\">{{ providerInitial(cred.provider) }}</span>\n\t\t\t\t\t\t\t<div class=\"cn-credentials__item-meta\">\n\t\t\t\t\t\t\t\t<span class=\"cn-credentials__item-name\">{{ cred.name }}</span>\n\t\t\t\t\t\t\t\t<span class=\"cn-credentials__item-provider\">{{ providerTitle(cred.provider) }}</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"cn-credentials__item-actions\">\n\t\t\t\t\t\t\t\t<template v-if=\"cred.confirmingDelete\">\n\t\t\t\t\t\t\t\t\t<NcButton variant=\"error\"\n\t\t\t\t\t\t\t\t\t\t:disabled=\"cred.saving\"\n\t\t\t\t\t\t\t\t\t\t@click=\"onDelete(cred)\">\n\t\t\t\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'Confirm delete') }}\n\t\t\t\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t\t\t\t\t<NcButton variant=\"tertiary\"\n\t\t\t\t\t\t\t\t\t\t:disabled=\"cred.saving\"\n\t\t\t\t\t\t\t\t\t\t@click=\"cred.confirmingDelete = false\">\n\t\t\t\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'Cancel') }}\n\t\t\t\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t\t<NcButton v-else\n\t\t\t\t\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\t\t\t\t\t:disabled=\"cred.saving\"\n\t\t\t\t\t\t\t\t\t@click=\"cred.confirmingDelete = true\">\n\t\t\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'Delete') }}\n\t\t\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t<!-- Personal: only toggle THIS app in/out of the credential. -->\n\t\t\t\t\t\t<div v-if=\"scope === 'personal'\" class=\"cn-credentials__item-toggle\">\n\t\t\t\t\t\t\t<NcCheckboxRadioSwitch :modelValue=\"appAllowed(cred)\"\n\t\t\t\t\t\t\t\t:disabled=\"cred.saving\"\n\t\t\t\t\t\t\t\ttype=\"switch\"\n\t\t\t\t\t\t\t\t@update:modelValue=\"toggleThisApp(cred, $event)\">\n\t\t\t\t\t\t\t\t{{ t('nextcloud-vue', '{app} may use this credential', { app: appDisplayName }) }}\n\t\t\t\t\t\t\t</NcCheckboxRadioSwitch>\n\t\t\t\t\t\t\t<p v-if=\"otherApps(cred).length\" class=\"cn-credentials__also muted\">\n\t\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'Also allowed for: {apps}', { apps: otherApps(cred).join(', ') }) }}\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t<!-- Organisation: admin manages the full allowed-app list. -->\n\t\t\t\t\t\t<NcSelect v-else\n\t\t\t\t\t\t\tclass=\"cn-credentials__item-apps\"\n\t\t\t\t\t\t\t:options=\"appOptions\"\n\t\t\t\t\t\t\t:modelValue=\"cred.allowedApps\"\n\t\t\t\t\t\t\t:multiple=\"true\"\n\t\t\t\t\t\t\t:taggable=\"true\"\n\t\t\t\t\t\t\tkeepOpen\n\t\t\t\t\t\t\t:disabled=\"cred.saving\"\n\t\t\t\t\t\t\t:inputLabel=\"t('nextcloud-vue', 'Allowed apps')\"\n\t\t\t\t\t\t\t:placeholder=\"t('nextcloud-vue', 'No app may use this credential yet')\"\n\t\t\t\t\t\t\t@update:modelValue=\"onAllowedAppsChange(cred, $event)\" />\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\t\t\t</section>\n\n\t\t\t<!-- Add credential — inline wizard: pick a provider, then fill it in. -->\n\t\t\t<section class=\"cn-credentials__section\">\n\t\t\t\t<NcButton v-if=\"!adding\"\n\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\t@click=\"startAdd\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<Plus :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ t('nextcloud-vue', 'Add credential') }}\n\t\t\t\t</NcButton>\n\n\t\t\t\t<div v-else class=\"cn-credentials__wizard\">\n\t\t\t\t\t<!-- Step 1: provider icon grid -->\n\t\t\t\t\t<template v-if=\"wizardStep === 'provider'\">\n\t\t\t\t\t\t<h4 class=\"cn-credentials__section-title\">\n\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'Which credential do you want to add?') }}\n\t\t\t\t\t\t</h4>\n\t\t\t\t\t\t<div class=\"cn-credentials__grid\">\n\t\t\t\t\t\t\t<button v-for=\"p in providerGrid\"\n\t\t\t\t\t\t\t\t:key=\"p.identifier\"\n\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\tclass=\"cn-credentials__grid-tile\"\n\t\t\t\t\t\t\t\t@click=\"pickProvider(p.identifier)\">\n\t\t\t\t\t\t\t\t<span class=\"cn-credentials__tile-dot cn-credentials__tile-dot--lg\" :style=\"dotStyle(p.identifier)\">{{ providerInitial(p.identifier) }}</span>\n\t\t\t\t\t\t\t\t<span class=\"cn-credentials__grid-title\">{{ p.title }}</span>\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"cn-credentials__wizard-actions\">\n\t\t\t\t\t\t\t<NcButton variant=\"tertiary\" @click=\"cancelAdd\">\n\t\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'Cancel') }}\n\t\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</template>\n\n\t\t\t\t\t<!-- Step 2: provider-specific form + how-to -->\n\t\t\t\t\t<template v-else>\n\t\t\t\t\t\t<div class=\"cn-credentials__wizard-head\">\n\t\t\t\t\t\t\t<NcButton variant=\"tertiary\" @click=\"wizardStep = 'provider'\">\n\t\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t\t<ChevronLeft :size=\"20\" />\n\t\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'Back') }}\n\t\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t\t\t<span class=\"cn-credentials__tile-dot\" :style=\"dotStyle(form.provider)\">{{ providerInitial(form.provider) }}</span>\n\t\t\t\t\t\t\t<span class=\"cn-credentials__wizard-title\">{{ providerTitle(form.provider) }}</span>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t<NcNoteCard v-if=\"activeMeta.setupHelp\" type=\"info\" class=\"cn-credentials__howto\">\n\t\t\t\t\t\t\t{{ activeMeta.setupHelp }}\n\t\t\t\t\t\t\t<a v-if=\"activeMeta.setupUrl\"\n\t\t\t\t\t\t\t\t:href=\"activeMeta.setupUrl\"\n\t\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t\t\t\tclass=\"cn-credentials__howto-link\">\n\t\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'Open {provider}', { provider: providerTitle(form.provider) }) }} ↗\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t</NcNoteCard>\n\n\t\t\t\t\t\t<form class=\"cn-credentials__form\" @submit.prevent=\"onCreate\">\n\t\t\t\t\t\t\t<NcTextField v-model=\"form.name\"\n\t\t\t\t\t\t\t\t:label=\"t('nextcloud-vue', 'Name')\"\n\t\t\t\t\t\t\t\t:helperText=\"t('nextcloud-vue', 'A label to recognise this credential later.')\"\n\t\t\t\t\t\t\t\t:disabled=\"saving\"\n\t\t\t\t\t\t\t\trequired />\n\n\t\t\t\t\t\t\t<NcTextField v-model=\"form.secret\"\n\t\t\t\t\t\t\t\ttype=\"password\"\n\t\t\t\t\t\t\t\t:label=\"activeMeta.secretLabel || t('nextcloud-vue', 'Secret')\"\n\t\t\t\t\t\t\t\t:helperText=\"t('nextcloud-vue', 'Sent to OpenRegister and stored in Keepiq. It is never shown again.')\"\n\t\t\t\t\t\t\t\t:disabled=\"saving\"\n\t\t\t\t\t\t\t\tautocomplete=\"new-password\"\n\t\t\t\t\t\t\t\trequired />\n\n\t\t\t\t\t\t\t<!-- Organisation add: admin also chooses the allowed apps up-front. -->\n\t\t\t\t\t\t\t<NcSelect v-if=\"scope === 'organisation'\"\n\t\t\t\t\t\t\t\t:options=\"appOptions\"\n\t\t\t\t\t\t\t\t:modelValue=\"form.allowedApps\"\n\t\t\t\t\t\t\t\t:multiple=\"true\"\n\t\t\t\t\t\t\t\t:taggable=\"true\"\n\t\t\t\t\t\t\t\tkeepOpen\n\t\t\t\t\t\t\t\t:disabled=\"saving\"\n\t\t\t\t\t\t\t\t:inputLabel=\"t('nextcloud-vue', 'Allowed apps')\"\n\t\t\t\t\t\t\t\t:placeholder=\"t('nextcloud-vue', 'Choose which apps may use it')\"\n\t\t\t\t\t\t\t\t@update:modelValue=\"form.allowedApps = normaliseApps($event)\" />\n\t\t\t\t\t\t\t<p v-else class=\"cn-credentials__muted cn-credentials__addnote\">\n\t\t\t\t\t\t\t\t{{ t('nextcloud-vue', '{app} will be allowed to use this credential. You can change that afterwards.', { app: appDisplayName }) }}\n\t\t\t\t\t\t\t</p>\n\n\t\t\t\t\t\t\t<div class=\"cn-credentials__form-actions\">\n\t\t\t\t\t\t\t\t<NcButton variant=\"tertiary\" :disabled=\"saving\" @click=\"cancelAdd\">\n\t\t\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'Cancel') }}\n\t\t\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t\t\t\t<NcButton variant=\"primary\"\n\t\t\t\t\t\t\t\t\ttype=\"submit\"\n\t\t\t\t\t\t\t\t\t:disabled=\"!canSubmit\">\n\t\t\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'Add credential') }}\n\t\t\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</form>\n\t\t\t\t\t</template>\n\t\t\t\t</div>\n\t\t\t</section>\n\t\t</template>\n\t</div>\n</template>\n\n<script>\nimport axios from '@nextcloud/axios'\nimport { translate as t } from '@nextcloud/l10n'\nimport { generateUrl } from '@nextcloud/router'\nimport { NcButton, NcCheckboxRadioSwitch, NcLoadingIcon, NcNoteCard, NcSelect, NcTextField } from '@nextcloud/vue'\nimport ChevronLeft from 'vue-material-design-icons/ChevronLeft.vue'\nimport Plus from 'vue-material-design-icons/Plus.vue'\n\nconst CREDENTIALS_PATH = '/apps/openregister/api/credentials'\nconst PROVIDERS_PATH = '/apps/openregister/api/credentials/providers'\n\n/**\n * Hardcoded per-provider presentation. Keyed by the OpenRegister catalogue\n * identifier. `colour` drives the tile; `setupHelp`/`setupUrl` explain how to\n * obtain the secret; `secretLabel` names the field. Providers not listed here\n * still work — they render a neutral tile from their identifier.\n */\nconst PROVIDER_META = {\n\tgithub: {\n\t\ttitle: 'GitHub',\n\t\tcolour: '#1f2328',\n\t\tsetupUrl: 'https://github.com/settings/personal-access-tokens',\n\t\t// No permission level here on purpose. This text is shared by every\n\t\t// consuming app, and the level each one needs is already on screen:\n\t\t// the requests list above renders each app's own `reason`. Asserting\n\t\t// \"read-only\" contradicted Buildiq two blocks higher, which asks for\n\t\t// Administration and Contents at read AND write to publish an app, so a\n\t\t// reader who followed this line made a token that cannot publish.\n\t\tsetupHelp: 'Create a fine-grained personal access token scoped to the repositories or organisation this app should reach, granting the permissions listed above, then paste it below.',\n\t\tsecretLabel: 'Personal access token',\n\t},\n\tgitlab: {\n\t\ttitle: 'GitLab',\n\t\tcolour: '#fc6d26',\n\t\tsetupUrl: 'https://gitlab.com/-/user_settings/personal_access_tokens',\n\t\tsetupHelp: 'Create a personal access token with the read_api (and read_repository) scopes, then paste it below.',\n\t\tsecretLabel: 'Personal access token',\n\t},\n\tdoffin: {\n\t\ttitle: 'Doffin (Norway)',\n\t\tcolour: '#00509e',\n\t\tsetupUrl: 'https://dfo.no/anskaffelser/doffin',\n\t\tsetupHelp: 'Request an APIM subscription key for the Doffin public procurement API, then paste it below.',\n\t\tsecretLabel: 'Subscription key',\n\t},\n}\n\nexport default {\n\tname: 'CnCredentials',\n\n\tcomponents: {\n\t\tNcButton,\n\t\tNcCheckboxRadioSwitch,\n\t\tNcLoadingIcon,\n\t\tNcNoteCard,\n\t\tNcSelect,\n\t\tNcTextField,\n\t\tChevronLeft,\n\t\tPlus,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The consuming Nextcloud app id (e.g. \"openbuild-spectr\"). In personal\n\t\t * scope this is the only app you can toggle in/out of a credential.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tappId: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * A friendly app name for copy (\"{app} may use this credential\"). Falls\n\t\t * back to the appId.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tappName: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * The current app's manifest `credentials[]` declarations — the\n\t\t * providers this app can reach through the broker. Shape:\n\t\t * `[{ provider, reason, scopes }]`. Rendered read-only under the intro.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\tappCredentials: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Which credential set to manage: `\"personal\"` (the signed-in user's\n\t\t * own; app-scoped toggle only) or `\"organisation\"` (org-wide; full\n\t\t * allowed-app management, admin surface).\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tscope: {\n\t\t\ttype: String,\n\t\t\tdefault: 'personal',\n\t\t\tvalidator: (v) => ['personal', 'organisation'].includes(v),\n\t\t},\n\n\t\t/**\n\t\t * Optional link target explaining the Keepiq vault. Defaults to the\n\t\t * Keepiq app route; pass '' to hide the link.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tvaultUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tloading: true,\n\t\t\terror: false,\n\t\t\tsaving: false,\n\t\t\tcredentials: [],\n\t\t\tproviders: [],\n\t\t\tadding: false,\n\t\t\twizardStep: 'provider',\n\t\t\tform: {\n\t\t\t\tname: '',\n\t\t\t\tprovider: null,\n\t\t\t\tsecret: '',\n\t\t\t\tallowedApps: [],\n\t\t\t},\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Friendly current-app label.\n\t\t *\n\t\t * @return {string} App display name.\n\t\t */\n\t\tappDisplayName() {\n\t\t\treturn this.appName || this.appId || t('nextcloud-vue', 'This app')\n\t\t},\n\n\t\t/**\n\t\t * Reframed intro copy per scope.\n\t\t *\n\t\t * @return {string} Intro text.\n\t\t */\n\t\tintroText() {\n\t\t\tif (this.scope === 'organisation') {\n\t\t\t\treturn t('nextcloud-vue', 'Organisation credentials let apps act on behalf of your organisation. The secret is stored in Keepiq — Nextcloud\\'s native credential vault — never in the app. You choose which apps may use each credential.')\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', 'Apps sometimes need to act on your behalf against an external service. So they never hold your secrets, you give a secret to Nextcloud once and it is kept in Keepiq — a native, encrypted credential vault. Apps then make the call through Keepiq and never see the secret. You decide which apps may use each credential — share one across apps, or keep one per app.')\n\t\t},\n\n\t\t/**\n\t\t * Resolved Keepiq link (explicit prop, or the app route by default).\n\t\t *\n\t\t * @return {string} URL or ''.\n\t\t */\n\t\tresolvedVaultUrl() {\n\t\t\tif (this.vaultUrl === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn this.vaultUrl || generateUrl('/apps/doriath')\n\t\t},\n\n\t\t/**\n\t\t * The provider tiles offered in the add-wizard grid: what the server\n\t\t * actually offers when known (so we never offer a provider the broker\n\t\t * can't honour), else the full hardcoded set.\n\t\t *\n\t\t * @return {Array<{identifier: string, title: string}>} Grid tiles.\n\t\t */\n\t\tproviderGrid() {\n\t\t\tconst serverIds = this.providers.map((p) => p.identifier)\n\t\t\tlet ids = serverIds.length ? serverIds : Object.keys(PROVIDER_META)\n\t\t\t// When the app declares which providers it uses (appCredentials),\n\t\t\t// only offer those — an app should not let you add a credential for\n\t\t\t// a provider it has no code path to use.\n\t\t\tif (this.supportedProviders.size > 0) {\n\t\t\t\tids = ids.filter((id) => this.supportedProviders.has(id))\n\t\t\t}\n\t\t\treturn ids.map((id) => ({ identifier: id, title: this.providerTitle(id) }))\n\t\t},\n\n\t\t/**\n\t\t * Provider identifiers the app declares it uses, derived from\n\t\t * `appCredentials`. Empty when the app declares nothing — filtering is\n\t\t * then a no-op, so consumers that don't declare providers are unchanged.\n\t\t *\n\t\t * @return {Set<string>} Supported provider identifiers.\n\t\t */\n\t\tsupportedProviders() {\n\t\t\treturn new Set((this.appCredentials || [])\n\t\t\t\t.map((r) => r && r.provider)\n\t\t\t\t.filter(Boolean))\n\t\t},\n\n\t\t/**\n\t\t * Stored credentials to display — filtered to the app's supported\n\t\t * providers when it declares any, so you can only see and authorise\n\t\t * credentials the app can actually use; otherwise all of them.\n\t\t *\n\t\t * @return {Array<object>} Visible credentials.\n\t\t */\n\t\tvisibleCredentials() {\n\t\t\tif (this.supportedProviders.size === 0) {\n\t\t\t\treturn this.credentials\n\t\t\t}\n\t\t\treturn this.credentials.filter((c) => this.supportedProviders.has(c.provider))\n\t\t},\n\n\t\t/**\n\t\t * The union of app ids the organisation allowed-apps picker offers.\n\t\t *\n\t\t * @return {Array<string>} Distinct app ids.\n\t\t */\n\t\tappOptions() {\n\t\t\tconst ids = new Set()\n\t\t\tif (this.appId) {\n\t\t\t\tids.add(this.appId)\n\t\t\t}\n\t\t\tfor (const cred of this.credentials) {\n\t\t\t\tfor (const appId of (cred.allowedApps || [])) {\n\t\t\t\t\tif (appId) {\n\t\t\t\t\t\tids.add(appId)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn Array.from(ids)\n\t\t},\n\n\t\t/**\n\t\t * Presentation metadata for the provider currently being added.\n\t\t *\n\t\t * @return {object} Provider meta (may be a neutral fallback).\n\t\t */\n\t\tactiveMeta() {\n\t\t\treturn PROVIDER_META[this.form.provider] || {}\n\t\t},\n\n\t\t/**\n\t\t * Whether the add form can be submitted — name, provider and a secret\n\t\t * are required (the whole point is to store a secret).\n\t\t *\n\t\t * @return {boolean} True when submittable.\n\t\t */\n\t\tcanSubmit() {\n\t\t\treturn !this.saving\n\t\t\t\t&& this.form.name.trim() !== ''\n\t\t\t\t&& !!this.form.provider\n\t\t\t\t&& this.form.secret.trim() !== ''\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.load()\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * Load credentials (scoped) and the provider catalogue in parallel.\n\t\t * Fails soft on a rejected request.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync load() {\n\t\t\tthis.loading = true\n\t\t\tthis.error = false\n\t\t\ttry {\n\t\t\t\tconst [credsRes, provRes] = await Promise.all([\n\t\t\t\t\taxios.get(generateUrl(CREDENTIALS_PATH), { params: { scope: this.scope } }),\n\t\t\t\t\taxios.get(generateUrl(PROVIDERS_PATH)),\n\t\t\t\t])\n\t\t\t\tthis.credentials = this.mapCredentials(credsRes?.data?.results)\n\t\t\t\tthis.providers = Array.isArray(provRes?.data?.results) ? provRes.data.results : []\n\t\t\t} catch {\n\t\t\t\tthis.error = true\n\t\t\t\tthis.credentials = []\n\t\t\t\tthis.providers = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Normalise API credential objects into display rows with local state.\n\t\t *\n\t\t * @param {Array<object>} results Raw credential objects.\n\t\t * @return {Array<object>} Display rows.\n\t\t */\n\t\tmapCredentials(results) {\n\t\t\tif (!Array.isArray(results)) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\treturn results.map((c) => ({\n\t\t\t\tid: c.id,\n\t\t\t\tname: c.name || '',\n\t\t\t\tprovider: c.provider || '',\n\t\t\t\tallowedApps: Array.isArray(c.allowedApps) ? c.allowedApps.slice() : [],\n\t\t\t\tsaving: false,\n\t\t\t\tconfirmingDelete: false,\n\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * Open the add-credential wizard at the provider grid.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tstartAdd() {\n\t\t\tthis.resetForm()\n\t\t\tthis.wizardStep = 'provider'\n\t\t\tthis.adding = true\n\t\t},\n\n\t\t/**\n\t\t * Cancel the wizard and reset.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tcancelAdd() {\n\t\t\tthis.adding = false\n\t\t\tthis.resetForm()\n\t\t},\n\n\t\t/**\n\t\t * Choose a provider in step 1 and advance to the form.\n\t\t *\n\t\t * @param {string} identifier The provider identifier.\n\t\t * @return {void}\n\t\t */\n\t\tpickProvider(identifier) {\n\t\t\tthis.form.provider = identifier\n\t\t\tthis.wizardStep = 'form'\n\t\t},\n\n\t\t/**\n\t\t * Create a credential. Personal scope auto-adds the current app; org\n\t\t * scope sends the admin-chosen allowed apps and the organisation scope.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onCreate() {\n\t\t\tif (!this.canSubmit) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.saving = true\n\t\t\ttry {\n\t\t\t\tconst body = {\n\t\t\t\t\tname: this.form.name.trim(),\n\t\t\t\t\tprovider: this.form.provider,\n\t\t\t\t\tsecret: this.form.secret,\n\t\t\t\t}\n\t\t\t\tif (this.scope === 'organisation') {\n\t\t\t\t\tbody.scope = 'organisation'\n\t\t\t\t\tbody.allowedApps = this.form.allowedApps.length ? this.form.allowedApps : (this.appId ? [this.appId] : [])\n\t\t\t\t} else {\n\t\t\t\t\tbody.allowedApps = this.appId ? [this.appId] : []\n\t\t\t\t}\n\t\t\t\tawait axios.post(generateUrl(CREDENTIALS_PATH), body)\n\t\t\t\tthis.cancelAdd()\n\t\t\t\tawait this.load()\n\t\t\t} catch {\n\t\t\t\tthis.showError(t('nextcloud-vue', 'Could not save the credential'))\n\t\t\t} finally {\n\t\t\t\tthis.saving = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Delete a credential (and its vault secret), then reload.\n\t\t *\n\t\t * @param {object} cred The credential row.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onDelete(cred) {\n\t\t\tcred.saving = true\n\t\t\ttry {\n\t\t\t\tawait axios.delete(generateUrl(`${CREDENTIALS_PATH}/${encodeURIComponent(cred.id)}`))\n\t\t\t\tawait this.load()\n\t\t\t} catch {\n\t\t\t\tthis.showError(t('nextcloud-vue', 'Could not delete the credential'))\n\t\t\t\tcred.saving = false\n\t\t\t\tcred.confirmingDelete = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Whether the current app is allowed to use a credential.\n\t\t *\n\t\t * @param {object} cred The credential row.\n\t\t * @return {boolean} True when appId is in allowedApps.\n\t\t */\n\t\tappAllowed(cred) {\n\t\t\treturn !!this.appId && (cred.allowedApps || []).includes(this.appId)\n\t\t},\n\n\t\t/**\n\t\t * The apps other than the current one that may use a credential.\n\t\t *\n\t\t * @param {object} cred The credential row.\n\t\t * @return {Array<string>} Other app ids.\n\t\t */\n\t\totherApps(cred) {\n\t\t\treturn (cred.allowedApps || []).filter((a) => a && a !== this.appId)\n\t\t},\n\n\t\t/**\n\t\t * Personal scope: add/remove ONLY the current app to/from a credential.\n\t\t * Reverts on failure.\n\t\t *\n\t\t * @param {object} cred The credential row.\n\t\t * @param {boolean} checked The switch's new state.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync toggleThisApp(cred, checked) {\n\t\t\tif (!this.appId) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst previous = cred.allowedApps.slice()\n\t\t\tconst set = new Set(previous)\n\t\t\tif (checked) {\n\t\t\t\tset.add(this.appId)\n\t\t\t} else {\n\t\t\t\tset.delete(this.appId)\n\t\t\t}\n\t\t\tconst next = Array.from(set)\n\t\t\tcred.allowedApps = next\n\t\t\tcred.saving = true\n\t\t\ttry {\n\t\t\t\tawait axios.put(generateUrl(`${CREDENTIALS_PATH}/${encodeURIComponent(cred.id)}`), { allowedApps: next })\n\t\t\t} catch {\n\t\t\t\tcred.allowedApps = previous\n\t\t\t\tthis.showError(t('nextcloud-vue', 'Could not update the credential'))\n\t\t\t} finally {\n\t\t\t\tcred.saving = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Organisation scope: persist a full allowed-apps change via PUT.\n\t\t * Reverts on failure.\n\t\t *\n\t\t * @param {object} cred The credential row.\n\t\t * @param {Array} updated The picker's new selection.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onAllowedAppsChange(cred, updated) {\n\t\t\tconst next = this.normaliseApps(updated)\n\t\t\tconst previous = cred.allowedApps.slice()\n\t\t\tcred.allowedApps = next\n\t\t\tcred.saving = true\n\t\t\ttry {\n\t\t\t\tawait axios.put(generateUrl(`${CREDENTIALS_PATH}/${encodeURIComponent(cred.id)}`), { allowedApps: next })\n\t\t\t} catch {\n\t\t\t\tcred.allowedApps = previous\n\t\t\t\tthis.showError(t('nextcloud-vue', 'Could not update allowed apps'))\n\t\t\t} finally {\n\t\t\t\tcred.saving = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Human-readable provider title — hardcoded catalogue first, then the\n\t\t * server title, then the raw identifier.\n\t\t *\n\t\t * @param {string} identifier The provider identifier.\n\t\t * @return {string} A display title.\n\t\t */\n\t\tproviderTitle(identifier) {\n\t\t\tif (PROVIDER_META[identifier] && PROVIDER_META[identifier].title) {\n\t\t\t\treturn PROVIDER_META[identifier].title\n\t\t\t}\n\t\t\tconst match = this.providers.find((p) => p.identifier === identifier)\n\t\t\treturn (match && match.title) || identifier || ''\n\t\t},\n\n\t\t/**\n\t\t * A one-letter badge for a provider tile.\n\t\t *\n\t\t * @param {string} identifier The provider identifier.\n\t\t * @return {string} An uppercase initial.\n\t\t */\n\t\tproviderInitial(identifier) {\n\t\t\tconst title = this.providerTitle(identifier)\n\t\t\treturn (title || '?').trim().charAt(0).toUpperCase() || '?'\n\t\t},\n\n\t\t/**\n\t\t * Inline style for a provider tile dot (its brand colour, or neutral).\n\t\t *\n\t\t * @param {string} identifier The provider identifier.\n\t\t * @return {object} A style binding.\n\t\t */\n\t\tdotStyle(identifier) {\n\t\t\tconst colour = (PROVIDER_META[identifier] && PROVIDER_META[identifier].colour) || 'var(--color-primary-element)'\n\t\t\treturn { backgroundColor: colour }\n\t\t},\n\n\t\t/**\n\t\t * Coerce an NcSelect multiple/taggable value into a string[] of app ids.\n\t\t *\n\t\t * @param {Array} value The picker value.\n\t\t * @return {Array<string>} App ids.\n\t\t */\n\t\tnormaliseApps(value) {\n\t\t\tif (!Array.isArray(value)) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\treturn value.map((v) => (typeof v === 'string' ? v : (v && v.id) || '')).filter((v) => v !== '')\n\t\t},\n\n\t\t/**\n\t\t * Reset the add-credential form.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tresetForm() {\n\t\t\tthis.form = { name: '', provider: null, secret: '', allowedApps: [] }\n\t\t},\n\n\t\t/**\n\t\t * Surface an error toast (lazy-loaded like the rest of the lib).\n\t\t *\n\t\t * @param {string} message The message to show.\n\t\t * @return {void}\n\t\t */\n\t\tshowError(message) {\n\t\t\timport('@nextcloud/dialogs').then(({ showError }) => showError(message))\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-credentials__intro {\n\tmargin-bottom: 16px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-credentials__vault-link,\n.cn-credentials__howto-link {\n\twhite-space: nowrap;\n\tcolor: var(--color-primary-element);\n}\n\n.cn-credentials__error {\n\tmargin-bottom: 12px;\n}\n\n.cn-credentials__section {\n\tmargin-bottom: 22px;\n}\n\n.cn-credentials__section-title {\n\tmargin: 8px 0 8px;\n\tfont-weight: bold;\n}\n\n.cn-credentials__muted,\n.muted {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-credentials__requests,\n.cn-credentials__list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-credentials__request {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 10px;\n\tpadding: 6px 0;\n}\n\n.cn-credentials__request-body {\n\tdisplay: flex;\n\tflex-direction: column;\n}\n\n.cn-credentials__request-provider {\n\tfont-weight: bold;\n}\n\n.cn-credentials__request-reason {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n/* Provider tile dot: a coloured badge with the provider's initial. */\n.cn-credentials__tile-dot {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tjustify-content: center;\n\tflex-shrink: 0;\n\twidth: 32px;\n\theight: 32px;\n\tborder-radius: var(--border-radius-large, 8px);\n\tcolor: #fff;\n\tfont-weight: bold;\n\tfont-size: 0.95em;\n\tline-height: 1;\n}\n\n.cn-credentials__tile-dot--lg {\n\twidth: 44px;\n\theight: 44px;\n\tfont-size: 1.3em;\n}\n\n.cn-credentials__item {\n\tpadding: 12px 0;\n\tborder-bottom: 1px solid var(--color-border);\n}\n\n.cn-credentials__item-head {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 10px;\n}\n\n.cn-credentials__item-meta {\n\tdisplay: flex;\n\tflex-direction: column;\n\tflex-grow: 1;\n\tmin-width: 0;\n}\n\n.cn-credentials__item-name {\n\tfont-weight: bold;\n}\n\n.cn-credentials__item-provider {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-credentials__item-actions {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 4px;\n\tflex-shrink: 0;\n}\n\n.cn-credentials__item-toggle {\n\tmargin: 8px 0 0 42px;\n}\n\n.cn-credentials__also {\n\tmargin: 4px 0 0;\n\tfont-size: 0.9em;\n}\n\n.cn-credentials__item-apps {\n\tmargin-top: 8px;\n\twidth: 100%;\n}\n\n.cn-credentials__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(120px, 1fr));\n\tgap: 10px;\n\tmargin-bottom: 12px;\n}\n\n.cn-credentials__grid-tile {\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: center;\n\tgap: 8px;\n\tpadding: 16px 8px;\n\tbackground: var(--color-background-hover);\n\tborder: 2px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 8px);\n\tcursor: pointer;\n}\n\n.cn-credentials__grid-tile:hover,\n.cn-credentials__grid-tile:focus-visible {\n\tborder-color: var(--color-primary-element);\n\tbackground: var(--color-background-dark);\n}\n\n.cn-credentials__grid-title {\n\tfont-weight: bold;\n\ttext-align: center;\n}\n\n.cn-credentials__wizard-head {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 10px;\n\tmargin-bottom: 12px;\n}\n\n.cn-credentials__wizard-title {\n\tfont-weight: bold;\n\tfont-size: 1.1em;\n}\n\n.cn-credentials__howto {\n\tmargin-bottom: 12px;\n}\n\n.cn-credentials__form {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n}\n\n.cn-credentials__addnote {\n\tmargin: 0;\n\tfont-size: 0.9em;\n}\n\n.cn-credentials__form-actions,\n.cn-credentials__wizard-actions {\n\tdisplay: flex;\n\tjustify-content: flex-end;\n\tgap: 8px;\n}\n</style>\n"],"names":["t","axios"],"mappings":"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"}
1
+ {"version":3,"file":"CnCredentials.vue2.js","sources":["../../../../src/components/CnCredentials/CnCredentials.vue"],"sourcesContent":["<!--\n - SPDX-License-Identifier: EUPL-1.2\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n -\n - CnCredentials: the settings surface for the OpenRegister credential\n - broker. Apps occasionally need to act on your behalf against an external\n - service (GitHub, GitLab, …). Rather than hand the secret to the app, you\n - give it once to OpenRegister, which stores it in Keepiq, Nextcloud's\n - native, encrypted credential vault. Apps then make the outbound call\n - THROUGH OpenRegister and never see the secret itself. You decide which\n - apps may use each credential, so one credential can be shared across apps\n - or kept dedicated to one.\n -\n - Two scopes render from this one component:\n - • scope=\"personal\" (default), the signed-in user's own\n - credentials, shown in an app's *personal* settings. From here you may\n - only add/remove *the app you are currently in* to/from a credential,\n - or add a new credential (the current app is auto-added). You never\n - manage the full app list of another app from here.\n - • scope=\"organisation\" , organisation-wide credentials, shown in an\n - app's *admin* settings. An admin manages the full allowed-app list.\n -\n - Talks to OpenRegister's credential endpoints (metadata only, a secret is\n - write-only and is NEVER returned or displayed):\n - GET /apps/openregister/api/credentials?scope={scope}\n - GET /apps/openregister/api/credentials/providers\n - POST /apps/openregister/api/credentials { name, provider, allowedApps?, secret?, scope? }\n - PUT /apps/openregister/api/credentials/{id} { name?, allowedApps?, secret? }\n - DELETE /apps/openregister/api/credentials/{id}\n -\n - Provider presentation (title, colour, how-to text + link, secret label) is\n - a hardcoded catalogue in this file (PROVIDER_META): new providers get a\n - richer card here without a server change; unknown providers fall back to a\n - neutral tile keyed off the identifier.\n -\n - Renders inside CnAppRoot's `#user-settings` (personal) or an admin surface\n - (organisation). Fails soft: a 404 (broker not installed) degrades to an\n - empty state with a friendly note rather than crashing.\n -->\n<template>\n\t<div class=\"cn-credentials\" :class=\"`cn-credentials--${scope}`\">\n\t\t<p class=\"cn-credentials__intro\">\n\t\t\t{{ introText }}\n\t\t\t<a v-if=\"resolvedVaultUrl\"\n\t\t\t\t:href=\"resolvedVaultUrl\"\n\t\t\t\tclass=\"cn-credentials__vault-link\"\n\t\t\t\ttarget=\"_self\">\n\t\t\t\t{{ t('nextcloud-vue', 'Learn about Keepiq') }} ↗\n\t\t\t</a>\n\t\t</p>\n\n\t\t<!-- What this app uses (the app's manifest credentials[] requirements) -->\n\t\t<section v-if=\"scope === 'personal'\" class=\"cn-credentials__section cn-credentials__supports\">\n\t\t\t<h4 class=\"cn-credentials__section-title\">\n\t\t\t\t{{ t('nextcloud-vue', 'What {app} uses', { app: appDisplayName }) }}\n\t\t\t</h4>\n\t\t\t<p v-if=\"appCredentials.length === 0\" class=\"cn-credentials__muted\">\n\t\t\t\t{{ t('nextcloud-vue', 'This app does not use any external credentials.') }}\n\t\t\t</p>\n\t\t\t<ul v-else class=\"cn-credentials__requests\">\n\t\t\t\t<li v-for=\"(req, index) in appCredentials\"\n\t\t\t\t\t:key=\"`req-${index}`\"\n\t\t\t\t\tclass=\"cn-credentials__request\">\n\t\t\t\t\t<span class=\"cn-credentials__tile-dot\" :style=\"dotStyle(req.provider)\">{{ providerInitial(req.provider) }}</span>\n\t\t\t\t\t<span class=\"cn-credentials__request-body\">\n\t\t\t\t\t\t<span class=\"cn-credentials__request-provider\">{{ providerTitle(req.provider) }}</span>\n\t\t\t\t\t\t<span v-if=\"req.reason\" class=\"cn-credentials__request-reason\">{{ req.reason }}</span>\n\t\t\t\t\t</span>\n\t\t\t\t</li>\n\t\t\t</ul>\n\t\t</section>\n\n\t\t<NcLoadingIcon v-if=\"loading\" :size=\"24\" />\n\n\t\t<template v-else>\n\t\t\t<NcNoteCard v-if=\"error\"\n\t\t\t\ttype=\"warning\"\n\t\t\t\tclass=\"cn-credentials__error\">\n\t\t\t\t{{ t('nextcloud-vue', 'Credentials are provided by OpenRegister and could not be loaded here.') }}\n\t\t\t</NcNoteCard>\n\n\t\t\t<!-- Your / the organisation's credentials -->\n\t\t\t<section class=\"cn-credentials__section\">\n\t\t\t\t<h4 class=\"cn-credentials__section-title\">\n\t\t\t\t\t{{ scope === 'organisation' ? t('nextcloud-vue', 'Organisation credentials') : t('nextcloud-vue', 'Your credentials') }}\n\t\t\t\t</h4>\n\t\t\t\t<p v-if=\"visibleCredentials.length === 0\" class=\"cn-credentials__muted\">\n\t\t\t\t\t{{ t('nextcloud-vue', 'No credentials stored yet.') }}\n\t\t\t\t</p>\n\t\t\t\t<ul v-else class=\"cn-credentials__list\">\n\t\t\t\t\t<li v-for=\"cred in visibleCredentials\"\n\t\t\t\t\t\t:key=\"cred.id\"\n\t\t\t\t\t\tclass=\"cn-credentials__item\">\n\t\t\t\t\t\t<div class=\"cn-credentials__item-head\">\n\t\t\t\t\t\t\t<span class=\"cn-credentials__tile-dot\" :style=\"dotStyle(cred.provider)\">{{ providerInitial(cred.provider) }}</span>\n\t\t\t\t\t\t\t<div class=\"cn-credentials__item-meta\">\n\t\t\t\t\t\t\t\t<span class=\"cn-credentials__item-name\">{{ cred.name }}</span>\n\t\t\t\t\t\t\t\t<span class=\"cn-credentials__item-provider\">{{ providerTitle(cred.provider) }}</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"cn-credentials__item-actions\">\n\t\t\t\t\t\t\t\t<template v-if=\"cred.confirmingDelete\">\n\t\t\t\t\t\t\t\t\t<NcButton variant=\"error\"\n\t\t\t\t\t\t\t\t\t\t:disabled=\"cred.saving\"\n\t\t\t\t\t\t\t\t\t\t@click=\"onDelete(cred)\">\n\t\t\t\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'Confirm delete') }}\n\t\t\t\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t\t\t\t\t<NcButton variant=\"tertiary\"\n\t\t\t\t\t\t\t\t\t\t:disabled=\"cred.saving\"\n\t\t\t\t\t\t\t\t\t\t@click=\"cred.confirmingDelete = false\">\n\t\t\t\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'Cancel') }}\n\t\t\t\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t\t<NcButton v-else\n\t\t\t\t\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\t\t\t\t\t:disabled=\"cred.saving\"\n\t\t\t\t\t\t\t\t\t@click=\"cred.confirmingDelete = true\">\n\t\t\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'Delete') }}\n\t\t\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t<!-- Personal: only toggle THIS app in/out of the credential. -->\n\t\t\t\t\t\t<div v-if=\"scope === 'personal'\" class=\"cn-credentials__item-toggle\">\n\t\t\t\t\t\t\t<NcCheckboxRadioSwitch :modelValue=\"appAllowed(cred)\"\n\t\t\t\t\t\t\t\t:disabled=\"cred.saving\"\n\t\t\t\t\t\t\t\ttype=\"switch\"\n\t\t\t\t\t\t\t\t@update:modelValue=\"toggleThisApp(cred, $event)\">\n\t\t\t\t\t\t\t\t{{ t('nextcloud-vue', '{app} may use this credential', { app: appDisplayName }) }}\n\t\t\t\t\t\t\t</NcCheckboxRadioSwitch>\n\t\t\t\t\t\t\t<p v-if=\"otherApps(cred).length\" class=\"cn-credentials__also muted\">\n\t\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'Also allowed for: {apps}', { apps: otherApps(cred).join(', ') }) }}\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t<!-- Organisation: admin manages the full allowed-app list. -->\n\t\t\t\t\t\t<NcSelect v-else\n\t\t\t\t\t\t\tclass=\"cn-credentials__item-apps\"\n\t\t\t\t\t\t\t:options=\"appOptions\"\n\t\t\t\t\t\t\t:modelValue=\"cred.allowedApps\"\n\t\t\t\t\t\t\t:multiple=\"true\"\n\t\t\t\t\t\t\t:taggable=\"true\"\n\t\t\t\t\t\t\tkeepOpen\n\t\t\t\t\t\t\t:disabled=\"cred.saving\"\n\t\t\t\t\t\t\t:inputLabel=\"t('nextcloud-vue', 'Allowed apps')\"\n\t\t\t\t\t\t\t:placeholder=\"t('nextcloud-vue', 'No app may use this credential yet')\"\n\t\t\t\t\t\t\t@update:modelValue=\"onAllowedAppsChange(cred, $event)\" />\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\t\t\t</section>\n\n\t\t\t<!-- Add credential: inline wizard: pick a provider, then fill it in. -->\n\t\t\t<section class=\"cn-credentials__section\">\n\t\t\t\t<NcButton v-if=\"!adding\"\n\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\t@click=\"startAdd\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<Plus :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ t('nextcloud-vue', 'Add credential') }}\n\t\t\t\t</NcButton>\n\n\t\t\t\t<div v-else class=\"cn-credentials__wizard\">\n\t\t\t\t\t<!-- Step 1: provider icon grid -->\n\t\t\t\t\t<template v-if=\"wizardStep === 'provider'\">\n\t\t\t\t\t\t<h4 class=\"cn-credentials__section-title\">\n\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'Which credential do you want to add?') }}\n\t\t\t\t\t\t</h4>\n\t\t\t\t\t\t<div class=\"cn-credentials__grid\">\n\t\t\t\t\t\t\t<button v-for=\"p in providerGrid\"\n\t\t\t\t\t\t\t\t:key=\"p.identifier\"\n\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\tclass=\"cn-credentials__grid-tile\"\n\t\t\t\t\t\t\t\t@click=\"pickProvider(p.identifier)\">\n\t\t\t\t\t\t\t\t<span class=\"cn-credentials__tile-dot cn-credentials__tile-dot--lg\" :style=\"dotStyle(p.identifier)\">{{ providerInitial(p.identifier) }}</span>\n\t\t\t\t\t\t\t\t<span class=\"cn-credentials__grid-title\">{{ p.title }}</span>\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"cn-credentials__wizard-actions\">\n\t\t\t\t\t\t\t<NcButton variant=\"tertiary\" @click=\"cancelAdd\">\n\t\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'Cancel') }}\n\t\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</template>\n\n\t\t\t\t\t<!-- Step 2: provider-specific form + how-to -->\n\t\t\t\t\t<template v-else>\n\t\t\t\t\t\t<div class=\"cn-credentials__wizard-head\">\n\t\t\t\t\t\t\t<NcButton variant=\"tertiary\" @click=\"wizardStep = 'provider'\">\n\t\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t\t<ChevronLeft :size=\"20\" />\n\t\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'Back') }}\n\t\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t\t\t<span class=\"cn-credentials__tile-dot\" :style=\"dotStyle(form.provider)\">{{ providerInitial(form.provider) }}</span>\n\t\t\t\t\t\t\t<span class=\"cn-credentials__wizard-title\">{{ providerTitle(form.provider) }}</span>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t<NcNoteCard v-if=\"activeMeta.setupHelp\" type=\"info\" class=\"cn-credentials__howto\">\n\t\t\t\t\t\t\t{{ activeMeta.setupHelp }}\n\t\t\t\t\t\t\t<a v-if=\"activeMeta.setupUrl\"\n\t\t\t\t\t\t\t\t:href=\"activeMeta.setupUrl\"\n\t\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t\t\t\tclass=\"cn-credentials__howto-link\">\n\t\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'Open {provider}', { provider: providerTitle(form.provider) }) }} ↗\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t</NcNoteCard>\n\n\t\t\t\t\t\t<form class=\"cn-credentials__form\" @submit.prevent=\"onCreate\">\n\t\t\t\t\t\t\t<NcTextField v-model=\"form.name\"\n\t\t\t\t\t\t\t\t:label=\"t('nextcloud-vue', 'Name')\"\n\t\t\t\t\t\t\t\t:helperText=\"t('nextcloud-vue', 'A label to recognise this credential later.')\"\n\t\t\t\t\t\t\t\t:disabled=\"saving\"\n\t\t\t\t\t\t\t\trequired />\n\n\t\t\t\t\t\t\t<NcTextField v-model=\"form.secret\"\n\t\t\t\t\t\t\t\ttype=\"password\"\n\t\t\t\t\t\t\t\t:label=\"activeMeta.secretLabel || t('nextcloud-vue', 'Secret')\"\n\t\t\t\t\t\t\t\t:helperText=\"t('nextcloud-vue', 'Sent to OpenRegister and stored in Keepiq. It is never shown again.')\"\n\t\t\t\t\t\t\t\t:disabled=\"saving\"\n\t\t\t\t\t\t\t\tautocomplete=\"new-password\"\n\t\t\t\t\t\t\t\trequired />\n\n\t\t\t\t\t\t\t<!-- Organisation add: admin also chooses the allowed apps up-front. -->\n\t\t\t\t\t\t\t<NcSelect v-if=\"scope === 'organisation'\"\n\t\t\t\t\t\t\t\t:options=\"appOptions\"\n\t\t\t\t\t\t\t\t:modelValue=\"form.allowedApps\"\n\t\t\t\t\t\t\t\t:multiple=\"true\"\n\t\t\t\t\t\t\t\t:taggable=\"true\"\n\t\t\t\t\t\t\t\tkeepOpen\n\t\t\t\t\t\t\t\t:disabled=\"saving\"\n\t\t\t\t\t\t\t\t:inputLabel=\"t('nextcloud-vue', 'Allowed apps')\"\n\t\t\t\t\t\t\t\t:placeholder=\"t('nextcloud-vue', 'Choose which apps may use it')\"\n\t\t\t\t\t\t\t\t@update:modelValue=\"form.allowedApps = normaliseApps($event)\" />\n\t\t\t\t\t\t\t<p v-else class=\"cn-credentials__muted cn-credentials__addnote\">\n\t\t\t\t\t\t\t\t{{ t('nextcloud-vue', '{app} will be allowed to use this credential. You can change that afterwards.', { app: appDisplayName }) }}\n\t\t\t\t\t\t\t</p>\n\n\t\t\t\t\t\t\t<div class=\"cn-credentials__form-actions\">\n\t\t\t\t\t\t\t\t<NcButton variant=\"tertiary\" :disabled=\"saving\" @click=\"cancelAdd\">\n\t\t\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'Cancel') }}\n\t\t\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t\t\t\t<NcButton variant=\"primary\"\n\t\t\t\t\t\t\t\t\ttype=\"submit\"\n\t\t\t\t\t\t\t\t\t:disabled=\"!canSubmit\">\n\t\t\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'Add credential') }}\n\t\t\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</form>\n\t\t\t\t\t</template>\n\t\t\t\t</div>\n\t\t\t</section>\n\t\t</template>\n\t</div>\n</template>\n\n<script>\nimport axios from '@nextcloud/axios'\nimport { translate as t } from '@nextcloud/l10n'\nimport { generateUrl } from '@nextcloud/router'\nimport { NcButton, NcCheckboxRadioSwitch, NcLoadingIcon, NcNoteCard, NcSelect, NcTextField } from '@nextcloud/vue'\nimport ChevronLeft from 'vue-material-design-icons/ChevronLeft.vue'\nimport Plus from 'vue-material-design-icons/Plus.vue'\n\nconst CREDENTIALS_PATH = '/apps/openregister/api/credentials'\nconst PROVIDERS_PATH = '/apps/openregister/api/credentials/providers'\n\n/**\n * Hardcoded per-provider presentation. Keyed by the OpenRegister catalogue\n * identifier. `colour` drives the tile; `setupHelp`/`setupUrl` explain how to\n * obtain the secret; `secretLabel` names the field. Providers not listed here\n * still work, they render a neutral tile from their identifier.\n */\nconst PROVIDER_META = {\n\tgithub: {\n\t\ttitle: 'GitHub',\n\t\tcolour: '#1f2328',\n\t\tsetupUrl: 'https://github.com/settings/personal-access-tokens',\n\t\t// No permission level here on purpose. This text is shared by every\n\t\t// consuming app, and the level each one needs is already on screen:\n\t\t// the requests list above renders each app's own `reason`. Asserting\n\t\t// \"read-only\" contradicted Buildiq two blocks higher, which asks for\n\t\t// Administration and Contents at read AND write to publish an app, so a\n\t\t// reader who followed this line made a token that cannot publish.\n\t\tsetupHelp: 'Create a fine-grained personal access token scoped to the repositories or organisation this app should reach, granting the permissions listed above, then paste it below.',\n\t\tsecretLabel: 'Personal access token',\n\t},\n\tgitlab: {\n\t\ttitle: 'GitLab',\n\t\tcolour: '#fc6d26',\n\t\tsetupUrl: 'https://gitlab.com/-/user_settings/personal_access_tokens',\n\t\tsetupHelp: 'Create a personal access token with the read_api (and read_repository) scopes, then paste it below.',\n\t\tsecretLabel: 'Personal access token',\n\t},\n\tdoffin: {\n\t\ttitle: 'Doffin (Norway)',\n\t\tcolour: '#00509e',\n\t\tsetupUrl: 'https://dfo.no/anskaffelser/doffin',\n\t\tsetupHelp: 'Request an APIM subscription key for the Doffin public procurement API, then paste it below.',\n\t\tsecretLabel: 'Subscription key',\n\t},\n}\n\nexport default {\n\tname: 'CnCredentials',\n\n\tcomponents: {\n\t\tNcButton,\n\t\tNcCheckboxRadioSwitch,\n\t\tNcLoadingIcon,\n\t\tNcNoteCard,\n\t\tNcSelect,\n\t\tNcTextField,\n\t\tChevronLeft,\n\t\tPlus,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The consuming Nextcloud app id (e.g. \"openbuild-spectr\"). In personal\n\t\t * scope this is the only app you can toggle in/out of a credential.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tappId: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * A friendly app name for copy (\"{app} may use this credential\"). Falls\n\t\t * back to the appId.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tappName: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * The current app's manifest `credentials[]` declarations: the\n\t\t * providers this app can reach through the broker. Shape:\n\t\t * `[{ provider, reason, scopes }]`. Rendered read-only under the intro.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\tappCredentials: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Which credential set to manage: `\"personal\"` (the signed-in user's\n\t\t * own; app-scoped toggle only) or `\"organisation\"` (org-wide; full\n\t\t * allowed-app management, admin surface).\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tscope: {\n\t\t\ttype: String,\n\t\t\tdefault: 'personal',\n\t\t\tvalidator: (v) => ['personal', 'organisation'].includes(v),\n\t\t},\n\n\t\t/**\n\t\t * Optional link target explaining the Keepiq vault. Defaults to the\n\t\t * Keepiq app route; pass '' to hide the link.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tvaultUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tloading: true,\n\t\t\terror: false,\n\t\t\tsaving: false,\n\t\t\tcredentials: [],\n\t\t\tproviders: [],\n\t\t\tadding: false,\n\t\t\twizardStep: 'provider',\n\t\t\tform: {\n\t\t\t\tname: '',\n\t\t\t\tprovider: null,\n\t\t\t\tsecret: '',\n\t\t\t\tallowedApps: [],\n\t\t\t},\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Friendly current-app label.\n\t\t *\n\t\t * @return {string} App display name.\n\t\t */\n\t\tappDisplayName() {\n\t\t\treturn this.appName || this.appId || t('nextcloud-vue', 'This app')\n\t\t},\n\n\t\t/**\n\t\t * Reframed intro copy per scope. No em-dashes, sentences under 16 words.\n\t\t *\n\t\t * @spec openspec/changes/r4-object-lock-url-and-credentials-copy/specs/credentials-settings/spec.md\n\t\t * @return {string} Intro text.\n\t\t */\n\t\tintroText() {\n\t\t\tif (this.scope === 'organisation') {\n\t\t\t\treturn t('nextcloud-vue', 'Organisation credentials let apps act for your organisation. Keepiq, the credential vault of Nextcloud, keeps the secret. The app never holds it. You choose which apps may use each credential.')\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', 'Apps sometimes act for you on an external service. You give the secret to Nextcloud once, and Keepiq keeps it in an encrypted vault. Apps make the call through Keepiq and never see the secret. You choose which apps may use each credential. Share one across apps, or keep one per app.')\n\t\t},\n\n\t\t/**\n\t\t * Resolved Keepiq link (explicit prop, or the app route by default).\n\t\t *\n\t\t * @return {string} URL or ''.\n\t\t */\n\t\tresolvedVaultUrl() {\n\t\t\tif (this.vaultUrl === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn this.vaultUrl || generateUrl('/apps/doriath')\n\t\t},\n\n\t\t/**\n\t\t * The provider tiles offered in the add-wizard grid: what the server\n\t\t * actually offers when known (so we never offer a provider the broker\n\t\t * can't honour), else the full hardcoded set.\n\t\t *\n\t\t * @return {Array<{identifier: string, title: string}>} Grid tiles.\n\t\t */\n\t\tproviderGrid() {\n\t\t\tconst serverIds = this.providers.map((p) => p.identifier)\n\t\t\tlet ids = serverIds.length ? serverIds : Object.keys(PROVIDER_META)\n\t\t\t// When the app declares which providers it uses (appCredentials),\n\t\t\t// only offer those, an app should not let you add a credential for\n\t\t\t// a provider it has no code path to use.\n\t\t\tif (this.supportedProviders.size > 0) {\n\t\t\t\tids = ids.filter((id) => this.supportedProviders.has(id))\n\t\t\t}\n\t\t\treturn ids.map((id) => ({ identifier: id, title: this.providerTitle(id) }))\n\t\t},\n\n\t\t/**\n\t\t * Provider identifiers the app declares it uses, derived from\n\t\t * `appCredentials`. Empty when the app declares nothing, filtering is\n\t\t * then a no-op, so consumers that don't declare providers are unchanged.\n\t\t *\n\t\t * @return {Set<string>} Supported provider identifiers.\n\t\t */\n\t\tsupportedProviders() {\n\t\t\treturn new Set((this.appCredentials || [])\n\t\t\t\t.map((r) => r && r.provider)\n\t\t\t\t.filter(Boolean))\n\t\t},\n\n\t\t/**\n\t\t * Stored credentials to display: filtered to the app's supported\n\t\t * providers when it declares any, so you can only see and authorise\n\t\t * credentials the app can actually use; otherwise all of them.\n\t\t *\n\t\t * @return {Array<object>} Visible credentials.\n\t\t */\n\t\tvisibleCredentials() {\n\t\t\tif (this.supportedProviders.size === 0) {\n\t\t\t\treturn this.credentials\n\t\t\t}\n\t\t\treturn this.credentials.filter((c) => this.supportedProviders.has(c.provider))\n\t\t},\n\n\t\t/**\n\t\t * The union of app ids the organisation allowed-apps picker offers.\n\t\t *\n\t\t * @return {Array<string>} Distinct app ids.\n\t\t */\n\t\tappOptions() {\n\t\t\tconst ids = new Set()\n\t\t\tif (this.appId) {\n\t\t\t\tids.add(this.appId)\n\t\t\t}\n\t\t\tfor (const cred of this.credentials) {\n\t\t\t\tfor (const appId of (cred.allowedApps || [])) {\n\t\t\t\t\tif (appId) {\n\t\t\t\t\t\tids.add(appId)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn Array.from(ids)\n\t\t},\n\n\t\t/**\n\t\t * Presentation metadata for the provider currently being added.\n\t\t *\n\t\t * @return {object} Provider meta (may be a neutral fallback).\n\t\t */\n\t\tactiveMeta() {\n\t\t\treturn PROVIDER_META[this.form.provider] || {}\n\t\t},\n\n\t\t/**\n\t\t * Whether the add form can be submitted: name, provider and a secret\n\t\t * are required (the whole point is to store a secret).\n\t\t *\n\t\t * @return {boolean} True when submittable.\n\t\t */\n\t\tcanSubmit() {\n\t\t\treturn !this.saving\n\t\t\t\t&& this.form.name.trim() !== ''\n\t\t\t\t&& !!this.form.provider\n\t\t\t\t&& this.form.secret.trim() !== ''\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.load()\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * Load credentials (scoped) and the provider catalogue in parallel.\n\t\t * Fails soft on a rejected request.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync load() {\n\t\t\tthis.loading = true\n\t\t\tthis.error = false\n\t\t\ttry {\n\t\t\t\tconst [credsRes, provRes] = await Promise.all([\n\t\t\t\t\taxios.get(generateUrl(CREDENTIALS_PATH), { params: { scope: this.scope } }),\n\t\t\t\t\taxios.get(generateUrl(PROVIDERS_PATH)),\n\t\t\t\t])\n\t\t\t\tthis.credentials = this.mapCredentials(credsRes?.data?.results)\n\t\t\t\tthis.providers = Array.isArray(provRes?.data?.results) ? provRes.data.results : []\n\t\t\t} catch {\n\t\t\t\tthis.error = true\n\t\t\t\tthis.credentials = []\n\t\t\t\tthis.providers = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Normalise API credential objects into display rows with local state.\n\t\t *\n\t\t * @param {Array<object>} results Raw credential objects.\n\t\t * @return {Array<object>} Display rows.\n\t\t */\n\t\tmapCredentials(results) {\n\t\t\tif (!Array.isArray(results)) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\treturn results.map((c) => ({\n\t\t\t\tid: c.id,\n\t\t\t\tname: c.name || '',\n\t\t\t\tprovider: c.provider || '',\n\t\t\t\tallowedApps: Array.isArray(c.allowedApps) ? c.allowedApps.slice() : [],\n\t\t\t\tsaving: false,\n\t\t\t\tconfirmingDelete: false,\n\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * Open the add-credential wizard at the provider grid.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tstartAdd() {\n\t\t\tthis.resetForm()\n\t\t\tthis.wizardStep = 'provider'\n\t\t\tthis.adding = true\n\t\t},\n\n\t\t/**\n\t\t * Cancel the wizard and reset.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tcancelAdd() {\n\t\t\tthis.adding = false\n\t\t\tthis.resetForm()\n\t\t},\n\n\t\t/**\n\t\t * Choose a provider in step 1 and advance to the form.\n\t\t *\n\t\t * @param {string} identifier The provider identifier.\n\t\t * @return {void}\n\t\t */\n\t\tpickProvider(identifier) {\n\t\t\tthis.form.provider = identifier\n\t\t\tthis.wizardStep = 'form'\n\t\t},\n\n\t\t/**\n\t\t * Create a credential. Personal scope auto-adds the current app; org\n\t\t * scope sends the admin-chosen allowed apps and the organisation scope.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onCreate() {\n\t\t\tif (!this.canSubmit) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.saving = true\n\t\t\ttry {\n\t\t\t\tconst body = {\n\t\t\t\t\tname: this.form.name.trim(),\n\t\t\t\t\tprovider: this.form.provider,\n\t\t\t\t\tsecret: this.form.secret,\n\t\t\t\t}\n\t\t\t\tif (this.scope === 'organisation') {\n\t\t\t\t\tbody.scope = 'organisation'\n\t\t\t\t\tbody.allowedApps = this.form.allowedApps.length ? this.form.allowedApps : (this.appId ? [this.appId] : [])\n\t\t\t\t} else {\n\t\t\t\t\tbody.allowedApps = this.appId ? [this.appId] : []\n\t\t\t\t}\n\t\t\t\tawait axios.post(generateUrl(CREDENTIALS_PATH), body)\n\t\t\t\tthis.cancelAdd()\n\t\t\t\tawait this.load()\n\t\t\t} catch {\n\t\t\t\tthis.showError(t('nextcloud-vue', 'Could not save the credential'))\n\t\t\t} finally {\n\t\t\t\tthis.saving = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Delete a credential (and its vault secret), then reload.\n\t\t *\n\t\t * @param {object} cred The credential row.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onDelete(cred) {\n\t\t\tcred.saving = true\n\t\t\ttry {\n\t\t\t\tawait axios.delete(generateUrl(`${CREDENTIALS_PATH}/${encodeURIComponent(cred.id)}`))\n\t\t\t\tawait this.load()\n\t\t\t} catch {\n\t\t\t\tthis.showError(t('nextcloud-vue', 'Could not delete the credential'))\n\t\t\t\tcred.saving = false\n\t\t\t\tcred.confirmingDelete = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Whether the current app is allowed to use a credential.\n\t\t *\n\t\t * @param {object} cred The credential row.\n\t\t * @return {boolean} True when appId is in allowedApps.\n\t\t */\n\t\tappAllowed(cred) {\n\t\t\treturn !!this.appId && (cred.allowedApps || []).includes(this.appId)\n\t\t},\n\n\t\t/**\n\t\t * The apps other than the current one that may use a credential.\n\t\t *\n\t\t * @param {object} cred The credential row.\n\t\t * @return {Array<string>} Other app ids.\n\t\t */\n\t\totherApps(cred) {\n\t\t\treturn (cred.allowedApps || []).filter((a) => a && a !== this.appId)\n\t\t},\n\n\t\t/**\n\t\t * Personal scope: add/remove ONLY the current app to/from a credential.\n\t\t * Reverts on failure.\n\t\t *\n\t\t * @param {object} cred The credential row.\n\t\t * @param {boolean} checked The switch's new state.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync toggleThisApp(cred, checked) {\n\t\t\tif (!this.appId) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst previous = cred.allowedApps.slice()\n\t\t\tconst set = new Set(previous)\n\t\t\tif (checked) {\n\t\t\t\tset.add(this.appId)\n\t\t\t} else {\n\t\t\t\tset.delete(this.appId)\n\t\t\t}\n\t\t\tconst next = Array.from(set)\n\t\t\tcred.allowedApps = next\n\t\t\tcred.saving = true\n\t\t\ttry {\n\t\t\t\tawait axios.put(generateUrl(`${CREDENTIALS_PATH}/${encodeURIComponent(cred.id)}`), { allowedApps: next })\n\t\t\t} catch {\n\t\t\t\tcred.allowedApps = previous\n\t\t\t\tthis.showError(t('nextcloud-vue', 'Could not update the credential'))\n\t\t\t} finally {\n\t\t\t\tcred.saving = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Organisation scope: persist a full allowed-apps change via PUT.\n\t\t * Reverts on failure.\n\t\t *\n\t\t * @param {object} cred The credential row.\n\t\t * @param {Array} updated The picker's new selection.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onAllowedAppsChange(cred, updated) {\n\t\t\tconst next = this.normaliseApps(updated)\n\t\t\tconst previous = cred.allowedApps.slice()\n\t\t\tcred.allowedApps = next\n\t\t\tcred.saving = true\n\t\t\ttry {\n\t\t\t\tawait axios.put(generateUrl(`${CREDENTIALS_PATH}/${encodeURIComponent(cred.id)}`), { allowedApps: next })\n\t\t\t} catch {\n\t\t\t\tcred.allowedApps = previous\n\t\t\t\tthis.showError(t('nextcloud-vue', 'Could not update allowed apps'))\n\t\t\t} finally {\n\t\t\t\tcred.saving = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Human-readable provider title: hardcoded catalogue first, then the\n\t\t * server title, then the raw identifier.\n\t\t *\n\t\t * @param {string} identifier The provider identifier.\n\t\t * @return {string} A display title.\n\t\t */\n\t\tproviderTitle(identifier) {\n\t\t\tif (PROVIDER_META[identifier] && PROVIDER_META[identifier].title) {\n\t\t\t\treturn PROVIDER_META[identifier].title\n\t\t\t}\n\t\t\tconst match = this.providers.find((p) => p.identifier === identifier)\n\t\t\treturn (match && match.title) || identifier || ''\n\t\t},\n\n\t\t/**\n\t\t * A one-letter badge for a provider tile.\n\t\t *\n\t\t * @param {string} identifier The provider identifier.\n\t\t * @return {string} An uppercase initial.\n\t\t */\n\t\tproviderInitial(identifier) {\n\t\t\tconst title = this.providerTitle(identifier)\n\t\t\treturn (title || '?').trim().charAt(0).toUpperCase() || '?'\n\t\t},\n\n\t\t/**\n\t\t * Inline style for a provider tile dot (its brand colour, or neutral).\n\t\t *\n\t\t * @param {string} identifier The provider identifier.\n\t\t * @return {object} A style binding.\n\t\t */\n\t\tdotStyle(identifier) {\n\t\t\tconst colour = (PROVIDER_META[identifier] && PROVIDER_META[identifier].colour) || 'var(--color-primary-element)'\n\t\t\treturn { backgroundColor: colour }\n\t\t},\n\n\t\t/**\n\t\t * Coerce an NcSelect multiple/taggable value into a string[] of app ids.\n\t\t *\n\t\t * @param {Array} value The picker value.\n\t\t * @return {Array<string>} App ids.\n\t\t */\n\t\tnormaliseApps(value) {\n\t\t\tif (!Array.isArray(value)) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\treturn value.map((v) => (typeof v === 'string' ? v : (v && v.id) || '')).filter((v) => v !== '')\n\t\t},\n\n\t\t/**\n\t\t * Reset the add-credential form.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tresetForm() {\n\t\t\tthis.form = { name: '', provider: null, secret: '', allowedApps: [] }\n\t\t},\n\n\t\t/**\n\t\t * Surface an error toast (lazy-loaded like the rest of the lib).\n\t\t *\n\t\t * @param {string} message The message to show.\n\t\t * @return {void}\n\t\t */\n\t\tshowError(message) {\n\t\t\timport('@nextcloud/dialogs').then(({ showError }) => showError(message))\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-credentials__intro {\n\tmargin-bottom: 16px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-credentials__vault-link,\n.cn-credentials__howto-link {\n\twhite-space: nowrap;\n\tcolor: var(--color-primary-element);\n}\n\n.cn-credentials__error {\n\tmargin-bottom: 12px;\n}\n\n.cn-credentials__section {\n\tmargin-bottom: 22px;\n}\n\n.cn-credentials__section-title {\n\tmargin: 8px 0 8px;\n\tfont-weight: bold;\n}\n\n.cn-credentials__muted,\n.muted {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-credentials__requests,\n.cn-credentials__list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-credentials__request {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 10px;\n\tpadding: 6px 0;\n}\n\n.cn-credentials__request-body {\n\tdisplay: flex;\n\tflex-direction: column;\n}\n\n.cn-credentials__request-provider {\n\tfont-weight: bold;\n}\n\n.cn-credentials__request-reason {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n/* Provider tile dot: a coloured badge with the provider's initial. */\n.cn-credentials__tile-dot {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tjustify-content: center;\n\tflex-shrink: 0;\n\twidth: 32px;\n\theight: 32px;\n\tborder-radius: var(--border-radius-large, 8px);\n\tcolor: #fff;\n\tfont-weight: bold;\n\tfont-size: 0.95em;\n\tline-height: 1;\n}\n\n.cn-credentials__tile-dot--lg {\n\twidth: 44px;\n\theight: 44px;\n\tfont-size: 1.3em;\n}\n\n.cn-credentials__item {\n\tpadding: 12px 0;\n\tborder-bottom: 1px solid var(--color-border);\n}\n\n.cn-credentials__item-head {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 10px;\n}\n\n.cn-credentials__item-meta {\n\tdisplay: flex;\n\tflex-direction: column;\n\tflex-grow: 1;\n\tmin-width: 0;\n}\n\n.cn-credentials__item-name {\n\tfont-weight: bold;\n}\n\n.cn-credentials__item-provider {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-credentials__item-actions {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 4px;\n\tflex-shrink: 0;\n}\n\n.cn-credentials__item-toggle {\n\tmargin: 8px 0 0 42px;\n}\n\n.cn-credentials__also {\n\tmargin: 4px 0 0;\n\tfont-size: 0.9em;\n}\n\n.cn-credentials__item-apps {\n\tmargin-top: 8px;\n\twidth: 100%;\n}\n\n.cn-credentials__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(120px, 1fr));\n\tgap: 10px;\n\tmargin-bottom: 12px;\n}\n\n.cn-credentials__grid-tile {\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: center;\n\tgap: 8px;\n\tpadding: 16px 8px;\n\tbackground: var(--color-background-hover);\n\tborder: 2px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 8px);\n\tcursor: pointer;\n}\n\n.cn-credentials__grid-tile:hover,\n.cn-credentials__grid-tile:focus-visible {\n\tborder-color: var(--color-primary-element);\n\tbackground: var(--color-background-dark);\n}\n\n.cn-credentials__grid-title {\n\tfont-weight: bold;\n\ttext-align: center;\n}\n\n.cn-credentials__wizard-head {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 10px;\n\tmargin-bottom: 12px;\n}\n\n.cn-credentials__wizard-title {\n\tfont-weight: bold;\n\tfont-size: 1.1em;\n}\n\n.cn-credentials__howto {\n\tmargin-bottom: 12px;\n}\n\n.cn-credentials__form {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n}\n\n.cn-credentials__addnote {\n\tmargin: 0;\n\tfont-size: 0.9em;\n}\n\n.cn-credentials__form-actions,\n.cn-credentials__wizard-actions {\n\tdisplay: flex;\n\tjustify-content: flex-end;\n\tgap: 8px;\n}\n</style>\n"],"names":["t","axios"],"mappings":"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"}