@conduction/nextcloud-vue 2.90.0 → 2.92.0

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 (88) hide show
  1. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +10 -5
  2. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  3. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  4. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  5. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +15 -1
  6. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  7. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +1 -1
  8. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  9. package/dist/esm/components/CnDialog/CnDialog.vue2.js +1 -1
  10. package/dist/esm/components/CnDialog/CnDialog.vue2.js.map +1 -1
  11. package/dist/esm/components/CnDialog/CnDialog.vue3.js.map +1 -1
  12. package/dist/esm/components/CnDialogHeader/CnDialogHeader.vue2.js +1 -1
  13. package/dist/esm/components/CnDialogHeader/CnDialogHeader.vue2.js.map +1 -1
  14. package/dist/esm/components/CnDialogHeader/CnDialogHeader.vue3.js.map +1 -1
  15. package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
  16. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +18 -10
  17. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  18. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +16 -8
  19. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  20. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js +1 -1
  21. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js +16 -1
  22. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js.map +1 -1
  23. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js +1 -1
  24. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js.map +1 -1
  25. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue.js +1 -1
  26. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js +156 -3
  27. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js.map +1 -1
  28. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js +168 -52
  29. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js.map +1 -1
  30. package/dist/esm/components/CnModuleCard/CnModuleCard.vue.js +9 -0
  31. package/dist/esm/components/CnModuleCard/CnModuleCard.vue.js.map +1 -0
  32. package/dist/esm/components/CnModuleCard/CnModuleCard.vue2.js +83 -0
  33. package/dist/esm/components/CnModuleCard/CnModuleCard.vue2.js.map +1 -0
  34. package/dist/esm/components/CnModuleCard/CnModuleCard.vue3.js +119 -0
  35. package/dist/esm/components/CnModuleCard/CnModuleCard.vue3.js.map +1 -0
  36. package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue.js +1 -1
  37. package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue2.js +6 -3
  38. package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue2.js.map +1 -1
  39. package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue3.js.map +1 -1
  40. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  41. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +33 -2
  42. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  43. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  44. package/dist/esm/components/CnTextWidget/CnTextWidget.vue.js +1 -1
  45. package/dist/esm/components/CnTextWidget/CnTextWidget.vue2.js +63 -4
  46. package/dist/esm/components/CnTextWidget/CnTextWidget.vue2.js.map +1 -1
  47. package/dist/esm/components/CnTextWidget/CnTextWidget.vue3.js +19 -2
  48. package/dist/esm/components/CnTextWidget/CnTextWidget.vue3.js.map +1 -1
  49. package/dist/esm/index.js +2 -0
  50. package/dist/esm/index.js.map +1 -1
  51. package/dist/esm/l10n/en.json.js +22 -1
  52. package/dist/esm/l10n/en.json.js.map +1 -1
  53. package/dist/esm/l10n/nl.json.js +21 -1
  54. package/dist/esm/l10n/nl.json.js.map +1 -1
  55. package/dist/esm/library-style.js +1 -1
  56. package/dist/esm/nextcloud-vue.css +328 -114
  57. package/dist/esm/utils/boardDate.js +35 -1
  58. package/dist/esm/utils/boardDate.js.map +1 -1
  59. package/dist/esm/utils/fillTemplate.js +12 -3
  60. package/dist/esm/utils/fillTemplate.js.map +1 -1
  61. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  62. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  63. package/dist/nextcloud-vue.cjs.js +11569 -10871
  64. package/dist/nextcloud-vue.cjs.js.map +2 -2
  65. package/dist/nextcloud-vue.css +328 -114
  66. package/l10n/en.json +22 -1
  67. package/l10n/nl.json +21 -1
  68. package/package.json +1 -1
  69. package/src/components/CnDataTable/CnDataTable.vue +10 -5
  70. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +16 -2
  71. package/src/components/CnDialog/CnDialog.vue +1 -1
  72. package/src/components/CnDialog/index.js +7 -0
  73. package/src/components/CnDialogHeader/CnDialogHeader.vue +1 -1
  74. package/src/components/CnDialogHeader/index.js +7 -0
  75. package/src/components/CnFormDialog/CnFormDialog.md +1 -0
  76. package/src/components/CnFormDialog/CnFormDialog.vue +13 -0
  77. package/src/components/CnIndexPage/CnIndexPageWidgets.vue +17 -2
  78. package/src/components/CnLinkCardsPage/CnLinkCardsPage.vue +338 -5
  79. package/src/components/CnModuleCard/CnModuleCard.vue +258 -0
  80. package/src/components/CnObjectSidebar/CnObjectSidebar.vue +6 -3
  81. package/src/components/CnTabsWidget/CnTabsWidget.vue +33 -2
  82. package/src/components/CnTextWidget/CnTextWidget.vue +89 -4
  83. package/src/components/index.js +2 -0
  84. package/src/index.js +2 -0
  85. package/src/schemas/app-manifest-v2.schema.json +59 -2
  86. package/src/utils/boardDate.js +34 -0
  87. package/src/utils/fillTemplate.js +12 -3
  88. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -1 +1 @@
