@conduction/nextcloud-vue 2.56.0-beta.7 → 2.56.0-beta.9

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 (87) hide show
  1. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +169 -1
  2. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  4. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +88 -4
  5. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
  6. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js +38 -25
  7. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
  8. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js +21 -1
  9. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js.map +1 -1
  10. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js +4 -1
  11. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js.map +1 -1
  12. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +40 -8
  13. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  14. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  15. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +40 -15
  16. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  17. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +2 -2
  18. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  19. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  20. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +6 -5
  21. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  22. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +3 -1
  23. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
  24. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
  25. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue.js +10 -0
  26. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue.js.map +1 -0
  27. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js +316 -0
  28. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js.map +1 -0
  29. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js +105 -0
  30. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js.map +1 -0
  31. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +53 -0
  32. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  33. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +22 -5
  34. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  35. package/dist/esm/components/CnPageRenderer/pageTypes.js +1 -0
  36. package/dist/esm/components/CnPageRenderer/pageTypes.js.map +1 -1
  37. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
  38. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
  39. package/dist/esm/components/CnStorePage/CnStorePage.vue2.js.map +1 -1
  40. package/dist/esm/components/CnStorePage/CnStorePage.vue3.js.map +1 -1
  41. package/dist/esm/components/CnWikiPage/CnWikiPage.vue2.js.map +1 -1
  42. package/dist/esm/components/CnWikiPage/CnWikiPage.vue3.js.map +1 -1
  43. package/dist/esm/index.js +6 -0
  44. package/dist/esm/index.js.map +1 -1
  45. package/dist/esm/l10n/en.json.js +6 -0
  46. package/dist/esm/l10n/en.json.js.map +1 -1
  47. package/dist/esm/l10n/nl.json.js +6 -0
  48. package/dist/esm/l10n/nl.json.js.map +1 -1
  49. package/dist/esm/nextcloud-vue.css +198 -9
  50. package/dist/esm/utils/headers.js +28 -2
  51. package/dist/esm/utils/headers.js.map +1 -1
  52. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  53. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  54. package/dist/nextcloud-vue.cjs.js +9162 -8283
  55. package/dist/nextcloud-vue.cjs.js.map +2 -2
  56. package/dist/nextcloud-vue.css +198 -9
  57. package/l10n/en.json +6 -0
  58. package/l10n/nl.json +6 -0
  59. package/package.json +3 -2
  60. package/scripts/.jsdoc-baselines.json +1 -0
  61. package/scripts/manifest-schema-hash.js +41 -0
  62. package/scripts/record-manifest-schema-hash.js +52 -0
  63. package/src/components/CnAppNav/CnAppNav.vue +169 -1
  64. package/src/components/CnBannerWidget/CnBannerWidget.vue +122 -4
  65. package/src/components/CnBrandStripe/CnBrandStripe.md +16 -0
  66. package/src/components/CnBrandStripe/CnBrandStripe.vue +58 -5
  67. package/src/components/CnDashboardPage/CnDashboardPage.vue +40 -8
  68. package/src/components/CnDataTable/CnDataTable.md +1 -1
  69. package/src/components/CnDataTable/CnDataTable.vue +42 -17
  70. package/src/components/CnDetailPage/CnDetailPage.vue +4 -1
  71. package/src/components/CnHeaderWidget/CnHeaderWidget.vue +3 -1
  72. package/src/components/CnLinkCardsPage/CnLinkCardsPage.vue +517 -0
  73. package/src/components/CnLinkCardsPage/index.js +4 -0
  74. package/src/components/CnObjectSidebar/CnFilesTab.vue +65 -0
  75. package/src/components/CnPageRenderer/pageTypes.js +1 -0
  76. package/src/components/CnReportsPage/CnReportsPage.vue +7 -0
  77. package/src/components/CnStorePage/CnStorePage.vue +7 -0
  78. package/src/components/CnWikiPage/CnWikiPage.vue +7 -0
  79. package/src/components/index.js +1 -0
  80. package/src/css/detail-page.css +10 -2
  81. package/src/css/table.css +1 -2
  82. package/src/index.js +7 -0
  83. package/src/public/index.js +5 -0
  84. package/src/schemas/app-manifest-v2.schema.json +45 -3
  85. package/src/types/manifest.d.ts +1 -1
  86. package/src/utils/headers.js +28 -2
  87. package/src/utils/validateManifestV2.compiled.js +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"CnWikiPage.vue2.js","sources":["../../../../src/components/CnWikiPage/CnWikiPage.vue"],"sourcesContent":["<!--\n CnWikiPage — Manifest-driven markdown article surface.\n\n Renders one wiki / docs / knowledge-base article declared in\n `pages[].config` for `type: \"wiki\"` pages. Closes the gap that\n forces every consumer's docs / help / article-detail route onto\n `type: \"custom\"`.\n\n The page reads its content from props rather than fetching itself.\n Following the precedent set by CnFilesPage and CnFormPage, the\n consumer (or a wiki-aware wrapper) populates `article` (object)\n and `tree` (array) — typically from a `useDetailView()` /\n list-call against the manifest-declared register/schema. This\n keeps the lib zero-dep on a particular OpenRegister fetcher\n while still making the manifest config the source of truth for\n field names.\n\n Markdown rendering is delegated to the `cnRenderMarkdown` composable\n which configures `marked` with `{ gfm: true, breaks: false }` —\n GitHub-flavoured markdown, no auto-`<br>` on newlines.\n\n Slots:\n - `#header` — overrides the default header. Scope `{ title, article }`.\n - `#sidebar` — overrides the default tree sidebar. Scope `{ tree, onClick }`.\n - `#body` — overrides the default markdown body. Scope `{ article, html }`.\n - `#empty` — replaces the empty-state when `article` is null.\n\n Events:\n - `@tree-click` — `node` whenever a sidebar tree node is clicked.\n - `@error` — error object if markdown parsing throws.\n\n Spec: REQ-MWPT-* (manifest-wiki-page-type).\n-->\n<template>\n\t<div class=\"cn-wiki-page\" data-testid=\"cn-wiki-page\" :class=\"{ 'cn-wiki-page--has-sidebar': hasSidebar }\">\n\t\t<!--\n\t\t\t@slot sidebar\n\t\t\t@description Replaces the default sidebar tree. Scoped props `{ tree, onClick }` —\n\t\t\t`onClick(node)` re-emits `@tree-click` with the node.\n\t\t-->\n\t\t<slot\n\t\t\tv-if=\"hasSidebar\"\n\t\t\tname=\"sidebar\"\n\t\t\t:tree=\"tree\"\n\t\t\t:onClick=\"onTreeClick\">\n\t\t\t<nav class=\"cn-wiki-page__sidebar\">\n\t\t\t\t<ul class=\"cn-wiki-page__tree\">\n\t\t\t\t\t<CnWikiTreeNode\n\t\t\t\t\t\tv-for=\"node in tree\"\n\t\t\t\t\t\t:key=\"treeKey(node)\"\n\t\t\t\t\t\t:node=\"node\"\n\t\t\t\t\t\t:titleField=\"effectiveSidebarTitleField\"\n\t\t\t\t\t\t:treeField=\"treeField\"\n\t\t\t\t\t\t@click=\"onTreeClick\" />\n\t\t\t\t</ul>\n\t\t\t</nav>\n\t\t</slot>\n\n\t\t<div class=\"cn-wiki-page__main\">\n\t\t\t<!--\n\t\t\t\t@slot header\n\t\t\t\t@description Replaces the default page header. Scoped props `{ title, article }`.\n\t\t\t-->\n\t\t\t<slot\n\t\t\t\tname=\"header\"\n\t\t\t\t:title=\"resolvedTitle\"\n\t\t\t\t:article=\"article\">\n\t\t\t\t<header v-if=\"resolvedTitle\" class=\"cn-wiki-page__header\">\n\t\t\t\t\t<h1 class=\"cn-wiki-page__title\">\n\t\t\t\t\t\t{{ resolvedTitle }}\n\t\t\t\t\t</h1>\n\t\t\t\t</header>\n\t\t\t</slot>\n\n\t\t\t<!--\n\t\t\t\t@slot empty\n\t\t\t\t@description Replaces the empty-state shown when `article` is null/undefined.\n\t\t\t-->\n\t\t\t<slot v-if=\"!article\" name=\"empty\">\n\t\t\t\t<NcEmptyContent\n\t\t\t\t\t:name=\"emptyText\"\n\t\t\t\t\t:description=\"emptyDescription\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<FileDocumentOutline :size=\"64\" />\n\t\t\t\t\t</template>\n\t\t\t\t</NcEmptyContent>\n\t\t\t</slot>\n\n\t\t\t<!--\n\t\t\t\t@slot body\n\t\t\t\t@description Replaces the default markdown body. Scoped props `{ article, html }` —\n\t\t\t\t`html` is the pre-parsed markdown so a custom renderer can wrap it.\n\t\t\t-->\n\t\t\t<slot\n\t\t\t\tv-else\n\t\t\t\tname=\"body\"\n\t\t\t\t:article=\"article\"\n\t\t\t\t:html=\"renderedBody\">\n\t\t\t\t<div\n\t\t\t\t\tv-if=\"renderedBody\"\n\t\t\t\t\tclass=\"cn-wiki-page__body\"\n\t\t\t\t\tv-html=\"renderedBody\" /><!-- eslint-disable-line vue/no-v-html -->\n\t\t\t\t<NcEmptyContent\n\t\t\t\t\tv-else\n\t\t\t\t\t:name=\"emptyBodyText\"\n\t\t\t\t\t:description=\"emptyBodyDescription\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<FileDocumentOutline :size=\"64\" />\n\t\t\t\t\t</template>\n\t\t\t\t</NcEmptyContent>\n\t\t\t</slot>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcEmptyContent } from '@nextcloud/vue'\nimport FileDocumentOutline from 'vue-material-design-icons/FileDocumentOutline.vue'\nimport CnWikiTreeNode from './CnWikiTreeNode.vue'\nimport { cnRenderMarkdown } from '../../composables/cnRenderMarkdown.js'\n\n/**\n * CnWikiPage — Manifest-driven markdown article surface.\n *\n * Reads `article` (object) and `tree` (array) as props rather than\n * fetching itself; consumers populate these from their own data\n * store or a wrapper composable. The manifest config still drives\n * which fields/registers are involved (via `contentField`,\n * `titleField`, `sidebarSchema`, …) so the validator stays\n * meaningful even though the lib does not own the fetch.\n *\n * Basic usage:\n *\n * <CnWikiPage\n * :article=\"article\"\n * :register=\"'pipelinq'\"\n * :schema=\"'article'\"\n * :content-field=\"'body'\" />\n *\n * With sidebar tree:\n *\n * <CnWikiPage\n * :article=\"article\"\n * :tree=\"categoryTree\"\n * :sidebar-schema=\"'category'\"\n *\n * @tree-click=\"onCategoryClick\" />\n */\nexport default {\n\tname: 'CnWikiPage',\n\n\tcomponents: {\n\t\tNcEmptyContent,\n\t\tFileDocumentOutline,\n\t\tCnWikiTreeNode,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The article record to render. When null/undefined the\n\t\t * component shows its empty-state. The shape is consumer-defined;\n\t\t * the component reads only `article[contentField]` and\n\t\t * `article[titleField]`.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tarticle: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The (optional) sidebar tree. Each node:\n\t\t * `{ id, [titleField], children?: [...] }`. Empty array hides\n\t\t * the sidebar entirely.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\ttree: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * OpenRegister register slug. Informational at this layer\n\t\t * (the consumer does the fetch); declared as a prop so the\n\t\t * manifest-driven prop forwarder in CnPageRenderer surfaces it\n\t\t * for child components / debugging.\n\t\t */\n\t\tregister: { // eslint-disable-line vue/no-unused-properties -- informational at this layer; CnPageRenderer forwards it from the manifest for child components.\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * OpenRegister schema slug. Informational at this layer —\n\t\t * see `register` above.\n\t\t */\n\t\tschema: { // eslint-disable-line vue/no-unused-properties -- informational at this layer; CnPageRenderer forwards it from the manifest for child components.\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Property on `article` holding the markdown body. Defaults\n\t\t * to `'body'`.\n\t\t */\n\t\tcontentField: {\n\t\t\ttype: String,\n\t\t\tdefault: 'body',\n\t\t},\n\n\t\t/**\n\t\t * Property on `article` holding the page title. Defaults\n\t\t * to `'title'`.\n\t\t */\n\t\ttitleField: {\n\t\t\ttype: String,\n\t\t\tdefault: 'title',\n\t\t},\n\n\t\t/**\n\t\t * `$route.params` key holding the article id. Informational at\n\t\t * this layer; declared so the consumer's data store can wire\n\t\t * the same default the validator and renderer both reference.\n\t\t */\n\t\tidParam: { // eslint-disable-line vue/no-unused-properties -- informational at this layer; CnPageRenderer forwards it from the manifest for child components.\n\t\t\ttype: String,\n\t\t\tdefault: 'id',\n\t\t},\n\n\t\t/**\n\t\t * Schema slug for the sidebar tree. When set AND `tree` is\n\t\t * non-empty the sidebar renders. Unset suppresses the sidebar\n\t\t * regardless of `tree` content.\n\t\t */\n\t\tsidebarSchema: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Register slug for the sidebar tree. Defaults to `register`\n\t\t * when unset (the common case).\n\t\t */\n\t\tsidebarRegister: { // eslint-disable-line vue/no-unused-properties -- informational at this layer; CnPageRenderer forwards it from the manifest for child components.\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Property on each sidebar node holding its child array.\n\t\t * Defaults to `'children'`.\n\t\t */\n\t\ttreeField: {\n\t\t\ttype: String,\n\t\t\tdefault: 'children',\n\t\t},\n\n\t\t/**\n\t\t * Property on each sidebar node holding its label. Defaults\n\t\t * to `titleField` (so `'title'` unless overridden).\n\t\t */\n\t\tsidebarTitleField: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Empty-state heading when `article` is null. */\n\t\temptyText: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Article not found'),\n\t\t},\n\n\t\t/** Empty-state description when `article` is null. */\n\t\temptyDescription: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'The requested article could not be found.'),\n\t\t},\n\n\t\t/** Empty-body heading when the article has no `contentField`. */\n\t\temptyBodyText: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'No content'),\n\t\t},\n\n\t\t/** Empty-body description when the article has no `contentField`. */\n\t\temptyBodyDescription: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'This article has no content yet.'),\n\t\t},\n\t},\n\n\temits: [\n\t\t/**\n\t\t * Emitted when a sidebar tree node is clicked. The payload is\n\t\t * the clicked node object (the same shape the consumer placed\n\t\t * in `tree`).\n\t\t */\n\t\t'tree-click',\n\t\t/**\n\t\t * Emitted when markdown parsing throws. Rare — `marked` and\n\t\t * the helper are defensive — but the contract is explicit so\n\t\t * consumers can log / surface a banner.\n\t\t */\n\t\t'error',\n\t],\n\n\tcomputed: {\n\t\t/**\n\t\t * Effective sidebar-title field — falls back to the article\n\t\t * title field when unset so consumers can leave it off in\n\t\t * the simple case.\n\t\t */\n\t\teffectiveSidebarTitleField() {\n\t\t\treturn this.sidebarTitleField || this.titleField\n\t\t},\n\n\t\t/**\n\t\t * Whether the sidebar should render. Both a non-empty tree\n\t\t * AND an explicit `sidebarSchema` are required — the schema\n\t\t * acts as the manifest-author's \"yes, I want a sidebar\" signal,\n\t\t * matching the spec's REQ-MWPT-5.\n\t\t */\n\t\thasSidebar() {\n\t\t\treturn Boolean(this.sidebarSchema) && Array.isArray(this.tree) && this.tree.length > 0\n\t\t},\n\n\t\t/** Article title resolved through `titleField`. */\n\t\tresolvedTitle() {\n\t\t\tif (!this.article) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst value = this.article[this.titleField]\n\t\t\treturn typeof value === 'string' ? value : ''\n\t\t},\n\n\t\t/**\n\t\t * Article body parsed to HTML through `cnRenderMarkdown`.\n\t\t * Empty string when the article is null or has no body.\n\t\t */\n\t\trenderedBody() {\n\t\t\tif (!this.article) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst source = this.article[this.contentField]\n\t\t\ttry {\n\t\t\t\treturn cnRenderMarkdown(source)\n\t\t\t} catch (error) {\n\t\t\t\tthis.$emit('error', error)\n\t\t\t\treturn ''\n\t\t\t}\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Re-emit `@tree-click` with the clicked node.\n\t\t *\n\t\t * @param {object} node The tree node the user clicked.\n\t\t */\n\t\tonTreeClick(node) {\n\t\t\tthis.$emit('tree-click', node)\n\t\t},\n\n\t\t/**\n\t\t * Stable v-for key for tree nodes — prefer `id`, fall back\n\t\t * to the title field, finally to JSON-stringified shape.\n\t\t *\n\t\t * @param {object} node Tree node.\n\t\t * @return {string}\n\t\t */\n\t\ttreeKey(node) {\n\t\t\tif (!node) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (typeof node.id === 'string' || typeof node.id === 'number') {\n\t\t\t\treturn String(node.id)\n\t\t\t}\n\t\t\tconst title = node[this.effectiveSidebarTitleField]\n\t\t\tif (typeof title === 'string') {\n\t\t\t\treturn title\n\t\t\t}\n\t\t\treturn JSON.stringify(node)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-wiki-page {\n\tdisplay: flex;\n\tgap: 24px;\n\tpadding: 20px;\n\tmax-width: 1200px;\n\tmargin: 0 auto;\n}\n\n.cn-wiki-page__sidebar {\n\tflex: 0 0 240px;\n\tborder-right: 1px solid var(--color-border);\n\tpadding-right: 16px;\n}\n\n.cn-wiki-page__tree {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-wiki-page__main {\n\tflex: 1 1 auto;\n\tmin-width: 0;\n}\n\n.cn-wiki-page__header {\n\tmargin-bottom: 16px;\n}\n\n.cn-wiki-page__title {\n\tfont-size: 1.6em;\n\tmargin: 0;\n\tcolor: var(--color-main-text);\n}\n\n.cn-wiki-page__body {\n\tline-height: 1.7;\n\tcolor: var(--color-main-text);\n}\n\n.cn-wiki-page__body :deep(h1),\n.cn-wiki-page__body :deep(h2),\n.cn-wiki-page__body :deep(h3) {\n\tcolor: var(--color-main-text);\n\tmargin-top: 1.4em;\n\tmargin-bottom: 0.5em;\n}\n\n.cn-wiki-page__body :deep(p) {\n\tmargin: 0 0 1em;\n}\n\n.cn-wiki-page__body :deep(code) {\n\tbackground: var(--color-background-dark);\n\tpadding: 2px 6px;\n\tborder-radius: var(--border-radius);\n\tfont-family: var(--font-face-monospace, monospace);\n\tfont-size: 0.9em;\n}\n\n.cn-wiki-page__body :deep(pre) {\n\tbackground: var(--color-background-dark);\n\tpadding: 12px;\n\tborder-radius: var(--border-radius);\n\toverflow-x: auto;\n}\n\n.cn-wiki-page__body :deep(pre code) {\n\tbackground: transparent;\n\tpadding: 0;\n}\n\n.cn-wiki-page__body :deep(blockquote) {\n\tborder-left: 3px solid var(--color-border);\n\tpadding-left: 12px;\n\tcolor: var(--color-text-lighter);\n\tmargin: 0 0 1em;\n}\n\n.cn-wiki-page__body :deep(table) {\n\tborder-collapse: collapse;\n\tmargin: 0 0 1em;\n}\n\n.cn-wiki-page__body :deep(th),\n.cn-wiki-page__body :deep(td) {\n\tborder: 1px solid var(--color-border);\n\tpadding: 6px 10px;\n}\n\n.cn-wiki-page--has-sidebar .cn-wiki-page__main {\n\tpadding-left: 8px;\n}\n</style>\n"],"names":["CnWikiTreeNode","t"],"mappings":"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"}