1
- {"version":3,"file":"CnTextWidget.vue3.js","sources":["../../../../src/components/CnTextWidget/CnTextWidget.vue?vue&type=template&id=2d4184ef&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-text-widget\" :style=\"wrapperStyle\">\n\t\t<table\n\t\t\tv-if=\"tableMode\"\n\t\t\tclass=\"cn-text-widget__table\"\n\t\t\t:style=\"tableContainerStyle\">\n\t\t\t<tbody>\n\t\t\t\t<tr v-for=\"(row, rIdx) in tableRows\" :key=\"`r-${rIdx}`\">\n\t\t\t\t\t<template v-for=\"(cell, cIdx) in row\">\n\t\t\t\t\t\t<th\n\t\t\t\t\t\t\tv-if=\"isHeaderCell(rIdx)\"\n\t\t\t\t\t\t\tv-show=\"!isPlaceholderCellAt(rIdx, cIdx)\"\n\t\t\t\t\t\t\t:key=\"`c-${rIdx}-${cIdx}`\"\n\t\t\t\t\t\t\t:rowspan=\"cell.rowSpan || 1\"\n\t\t\t\t\t\t\t:colspan=\"cell.colSpan || 1\"\n\t\t\t\t\t\t\t:style=\"headerCellStyle(cIdx)\">\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tv-if=\"cellHasText(cell)\"\n\t\t\t\t\t\t\t\tv-html=\"sanitizeCell(cell.text)\" /><!-- eslint-disable-line vue/no-v-html -->\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tv-else\n\t\t\t\t\t\t\t\tclass=\"cn-text-widget__cell-placeholder\">\n\t\t\t\t\t\t\t\t{{ emptyCellPlaceholder }}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</th>\n\t\t\t\t\t\t<td\n\t\t\t\t\t\t\tv-else\n\t\t\t\t\t\t\tv-show=\"!isPlaceholderCellAt(rIdx, cIdx)\"\n\t\t\t\t\t\t\t:key=\"`c-${rIdx}-${cIdx}`\"\n\t\t\t\t\t\t\t:rowspan=\"cell.rowSpan || 1\"\n\t\t\t\t\t\t\t:colspan=\"cell.colSpan || 1\"\n\t\t\t\t\t\t\t:style=\"dataCellStyle(cIdx)\">\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tv-if=\"cellHasText(cell)\"\n\t\t\t\t\t\t\t\tv-html=\"sanitizeCell(cell.text)\" /><!-- eslint-disable-line vue/no-v-html -->\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tv-else\n\t\t\t\t\t\t\t\tclass=\"cn-text-widget__cell-placeholder\">\n\t\t\t\t\t\t\t\t{{ emptyCellPlaceholder }}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</td>\n\t\t\t\t\t</template>\n\t\t\t\t</tr>\n\t\t\t</tbody>\n\t\t</table>\n\t\t<div\n\t\t\tv-else-if=\"hasText\"\n\t\t\tclass=\"cn-text-widget__content\"\n\t\t\t:class=\"contentClass\"\n\t\t\t:style=\"contentStyle\"\n\t\t\tv-html=\"sanitizedHtml\" /><!-- eslint-disable-line vue/no-v-html -->\n\t\t<span\n\t\t\tv-else\n\t\t\tclass=\"cn-text-widget__placeholder\"\n\t\t\t:style=\"contentStyle\">\n\t\t\t{{ placeholderText }}\n\t\t</span>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport DOMPurify from 'dompurify'\nimport { Marked } from 'marked'\nimport { isPlaceholderCell } from '../../utils/textTable.js'\n\n// Per-module `Marked` instance so configuration is scoped here and does NOT\n// mutate the global `marked` singleton (which would affect any other code in\n// the same app that imports `marked`). `gfm: true` enables tables; `breaks:\n// false` keeps CommonMark-strict newline behaviour.\nconst markedInstance = new Marked({\n\tgfm: true,\n\tbreaks: false,\n})\n\n// DOMPurify allow-list hook: author-supplied `target=_blank` links survive\n// sanitisation (we add `target` to ADD_ATTR below); the hook then appends\n// `rel=\"noopener noreferrer\"` to mitigate reverse-tabnabbing. The hook is\n// idempotent and registered once.\nlet hookRegistered = false\nfunction ensureDomPurifyHook() {\n\tif (hookRegistered) {\n\t\treturn\n\t}\n\tDOMPurify.addHook('afterSanitizeAttributes', (node) => {\n\t\tif (node.tagName === 'A' && node.getAttribute('target') === '_blank') {\n\t\t\tnode.setAttribute('rel', 'noopener noreferrer')\n\t\t}\n\t})\n\thookRegistered = true\n}\n\n/**\n * CnTextWidget — renders user-authored multi-line text (or a small table)\n * inside a dashboard cell. Text content is passed through DOMPurify before\n * injection via `v-html` so common formatting tags survive while XSS vectors\n * (`<script>`, `on*` attributes, `javascript:` URLs) are stripped.\n *\n * Persisted shape: `{text, fontSize, color, backgroundColor, textAlign,\n * contentMode, tableMode, tableData}`. Defaults: `fontSize='14px'`,\n * `color='var(--color-main-text)'`, `backgroundColor='transparent'`,\n * `textAlign='left'`. `contentMode` absent means the legacy `'html'` branch;\n * the literal `'markdown'` parses via marked (GFM tables) then sanitises\n * through the same allow-list — a single trust boundary protects both modes.\n *\n * When `tableMode === true`, the renderer draws an HTML `<table>` from\n * `tableData` instead: header rows promote row 0 to `<th>`, per-column\n * alignment is applied inline, and `colSpan` / `rowSpan` become native HTML\n * attributes. Merge-swallowed cells are hidden via `v-show` so the visual\n * grid stays rectangular.\n *\n * Registered as the `text` dashboard widget type via the renderer's\n * `index.js`.\n */\nexport default {\n\tname: 'CnTextWidget',\n\n\tprops: {\n\t\t/**\n\t\t * Persisted widget content blob: `{text, fontSize, color,\n\t\t * backgroundColor, textAlign, contentMode, tableMode, tableData}`.\n\t\t * All keys are optional; missing keys collapse to documented defaults.\n\t\t *\n\t\t * @type {{text: string, fontSize: string, color: string, backgroundColor: string, textAlign: string, contentMode: string, tableMode: boolean, tableData: object}}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\t},\n\n\tcomputed: {\n\t\t/** The author text, or '' when absent / non-string. */\n\t\ttext() {\n\t\t\treturn typeof this.content?.text === 'string' ? this.content.text : ''\n\t\t},\n\n\t\t/** Whether the text has any non-whitespace content. */\n\t\thasText() {\n\t\t\treturn this.text.trim() !== ''\n\t\t},\n\n\t\t/** Resolved content mode — only `'markdown'` switches the branch. */\n\t\tcontentMode() {\n\t\t\treturn this.content?.contentMode === 'markdown' ? 'markdown' : 'html'\n\t\t},\n\n\t\t/** CSS class selecting the markdown vs html content-spacing rules. */\n\t\tcontentClass() {\n\t\t\treturn this.contentMode === 'markdown'\n\t\t\t\t? 'cn-text-widget__content--markdown'\n\t\t\t\t: 'cn-text-widget__content--html'\n\t\t},\n\n\t\t/**\n\t\t * The HTML to render: either the raw author HTML (sanitised) or the\n\t\t * markdown rendering of the author text (sanitised). Both branches go\n\t\t * through DOMPurify, which strips `<script>`, `<style>`, `<link>`,\n\t\t * `on*` attributes, and `javascript:` URLs.\n\t\t *\n\t\t * @return {string} sanitised HTML safe for `v-html` injection.\n\t\t */\n\t\tsanitizedHtml() {\n\t\t\tlet html\n\t\t\tif (this.contentMode === 'markdown') {\n\t\t\t\ttry {\n\t\t\t\t\thtml = markedInstance.parse(this.text)\n\t\t\t\t\tif (typeof html !== 'string') {\n\t\t\t\t\t\thtml = String(html)\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Defensive: fall back to the raw text rather than crash\n\t\t\t\t\t// the dashboard if the parser throws.\n\t\t\t\t\thtml = this.text\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\thtml = this.text\n\t\t\t}\n\t\t\treturn DOMPurify.sanitize(html, {\n\t\t\t\tADD_ATTR: ['target'],\n\t\t\t})\n\t\t},\n\n\t\t/** Localised placeholder shown when there is no text content. */\n\t\tplaceholderText() {\n\t\t\treturn t('nextcloud-vue', 'No text content')\n\t\t},\n\n\t\t/** Localised placeholder shown inside an empty table cell. */\n\t\temptyCellPlaceholder() {\n\t\t\treturn t('nextcloud-vue', 'Empty cell')\n\t\t},\n\n\t\t/** Resolved font size (default `14px`). */\n\t\tfontSize() {\n\t\t\treturn this.content?.fontSize || '14px'\n\t\t},\n\n\t\t/** Resolved text colour (default theme main text). */\n\t\tcolor() {\n\t\t\treturn this.content?.color || 'var(--color-main-text)'\n\t\t},\n\n\t\t/** Resolved background colour (default transparent). */\n\t\tbackgroundColor() {\n\t\t\treturn this.content?.backgroundColor || 'transparent'\n\t\t},\n\n\t\t/** Resolved text alignment (default `left`). */\n\t\ttextAlign() {\n\t\t\treturn this.content?.textAlign || 'left'\n\t\t},\n\n\t\t/** Whether the renderer is in table mode. */\n\t\ttableMode() {\n\t\t\treturn this.content?.tableMode === true\n\t\t},\n\n\t\t/** The persisted tableData object, or null. */\n\t\ttableData() {\n\t\t\treturn this.content?.tableData || null\n\t\t},\n\n\t\t/** The table rows array (empty when no table data). */\n\t\ttableRows() {\n\t\t\treturn Array.isArray(this.tableData?.rows) ? this.tableData.rows : []\n\t\t},\n\n\t\t/** Whether row 0 should render as a header row. */\n\t\theaderRow() {\n\t\t\treturn this.tableData?.headerRow === true\n\t\t},\n\n\t\t/** Per-column text-alignment array. */\n\t\tcolumnAlignments() {\n\t\t\tconst aligns = this.tableData?.columnAlignments\n\t\t\treturn Array.isArray(aligns) ? aligns : []\n\t\t},\n\n\t\t/** Inline style for the flex wrapper. */\n\t\twrapperStyle() {\n\t\t\treturn {\n\t\t\t\twidth: '100%',\n\t\t\t\theight: '100%',\n\t\t\t\tpadding: '16px',\n\t\t\t\tdisplay: 'flex',\n\t\t\t\t'align-items': 'center',\n\t\t\t\t'justify-content': 'center',\n\t\t\t\toverflow: 'auto',\n\t\t\t\t'background-color': this.backgroundColor,\n\t\t\t}\n\t\t},\n\n\t\t/** Inline style for the text / placeholder content. */\n\t\tcontentStyle() {\n\t\t\tconst base = {\n\t\t\t\t'font-size': this.fontSize,\n\t\t\t\t'text-align': this.textAlign,\n\t\t\t\tcolor: this.color,\n\t\t\t\twidth: '100%',\n\t\t\t\t'overflow-wrap': 'break-word',\n\t\t\t}\n\t\t\tif (!this.hasText) {\n\t\t\t\tbase['font-style'] = 'italic'\n\t\t\t\tbase.color = 'var(--color-text-maxcontrast)'\n\t\t\t}\n\t\t\treturn base\n\t\t},\n\n\t\t/** Inline style for the table container. */\n\t\ttableContainerStyle() {\n\t\t\treturn {\n\t\t\t\t'font-size': this.fontSize,\n\t\t\t\tcolor: this.color,\n\t\t\t\twidth: '100%',\n\t\t\t}\n\t\t},\n\t},\n\n\tcreated() {\n\t\tensureDomPurifyHook()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Run cell text through DOMPurify so the table-mode render path has\n\t\t * the same XSS-stripping guarantees as the text-mode path.\n\t\t *\n\t\t * @param {string} text raw cell text (may contain HTML).\n\t\t * @return {string} sanitised HTML safe to render via `v-html`.\n\t\t */\n\t\tsanitizeCell(text) {\n\t\t\treturn DOMPurify.sanitize(typeof text === 'string' ? text : '')\n\t\t},\n\n\t\t/**\n\t\t * Whether the given row index renders as `<th>` (header row).\n\t\t *\n\t\t * @param {number} rIdx the 0-based row index.\n\t\t * @return {boolean} true when row 0 and the header flag is set.\n\t\t */\n\t\tisHeaderCell(rIdx) {\n\t\t\treturn this.headerRow && rIdx === 0\n\t\t},\n\n\t\t/**\n\t\t * Whether a cell is covered by another cell's `rowSpan` / `colSpan`\n\t\t * and so must be hidden to keep the grid rectangular.\n\t\t *\n\t\t * @param {number} rIdx the row index.\n\t\t * @param {number} cIdx the column index.\n\t\t * @return {boolean} true when the cell is a covered placeholder.\n\t\t */\n\t\tisPlaceholderCellAt(rIdx, cIdx) {\n\t\t\treturn isPlaceholderCell(this.tableRows, rIdx, cIdx)\n\t\t},\n\n\t\t/**\n\t\t * Whether a cell has any non-whitespace text.\n\t\t *\n\t\t * @param {object} cell the cell object.\n\t\t * @return {boolean} true when the cell has text.\n\t\t */\n\t\tcellHasText(cell) {\n\t\t\treturn typeof cell?.text === 'string' && cell.text.trim() !== ''\n\t\t},\n\n\t\t/**\n\t\t * Inline style for a data cell at a given column.\n\t\t *\n\t\t * @param {number} cIdx the column index.\n\t\t * @return {object} the cell style object.\n\t\t */\n\t\tdataCellStyle(cIdx) {\n\t\t\treturn {\n\t\t\t\t'text-align': this.columnAlignments[cIdx] || 'left',\n\t\t\t\tborder: '1px solid var(--color-border)',\n\t\t\t\tpadding: '8px',\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Inline style for a header cell at a given column.\n\t\t *\n\t\t * @param {number} cIdx the column index.\n\t\t * @return {object} the header cell style object.\n\t\t */\n\t\theaderCellStyle(cIdx) {\n\t\t\treturn {\n\t\t\t\t...this.dataCellStyle(cIdx),\n\t\t\t\t'background-color': 'var(--color-background-hover)',\n\t\t\t\t'font-weight': 'bold',\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-text-widget {\n\twidth: 100%;\n\theight: 100%;\n}\n\n.cn-text-widget__content,\n.cn-text-widget__placeholder {\n\t/* Safety net — ensures long URLs / words inside the sanitised HTML wrap\n\t rather than overflowing horizontally. */\n\toverflow-wrap: break-word;\n\tmax-width: 100%;\n}\n\n/* Restore inline-emphasis semantics inside rendered content. Nextcloud's\n server CSS styles bare <em> as muted (var(--color-text-maxcontrast)) and can\n drop the italic, so `_x_` / `**_x_**` looked greyed-out and non-italic. Force\n italic <em> / bold <strong> that inherit the widget's own text colour. */\n.cn-text-widget :deep(em),\n.cn-text-widget :deep(i) {\n\tfont-style: italic;\n\tcolor: inherit;\n}\n\n.cn-text-widget :deep(strong),\n.cn-text-widget :deep(b) {\n\tfont-weight: bold;\n\tcolor: inherit;\n}\n\n/* Nextcloud's server CSS strips list markers/indent (`list-style: none`,\n no padding), so rendered `<ul>`/`<ol>` looked like plain text. Restore\n bullets/numbers and indentation, including nested levels. */\n.cn-text-widget :deep(ul),\n.cn-text-widget :deep(ol) {\n\tmargin: 4px 0;\n\tpadding-left: 1.5em;\n}\n\n.cn-text-widget :deep(ul) {\n\tlist-style: disc outside;\n}\n\n.cn-text-widget :deep(ol) {\n\tlist-style: decimal outside;\n}\n\n.cn-text-widget :deep(ul ul) {\n\tlist-style-type: circle;\n}\n\n.cn-text-widget :deep(ul ul ul) {\n\tlist-style-type: square;\n}\n\n.cn-text-widget :deep(li) {\n\tdisplay: list-item;\n}\n\n/* Nextcloud's server CSS flattens heading sizes (e.g. h1 → 100%, so it fell\n back to the ~14px body size while h2 stayed 1.8em), leaving them oddly\n scaled. Apply a consistent em-based scale that grows with the widget's own\n font size. */\n.cn-text-widget :deep(h1) {\n\tfont-size: 2em;\n}\n\n.cn-text-widget :deep(h2) {\n\tfont-size: 1.5em;\n}\n\n.cn-text-widget :deep(h3) {\n\tfont-size: 1.25em;\n}\n\n.cn-text-widget :deep(h4) {\n\tfont-size: 1.1em;\n}\n\n.cn-text-widget :deep(h5) {\n\tfont-size: 1em;\n}\n\n.cn-text-widget :deep(h6) {\n\tfont-size: 0.85em;\n}\n\n.cn-text-widget__content--markdown :deep(h1),\n.cn-text-widget__content--markdown :deep(h2),\n.cn-text-widget__content--markdown :deep(h3),\n.cn-text-widget__content--markdown :deep(h4),\n.cn-text-widget__content--markdown :deep(h5),\n.cn-text-widget__content--markdown :deep(h6) {\n\tmargin: 8px 0 4px;\n\tfont-weight: 600;\n\tline-height: 1.3;\n}\n\n.cn-text-widget__content--markdown :deep(p),\n.cn-text-widget__content--markdown :deep(ul),\n.cn-text-widget__content--markdown :deep(ol),\n.cn-text-widget__content--markdown :deep(blockquote) {\n\tmargin: 4px 0;\n}\n\n.cn-text-widget__content--markdown :deep(code) {\n\tpadding: 1px 4px;\n\tborder-radius: 3px;\n\tbackground: var(--color-background-hover);\n\tfont-family: var(--font-monospace, monospace);\n\tfont-size: 0.95em;\n}\n\n.cn-text-widget__content--markdown :deep(pre) {\n\tpadding: 8px;\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-background-hover);\n\toverflow: auto;\n}\n\n.cn-text-widget__content--markdown :deep(pre) :deep(code) {\n\tpadding: 0;\n\tbackground: transparent;\n}\n\n.cn-text-widget__content--markdown :deep(blockquote) {\n\tpadding-left: 8px;\n\tborder-left: 3px solid var(--color-border);\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-text-widget__content--markdown :deep(table) {\n\tborder-collapse: collapse;\n\tmargin: 4px 0;\n}\n\n.cn-text-widget__content--markdown :deep(th),\n.cn-text-widget__content--markdown :deep(td) {\n\tpadding: 4px 8px;\n\tborder: 1px solid var(--color-border);\n}\n\n.cn-text-widget__table {\n\tborder-collapse: collapse;\n\twidth: 100%;\n}\n\n.cn-text-widget__table th,\n.cn-text-widget__table td {\n\toverflow-wrap: break-word;\n\tvertical-align: top;\n}\n\n.cn-text-widget__cell-placeholder {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-style: italic;\n}\n</style>\n"],"names":["_createElementBlock","_createElementVNode","_Fragment","_renderList","_normalizeStyle","_toDisplayString","_normalizeClass"],"mappings":";;;;;;AA+BQ,EAAA,KAAI,EAAA,kCAAA;;;;;;AAgBJ,EAAA,KAAA,EAAA,kCAAA;;;;;uBApCLA;AAAG,IAAA,KAAG,EAAA,gBAAA;AAAA,IAAA,KAAA,iBAAA,QAAA,CAAA,YAAA,CAAA;;KAEA,QAAS,CAAA,SAAA;wCAyCR,CAAA,OAAA,EAAA;;AAxCJ,UAAA,KAAE,EAAA,uBAAA;eACC,iBAAE,QAAmB,CAAA,mBAAA,CAAA;;UACvBC,kBAqCI,CAAA,OAAA,EAAA,IAAA,EAAA;iDADFC,QAnCqB,EAAA,IAAA,EAAAC,UAAA,CAAA,QAAA,CAAA,SAAE,EAAhB,CAAA,GAAG,EAAE,IAAI,KAAA;qDAmChB,CAAA,IAAA,EAAA;AAnCuB,gBAAA,GAAA,OAAA,IAAA,CAAA,CAAA;;qDAkChBD,QAjCkB,EAAA,IAAA,EAAAC,UAAA,CAAA,GAAA,EAAX,CAAA,IAAI,EAAC,IAAK,KAAA;;AAEpB,oBAAA,CAAA,QAAA,CAAA,YAAY,CAAC,IAAI,CAAA;uEAcnB,CAAA,IAAA,EAAA;0BAZH,GAAG,EAAA,CAAA,EAAA,EAAO,IAAI,CAAA,CAAA,EAAI,IAAI,CAAA,CAAA;0BACtB,OAAO,EAAE,IAAI,CAAC,OAAO,IAAA,CAAA;0BACrB,OAAG,EAAA,IAAA,CAAA,OAAA,IAAA,CAAA;0BACH,KAAK,EAAAC,cAAA,CAAA,QAAA,CAAA,eAAA,CAAA,IAAA,CAAA,CAAA;;AAEC,0BAAA,CAAA,QAAA,CAAA,WAAW,CAAC,IAAI,CAAA;4CADtBJ,kBAEK,CAAA,MAAA,EAAA;;AAAL,gCAAA,SAAK,EAAA,QAAA,CAAA,YAAA,CAAA,IAAA,CAAA,IAAA,CAAA;;4CACNA,kBAIO,CAAA,MAAA,EAJP,UAIO,EAAAK,eAAA,CADH,QAAoB,CAAA,oBAAA,CAAA,EAAA,CAAA,YAAA,CAAA;;mCAXpB,QAAA,CAAA,mBAAA,CAAA,IAAA,EAAA,IAAA,CAAA,CAAA;;uEA6BN,CAAA,IAAA,EAAA;0BAZG,GAAG,EAAA,CAAA,EAAA,EAAO,IAAI,CAAA,CAAA,EAAI,IAAI,CAAA,CAAA;0BACtB,OAAO,EAAE,IAAI,CAAC,OAAO,IAAA,CAAA;0BACrB,OAAG,EAAA,IAAA,CAAA,OAAA,IAAA,CAAA;0BACH,KAAK,EAAAD,cAAA,CAAA,QAAA,CAAA,aAAA,CAAA,IAAA,CAAA,CAAA;;AAEC,0BAAA,CAAA,QAAA,CAAA,WAAW,CAAC,IAAI,CAAA;4CADtBJ,kBAEK,CAAA,MAAA,EAAA;;AAAL,gCAAA,SAAK,EAAA,QAAA,CAAA,YAAA,CAAA,IAAA,CAAA,IAAA,CAAA;;4CACNA,kBAIE,CAAA,MAAA,EAJF,UAIE,EAAAK,eAAA,CADC,QAAA,CAAA,oBAAA,CAAA,EAAA,CAAA,YAAA,CAAA;;mCAXC,QAAA,CAAA,mBAAA,CAAA,IAAA,EAAA,IAAA,CAAA,CAAA;;;;;;;;SAmBL,QAAA,CAAA,OAAA;wBADHL,kBAKoB,CAAA,KAAA,EAAA;;YAHpB,KAAK,EAAAM,cAAA,CAAA,CAAA,yBAAA,EACG,QAAY,CAAA,YAAA,CAAA,CAAA;AACnB,YAAA,KAAK,iBAAE,QAAY,CAAA,YAAA,CAAA;AACpB,YAAA,SAAoB,EAAZ,QAAY,CAAA,aAAA;;wBACrBN;;;;;;;;;;"}
1
+ {"version":3,"file":"CnTextWidget.vue3.js","sources":["../../../../src/components/CnTextWidget/CnTextWidget.vue?vue&type=template&id=f00a6820&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-text-widget\" :style=\"wrapperStyle\">\n\t\t<table\n\t\t\tv-if=\"tableMode\"\n\t\t\tclass=\"cn-text-widget__table\"\n\t\t\t:style=\"tableContainerStyle\">\n\t\t\t<tbody>\n\t\t\t\t<tr v-for=\"(row, rIdx) in tableRows\" :key=\"`r-${rIdx}`\">\n\t\t\t\t\t<template v-for=\"(cell, cIdx) in row\">\n\t\t\t\t\t\t<th\n\t\t\t\t\t\t\tv-if=\"isHeaderCell(rIdx)\"\n\t\t\t\t\t\t\tv-show=\"!isPlaceholderCellAt(rIdx, cIdx)\"\n\t\t\t\t\t\t\t:key=\"`c-${rIdx}-${cIdx}`\"\n\t\t\t\t\t\t\t:rowspan=\"cell.rowSpan || 1\"\n\t\t\t\t\t\t\t:colspan=\"cell.colSpan || 1\"\n\t\t\t\t\t\t\t:style=\"headerCellStyle(cIdx)\">\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tv-if=\"cellHasText(cell)\"\n\t\t\t\t\t\t\t\tv-html=\"sanitizeCell(cell.text)\" /><!-- eslint-disable-line vue/no-v-html -->\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tv-else\n\t\t\t\t\t\t\t\tclass=\"cn-text-widget__cell-placeholder\">\n\t\t\t\t\t\t\t\t{{ emptyCellPlaceholder }}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</th>\n\t\t\t\t\t\t<td\n\t\t\t\t\t\t\tv-else\n\t\t\t\t\t\t\tv-show=\"!isPlaceholderCellAt(rIdx, cIdx)\"\n\t\t\t\t\t\t\t:key=\"`c-${rIdx}-${cIdx}`\"\n\t\t\t\t\t\t\t:rowspan=\"cell.rowSpan || 1\"\n\t\t\t\t\t\t\t:colspan=\"cell.colSpan || 1\"\n\t\t\t\t\t\t\t:style=\"dataCellStyle(cIdx)\">\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tv-if=\"cellHasText(cell)\"\n\t\t\t\t\t\t\t\tv-html=\"sanitizeCell(cell.text)\" /><!-- eslint-disable-line vue/no-v-html -->\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tv-else\n\t\t\t\t\t\t\t\tclass=\"cn-text-widget__cell-placeholder\">\n\t\t\t\t\t\t\t\t{{ emptyCellPlaceholder }}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</td>\n\t\t\t\t\t</template>\n\t\t\t\t</tr>\n\t\t\t</tbody>\n\t\t</table>\n\t\t<div\n\t\t\tv-else-if=\"hasText\"\n\t\t\tclass=\"cn-text-widget__content\"\n\t\t\t:class=\"contentClass\"\n\t\t\t:style=\"contentStyle\"\n\t\t\tv-html=\"sanitizedHtml\" /><!-- eslint-disable-line vue/no-v-html -->\n\t\t<span\n\t\t\tv-else\n\t\t\tclass=\"cn-text-widget__placeholder\"\n\t\t\t:style=\"contentStyle\">\n\t\t\t{{ placeholderText }}\n\t\t</span>\n\t\t<a\n\t\t\tv-if=\"link\"\n\t\t\tclass=\"cn-text-widget__link\"\n\t\t\tdata-testid=\"cn-text-widget-link\"\n\t\t\t:href=\"link.url\"\n\t\t\t:target=\"link.external ? '_blank' : null\"\n\t\t\t:rel=\"link.external ? 'noopener noreferrer' : null\"\n\t\t\t@click=\"openLink\">\n\t\t\t{{ link.label }}\n\t\t\t<ChevronRight :size=\"16\" />\n\t\t</a>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport DOMPurify from 'dompurify'\nimport { Marked } from 'marked'\nimport ChevronRight from 'vue-material-design-icons/ChevronRight.vue'\nimport { followLinkClick, resolveHref } from '../../utils/linkNavigation.js'\nimport { isPlaceholderCell } from '../../utils/textTable.js'\n\n// Per-module `Marked` instance so configuration is scoped here and does NOT\n// mutate the global `marked` singleton (which would affect any other code in\n// the same app that imports `marked`). `gfm: true` enables tables; `breaks:\n// false` keeps CommonMark-strict newline behaviour.\nconst markedInstance = new Marked({\n\tgfm: true,\n\tbreaks: false,\n})\n\n// DOMPurify allow-list hook: author-supplied `target=_blank` links survive\n// sanitisation (we add `target` to ADD_ATTR below); the hook then appends\n// `rel=\"noopener noreferrer\"` to mitigate reverse-tabnabbing. The hook is\n// idempotent and registered once.\nlet hookRegistered = false\nfunction ensureDomPurifyHook() {\n\tif (hookRegistered) {\n\t\treturn\n\t}\n\tDOMPurify.addHook('afterSanitizeAttributes', (node) => {\n\t\tif (node.tagName === 'A' && node.getAttribute('target') === '_blank') {\n\t\t\tnode.setAttribute('rel', 'noopener noreferrer')\n\t\t}\n\t})\n\thookRegistered = true\n}\n\n/**\n * CnTextWidget — renders user-authored multi-line text (or a small table)\n * inside a dashboard cell. Text content is passed through DOMPurify before\n * injection via `v-html` so common formatting tags survive while XSS vectors\n * (`<script>`, `on*` attributes, `javascript:` URLs) are stripped.\n *\n * Persisted shape: `{text, fontSize, color, backgroundColor, textAlign,\n * contentMode, tableMode, tableData}`. Defaults: `fontSize='14px'`,\n * `color='var(--color-main-text)'`, `backgroundColor='transparent'`,\n * `textAlign='left'`. `contentMode` absent means the legacy `'html'` branch;\n * the literal `'markdown'` parses via marked (GFM tables) then sanitises\n * through the same allow-list — a single trust boundary protects both modes.\n *\n * When `tableMode === true`, the renderer draws an HTML `<table>` from\n * `tableData` instead: header rows promote row 0 to `<th>`, per-column\n * alignment is applied inline, and `colSpan` / `rowSpan` become native HTML\n * attributes. Merge-swallowed cells are hidden via `v-show` so the visual\n * grid stays rectangular.\n *\n * Registered as the `text` dashboard widget type via the renderer's\n * `index.js`.\n */\nexport default {\n\tname: 'CnTextWidget',\n\n\tcomponents: { ChevronRight },\n\n\tinject: {\n\t\t/** The app's translate function, provided by CnAppRoot. Identity outside one. */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Persisted widget content blob: `{text, fontSize, color,\n\t\t * backgroundColor, textAlign, contentMode, tableMode, tableData, link}`.\n\t\t * All keys are optional; missing keys collapse to documented defaults.\n\t\t * `text` and `link.label` go through the app's translate function.\n\t\t * `link` is `{label, route}` (a page id, or a path starting with `/`) or\n\t\t * `{label, href}` (an external URL), drawn under the text.\n\t\t *\n\t\t * @type {{text: string, fontSize: string, color: string, backgroundColor: string, textAlign: string, contentMode: string, tableMode: boolean, tableData: object, link: {label: string, route: string, href: string}}}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\t},\n\n\tcomputed: {\n\t\t/** The author text through the app's translate function, or '' when absent / non-string. */\n\t\ttext() {\n\t\t\treturn typeof this.content?.text === 'string' ? this.tr(this.content.text) : ''\n\t\t},\n\n\t\t/**\n\t\t * The link under the text, from `content.link`: `{ label, route }`\n\t\t * (a page id, or a router path when it starts with `/`) or\n\t\t * `{ label, href }` (an external URL, opened in a new tab). The label\n\t\t * goes through the app's translate function. Null without a label or\n\t\t * a target.\n\t\t *\n\t\t * @return {{label: string, url: string, location: (object|null), external: boolean}|null} The link.\n\t\t */\n\t\tlink() {\n\t\t\tconst link = this.content?.link\n\t\t\tif (!link || typeof link !== 'object' || typeof link.label !== 'string' || link.label === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (typeof link.route === 'string' && link.route !== '') {\n\t\t\t\tconst location = link.route.startsWith('/') ? { path: link.route } : { name: link.route }\n\t\t\t\tconst url = resolveHref(location, this.$router)\n\t\t\t\treturn url ? { label: this.tr(link.label), url, location, external: false } : null\n\t\t\t}\n\t\t\tif (typeof link.href === 'string' && link.href !== '') {\n\t\t\t\treturn { label: this.tr(link.label), url: link.href, location: null, external: true }\n\t\t\t}\n\t\t\treturn null\n\t\t},\n\n\t\t/** Whether the text has any non-whitespace content. */\n\t\thasText() {\n\t\t\treturn this.text.trim() !== ''\n\t\t},\n\n\t\t/** Resolved content mode — only `'markdown'` switches the branch. */\n\t\tcontentMode() {\n\t\t\treturn this.content?.contentMode === 'markdown' ? 'markdown' : 'html'\n\t\t},\n\n\t\t/** CSS class selecting the markdown vs html content-spacing rules. */\n\t\tcontentClass() {\n\t\t\treturn this.contentMode === 'markdown'\n\t\t\t\t? 'cn-text-widget__content--markdown'\n\t\t\t\t: 'cn-text-widget__content--html'\n\t\t},\n\n\t\t/**\n\t\t * The HTML to render: either the raw author HTML (sanitised) or the\n\t\t * markdown rendering of the author text (sanitised). Both branches go\n\t\t * through DOMPurify, which strips `<script>`, `<style>`, `<link>`,\n\t\t * `on*` attributes, and `javascript:` URLs.\n\t\t *\n\t\t * @return {string} sanitised HTML safe for `v-html` injection.\n\t\t */\n\t\tsanitizedHtml() {\n\t\t\tlet html\n\t\t\tif (this.contentMode === 'markdown') {\n\t\t\t\ttry {\n\t\t\t\t\thtml = markedInstance.parse(this.text)\n\t\t\t\t\tif (typeof html !== 'string') {\n\t\t\t\t\t\thtml = String(html)\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Defensive: fall back to the raw text rather than crash\n\t\t\t\t\t// the dashboard if the parser throws.\n\t\t\t\t\thtml = this.text\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\thtml = this.text\n\t\t\t}\n\t\t\treturn DOMPurify.sanitize(html, {\n\t\t\t\tADD_ATTR: ['target'],\n\t\t\t})\n\t\t},\n\n\t\t/** Localised placeholder shown when there is no text content. */\n\t\tplaceholderText() {\n\t\t\treturn t('nextcloud-vue', 'No text content')\n\t\t},\n\n\t\t/** Localised placeholder shown inside an empty table cell. */\n\t\temptyCellPlaceholder() {\n\t\t\treturn t('nextcloud-vue', 'Empty cell')\n\t\t},\n\n\t\t/** Resolved font size (default `14px`). */\n\t\tfontSize() {\n\t\t\treturn this.content?.fontSize || '14px'\n\t\t},\n\n\t\t/** Resolved text colour (default theme main text). */\n\t\tcolor() {\n\t\t\treturn this.content?.color || 'var(--color-main-text)'\n\t\t},\n\n\t\t/** Resolved background colour (default transparent). */\n\t\tbackgroundColor() {\n\t\t\treturn this.content?.backgroundColor || 'transparent'\n\t\t},\n\n\t\t/** Resolved text alignment (default `left`). */\n\t\ttextAlign() {\n\t\t\treturn this.content?.textAlign || 'left'\n\t\t},\n\n\t\t/** Whether the renderer is in table mode. */\n\t\ttableMode() {\n\t\t\treturn this.content?.tableMode === true\n\t\t},\n\n\t\t/** The persisted tableData object, or null. */\n\t\ttableData() {\n\t\t\treturn this.content?.tableData || null\n\t\t},\n\n\t\t/** The table rows array (empty when no table data). */\n\t\ttableRows() {\n\t\t\treturn Array.isArray(this.tableData?.rows) ? this.tableData.rows : []\n\t\t},\n\n\t\t/** Whether row 0 should render as a header row. */\n\t\theaderRow() {\n\t\t\treturn this.tableData?.headerRow === true\n\t\t},\n\n\t\t/** Per-column text-alignment array. */\n\t\tcolumnAlignments() {\n\t\t\tconst aligns = this.tableData?.columnAlignments\n\t\t\treturn Array.isArray(aligns) ? aligns : []\n\t\t},\n\n\t\t/** Inline style for the flex wrapper. */\n\t\twrapperStyle() {\n\t\t\treturn {\n\t\t\t\twidth: '100%',\n\t\t\t\theight: '100%',\n\t\t\t\tpadding: '16px',\n\t\t\t\tdisplay: 'flex',\n\t\t\t\t'align-items': 'center',\n\t\t\t\t'justify-content': 'center',\n\t\t\t\toverflow: 'auto',\n\t\t\t\t'background-color': this.backgroundColor,\n\t\t\t\t...(this.link ? { 'flex-direction': 'column', gap: '8px', 'align-items': 'flex-start' } : {}),\n\t\t\t}\n\t\t},\n\n\t\t/** Inline style for the text / placeholder content. */\n\t\tcontentStyle() {\n\t\t\tconst base = {\n\t\t\t\t'font-size': this.fontSize,\n\t\t\t\t'text-align': this.textAlign,\n\t\t\t\tcolor: this.color,\n\t\t\t\twidth: '100%',\n\t\t\t\t'overflow-wrap': 'break-word',\n\t\t\t}\n\t\t\tif (!this.hasText) {\n\t\t\t\tbase['font-style'] = 'italic'\n\t\t\t\tbase.color = 'var(--color-text-maxcontrast)'\n\t\t\t}\n\t\t\treturn base\n\t\t},\n\n\t\t/** Inline style for the table container. */\n\t\ttableContainerStyle() {\n\t\t\treturn {\n\t\t\t\t'font-size': this.fontSize,\n\t\t\t\tcolor: this.color,\n\t\t\t\twidth: '100%',\n\t\t\t}\n\t\t},\n\t},\n\n\tcreated() {\n\t\tensureDomPurifyHook()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * A manifest string through the app's translate function.\n\t\t *\n\t\t * @param {string} text The string.\n\t\t * @return {string} The translated string.\n\t\t */\n\t\ttr(text) {\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text\n\t\t},\n\n\t\t/**\n\t\t * Route a plain click on an in-app link, so the page does not reload.\n\t\t *\n\t\t * @param {MouseEvent} event The click.\n\t\t */\n\t\topenLink(event) {\n\t\t\tif (this.link && !this.link.external) {\n\t\t\t\tfollowLinkClick(event, this.link.location, this.$router)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Run cell text through DOMPurify so the table-mode render path has\n\t\t * the same XSS-stripping guarantees as the text-mode path.\n\t\t *\n\t\t * @param {string} text raw cell text (may contain HTML).\n\t\t * @return {string} sanitised HTML safe to render via `v-html`.\n\t\t */\n\t\tsanitizeCell(text) {\n\t\t\treturn DOMPurify.sanitize(typeof text === 'string' ? text : '')\n\t\t},\n\n\t\t/**\n\t\t * Whether the given row index renders as `<th>` (header row).\n\t\t *\n\t\t * @param {number} rIdx the 0-based row index.\n\t\t * @return {boolean} true when row 0 and the header flag is set.\n\t\t */\n\t\tisHeaderCell(rIdx) {\n\t\t\treturn this.headerRow && rIdx === 0\n\t\t},\n\n\t\t/**\n\t\t * Whether a cell is covered by another cell's `rowSpan` / `colSpan`\n\t\t * and so must be hidden to keep the grid rectangular.\n\t\t *\n\t\t * @param {number} rIdx the row index.\n\t\t * @param {number} cIdx the column index.\n\t\t * @return {boolean} true when the cell is a covered placeholder.\n\t\t */\n\t\tisPlaceholderCellAt(rIdx, cIdx) {\n\t\t\treturn isPlaceholderCell(this.tableRows, rIdx, cIdx)\n\t\t},\n\n\t\t/**\n\t\t * Whether a cell has any non-whitespace text.\n\t\t *\n\t\t * @param {object} cell the cell object.\n\t\t * @return {boolean} true when the cell has text.\n\t\t */\n\t\tcellHasText(cell) {\n\t\t\treturn typeof cell?.text === 'string' && cell.text.trim() !== ''\n\t\t},\n\n\t\t/**\n\t\t * Inline style for a data cell at a given column.\n\t\t *\n\t\t * @param {number} cIdx the column index.\n\t\t * @return {object} the cell style object.\n\t\t */\n\t\tdataCellStyle(cIdx) {\n\t\t\treturn {\n\t\t\t\t'text-align': this.columnAlignments[cIdx] || 'left',\n\t\t\t\tborder: '1px solid var(--color-border)',\n\t\t\t\tpadding: '8px',\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Inline style for a header cell at a given column.\n\t\t *\n\t\t * @param {number} cIdx the column index.\n\t\t * @return {object} the header cell style object.\n\t\t */\n\t\theaderCellStyle(cIdx) {\n\t\t\treturn {\n\t\t\t\t...this.dataCellStyle(cIdx),\n\t\t\t\t'background-color': 'var(--color-background-hover)',\n\t\t\t\t'font-weight': 'bold',\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-text-widget {\n\twidth: 100%;\n\theight: 100%;\n}\n\n.cn-text-widget__content,\n.cn-text-widget__placeholder {\n\t/* Safety net — ensures long URLs / words inside the sanitised HTML wrap\n\t rather than overflowing horizontally. */\n\toverflow-wrap: break-word;\n\tmax-width: 100%;\n}\n\n/* Restore inline-emphasis semantics inside rendered content. Nextcloud's\n server CSS styles bare <em> as muted (var(--color-text-maxcontrast)) and can\n drop the italic, so `_x_` / `**_x_**` looked greyed-out and non-italic. Force\n italic <em> / bold <strong> that inherit the widget's own text colour. */\n.cn-text-widget :deep(em),\n.cn-text-widget :deep(i) {\n\tfont-style: italic;\n\tcolor: inherit;\n}\n\n.cn-text-widget :deep(strong),\n.cn-text-widget :deep(b) {\n\tfont-weight: bold;\n\tcolor: inherit;\n}\n\n/* Nextcloud's server CSS strips list markers/indent (`list-style: none`,\n no padding), so rendered `<ul>`/`<ol>` looked like plain text. Restore\n bullets/numbers and indentation, including nested levels. */\n.cn-text-widget :deep(ul),\n.cn-text-widget :deep(ol) {\n\tmargin: 4px 0;\n\tpadding-left: 1.5em;\n}\n\n.cn-text-widget :deep(ul) {\n\tlist-style: disc outside;\n}\n\n.cn-text-widget :deep(ol) {\n\tlist-style: decimal outside;\n}\n\n.cn-text-widget :deep(ul ul) {\n\tlist-style-type: circle;\n}\n\n.cn-text-widget :deep(ul ul ul) {\n\tlist-style-type: square;\n}\n\n.cn-text-widget :deep(li) {\n\tdisplay: list-item;\n}\n\n/* Nextcloud's server CSS flattens heading sizes (e.g. h1 → 100%, so it fell\n back to the ~14px body size while h2 stayed 1.8em), leaving them oddly\n scaled. Apply a consistent em-based scale that grows with the widget's own\n font size. */\n.cn-text-widget :deep(h1) {\n\tfont-size: 2em;\n}\n\n.cn-text-widget :deep(h2) {\n\tfont-size: 1.5em;\n}\n\n.cn-text-widget :deep(h3) {\n\tfont-size: 1.25em;\n}\n\n.cn-text-widget :deep(h4) {\n\tfont-size: 1.1em;\n}\n\n.cn-text-widget :deep(h5) {\n\tfont-size: 1em;\n}\n\n.cn-text-widget :deep(h6) {\n\tfont-size: 0.85em;\n}\n\n.cn-text-widget__content--markdown :deep(h1),\n.cn-text-widget__content--markdown :deep(h2),\n.cn-text-widget__content--markdown :deep(h3),\n.cn-text-widget__content--markdown :deep(h4),\n.cn-text-widget__content--markdown :deep(h5),\n.cn-text-widget__content--markdown :deep(h6) {\n\tmargin: 8px 0 4px;\n\tfont-weight: 600;\n\tline-height: 1.3;\n}\n\n.cn-text-widget__content--markdown :deep(p),\n.cn-text-widget__content--markdown :deep(ul),\n.cn-text-widget__content--markdown :deep(ol),\n.cn-text-widget__content--markdown :deep(blockquote) {\n\tmargin: 4px 0;\n}\n\n.cn-text-widget__content--markdown :deep(code) {\n\tpadding: 1px 4px;\n\tborder-radius: 3px;\n\tbackground: var(--color-background-hover);\n\tfont-family: var(--font-monospace, monospace);\n\tfont-size: 0.95em;\n}\n\n.cn-text-widget__content--markdown :deep(pre) {\n\tpadding: 8px;\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-background-hover);\n\toverflow: auto;\n}\n\n.cn-text-widget__content--markdown :deep(pre) :deep(code) {\n\tpadding: 0;\n\tbackground: transparent;\n}\n\n.cn-text-widget__content--markdown :deep(blockquote) {\n\tpadding-left: 8px;\n\tborder-left: 3px solid var(--color-border);\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-text-widget__content--markdown :deep(table) {\n\tborder-collapse: collapse;\n\tmargin: 4px 0;\n}\n\n.cn-text-widget__content--markdown :deep(th),\n.cn-text-widget__content--markdown :deep(td) {\n\tpadding: 4px 8px;\n\tborder: 1px solid var(--color-border);\n}\n\n.cn-text-widget__table {\n\tborder-collapse: collapse;\n\twidth: 100%;\n}\n\n.cn-text-widget__table th,\n.cn-text-widget__table td {\n\toverflow-wrap: break-word;\n\tvertical-align: top;\n}\n\n.cn-text-widget__cell-placeholder {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-style: italic;\n}\n\n.cn-text-widget__link {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 2px;\n\tfont-size: 14px;\n\tfont-weight: 600;\n\tcolor: var(--color-primary-element);\n\ttext-decoration: none;\n}\n\n.cn-text-widget__link:hover,\n.cn-text-widget__link:focus-visible {\n\ttext-decoration: underline;\n}\n</style>\n"],"names":["_createElementBlock","_createElementVNode","_Fragment","_renderList","_normalizeStyle","_toDisplayString","_normalizeClass"],"mappings":";;;;;;AA+BQ,EAAA,KAAI,EAAA,kCAAA;;;;;;AAgBJ,EAAA,KAAA,EAAA,kCAAA;;;;;;;;uBApCLA;AAAG,IAAA,KAAG,EAAA,gBAAA;AAAA,IAAA,KAAA,iBAAA,QAAA,CAAA,YAAA,CAAA;;KAEA,QAAS,CAAA,SAAA;wCAyCR,CAAA,OAAA,EAAA;;AAxCJ,UAAA,KAAE,EAAA,uBAAA;eACC,iBAAE,QAAmB,CAAA,mBAAA,CAAA;;UACvBC,kBAqCI,CAAA,OAAA,EAAA,IAAA,EAAA;iDADFC,QAnCqB,EAAA,IAAA,EAAAC,UAAA,CAAA,QAAA,CAAA,SAAE,EAAhB,CAAA,GAAG,EAAE,IAAI,KAAA;qDAmChB,CAAA,IAAA,EAAA;AAnCuB,gBAAA,GAAA,OAAA,IAAA,CAAA,CAAA;;qDAkChBD,QAjCkB,EAAA,IAAA,EAAAC,UAAA,CAAA,GAAA,EAAX,CAAA,IAAI,EAAC,IAAK,KAAA;;AAEpB,oBAAA,CAAA,QAAA,CAAA,YAAY,CAAC,IAAI,CAAA;uEAcnB,CAAA,IAAA,EAAA;0BAZH,GAAG,EAAA,CAAA,EAAA,EAAO,IAAI,CAAA,CAAA,EAAI,IAAI,CAAA,CAAA;0BACtB,OAAO,EAAE,IAAI,CAAC,OAAO,IAAA,CAAA;0BACrB,OAAG,EAAA,IAAA,CAAA,OAAA,IAAA,CAAA;0BACH,KAAK,EAAAC,cAAA,CAAA,QAAA,CAAA,eAAA,CAAA,IAAA,CAAA,CAAA;;AAEC,0BAAA,CAAA,QAAA,CAAA,WAAW,CAAC,IAAI,CAAA;4CADtBJ,kBAEK,CAAA,MAAA,EAAA;;AAAL,gCAAA,SAAK,EAAA,QAAA,CAAA,YAAA,CAAA,IAAA,CAAA,IAAA,CAAA;;4CACNA,kBAIO,CAAA,MAAA,EAJP,UAIO,EAAAK,eAAA,CADH,QAAoB,CAAA,oBAAA,CAAA,EAAA,CAAA,YAAA,CAAA;;mCAXpB,QAAA,CAAA,mBAAA,CAAA,IAAA,EAAA,IAAA,CAAA,CAAA;;uEA6BN,CAAA,IAAA,EAAA;0BAZG,GAAG,EAAA,CAAA,EAAA,EAAO,IAAI,CAAA,CAAA,EAAI,IAAI,CAAA,CAAA;0BACtB,OAAO,EAAE,IAAI,CAAC,OAAO,IAAA,CAAA;0BACrB,OAAG,EAAA,IAAA,CAAA,OAAA,IAAA,CAAA;0BACH,KAAK,EAAAD,cAAA,CAAA,QAAA,CAAA,aAAA,CAAA,IAAA,CAAA,CAAA;;AAEC,0BAAA,CAAA,QAAA,CAAA,WAAW,CAAC,IAAI,CAAA;4CADtBJ,kBAEK,CAAA,MAAA,EAAA;;AAAL,gCAAA,SAAK,EAAA,QAAA,CAAA,YAAA,CAAA,IAAA,CAAA,IAAA,CAAA;;4CACNA,kBAIE,CAAA,MAAA,EAJF,UAIE,EAAAK,eAAA,CADC,QAAA,CAAA,oBAAA,CAAA,EAAA,CAAA,YAAA,CAAA;;mCAXC,QAAA,CAAA,mBAAA,CAAA,IAAA,EAAA,IAAA,CAAA,CAAA;;;;;;;;SAmBL,QAAA,CAAA,OAAA;wBADHL,kBAKoB,CAAA,KAAA,EAAA;;YAHpB,KAAK,EAAAM,cAAA,CAAA,CAAA,yBAAA,EACG,QAAY,CAAA,YAAA,CAAA,CAAA;AACnB,YAAA,KAAK,iBAAE,QAAY,CAAA,YAAA,CAAA;AACpB,YAAA,SAAoB,EAAZ,QAAY,CAAA,aAAA;;wBACrBN,kBAKO,CAAA,MAAA,EAAA;;AAHN,YAAA,KAAK,EAAC,6BAAI;AACT,YAAA,KAAK,iBAAE,QAAY,CAAA,YAAA,CAAA;6BACjB,QAAe,CAAA,eAAA,CAAA,EAAA,CAAA,mBAAA,CAAA;KAGZ,QAAI,CAAA,IAAA;sBADVA;;AAEA,UAAA,KAAK,EAAC,sBAAW;AACjB,UAAA,aAAW,EAAC,qBAAG;UACd,IAAI,EAAE,QAAI,CAAA,IAAA,CAAC,GAAG;UACd,MAAC,EAAA,QAAA,CAAA,IAAA,CAAA,QAAA,GAAA,QAAA,GAAA,IAAA;UACD,GAAE,EAAA,QAAA,CAAA,IAAA,CAAA,QAAA,GAAA,qBAAA,GAAA,IAAA;;;;;;;;;;;;"}
package/dist/esm/index.js CHANGED
@@ -117,6 +117,8 @@ export { default as CnDetailCard } from './components/CnDetailCard/CnDetailCard.
117
117
  export { default as CnDetailGrid } from './components/CnDetailGrid/CnDetailGrid.vue.js';