1
+ {"version":3,"file":"CnWikiPage.vue2.js","sources":["../../../../src/components/CnWikiPage/CnWikiPage.vue"],"sourcesContent":["<!--\n CnWikiPage — Manifest-driven markdown article surface.\n\n Renders one wiki / docs / knowledge-base article declared in\n `pages[].config` for `type: \"wiki\"` pages. Closes the gap that\n forces every consumer's docs / help / article-detail route onto\n `type: \"custom\"`.\n\n The page reads its content from props rather than fetching itself.\n Following the precedent set by CnFilesPage and CnFormPage, the\n consumer (or a wiki-aware wrapper) populates `article` (object)\n and `tree` (array) — typically from a `useDetailView()` /\n list-call against the manifest-declared register/schema. This\n keeps the lib zero-dep on a particular OpenRegister fetcher\n while still making the manifest config the source of truth for\n field names.\n\n Markdown rendering is delegated to the `cnRenderMarkdown` composable\n which configures `marked` with `{ gfm: true, breaks: false }` —\n GitHub-flavoured markdown, no auto-`<br>` on newlines.\n\n Slots:\n - `#header` — overrides the default header. Scope `{ title, article }`.\n - `#sidebar` — overrides the default tree sidebar. Scope `{ tree, onClick }`.\n - `#body` — overrides the default markdown body. Scope `{ article, html }`.\n - `#empty` — replaces the empty-state when `article` is null.\n\n Events:\n - `@tree-click` — `node` whenever a sidebar tree node is clicked.\n - `@error` — error object if markdown parsing throws.\n\n Spec: REQ-MWPT-* (manifest-wiki-page-type).\n-->\n<template>\n\t<div class=\"cn-wiki-page\" data-testid=\"cn-wiki-page\" :class=\"{ 'cn-wiki-page--has-sidebar': hasSidebar }\">\n\t\t<!--\n\t\t\t@slot sidebar\n\t\t\t@description Replaces the default sidebar tree. Scoped props `{ tree, onClick }` —\n\t\t\t`onClick(node)` re-emits `@tree-click` with the node.\n\t\t-->\n\t\t<slot\n\t\t\tv-if=\"hasSidebar\"\n\t\t\tname=\"sidebar\"\n\t\t\t:tree=\"tree\"\n\t\t\t:onClick=\"onTreeClick\">\n\t\t\t<nav class=\"cn-wiki-page__sidebar\">\n\t\t\t\t<ul class=\"cn-wiki-page__tree\">\n\t\t\t\t\t<CnWikiTreeNode\n\t\t\t\t\t\tv-for=\"node in tree\"\n\t\t\t\t\t\t:key=\"treeKey(node)\"\n\t\t\t\t\t\t:node=\"node\"\n\t\t\t\t\t\t:titleField=\"effectiveSidebarTitleField\"\n\t\t\t\t\t\t:treeField=\"treeField\"\n\t\t\t\t\t\t@click=\"onTreeClick\" />\n\t\t\t\t</ul>\n\t\t\t</nav>\n\t\t</slot>\n\n\t\t<div class=\"cn-wiki-page__main\">\n\t\t\t<!--\n\t\t\t\t@slot header\n\t\t\t\t@description Replaces the default page header. Scoped props `{ title, article }`.\n\t\t\t-->\n\t\t\t<slot\n\t\t\t\tname=\"header\"\n\t\t\t\t:title=\"resolvedTitle\"\n\t\t\t\t:article=\"article\">\n\t\t\t\t<header v-if=\"resolvedTitle\" class=\"cn-wiki-page__header\">\n\t\t\t\t\t<h1 class=\"cn-wiki-page__title\">\n\t\t\t\t\t\t{{ resolvedTitle }}\n\t\t\t\t\t</h1>\n\t\t\t\t</header>\n\t\t\t</slot>\n\n\t\t\t<!--\n\t\t\t\t@slot empty\n\t\t\t\t@description Replaces the empty-state shown when `article` is null/undefined.\n\t\t\t-->\n\t\t\t<slot v-if=\"!article\" name=\"empty\">\n\t\t\t\t<NcEmptyContent\n\t\t\t\t\t:name=\"emptyText\"\n\t\t\t\t\t:description=\"emptyDescription\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<FileDocumentOutline :size=\"64\" />\n\t\t\t\t\t</template>\n\t\t\t\t</NcEmptyContent>\n\t\t\t</slot>\n\n\t\t\t<!--\n\t\t\t\t@slot body\n\t\t\t\t@description Replaces the default markdown body. Scoped props `{ article, html }` —\n\t\t\t\t`html` is the pre-parsed markdown so a custom renderer can wrap it.\n\t\t\t-->\n\t\t\t<slot\n\t\t\t\tv-else\n\t\t\t\tname=\"body\"\n\t\t\t\t:article=\"article\"\n\t\t\t\t:html=\"renderedBody\">\n\t\t\t\t<div\n\t\t\t\t\tv-if=\"renderedBody\"\n\t\t\t\t\tclass=\"cn-wiki-page__body\"\n\t\t\t\t\tv-html=\"renderedBody\" /><!-- eslint-disable-line vue/no-v-html -->\n\t\t\t\t<NcEmptyContent\n\t\t\t\t\tv-else\n\t\t\t\t\t:name=\"emptyBodyText\"\n\t\t\t\t\t:description=\"emptyBodyDescription\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<FileDocumentOutline :size=\"64\" />\n\t\t\t\t\t</template>\n\t\t\t\t</NcEmptyContent>\n\t\t\t</slot>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcEmptyContent } from '@nextcloud/vue'\nimport FileDocumentOutline from 'vue-material-design-icons/FileDocumentOutline.vue'\nimport CnWikiTreeNode from './CnWikiTreeNode.vue'\nimport { cnRenderMarkdown } from '../../composables/cnRenderMarkdown.js'\n\n/**\n * CnWikiPage — Manifest-driven markdown article surface.\n *\n * Reads `article` (object) and `tree` (array) as props rather than\n * fetching itself; consumers populate these from their own data\n * store or a wrapper composable. The manifest config still drives\n * which fields/registers are involved (via `contentField`,\n * `titleField`, `sidebarSchema`, …) so the validator stays\n * meaningful even though the lib does not own the fetch.\n *\n * Basic usage:\n *\n * <CnWikiPage\n * :article=\"article\"\n * :register=\"'pipelinq'\"\n * :schema=\"'article'\"\n * :content-field=\"'body'\" />\n *\n * With sidebar tree:\n *\n * <CnWikiPage\n * :article=\"article\"\n * :tree=\"categoryTree\"\n * :sidebar-schema=\"'category'\"\n *\n * @tree-click=\"onCategoryClick\" />\n */\nexport default {\n\tname: 'CnWikiPage',\n\n\tcomponents: {\n\t\tNcEmptyContent,\n\t\tFileDocumentOutline,\n\t\tCnWikiTreeNode,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The article record to render. When null/undefined the\n\t\t * component shows its empty-state. The shape is consumer-defined;\n\t\t * the component reads only `article[contentField]` and\n\t\t * `article[titleField]`.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tarticle: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The (optional) sidebar tree. Each node:\n\t\t * `{ id, [titleField], children?: [...] }`. Empty array hides\n\t\t * the sidebar entirely.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\ttree: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * OpenRegister register slug. Informational at this layer\n\t\t * (the consumer does the fetch); declared as a prop so the\n\t\t * manifest-driven prop forwarder in CnPageRenderer surfaces it\n\t\t * for child components / debugging.\n\t\t */\n\t\tregister: { // eslint-disable-line vue/no-unused-properties -- informational at this layer; CnPageRenderer forwards it from the manifest for child components.\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * OpenRegister schema slug. Informational at this layer —\n\t\t * see `register` above.\n\t\t */\n\t\tschema: { // eslint-disable-line vue/no-unused-properties -- informational at this layer; CnPageRenderer forwards it from the manifest for child components.\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Property on `article` holding the markdown body. Defaults\n\t\t * to `'body'`.\n\t\t */\n\t\tcontentField: {\n\t\t\ttype: String,\n\t\t\tdefault: 'body',\n\t\t},\n\n\t\t/**\n\t\t * Property on `article` holding the page title. Defaults\n\t\t * to `'title'`.\n\t\t */\n\t\ttitleField: {\n\t\t\ttype: String,\n\t\t\tdefault: 'title',\n\t\t},\n\n\t\t/**\n\t\t * `$route.params` key holding the article id. Informational at\n\t\t * this layer; declared so the consumer's data store can wire\n\t\t * the same default the validator and renderer both reference.\n\t\t */\n\t\tidParam: { // eslint-disable-line vue/no-unused-properties -- informational at this layer; CnPageRenderer forwards it from the manifest for child components.\n\t\t\ttype: String,\n\t\t\tdefault: 'id',\n\t\t},\n\n\t\t/**\n\t\t * Schema slug for the sidebar tree. When set AND `tree` is\n\t\t * non-empty the sidebar renders. Unset suppresses the sidebar\n\t\t * regardless of `tree` content.\n\t\t */\n\t\tsidebarSchema: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Register slug for the sidebar tree. Defaults to `register`\n\t\t * when unset (the common case).\n\t\t */\n\t\tsidebarRegister: { // eslint-disable-line vue/no-unused-properties -- informational at this layer; CnPageRenderer forwards it from the manifest for child components.\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Property on each sidebar node holding its child array.\n\t\t * Defaults to `'children'`.\n\t\t */\n\t\ttreeField: {\n\t\t\ttype: String,\n\t\t\tdefault: 'children',\n\t\t},\n\n\t\t/**\n\t\t * Property on each sidebar node holding its label. Defaults\n\t\t * to `titleField` (so `'title'` unless overridden).\n\t\t */\n\t\tsidebarTitleField: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Empty-state heading when `article` is null. */\n\t\temptyText: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Article not found'),\n\t\t},\n\n\t\t/** Empty-state description when `article` is null. */\n\t\temptyDescription: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'The requested article could not be found.'),\n\t\t},\n\n\t\t/** Empty-body heading when the article has no `contentField`. */\n\t\temptyBodyText: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'No content'),\n\t\t},\n\n\t\t/** Empty-body description when the article has no `contentField`. */\n\t\temptyBodyDescription: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'This article has no content yet.'),\n\t\t},\n\t},\n\n\temits: [\n\t\t/**\n\t\t * Emitted when a sidebar tree node is clicked. The payload is\n\t\t * the clicked node object (the same shape the consumer placed\n\t\t * in `tree`).\n\t\t */\n\t\t'tree-click',\n\t\t/**\n\t\t * Emitted when markdown parsing throws. Rare — `marked` and\n\t\t * the helper are defensive — but the contract is explicit so\n\t\t * consumers can log / surface a banner.\n\t\t */\n\t\t'error',\n\t],\n\n\tcomputed: {\n\t\t/**\n\t\t * Effective sidebar-title field — falls back to the article\n\t\t * title field when unset so consumers can leave it off in\n\t\t * the simple case.\n\t\t */\n\t\teffectiveSidebarTitleField() {\n\t\t\treturn this.sidebarTitleField || this.titleField\n\t\t},\n\n\t\t/**\n\t\t * Whether the sidebar should render. Both a non-empty tree\n\t\t * AND an explicit `sidebarSchema` are required — the schema\n\t\t * acts as the manifest-author's \"yes, I want a sidebar\" signal,\n\t\t * matching the spec's REQ-MWPT-5.\n\t\t */\n\t\thasSidebar() {\n\t\t\treturn Boolean(this.sidebarSchema) && Array.isArray(this.tree) && this.tree.length > 0\n\t\t},\n\n\t\t/** Article title resolved through `titleField`. */\n\t\tresolvedTitle() {\n\t\t\tif (!this.article) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst value = this.article[this.titleField]\n\t\t\treturn typeof value === 'string' ? value : ''\n\t\t},\n\n\t\t/**\n\t\t * Article body parsed to HTML through `cnRenderMarkdown`.\n\t\t * Empty string when the article is null or has no body.\n\t\t */\n\t\trenderedBody() {\n\t\t\tif (!this.article) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst source = this.article[this.contentField]\n\t\t\ttry {\n\t\t\t\treturn cnRenderMarkdown(source)\n\t\t\t} catch (error) {\n\t\t\t\tthis.$emit('error', error)\n\t\t\t\treturn ''\n\t\t\t}\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Re-emit `@tree-click` with the clicked node.\n\t\t *\n\t\t * @param {object} node The tree node the user clicked.\n\t\t */\n\t\tonTreeClick(node) {\n\t\t\tthis.$emit('tree-click', node)\n\t\t},\n\n\t\t/**\n\t\t * Stable v-for key for tree nodes — prefer `id`, fall back\n\t\t * to the title field, finally to JSON-stringified shape.\n\t\t *\n\t\t * @param {object} node Tree node.\n\t\t * @return {string}\n\t\t */\n\t\ttreeKey(node) {\n\t\t\tif (!node) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (typeof node.id === 'string' || typeof node.id === 'number') {\n\t\t\t\treturn String(node.id)\n\t\t\t}\n\t\t\tconst title = node[this.effectiveSidebarTitleField]\n\t\t\tif (typeof title === 'string') {\n\t\t\t\treturn title\n\t\t\t}\n\t\t\treturn JSON.stringify(node)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-wiki-page {\n\tdisplay: flex;\n\tgap: 24px;\n\tpadding: 20px;\n\tmax-width: 1200px;\n\tmargin: 0 auto;\n}\n\n.cn-wiki-page__sidebar {\n\tflex: 0 0 240px;\n\tborder-right: 1px solid var(--color-border);\n\tpadding-right: 16px;\n}\n\n.cn-wiki-page__tree {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-wiki-page__main {\n\tflex: 1 1 auto;\n\tmin-width: 0;\n}\n\n.cn-wiki-page__header {\n\tmargin-bottom: 16px;\n}\n\n/* Without a sidebar the title starts at the left edge of the content, under\n the Nextcloud navigation toggle button (44px wide). Clear it as CnPageHeader\n does. With a sidebar the tree sits there instead and the title is clear. */\n.cn-wiki-page:not(.cn-wiki-page--has-sidebar) .cn-wiki-page__header {\n\tpadding-inline-start: 56px;\n}\n\n.cn-wiki-page__title {\n\tfont-size: 1.6em;\n\tmargin: 0;\n\tcolor: var(--color-main-text);\n}\n\n.cn-wiki-page__body {\n\tline-height: 1.7;\n\tcolor: var(--color-main-text);\n}\n\n.cn-wiki-page__body :deep(h1),\n.cn-wiki-page__body :deep(h2),\n.cn-wiki-page__body :deep(h3) {\n\tcolor: var(--color-main-text);\n\tmargin-top: 1.4em;\n\tmargin-bottom: 0.5em;\n}\n\n.cn-wiki-page__body :deep(p) {\n\tmargin: 0 0 1em;\n}\n\n.cn-wiki-page__body :deep(code) {\n\tbackground: var(--color-background-dark);\n\tpadding: 2px 6px;\n\tborder-radius: var(--border-radius);\n\tfont-family: var(--font-face-monospace, monospace);\n\tfont-size: 0.9em;\n}\n\n.cn-wiki-page__body :deep(pre) {\n\tbackground: var(--color-background-dark);\n\tpadding: 12px;\n\tborder-radius: var(--border-radius);\n\toverflow-x: auto;\n}\n\n.cn-wiki-page__body :deep(pre code) {\n\tbackground: transparent;\n\tpadding: 0;\n}\n\n.cn-wiki-page__body :deep(blockquote) {\n\tborder-left: 3px solid var(--color-border);\n\tpadding-left: 12px;\n\tcolor: var(--color-text-lighter);\n\tmargin: 0 0 1em;\n}\n\n.cn-wiki-page__body :deep(table) {\n\tborder-collapse: collapse;\n\tmargin: 0 0 1em;\n}\n\n.cn-wiki-page__body :deep(th),\n.cn-wiki-page__body :deep(td) {\n\tborder: 1px solid var(--color-border);\n\tpadding: 6px 10px;\n}\n\n.cn-wiki-page--has-sidebar .cn-wiki-page__main {\n\tpadding-left: 8px;\n}\n</style>\n"],"names":["CnWikiTreeNode","t"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"CnWikiPage.vue3.js","sources":["../../../../src/components/CnWikiPage/CnWikiPage.vue?vue&type=template&id=25a3d3cf&lang.js"],"sourcesContent":["<!--\n CnWikiPage — Manifest-driven markdown article surface.\n\n Renders one wiki / docs / knowledge-base article declared in\n `pages[].config` for `type: \"wiki\"` pages. Closes the gap that\n forces every consumer's docs / help / article-detail route onto\n `type: \"custom\"`.\n\n The page reads its content from props rather than fetching itself.\n Following the precedent set by CnFilesPage and CnFormPage, the\n consumer (or a wiki-aware wrapper) populates `article` (object)\n and `tree` (array) — typically from a `useDetailView()` /\n list-call against the manifest-declared register/schema. This\n keeps the lib zero-dep on a particular OpenRegister fetcher\n while still making the manifest config the source of truth for\n field names.\n\n Markdown rendering is delegated to the `cnRenderMarkdown` composable\n which configures `marked` with `{ gfm: true, breaks: false }` —\n GitHub-flavoured markdown, no auto-`<br>` on newlines.\n\n Slots:\n - `#header` — overrides the default header. Scope `{ title, article }`.\n - `#sidebar` — overrides the default tree sidebar. Scope `{ tree, onClick }`.\n - `#body` — overrides the default markdown body. Scope `{ article, html }`.\n - `#empty` — replaces the empty-state when `article` is null.\n\n Events:\n - `@tree-click` — `node` whenever a sidebar tree node is clicked.\n - `@error` — error object if markdown parsing throws.\n\n Spec: REQ-MWPT-* (manifest-wiki-page-type).\n-->\n<template>\n\t<div class=\"cn-wiki-page\" data-testid=\"cn-wiki-page\" :class=\"{ 'cn-wiki-page--has-sidebar': hasSidebar }\">\n\t\t<!--\n\t\t\t@slot sidebar\n\t\t\t@description Replaces the default sidebar tree. Scoped props `{ tree, onClick }` —\n\t\t\t`onClick(node)` re-emits `@tree-click` with the node.\n\t\t-->\n\t\t<slot\n\t\t\tv-if=\"hasSidebar\"\n\t\t\tname=\"sidebar\"\n\t\t\t:tree=\"tree\"\n\t\t\t:onClick=\"onTreeClick\">\n\t\t\t<nav class=\"cn-wiki-page__sidebar\">\n\t\t\t\t<ul class=\"cn-wiki-page__tree\">\n\t\t\t\t\t<CnWikiTreeNode\n\t\t\t\t\t\tv-for=\"node in tree\"\n\t\t\t\t\t\t:key=\"treeKey(node)\"\n\t\t\t\t\t\t:node=\"node\"\n\t\t\t\t\t\t:titleField=\"effectiveSidebarTitleField\"\n\t\t\t\t\t\t:treeField=\"treeField\"\n\t\t\t\t\t\t@click=\"onTreeClick\" />\n\t\t\t\t</ul>\n\t\t\t</nav>\n\t\t</slot>\n\n\t\t<div class=\"cn-wiki-page__main\">\n\t\t\t<!--\n\t\t\t\t@slot header\n\t\t\t\t@description Replaces the default page header. Scoped props `{ title, article }`.\n\t\t\t-->\n\t\t\t<slot\n\t\t\t\tname=\"header\"\n\t\t\t\t:title=\"resolvedTitle\"\n\t\t\t\t:article=\"article\">\n\t\t\t\t<header v-if=\"resolvedTitle\" class=\"cn-wiki-page__header\">\n\t\t\t\t\t<h1 class=\"cn-wiki-page__title\">\n\t\t\t\t\t\t{{ resolvedTitle }}\n\t\t\t\t\t</h1>\n\t\t\t\t</header>\n\t\t\t</slot>\n\n\t\t\t<!--\n\t\t\t\t@slot empty\n\t\t\t\t@description Replaces the empty-state shown when `article` is null/undefined.\n\t\t\t-->\n\t\t\t<slot v-if=\"!article\" name=\"empty\">\n\t\t\t\t<NcEmptyContent\n\t\t\t\t\t:name=\"emptyText\"\n\t\t\t\t\t:description=\"emptyDescription\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<FileDocumentOutline :size=\"64\" />\n\t\t\t\t\t</template>\n\t\t\t\t</NcEmptyContent>\n\t\t\t</slot>\n\n\t\t\t<!--\n\t\t\t\t@slot body\n\t\t\t\t@description Replaces the default markdown body. Scoped props `{ article, html }` —\n\t\t\t\t`html` is the pre-parsed markdown so a custom renderer can wrap it.\n\t\t\t-->\n\t\t\t<slot\n\t\t\t\tv-else\n\t\t\t\tname=\"body\"\n\t\t\t\t:article=\"article\"\n\t\t\t\t:html=\"renderedBody\">\n\t\t\t\t<div\n\t\t\t\t\tv-if=\"renderedBody\"\n\t\t\t\t\tclass=\"cn-wiki-page__body\"\n\t\t\t\t\tv-html=\"renderedBody\" /><!-- eslint-disable-line vue/no-v-html -->\n\t\t\t\t<NcEmptyContent\n\t\t\t\t\tv-else\n\t\t\t\t\t:name=\"emptyBodyText\"\n\t\t\t\t\t:description=\"emptyBodyDescription\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<FileDocumentOutline :size=\"64\" />\n\t\t\t\t\t</template>\n\t\t\t\t</NcEmptyContent>\n\t\t\t</slot>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcEmptyContent } from '@nextcloud/vue'\nimport FileDocumentOutline from 'vue-material-design-icons/FileDocumentOutline.vue'\nimport CnWikiTreeNode from './CnWikiTreeNode.vue'\nimport { cnRenderMarkdown } from '../../composables/cnRenderMarkdown.js'\n\n/**\n * CnWikiPage — Manifest-driven markdown article surface.\n *\n * Reads `article` (object) and `tree` (array) as props rather than\n * fetching itself; consumers populate these from their own data\n * store or a wrapper composable. The manifest config still drives\n * which fields/registers are involved (via `contentField`,\n * `titleField`, `sidebarSchema`, …) so the validator stays\n * meaningful even though the lib does not own the fetch.\n *\n * Basic usage:\n *\n * <CnWikiPage\n * :article=\"article\"\n * :register=\"'pipelinq'\"\n * :schema=\"'article'\"\n * :content-field=\"'body'\" />\n *\n * With sidebar tree:\n *\n * <CnWikiPage\n * :article=\"article\"\n * :tree=\"categoryTree\"\n * :sidebar-schema=\"'category'\"\n *\n * @tree-click=\"onCategoryClick\" />\n */\nexport default {\n\tname: 'CnWikiPage',\n\n\tcomponents: {\n\t\tNcEmptyContent,\n\t\tFileDocumentOutline,\n\t\tCnWikiTreeNode,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The article record to render. When null/undefined the\n\t\t * component shows its empty-state. The shape is consumer-defined;\n\t\t * the component reads only `article[contentField]` and\n\t\t * `article[titleField]`.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tarticle: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The (optional) sidebar tree. Each node:\n\t\t * `{ id, [titleField], children?: [...] }`. Empty array hides\n\t\t * the sidebar entirely.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\ttree: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * OpenRegister register slug. Informational at this layer\n\t\t * (the consumer does the fetch); declared as a prop so the\n\t\t * manifest-driven prop forwarder in CnPageRenderer surfaces it\n\t\t * for child components / debugging.\n\t\t */\n\t\tregister: { // eslint-disable-line vue/no-unused-properties -- informational at this layer; CnPageRenderer forwards it from the manifest for child components.\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * OpenRegister schema slug. Informational at this layer —\n\t\t * see `register` above.\n\t\t */\n\t\tschema: { // eslint-disable-line vue/no-unused-properties -- informational at this layer; CnPageRenderer forwards it from the manifest for child components.\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Property on `article` holding the markdown body. Defaults\n\t\t * to `'body'`.\n\t\t */\n\t\tcontentField: {\n\t\t\ttype: String,\n\t\t\tdefault: 'body',\n\t\t},\n\n\t\t/**\n\t\t * Property on `article` holding the page title. Defaults\n\t\t * to `'title'`.\n\t\t */\n\t\ttitleField: {\n\t\t\ttype: String,\n\t\t\tdefault: 'title',\n\t\t},\n\n\t\t/**\n\t\t * `$route.params` key holding the article id. Informational at\n\t\t * this layer; declared so the consumer's data store can wire\n\t\t * the same default the validator and renderer both reference.\n\t\t */\n\t\tidParam: { // eslint-disable-line vue/no-unused-properties -- informational at this layer; CnPageRenderer forwards it from the manifest for child components.\n\t\t\ttype: String,\n\t\t\tdefault: 'id',\n\t\t},\n\n\t\t/**\n\t\t * Schema slug for the sidebar tree. When set AND `tree` is\n\t\t * non-empty the sidebar renders. Unset suppresses the sidebar\n\t\t * regardless of `tree` content.\n\t\t */\n\t\tsidebarSchema: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Register slug for the sidebar tree. Defaults to `register`\n\t\t * when unset (the common case).\n\t\t */\n\t\tsidebarRegister: { // eslint-disable-line vue/no-unused-properties -- informational at this layer; CnPageRenderer forwards it from the manifest for child components.\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Property on each sidebar node holding its child array.\n\t\t * Defaults to `'children'`.\n\t\t */\n\t\ttreeField: {\n\t\t\ttype: String,\n\t\t\tdefault: 'children',\n\t\t},\n\n\t\t/**\n\t\t * Property on each sidebar node holding its label. Defaults\n\t\t * to `titleField` (so `'title'` unless overridden).\n\t\t */\n\t\tsidebarTitleField: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Empty-state heading when `article` is null. */\n\t\temptyText: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Article not found'),\n\t\t},\n\n\t\t/** Empty-state description when `article` is null. */\n\t\temptyDescription: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'The requested article could not be found.'),\n\t\t},\n\n\t\t/** Empty-body heading when the article has no `contentField`. */\n\t\temptyBodyText: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'No content'),\n\t\t},\n\n\t\t/** Empty-body description when the article has no `contentField`. */\n\t\temptyBodyDescription: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'This article has no content yet.'),\n\t\t},\n\t},\n\n\temits: [\n\t\t/**\n\t\t * Emitted when a sidebar tree node is clicked. The payload is\n\t\t * the clicked node object (the same shape the consumer placed\n\t\t * in `tree`).\n\t\t */\n\t\t'tree-click',\n\t\t/**\n\t\t * Emitted when markdown parsing throws. Rare — `marked` and\n\t\t * the helper are defensive — but the contract is explicit so\n\t\t * consumers can log / surface a banner.\n\t\t */\n\t\t'error',\n\t],\n\n\tcomputed: {\n\t\t/**\n\t\t * Effective sidebar-title field — falls back to the article\n\t\t * title field when unset so consumers can leave it off in\n\t\t * the simple case.\n\t\t */\n\t\teffectiveSidebarTitleField() {\n\t\t\treturn this.sidebarTitleField || this.titleField\n\t\t},\n\n\t\t/**\n\t\t * Whether the sidebar should render. Both a non-empty tree\n\t\t * AND an explicit `sidebarSchema` are required — the schema\n\t\t * acts as the manifest-author's \"yes, I want a sidebar\" signal,\n\t\t * matching the spec's REQ-MWPT-5.\n\t\t */\n\t\thasSidebar() {\n\t\t\treturn Boolean(this.sidebarSchema) && Array.isArray(this.tree) && this.tree.length > 0\n\t\t},\n\n\t\t/** Article title resolved through `titleField`. */\n\t\tresolvedTitle() {\n\t\t\tif (!this.article) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst value = this.article[this.titleField]\n\t\t\treturn typeof value === 'string' ? value : ''\n\t\t},\n\n\t\t/**\n\t\t * Article body parsed to HTML through `cnRenderMarkdown`.\n\t\t * Empty string when the article is null or has no body.\n\t\t */\n\t\trenderedBody() {\n\t\t\tif (!this.article) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst source = this.article[this.contentField]\n\t\t\ttry {\n\t\t\t\treturn cnRenderMarkdown(source)\n\t\t\t} catch (error) {\n\t\t\t\tthis.$emit('error', error)\n\t\t\t\treturn ''\n\t\t\t}\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Re-emit `@tree-click` with the clicked node.\n\t\t *\n\t\t * @param {object} node The tree node the user clicked.\n\t\t */\n\t\tonTreeClick(node) {\n\t\t\tthis.$emit('tree-click', node)\n\t\t},\n\n\t\t/**\n\t\t * Stable v-for key for tree nodes — prefer `id`, fall back\n\t\t * to the title field, finally to JSON-stringified shape.\n\t\t *\n\t\t * @param {object} node Tree node.\n\t\t * @return {string}\n\t\t */\n\t\ttreeKey(node) {\n\t\t\tif (!node) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (typeof node.id === 'string' || typeof node.id === 'number') {\n\t\t\t\treturn String(node.id)\n\t\t\t}\n\t\t\tconst title = node[this.effectiveSidebarTitleField]\n\t\t\tif (typeof title === 'string') {\n\t\t\t\treturn title\n\t\t\t}\n\t\t\treturn JSON.stringify(node)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-wiki-page {\n\tdisplay: flex;\n\tgap: 24px;\n\tpadding: 20px;\n\tmax-width: 1200px;\n\tmargin: 0 auto;\n}\n\n.cn-wiki-page__sidebar {\n\tflex: 0 0 240px;\n\tborder-right: 1px solid var(--color-border);\n\tpadding-right: 16px;\n}\n\n.cn-wiki-page__tree {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-wiki-page__main {\n\tflex: 1 1 auto;\n\tmin-width: 0;\n}\n\n.cn-wiki-page__header {\n\tmargin-bottom: 16px;\n}\n\n.cn-wiki-page__title {\n\tfont-size: 1.6em;\n\tmargin: 0;\n\tcolor: var(--color-main-text);\n}\n\n.cn-wiki-page__body {\n\tline-height: 1.7;\n\tcolor: var(--color-main-text);\n}\n\n.cn-wiki-page__body :deep(h1),\n.cn-wiki-page__body :deep(h2),\n.cn-wiki-page__body :deep(h3) {\n\tcolor: var(--color-main-text);\n\tmargin-top: 1.4em;\n\tmargin-bottom: 0.5em;\n}\n\n.cn-wiki-page__body :deep(p) {\n\tmargin: 0 0 1em;\n}\n\n.cn-wiki-page__body :deep(code) {\n\tbackground: var(--color-background-dark);\n\tpadding: 2px 6px;\n\tborder-radius: var(--border-radius);\n\tfont-family: var(--font-face-monospace, monospace);\n\tfont-size: 0.9em;\n}\n\n.cn-wiki-page__body :deep(pre) {\n\tbackground: var(--color-background-dark);\n\tpadding: 12px;\n\tborder-radius: var(--border-radius);\n\toverflow-x: auto;\n}\n\n.cn-wiki-page__body :deep(pre code) {\n\tbackground: transparent;\n\tpadding: 0;\n}\n\n.cn-wiki-page__body :deep(blockquote) {\n\tborder-left: 3px solid var(--color-border);\n\tpadding-left: 12px;\n\tcolor: var(--color-text-lighter);\n\tmargin: 0 0 1em;\n}\n\n.cn-wiki-page__body :deep(table) {\n\tborder-collapse: collapse;\n\tmargin: 0 0 1em;\n}\n\n.cn-wiki-page__body :deep(th),\n.cn-wiki-page__body :deep(td) {\n\tborder: 1px solid var(--color-border);\n\tpadding: 6px 10px;\n}\n\n.cn-wiki-page--has-sidebar .cn-wiki-page__main {\n\tpadding-left: 8px;\n}\n</style>\n"],"names":["_createElementVNode","_createElementBlock","_Fragment","_renderList","_createBlock","_renderSlot","_toDisplayString","_createVNode"],"mappings":";;AA8EO,MAAA,UAAA,GAAA,EAAA,KAAM,EAAC,uBAAuB,GAAA;AAC7B,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,oBAAI,GAAA;AAYX,MAAA,UAAA,GAAA,EAAA,KAAK,EAAE,oBAAmB,GAAA;;;AASH,EAAA,KAAA,EAAA,sBAAA;;AACrB,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,qBAAqB,GAAA;;;;;;;;;IAlC9B,uBAAM,cAAc,EAAA,EAAA,2BAAA,EAAmC,QAAA,CAAA,UAAA,EAAA,CAAA,CAAA;AAAlC,IAAA,aAAW,EAAC,cAAc;;KAO/C,QAAA,CAAA,UAAA;kBAeI,CAAA,IAAA,CAAA,MAAA,EAAA,SAAA,EAAA;AAbN,UAAA,IAAI,EAAE,MAAI,CAAA,IAAA;AACV,UAAA,OAAC,EAAA,QAAA,CAAA,WAAA;iBAYK;AAXP,UAAAA,kBAAA,CAUG,OAVH,UAUG,EAAA;AATF,YAAAA,kBAAA,OAAA;gCACCC,kBAMI,CAAAC,QAAA,EAAA,IAAA,EAAAC,UAAA,CALY,MAAI,CAAA,IAAA,EAAA,CAAZ,IAAI,KAAA;qCADZC,WAMI,CAAA,yBAAA,EAAA;kBAJF,GAAG,EAAE,QAAO,CAAA,OAAA,CAAC,IAAA,CAAA;AACb,kBAAA,IAAI,EAAE,IAAI;AACV,kBAAA,UAAQ,EAAA,QAAA,CAAA,0BAAA;AACR,kBAAA,SAAS,EAAE,MAAE,CAAA,SAAA;AACb,kBAAA,OAAE,EAAA,QAAA,CAAA,WAAA;;;;;;;;MAULC,UASM,CAAA,IAAA,CAAA,MAAA,EAAA,QAAA,EAAA;AAPL,QAAA,KAAE,EAAA,QAAA,CAAA,aAAA;AACF,QAAA,OAAO,EAAE,MAAO,CAAA,OAAA;SAHjB,MASM;SALQ,QAAY,CAAA,aAAA;6CAIjB,oBAAA,EAAA;cAHRL,kBAEK,CAAA,IAAA,EAFL,UAEK,EAAAM,eAAA,CADD,QAAS,CAAA,aAAA,CAAA,EAAA,CAAA,YAAA;;;;QASX,MAAA,CAAA,OAAA;AAAJ,UAAAD,UAAA,2BAAA;YACCE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"CnWikiPage.vue3.js","sources":["../../../../src/components/CnWikiPage/CnWikiPage.vue?vue&type=template&id=25a3d3cf&lang.js"],"sourcesContent":["<!--\n CnWikiPage — Manifest-driven markdown article surface.\n\n Renders one wiki / docs / knowledge-base article declared in\n `pages[].config` for `type: \"wiki\"` pages. Closes the gap that\n forces every consumer's docs / help / article-detail route onto\n `type: \"custom\"`.\n\n The page reads its content from props rather than fetching itself.\n Following the precedent set by CnFilesPage and CnFormPage, the\n consumer (or a wiki-aware wrapper) populates `article` (object)\n and `tree` (array) — typically from a `useDetailView()` /\n list-call against the manifest-declared register/schema. This\n keeps the lib zero-dep on a particular OpenRegister fetcher\n while still making the manifest config the source of truth for\n field names.\n\n Markdown rendering is delegated to the `cnRenderMarkdown` composable\n which configures `marked` with `{ gfm: true, breaks: false }` —\n GitHub-flavoured markdown, no auto-`<br>` on newlines.\n\n Slots:\n - `#header` — overrides the default header. Scope `{ title, article }`.\n - `#sidebar` — overrides the default tree sidebar. Scope `{ tree, onClick }`.\n - `#body` — overrides the default markdown body. Scope `{ article, html }`.\n - `#empty` — replaces the empty-state when `article` is null.\n\n Events:\n - `@tree-click` — `node` whenever a sidebar tree node is clicked.\n - `@error` — error object if markdown parsing throws.\n\n Spec: REQ-MWPT-* (manifest-wiki-page-type).\n-->\n<template>\n\t<div class=\"cn-wiki-page\" data-testid=\"cn-wiki-page\" :class=\"{ 'cn-wiki-page--has-sidebar': hasSidebar }\">\n\t\t<!--\n\t\t\t@slot sidebar\n\t\t\t@description Replaces the default sidebar tree. Scoped props `{ tree, onClick }` —\n\t\t\t`onClick(node)` re-emits `@tree-click` with the node.\n\t\t-->\n\t\t<slot\n\t\t\tv-if=\"hasSidebar\"\n\t\t\tname=\"sidebar\"\n\t\t\t:tree=\"tree\"\n\t\t\t:onClick=\"onTreeClick\">\n\t\t\t<nav class=\"cn-wiki-page__sidebar\">\n\t\t\t\t<ul class=\"cn-wiki-page__tree\">\n\t\t\t\t\t<CnWikiTreeNode\n\t\t\t\t\t\tv-for=\"node in tree\"\n\t\t\t\t\t\t:key=\"treeKey(node)\"\n\t\t\t\t\t\t:node=\"node\"\n\t\t\t\t\t\t:titleField=\"effectiveSidebarTitleField\"\n\t\t\t\t\t\t:treeField=\"treeField\"\n\t\t\t\t\t\t@click=\"onTreeClick\" />\n\t\t\t\t</ul>\n\t\t\t</nav>\n\t\t</slot>\n\n\t\t<div class=\"cn-wiki-page__main\">\n\t\t\t<!--\n\t\t\t\t@slot header\n\t\t\t\t@description Replaces the default page header. Scoped props `{ title, article }`.\n\t\t\t-->\n\t\t\t<slot\n\t\t\t\tname=\"header\"\n\t\t\t\t:title=\"resolvedTitle\"\n\t\t\t\t:article=\"article\">\n\t\t\t\t<header v-if=\"resolvedTitle\" class=\"cn-wiki-page__header\">\n\t\t\t\t\t<h1 class=\"cn-wiki-page__title\">\n\t\t\t\t\t\t{{ resolvedTitle }}\n\t\t\t\t\t</h1>\n\t\t\t\t</header>\n\t\t\t</slot>\n\n\t\t\t<!--\n\t\t\t\t@slot empty\n\t\t\t\t@description Replaces the empty-state shown when `article` is null/undefined.\n\t\t\t-->\n\t\t\t<slot v-if=\"!article\" name=\"empty\">\n\t\t\t\t<NcEmptyContent\n\t\t\t\t\t:name=\"emptyText\"\n\t\t\t\t\t:description=\"emptyDescription\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<FileDocumentOutline :size=\"64\" />\n\t\t\t\t\t</template>\n\t\t\t\t</NcEmptyContent>\n\t\t\t</slot>\n\n\t\t\t<!--\n\t\t\t\t@slot body\n\t\t\t\t@description Replaces the default markdown body. Scoped props `{ article, html }` —\n\t\t\t\t`html` is the pre-parsed markdown so a custom renderer can wrap it.\n\t\t\t-->\n\t\t\t<slot\n\t\t\t\tv-else\n\t\t\t\tname=\"body\"\n\t\t\t\t:article=\"article\"\n\t\t\t\t:html=\"renderedBody\">\n\t\t\t\t<div\n\t\t\t\t\tv-if=\"renderedBody\"\n\t\t\t\t\tclass=\"cn-wiki-page__body\"\n\t\t\t\t\tv-html=\"renderedBody\" /><!-- eslint-disable-line vue/no-v-html -->\n\t\t\t\t<NcEmptyContent\n\t\t\t\t\tv-else\n\t\t\t\t\t:name=\"emptyBodyText\"\n\t\t\t\t\t:description=\"emptyBodyDescription\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<FileDocumentOutline :size=\"64\" />\n\t\t\t\t\t</template>\n\t\t\t\t</NcEmptyContent>\n\t\t\t</slot>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcEmptyContent } from '@nextcloud/vue'\nimport FileDocumentOutline from 'vue-material-design-icons/FileDocumentOutline.vue'\nimport CnWikiTreeNode from './CnWikiTreeNode.vue'\nimport { cnRenderMarkdown } from '../../composables/cnRenderMarkdown.js'\n\n/**\n * CnWikiPage — Manifest-driven markdown article surface.\n *\n * Reads `article` (object) and `tree` (array) as props rather than\n * fetching itself; consumers populate these from their own data\n * store or a wrapper composable. The manifest config still drives\n * which fields/registers are involved (via `contentField`,\n * `titleField`, `sidebarSchema`, …) so the validator stays\n * meaningful even though the lib does not own the fetch.\n *\n * Basic usage:\n *\n * <CnWikiPage\n * :article=\"article\"\n * :register=\"'pipelinq'\"\n * :schema=\"'article'\"\n * :content-field=\"'body'\" />\n *\n * With sidebar tree:\n *\n * <CnWikiPage\n * :article=\"article\"\n * :tree=\"categoryTree\"\n * :sidebar-schema=\"'category'\"\n *\n * @tree-click=\"onCategoryClick\" />\n */\nexport default {\n\tname: 'CnWikiPage',\n\n\tcomponents: {\n\t\tNcEmptyContent,\n\t\tFileDocumentOutline,\n\t\tCnWikiTreeNode,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The article record to render. When null/undefined the\n\t\t * component shows its empty-state. The shape is consumer-defined;\n\t\t * the component reads only `article[contentField]` and\n\t\t * `article[titleField]`.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tarticle: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The (optional) sidebar tree. Each node:\n\t\t * `{ id, [titleField], children?: [...] }`. Empty array hides\n\t\t * the sidebar entirely.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\ttree: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * OpenRegister register slug. Informational at this layer\n\t\t * (the consumer does the fetch); declared as a prop so the\n\t\t * manifest-driven prop forwarder in CnPageRenderer surfaces it\n\t\t * for child components / debugging.\n\t\t */\n\t\tregister: { // eslint-disable-line vue/no-unused-properties -- informational at this layer; CnPageRenderer forwards it from the manifest for child components.\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * OpenRegister schema slug. Informational at this layer —\n\t\t * see `register` above.\n\t\t */\n\t\tschema: { // eslint-disable-line vue/no-unused-properties -- informational at this layer; CnPageRenderer forwards it from the manifest for child components.\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Property on `article` holding the markdown body. Defaults\n\t\t * to `'body'`.\n\t\t */\n\t\tcontentField: {\n\t\t\ttype: String,\n\t\t\tdefault: 'body',\n\t\t},\n\n\t\t/**\n\t\t * Property on `article` holding the page title. Defaults\n\t\t * to `'title'`.\n\t\t */\n\t\ttitleField: {\n\t\t\ttype: String,\n\t\t\tdefault: 'title',\n\t\t},\n\n\t\t/**\n\t\t * `$route.params` key holding the article id. Informational at\n\t\t * this layer; declared so the consumer's data store can wire\n\t\t * the same default the validator and renderer both reference.\n\t\t */\n\t\tidParam: { // eslint-disable-line vue/no-unused-properties -- informational at this layer; CnPageRenderer forwards it from the manifest for child components.\n\t\t\ttype: String,\n\t\t\tdefault: 'id',\n\t\t},\n\n\t\t/**\n\t\t * Schema slug for the sidebar tree. When set AND `tree` is\n\t\t * non-empty the sidebar renders. Unset suppresses the sidebar\n\t\t * regardless of `tree` content.\n\t\t */\n\t\tsidebarSchema: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Register slug for the sidebar tree. Defaults to `register`\n\t\t * when unset (the common case).\n\t\t */\n\t\tsidebarRegister: { // eslint-disable-line vue/no-unused-properties -- informational at this layer; CnPageRenderer forwards it from the manifest for child components.\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Property on each sidebar node holding its child array.\n\t\t * Defaults to `'children'`.\n\t\t */\n\t\ttreeField: {\n\t\t\ttype: String,\n\t\t\tdefault: 'children',\n\t\t},\n\n\t\t/**\n\t\t * Property on each sidebar node holding its label. Defaults\n\t\t * to `titleField` (so `'title'` unless overridden).\n\t\t */\n\t\tsidebarTitleField: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Empty-state heading when `article` is null. */\n\t\temptyText: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Article not found'),\n\t\t},\n\n\t\t/** Empty-state description when `article` is null. */\n\t\temptyDescription: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'The requested article could not be found.'),\n\t\t},\n\n\t\t/** Empty-body heading when the article has no `contentField`. */\n\t\temptyBodyText: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'No content'),\n\t\t},\n\n\t\t/** Empty-body description when the article has no `contentField`. */\n\t\temptyBodyDescription: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'This article has no content yet.'),\n\t\t},\n\t},\n\n\temits: [\n\t\t/**\n\t\t * Emitted when a sidebar tree node is clicked. The payload is\n\t\t * the clicked node object (the same shape the consumer placed\n\t\t * in `tree`).\n\t\t */\n\t\t'tree-click',\n\t\t/**\n\t\t * Emitted when markdown parsing throws. Rare — `marked` and\n\t\t * the helper are defensive — but the contract is explicit so\n\t\t * consumers can log / surface a banner.\n\t\t */\n\t\t'error',\n\t],\n\n\tcomputed: {\n\t\t/**\n\t\t * Effective sidebar-title field — falls back to the article\n\t\t * title field when unset so consumers can leave it off in\n\t\t * the simple case.\n\t\t */\n\t\teffectiveSidebarTitleField() {\n\t\t\treturn this.sidebarTitleField || this.titleField\n\t\t},\n\n\t\t/**\n\t\t * Whether the sidebar should render. Both a non-empty tree\n\t\t * AND an explicit `sidebarSchema` are required — the schema\n\t\t * acts as the manifest-author's \"yes, I want a sidebar\" signal,\n\t\t * matching the spec's REQ-MWPT-5.\n\t\t */\n\t\thasSidebar() {\n\t\t\treturn Boolean(this.sidebarSchema) && Array.isArray(this.tree) && this.tree.length > 0\n\t\t},\n\n\t\t/** Article title resolved through `titleField`. */\n\t\tresolvedTitle() {\n\t\t\tif (!this.article) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst value = this.article[this.titleField]\n\t\t\treturn typeof value === 'string' ? value : ''\n\t\t},\n\n\t\t/**\n\t\t * Article body parsed to HTML through `cnRenderMarkdown`.\n\t\t * Empty string when the article is null or has no body.\n\t\t */\n\t\trenderedBody() {\n\t\t\tif (!this.article) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst source = this.article[this.contentField]\n\t\t\ttry {\n\t\t\t\treturn cnRenderMarkdown(source)\n\t\t\t} catch (error) {\n\t\t\t\tthis.$emit('error', error)\n\t\t\t\treturn ''\n\t\t\t}\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Re-emit `@tree-click` with the clicked node.\n\t\t *\n\t\t * @param {object} node The tree node the user clicked.\n\t\t */\n\t\tonTreeClick(node) {\n\t\t\tthis.$emit('tree-click', node)\n\t\t},\n\n\t\t/**\n\t\t * Stable v-for key for tree nodes — prefer `id`, fall back\n\t\t * to the title field, finally to JSON-stringified shape.\n\t\t *\n\t\t * @param {object} node Tree node.\n\t\t * @return {string}\n\t\t */\n\t\ttreeKey(node) {\n\t\t\tif (!node) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (typeof node.id === 'string' || typeof node.id === 'number') {\n\t\t\t\treturn String(node.id)\n\t\t\t}\n\t\t\tconst title = node[this.effectiveSidebarTitleField]\n\t\t\tif (typeof title === 'string') {\n\t\t\t\treturn title\n\t\t\t}\n\t\t\treturn JSON.stringify(node)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-wiki-page {\n\tdisplay: flex;\n\tgap: 24px;\n\tpadding: 20px;\n\tmax-width: 1200px;\n\tmargin: 0 auto;\n}\n\n.cn-wiki-page__sidebar {\n\tflex: 0 0 240px;\n\tborder-right: 1px solid var(--color-border);\n\tpadding-right: 16px;\n}\n\n.cn-wiki-page__tree {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-wiki-page__main {\n\tflex: 1 1 auto;\n\tmin-width: 0;\n}\n\n.cn-wiki-page__header {\n\tmargin-bottom: 16px;\n}\n\n/* Without a sidebar the title starts at the left edge of the content, under\n the Nextcloud navigation toggle button (44px wide). Clear it as CnPageHeader\n does. With a sidebar the tree sits there instead and the title is clear. */\n.cn-wiki-page:not(.cn-wiki-page--has-sidebar) .cn-wiki-page__header {\n\tpadding-inline-start: 56px;\n}\n\n.cn-wiki-page__title {\n\tfont-size: 1.6em;\n\tmargin: 0;\n\tcolor: var(--color-main-text);\n}\n\n.cn-wiki-page__body {\n\tline-height: 1.7;\n\tcolor: var(--color-main-text);\n}\n\n.cn-wiki-page__body :deep(h1),\n.cn-wiki-page__body :deep(h2),\n.cn-wiki-page__body :deep(h3) {\n\tcolor: var(--color-main-text);\n\tmargin-top: 1.4em;\n\tmargin-bottom: 0.5em;\n}\n\n.cn-wiki-page__body :deep(p) {\n\tmargin: 0 0 1em;\n}\n\n.cn-wiki-page__body :deep(code) {\n\tbackground: var(--color-background-dark);\n\tpadding: 2px 6px;\n\tborder-radius: var(--border-radius);\n\tfont-family: var(--font-face-monospace, monospace);\n\tfont-size: 0.9em;\n}\n\n.cn-wiki-page__body :deep(pre) {\n\tbackground: var(--color-background-dark);\n\tpadding: 12px;\n\tborder-radius: var(--border-radius);\n\toverflow-x: auto;\n}\n\n.cn-wiki-page__body :deep(pre code) {\n\tbackground: transparent;\n\tpadding: 0;\n}\n\n.cn-wiki-page__body :deep(blockquote) {\n\tborder-left: 3px solid var(--color-border);\n\tpadding-left: 12px;\n\tcolor: var(--color-text-lighter);\n\tmargin: 0 0 1em;\n}\n\n.cn-wiki-page__body :deep(table) {\n\tborder-collapse: collapse;\n\tmargin: 0 0 1em;\n}\n\n.cn-wiki-page__body :deep(th),\n.cn-wiki-page__body :deep(td) {\n\tborder: 1px solid var(--color-border);\n\tpadding: 6px 10px;\n}\n\n.cn-wiki-page--has-sidebar .cn-wiki-page__main {\n\tpadding-left: 8px;\n}\n</style>\n"],"names":["_createElementVNode","_createElementBlock","_Fragment","_renderList","_createBlock","_renderSlot","_toDisplayString","_createVNode"],"mappings":";;AA8EO,MAAA,UAAA,GAAA,EAAA,KAAM,EAAC,uBAAuB,GAAA;AAC7B,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,oBAAI,GAAA;AAYX,MAAA,UAAA,GAAA,EAAA,KAAK,EAAE,oBAAmB,GAAA;;;AASH,EAAA,KAAA,EAAA,sBAAA;;AACrB,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,qBAAqB,GAAA;;;;;;;;;IAlC9B,uBAAM,cAAc,EAAA,EAAA,2BAAA,EAAmC,QAAA,CAAA,UAAA,EAAA,CAAA,CAAA;AAAlC,IAAA,aAAW,EAAC,cAAc;;KAO/C,QAAA,CAAA,UAAA;kBAeI,CAAA,IAAA,CAAA,MAAA,EAAA,SAAA,EAAA;AAbN,UAAA,IAAI,EAAE,MAAI,CAAA,IAAA;AACV,UAAA,OAAC,EAAA,QAAA,CAAA,WAAA;iBAYK;AAXP,UAAAA,kBAAA,CAUG,OAVH,UAUG,EAAA;AATF,YAAAA,kBAAA,OAAA;gCACCC,kBAMI,CAAAC,QAAA,EAAA,IAAA,EAAAC,UAAA,CALY,MAAI,CAAA,IAAA,EAAA,CAAZ,IAAI,KAAA;qCADZC,WAMI,CAAA,yBAAA,EAAA;kBAJF,GAAG,EAAE,QAAO,CAAA,OAAA,CAAC,IAAA,CAAA;AACb,kBAAA,IAAI,EAAE,IAAI;AACV,kBAAA,UAAQ,EAAA,QAAA,CAAA,0BAAA;AACR,kBAAA,SAAS,EAAE,MAAE,CAAA,SAAA;AACb,kBAAA,OAAE,EAAA,QAAA,CAAA,WAAA;;;;;;;;MAULC,UASM,CAAA,IAAA,CAAA,MAAA,EAAA,QAAA,EAAA;AAPL,QAAA,KAAE,EAAA,QAAA,CAAA,aAAA;AACF,QAAA,OAAO,EAAE,MAAO,CAAA,OAAA;SAHjB,MASM;SALQ,QAAY,CAAA,aAAA;6CAIjB,oBAAA,EAAA;cAHRL,kBAEK,CAAA,IAAA,EAFL,UAEK,EAAAM,eAAA,CADD,QAAS,CAAA,aAAA,CAAA,EAAA,CAAA,YAAA;;;;QASX,MAAA,CAAA,OAAA;AAAJ,UAAAD,UAAA,2BAAA;YACCE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
package/dist/esm/index.js CHANGED
@@ -312,6 +312,12 @@ export { default as CnStackedBarWidget } from './components/CnStackedBarWidget/C
312
312
  export { default as CnStackedBarWidgetForm } from './components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue.js';
313
313
  export { default as CnWeekStripWidget } from './components/CnWeekStripWidget/CnWeekStripWidget.vue.js';
314
314
  export { default as CnWeekStripWidgetForm } from './components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue.js';
315
+ export { default as CnFormPage } from './components/CnFormPage/CnFormPage.vue.js';
316
+ export { default as CnLinkCardsPage } from './components/CnLinkCardsPage/CnLinkCardsPage.vue.js';
317
+ export { default as CnMapPage } from './components/CnMapPage/CnMapPage.vue.js';
318
+ export { default as CnReportsPage } from './components/CnReportsPage/CnReportsPage.vue.js';
319
+ export { default as CnStorePage } from './components/CnStorePage/CnStorePage.vue.js';
320
+ export { default as CnWikiPage } from './components/CnWikiPage/CnWikiPage.vue.js';
315
321
  export { default as CnAiAgentPicker } from './components/CnAiCompanion/CnAiAgentPicker.vue.js';
316
322
  export { default as CnAiChatPanel } from './components/CnAiCompanion/CnAiChatPanel.vue.js';
317
323
  export { default as CnAiCompanion } from './components/CnAiCompanion/CnAiCompanion.vue.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -291,6 +291,11 @@ var translations = {
291
291
  "JSON pointer": "JSON pointer",
292
292
  Last: "Last",
293
293
  "Load more": "Load more",
294
+ "Nothing to open here.": "Nothing to open here.",
295
+ "Could not check": "Could not check",
296
+ "Could not check: {subject}": "Could not check: {subject}",
297
+ "The check failed.": "The check failed.",
298
+ "The check failed: {reason}": "The check failed: {reason}",
294
299
  "To do": "To do",
295
300
  "Next step": "Next step",
296
301
  Review: "Review",
@@ -608,6 +613,7 @@ var translations = {
608
613
  "No linked emails yet": "No linked emails yet",
609
614
  "Could not load emails": "Could not load emails",
610
615
  "Open in Mail": "Open in Mail",
616
+ "Open in Office": "Open in Office",
611
617
  "(no subject)": "(no subject)",
612
618
  "Unknown sender": "Unknown sender",
613
619
  "No conversations linked yet": "No conversations linked yet",
@@ -1 +1 @@
1
- {"version":3,"file":"en.json.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"en.json.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -291,6 +291,11 @@ var translations = {
291
291
  "JSON pointer": "JSON-aanwijzer",
292
292
  Last: "Laatste",
293
293
  "Load more": "Meer laden",
294
+ "Nothing to open here.": "Hier is niets te openen.",
295
+ "Could not check": "Kon niet controleren",
296
+ "Could not check: {subject}": "Kon niet controleren: {subject}",
297
+ "The check failed.": "De controle is mislukt.",
298
+ "The check failed: {reason}": "De controle is mislukt: {reason}",
294
299
  "To do": "Nog doen",
295
300
  "Next step": "Volgende stap",
296
301
  Review: "Beoordelen",
@@ -608,6 +613,7 @@ var translations = {
608
613
  "No linked emails yet": "Nog geen gekoppelde e-mails",
609
614
  "Could not load emails": "Kon e-mails niet laden",
610
615
  "Open in Mail": "Openen in Mail",
616
+ "Open in Office": "Openen in Office",
611
617
  "(no subject)": "(geen onderwerp)",
612
618
  "Unknown sender": "Onbekende afzender",
613
619
  "No conversations linked yet": "Nog geen gesprekken gekoppeld",
@@ -1 +1 @@
1
- {"version":3,"file":"nl.json.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"nl.json.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -325,8 +325,7 @@
325
325
  outline-offset: -2px;
326
326
  }
327
327
 
328
- /* Multi-column sort priority badge (1, 2, 3) — only rendered once more than
329
- one sort key is active; a single active sort shows the arrow alone. */
328
+ /* Multi-column sort priority badge (1, 2, 3), rendered only when two or more rendered, sortable columns are sorted. */
330
329
  .cn-table-sort-badge {
331
330
  display: inline-flex;
332
331
  align-items: center;
@@ -2660,9 +2659,17 @@
2660
2659
  font-weight: 700;
2661
2660
  line-height: 1.3;
2662
2661
  color: var(--color-main-text);
2663
- white-space: nowrap;
2662
+ /* Two lines, then an ellipsis. One line cut a long case title short on a
2663
+ page with room to spare, and the title is the one thing that says which
2664
+ record this is. The element carries the full text as its `title`, so a
2665
+ title cut after two lines is still reachable. `overflow-wrap` breaks a
2666
+ word longer than the line, which nowrap used to hide. */
2667
+ display: -webkit-box;
2668
+ -webkit-box-orient: vertical;
2669
+ -webkit-line-clamp: 2;
2670
+ line-clamp: 2;
2664
2671
  overflow: hidden;
2665
- text-overflow: ellipsis;
2672
+ overflow-wrap: anywhere;
2666
2673
  }
2667
2674
 
2668
2675
  .cn-detail-page__subtitle {
@@ -6352,8 +6359,7 @@ svg.leaflet-image-layer.leaflet-interactive path {
6352
6359
  outline-offset: -2px;
6353
6360
  }
6354
6361
 
6355
- /* Multi-column sort priority badge (1, 2, 3) — only rendered once more than
6356
- one sort key is active; a single active sort shows the arrow alone. */
6362
+ /* Multi-column sort priority badge (1, 2, 3), rendered only when two or more rendered, sortable columns are sorted. */
6357
6363
  .cn-table-sort-badge {
6358
6364
  display: inline-flex;
6359
6365
  align-items: center;
@@ -17505,6 +17511,30 @@ to { background-position: -200% 0;
17505
17511
  color: inherit;
17506
17512
  }
17507
17513
 
17514
+ /* The check failed. Muted text, no fill, no severity colour: it is a note
17515
+ that something is not known, not a warning. */
17516
+ .cn-banner-widget--unchecked[data-v-3531692f] {
17517
+ box-sizing: border-box;
17518
+ margin: 0;
17519
+ padding: 8px 12px;
17520
+ color: var(--color-text-maxcontrast);
17521
+ font-size: 0.9em;
17522
+ cursor: help;
17523
+ }
17524
+
17525
+ /* The reason, for assistive technology; sighted readers get the tooltip. */
17526
+ .cn-banner-widget__unchecked-reason[data-v-3531692f] {
17527
+ position: absolute;
17528
+ width: 1px;
17529
+ height: 1px;
17530
+ margin: -1px;
17531
+ padding: 0;
17532
+ overflow: hidden;
17533
+ clip-path: inset(50%);
17534
+ white-space: nowrap;
17535
+ border: 0;
17536
+ }
17537
+
17508
17538
  /* Attention card. The severity edge is an inset shadow, so it takes no width. */
17509
17539
  .cn-banner-widget--attention[data-v-3531692f] {
17510
17540
  --cn-banner-accent: var(--color-primary-element);
@@ -31807,6 +31837,13 @@ a.cn-integration-card__title[data-v-f7e9bbfa]:hover {
31807
31837
  .cn-wiki-page__header[data-v-25a3d3cf] {
31808
31838
  margin-bottom: 16px;
31809
31839
  }
31840
+
31841
+ /* Without a sidebar the title starts at the left edge of the content, under
31842
+ the Nextcloud navigation toggle button (44px wide). Clear it as CnPageHeader
31843
+ does. With a sidebar the tree sits there instead and the title is clear. */
31844
+ .cn-wiki-page:not(.cn-wiki-page--has-sidebar) .cn-wiki-page__header[data-v-25a3d3cf] {
31845
+ padding-inline-start: 56px;
31846
+ }
31810
31847
  .cn-wiki-page__title[data-v-25a3d3cf] {
31811
31848
  font-size: 1.6em;
31812
31849
  margin: 0;
@@ -31866,6 +31903,13 @@ a.cn-integration-card__title[data-v-f7e9bbfa]:hover {
31866
31903
  .cn-reports-page[data-v-41525752] {
31867
31904
  padding: 16px;
31868
31905
  }
31906
+
31907
+ /* Clear the Nextcloud navigation toggle button (44px wide, absolutely
31908
+ positioned at the left edge of .app-content) plus 12px breathing room, as
31909
+ CnPageHeader does. Only the header shifts; the body keeps the full width. */
31910
+ .cn-reports-page__header[data-v-41525752] {
31911
+ padding-inline-start: 56px;
31912
+ }
31869
31913
  .cn-reports-page__title[data-v-41525752] {
31870
31914
  margin: 0 0 4px;
31871
31915
  }
@@ -31922,6 +31966,13 @@ a.cn-integration-card__title[data-v-f7e9bbfa]:hover {
31922
31966
  padding: 16px;
31923
31967
  max-width: 1200px;
31924
31968
  }
31969
+
31970
+ /* Clear the Nextcloud navigation toggle button (44px wide, absolutely
31971
+ positioned at the left edge of .app-content) plus 12px breathing room, as
31972
+ CnPageHeader does. Only the header shifts; the body keeps the full width. */
31973
+ .cn-store-page__header[data-v-25c8171b] {
31974
+ padding-inline-start: 56px;
31975
+ }
31925
31976
  .cn-store-page__heading[data-v-25c8171b] {
31926
31977
  display: flex;
31927
31978
  flex-wrap: wrap;
@@ -32005,6 +32056,122 @@ a.cn-integration-card__title[data-v-f7e9bbfa]:hover {
32005
32056
  }
32006
32057
 
32007
32058
 
32059
+ .cn-link-cards-page[data-v-2f9a546b] {
32060
+ display: flex;
32061
+ flex-direction: column;
32062
+ gap: calc(6 * var(--default-grid-baseline));
32063
+ padding: calc(5 * var(--default-grid-baseline));
32064
+ }
32065
+
32066
+ /* Clear the Nextcloud navigation toggle button (44px wide, absolutely
32067
+ positioned at the left edge of .app-content) plus 12px breathing room, as
32068
+ CnPageHeader does. Only the header shifts; the cards keep the full width. */
32069
+ .cn-link-cards-page__header[data-v-2f9a546b] {
32070
+ padding-inline-start: 56px;
32071
+ }
32072
+ .cn-link-cards-page__title[data-v-2f9a546b] {
32073
+ margin: 0 0 var(--default-grid-baseline);
32074
+ }
32075
+ .cn-link-cards-page__description[data-v-2f9a546b],
32076
+ .cn-link-cards-page__empty[data-v-2f9a546b] {
32077
+ color: var(--color-text-maxcontrast);
32078
+ margin: 0;
32079
+ max-width: 70ch;
32080
+ }
32081
+ .cn-link-cards-page__group[data-v-2f9a546b] {
32082
+ display: flex;
32083
+ flex-direction: column;
32084
+ gap: calc(3 * var(--default-grid-baseline));
32085
+ }
32086
+ .cn-link-cards-page__caption[data-v-2f9a546b] {
32087
+ color: var(--color-text-maxcontrast);
32088
+ font-size: 0.85em;
32089
+ font-weight: 600;
32090
+ letter-spacing: 0.06em;
32091
+ margin: 0;
32092
+ text-transform: uppercase;
32093
+ }
32094
+ .cn-link-cards-page__grid[data-v-2f9a546b] {
32095
+ display: grid;
32096
+ gap: calc(3 * var(--default-grid-baseline));
32097
+ grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
32098
+ list-style: none;
32099
+ margin: 0;
32100
+ padding: 0;
32101
+ }
32102
+ .cn-link-cards-page__cell[data-v-2f9a546b] {
32103
+ display: flex;
32104
+ }
32105
+ .cn-link-cards-page__card[data-v-2f9a546b] {
32106
+ align-items: flex-start;
32107
+ background: var(--color-main-background);
32108
+ border: 1px solid var(--color-border-dark);
32109
+ border-radius: var(--border-radius-large);
32110
+ color: var(--color-main-text);
32111
+ display: flex;
32112
+ flex: 1;
32113
+ gap: calc(3 * var(--default-grid-baseline));
32114
+ min-width: 0;
32115
+ padding: calc(4 * var(--default-grid-baseline));
32116
+ transition: background-color var(--animation-quick, 100ms) ease, border-color var(--animation-quick, 100ms) ease, box-shadow var(--animation-quick, 100ms) ease;
32117
+ }
32118
+
32119
+ /* A card is a link, and it must not look like a line of prose. A theme may
32120
+ underline every link with `!important` (thematiq: `a { text-decoration:
32121
+ underline !important }`, specificity 0,0,1). Among `!important` rules the
32122
+ higher specificity wins, so this class rule has to carry it too; without it
32123
+ the label and the description both rendered underlined on a live Nextcloud. */
32124
+ .cn-link-cards-page__card[data-v-2f9a546b],
32125
+ .cn-link-cards-page__card[data-v-2f9a546b]:hover,
32126
+ .cn-link-cards-page__card[data-v-2f9a546b]:focus,
32127
+ .cn-link-cards-page__card[data-v-2f9a546b]:active {
32128
+ text-decoration: none !important;
32129
+ }
32130
+
32131
+ /* Hover moves the whole card: border, background and a soft lift. */
32132
+ .cn-link-cards-page__card[data-v-2f9a546b]:hover {
32133
+ background: var(--color-background-hover);
32134
+ border-color: var(--color-primary-element);
32135
+ box-shadow: 0 2px 6px var(--color-box-shadow);
32136
+ }
32137
+ @media (prefers-reduced-motion: reduce) {
32138
+ .cn-link-cards-page__card[data-v-2f9a546b] {
32139
+ transition: none;
32140
+ }
32141
+ }
32142
+ .cn-link-cards-page__card[data-v-2f9a546b]:focus-visible {
32143
+ border-color: var(--color-primary-element);
32144
+ outline: 2px solid var(--color-primary-element);
32145
+ outline-offset: 2px;
32146
+ }
32147
+ .cn-link-cards-page__icon[data-v-2f9a546b] {
32148
+ color: var(--color-primary-element);
32149
+ flex: none;
32150
+ }
32151
+ .cn-link-cards-page__body[data-v-2f9a546b] {
32152
+ display: flex;
32153
+ flex-direction: column;
32154
+ gap: var(--default-grid-baseline);
32155
+ min-width: 0;
32156
+ }
32157
+
32158
+ /* The label is the card's title. Its colour is set here, on the span, because
32159
+ a theme may force its link colour on the `a` itself; an inherited link
32160
+ colour made the title read as a hyperlink. */
32161
+ .cn-link-cards-page__label[data-v-2f9a546b] {
32162
+ color: var(--color-main-text);
32163
+ font-size: 1.05em;
32164
+ font-weight: 700;
32165
+ line-height: 1.3;
32166
+ overflow-wrap: anywhere;
32167
+ }
32168
+ .cn-link-cards-page__card-description[data-v-2f9a546b] {
32169
+ color: var(--color-text-maxcontrast);
32170
+ font-size: 0.9em;
32171
+ line-height: 1.4;
32172
+ }
32173
+
32174
+
32008
32175
  /* NcDialog supplies the padding and the heading (via `name`); this div keeps only
32009
32176
  the body's own column layout. */
32010
32177
  .cn-page-config[data-v-9e9a9cf8] {
@@ -34218,15 +34385,40 @@ a.cn-integration-card__title[data-v-f7e9bbfa]:hover {
34218
34385
 
34219
34386
 
34220
34387
  .cn-brand-stripe[data-v-6fdfa763] {
34388
+ --cn-brand-stripe-first: var(--cn-brand-stripe-ratio-1, 1);
34389
+ --cn-brand-stripe-second: var(--cn-brand-stripe-ratio-2, 1);
34390
+ --cn-brand-stripe-total: calc(
34391
+ var(--cn-brand-stripe-first) + var(--cn-brand-stripe-second) + var(--cn-brand-stripe-ratio-3, 1)
34392
+ );
34393
+ --cn-brand-stripe-stop-1: calc(var(--cn-brand-stripe-first) / var(--cn-brand-stripe-total) * 100%);
34394
+ --cn-brand-stripe-stop-2: calc(
34395
+ (var(--cn-brand-stripe-first) + var(--cn-brand-stripe-second)) / var(--cn-brand-stripe-total) * 100%
34396
+ );
34397
+ --cn-brand-stripe-direction: to right;
34398
+ --cn-brand-stripe-bands: linear-gradient(
34399
+ var(--cn-brand-stripe-direction),
34400
+ var(--cn-brand-stripe-color-1, var(--color-primary-element)) 0 var(--cn-brand-stripe-stop-1),
34401
+ var(--cn-brand-stripe-color-2, var(--color-primary-element)) var(--cn-brand-stripe-stop-1) var(--cn-brand-stripe-stop-2),
34402
+ var(--cn-brand-stripe-color-3, var(--color-primary-element)) var(--cn-brand-stripe-stop-2) 100%
34403
+ );
34221
34404
  display: flex;
34222
34405
  flex: none;
34223
34406
  width: 100%;
34224
34407
  height: var(--cn-brand-stripe-height, 4px);
34408
+ background-image: var(--cn-brand-stripe-image, var(--cn-brand-stripe-bands));
34409
+ background-repeat: repeat-x;
34410
+ background-size: auto 100%;
34411
+ }
34412
+ .cn-brand-stripe--inverse[data-v-6fdfa763] {
34413
+ background-image: var(--cn-brand-stripe-image-inverse, var(--cn-brand-stripe-image, var(--cn-brand-stripe-bands)));
34225
34414
  }
34226
34415
  .cn-brand-stripe--vertical[data-v-6fdfa763] {
34416
+ --cn-brand-stripe-direction: to bottom;
34227
34417
  flex-direction: column;
34228
34418
  width: var(--cn-brand-stripe-height, 4px);
34229
34419
  height: 100%;
34420
+ background-repeat: repeat-y;
34421
+ background-size: 100% auto;
34230
34422
  }
34231
34423
  .cn-brand-stripe__band[data-v-6fdfa763] {
34232
34424
  flex-basis: 0;
@@ -34234,15 +34426,12 @@ a.cn-integration-card__title[data-v-f7e9bbfa]:hover {
34234
34426
  }
34235
34427
  .cn-brand-stripe__band--1[data-v-6fdfa763] {
34236
34428
  flex-grow: var(--cn-brand-stripe-ratio-1, 1);
34237
- background-color: var(--cn-brand-stripe-color-1, var(--color-primary-element));
34238
34429
  }
34239
34430
  .cn-brand-stripe__band--2[data-v-6fdfa763] {
34240
34431
  flex-grow: var(--cn-brand-stripe-ratio-2, 1);
34241
- background-color: var(--cn-brand-stripe-color-2, var(--color-primary-element));
34242
34432
  }
34243
34433
  .cn-brand-stripe__band--3[data-v-6fdfa763] {
34244
34434
  flex-grow: var(--cn-brand-stripe-ratio-3, 1);
34245
- background-color: var(--cn-brand-stripe-color-3, var(--color-primary-element));
34246
34435
  }
34247
34436
  @media print {
34248
34437
  .cn-brand-stripe[data-v-6fdfa763] {
@@ -130,10 +130,15 @@ function capitalize(str) {
130
130
  *
131
131
  * Array values are serialized with PHP bracket notation (`key[]=a&key[]=b`)
132
132
  * so the Nextcloud/OpenRegister backend parses them as an array — a plain
133
- * repeated `key=a&key=b` collapses to the last value in PHP. Also handles
134
- * _order serialization (JSON.stringify for objects) and skips
133
+ * repeated `key=a&key=b` collapses to the last value in PHP. A plain object
134
+ * under a filter key is a set of operators and goes out in bracket form
135
+ * (`{ deadline: { lt: 'x' } }` becomes `deadline[lt]=x`); a key already
136
+ * written that way (`'deadline[lt]'`) passes through unchanged. Reserved keys
137
+ * that start with `_` keep their JSON form (`_order`). Skips
135
138
  * null/undefined/empty values.
136
139
  *
140
+ * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-filter-operator-is-serialised-in-bracket-form
141
+ *
137
142
  * @param {object} params Key-value pairs for query parameters
138
143
  * @return {string} Query string including leading '?' or empty string
139
144
  */
@@ -159,6 +164,27 @@ function buildQueryString(params = {}) {
159
164
  queryParams.append(arrayKey, String(item));
160
165
  }
161
166
  }
167
+ } else if (typeof value === 'object' && !key.startsWith('_')) {
168
+ // A filter operator: `{ deadline: { lt: '2026-10-06' } }` goes out
169
+ // as `deadline[lt]=2026-10-06`, the form OpenRegister reads. As
170
+ // JSON it reached the database as the literal value and a date
171
+ // column answered 500. Reserved `_` keys (`_order`) keep JSON.
172
+ for (const [op, operand] of Object.entries(value)) {
173
+ if (operand === undefined || operand === null || operand === '') {
174
+ continue
175
+ }
176
+ if (Array.isArray(operand)) {
177
+ for (const item of operand) {
178
+ if (item !== undefined && item !== null && item !== '') {
179
+ queryParams.append(`${key}[${op}][]`, String(item));
180
+ }
181
+ }
182
+ } else if (typeof operand === 'object') {
183
+ queryParams.set(`${key}[${op}]`, JSON.stringify(operand));
184
+ } else {
185
+ queryParams.set(`${key}[${op}]`, String(operand));
186
+ }
187
+ }
162
188
  } else if (typeof value === 'object') {
163
189
  queryParams.set(key, JSON.stringify(value));
164
190
  } else {