118
118
  export { default as CnDetailPage } from './components/CnDetailPage/CnDetailPage.vue.js';
119
119
  export { default as CnDetailWidgetHost } from './components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js';
120
+ export { default as CnDialog } from './components/CnDialog/CnDialog.vue.js';
121
+ export { default as CnDialogHeader } from './components/CnDialogHeader/CnDialogHeader.vue.js';
120
122
  export { default as CnDividerWidget } from './components/CnDividerWidget/CnDividerWidget.vue.js';
121
123
  export { default as CnDividerWidgetForm } from './components/CnDividerWidgetForm/CnDividerWidgetForm.vue.js';
122
124
  export { default as CnDocumentReviewList } from './components/CnDocumentReviewList/CnDocumentReviewList.vue.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -498,6 +498,7 @@ var translations = {
498
498
  "NC Activity is currently unavailable.": "NC Activity is currently unavailable.",
499
499
  "Could not load activity.": "Could not load activity.",
500
500
  Today: "Today",
501
+ "Today, {date}": "Today, {date}",
501
502
  "Last 8 hours": "Last 8 hours",
502
503
  "Last 24 hours": "Last 24 hours",
503
504
  "Last 7 days": "Last 7 days",
@@ -1472,7 +1473,27 @@ var translations = {
1472
1473
  "Link related object": "Link related object",
1473
1474
  "{type} parts": "{type} parts",
1474
1475
  "Cannot save: this record has no id, so the edit would create a duplicate instead of updating it.": "Cannot save: this record has no id, so the edit would create a duplicate instead of updating it.",
1475
- "Save failed": "Save failed"
1476
+ "Save failed": "Save failed",
1477
+ Modules: "Modules",
1478
+ "In the menu": "In the menu",
1479
+ Off: "Off",
1480
+ Pages: "Pages",
1481
+ "Open {module}": "Open {module}",
1482
+ "{list} and {last}": "{list} and {last}",
1483
+ "%n page: {pages}": [
1484
+ "%n page: {pages}",
1485
+ "%n pages: {pages}"
1486
+ ],
1487
+ "%n page, including {pages}": [
1488
+ "%n page, including {pages}",
1489
+ "%n pages, including {pages}"
1490
+ ],
1491
+ "{on} of %n is in the menu": [
1492
+ "{on} of %n is in the menu",
1493
+ "{on} of %n are in the menu"
1494
+ ],
1495
+ "Put it in the menu:": "Put it in the menu:",
1496
+ "Admin settings": "Admin settings"
1476
1497
  };
1477
1498
  var plurals = {
1478
1499
  "{count} selected": [
@@ -1 +1 @@
1
- {"version":3,"file":"en.json.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"en.json.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -520,6 +520,7 @@ var translations = {
520
520
  "NC Activity is currently unavailable.": "NC Activiteit is momenteel niet beschikbaar.",
521
521
  "Could not load activity.": "Kon activiteit niet laden.",
522
522
  Today: "Vandaag",
523
+ "Today, {date}": "Vandaag, {date}",
523
524
  "Last 8 hours": "Laatste 8 uur",
524
525
  "Last 24 hours": "Laatste 24 uur",
525
526
  "Last 7 days": "Laatste 7 dagen",
@@ -1659,7 +1660,26 @@ var translations = {
1659
1660
  "Link related object": "Gerelateerd object koppelen",
1660
1661
  "{type} parts": "Onderdelen van {type}",
1661
1662
  "Cannot save: this record has no id, so the edit would create a duplicate instead of updating it.": "Opslaan kan niet: dit record heeft geen id, dus de wijziging zou een kopie maken in plaats van het record bij te werken.",
1662
- "Save failed": "Opslaan is mislukt"
1663
+ "Save failed": "Opslaan is mislukt",
1664
+ Modules: "Modules",
1665
+ "In the menu": "In het menu",
1666
+ Off: "Uit",
1667
+ Pages: "Pagina's",
1668
+ "Open {module}": "{module} openen",
1669
+ "{list} and {last}": "{list} en {last}",
1670
+ "%n page: {pages}": [
1671
+ "%n pagina: {pages}",
1672
+ "%n pagina's: {pages}"
1673
+ ],
1674
+ "%n page, including {pages}": [
1675
+ "%n pagina, waaronder {pages}",
1676
+ "%n pagina's, waaronder {pages}"
1677
+ ],
1678
+ "{on} of %n is in the menu": [
1679
+ "{on} van %n staat in het menu",
1680
+ "{on} van %n staan in het menu"
1681
+ ],
1682
+ "Put it in the menu:": "In het menu zetten:"
1663
1683
  };
1664
1684
  var plurals = {
1665
1685
  "{count} selected": [
@@ -1 +1 @@
1
- {"version":3,"file":"nl.json.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"nl.json.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}