@conduction/nextcloud-vue 2.74.0 → 2.75.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.
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +19 -14
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +2 -2
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +77 -26
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +22 -14
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +23 -4
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnBrandBar/CnBrandBar.vue.js +9 -0
- package/dist/esm/components/CnBrandBar/CnBrandBar.vue.js.map +1 -0
- package/dist/esm/components/CnBrandBar/CnBrandBar.vue2.js +107 -0
- package/dist/esm/components/CnBrandBar/CnBrandBar.vue2.js.map +1 -0
- package/dist/esm/components/CnBrandBar/CnBrandBar.vue3.js +79 -0
- package/dist/esm/components/CnBrandBar/CnBrandBar.vue3.js.map +1 -0
- package/dist/esm/components/CnEnvironmentBanner/CnEnvironmentBanner.vue2.js +4 -1
- package/dist/esm/components/CnEnvironmentBanner/CnEnvironmentBanner.vue2.js.map +1 -1
- package/dist/esm/components/CnEnvironmentBanner/CnEnvironmentBanner.vue3.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +7 -0
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +14 -7
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +32 -0
- package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
- package/dist/esm/components/CnFormPage/CnFormPage.vue3.js +6 -3
- package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +2 -2
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js +18 -2
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js.map +1 -1
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js +2 -2
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js.map +1 -1
- package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue2.js +16 -0
- package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue2.js.map +1 -1
- package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue3.js +2 -2
- package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue3.js.map +1 -1
- package/dist/esm/directives/cnSelectAria.js +55 -0
- package/dist/esm/directives/cnSelectAria.js.map +1 -0
- package/dist/esm/nextcloud-vue.css +66 -1
- package/dist/esm/utils/cnFetch.js +17 -2
- package/dist/esm/utils/cnFetch.js.map +1 -1
- package/dist/esm/utils/resolveBrand.js +38 -0
- package/dist/esm/utils/resolveBrand.js.map +1 -0
- package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +8775 -8327
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +66 -1
- package/package.json +1 -1
- package/src/components/CnAppNav/CnAppNav.vue +21 -16
- package/src/components/CnAppRoot/CnAppRoot.vue +53 -0
- package/src/components/CnBannerWidget/CnBannerWidget.vue +23 -4
- package/src/components/CnBrandBar/CnBrandBar.vue +230 -0
- package/src/components/CnEnvironmentBanner/CnEnvironmentBanner.vue +4 -1
- package/src/components/CnFormDialog/CnFormDialog.vue +10 -0
- package/src/components/CnFormPage/CnFormPage.vue +33 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +2 -2
- package/src/components/CnQuickFilterBar/CnQuickFilterBar.vue +20 -4
- package/src/components/CnStatsBlock/CnStatsBlock.vue +18 -2
- package/src/css/form-field.css +1 -1
- package/src/directives/cnSelectAria.js +54 -0
- package/src/schemas/app-manifest-v2.schema.json +6 -1
- package/src/utils/cnFetch.js +16 -1
- package/src/utils/resolveBrand.js +37 -0
- package/src/utils/validateManifestV2.compiled.js +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnStatsBlock.vue3.js","sources":["../../../../src/components/CnStatsBlock/CnStatsBlock.vue?vue&type=template&id=722d9aef&lang.js"],"sourcesContent":["<!--\n - CnStatsBlock renders the CANONICAL KPI card (src/css/kpi-card.css) — the\n - same markup and the same stylesheet CnStatWidget renders, so OpenCatalogi's\n - KPIs and dossiq's are one design rather than two. This component's own\n - `cn-stats-block*` classes stay on the same elements for backwards\n - compatibility with app CSS that targets them; the LOOK comes from the\n - shared sheet.\n -\n - The comment lives OUT here rather than inside <template>: a comment node\n - beside the root element makes the component multi-root in Vue 3, and a\n - multi-root component has no root to put class attributes on — `rootClasses`\n - silently stopped reaching the DOM.\n-->\n<template>\n\t<component\n\t\t:is=\"componentTag\"\n\t\tclass=\"cn-kpi-card cn-stats-block\"\n\t\t:class=\"rootClasses\"\n\t\tv-bind=\"componentAttrs\"\n\t\t@click=\"onClick\">\n\t\t<!-- Icon -->\n\t\t<div v-if=\"hasIcon && !isStacked && !isBoardLook\" class=\"cn-kpi-card__icon cn-stats-block__icon\" :class=\"iconClasses\">\n\t\t\t<slot name=\"icon\">\n\t\t\t\t<component :is=\"icon\" v-if=\"icon\" :size=\"iconSize\" />\n\t\t\t</slot>\n\t\t</div>\n\n\t\t<!-- Content -->\n\t\t<div class=\"cn-kpi-card__body cn-stats-block__content\">\n\t\t\t<div class=\"cn-stats-block__header\">\n\t\t\t\t<!-- Board look: an 18px glyph before the label, only on a link tile. -->\n\t\t\t\t<span\n\t\t\t\t\tv-if=\"isBoardLook && isInteractive && hasIcon\"\n\t\t\t\t\tclass=\"cn-kpi-card__glyph cn-stats-block__glyph\"\n\t\t\t\t\tdata-testid=\"cn-stats-block-glyph\"\n\t\t\t\t\taria-hidden=\"true\">\n\t\t\t\t\t<slot name=\"icon\">\n\t\t\t\t\t\t<component :is=\"icon\" v-if=\"icon\" :size=\"18\" />\n\t\t\t\t\t</slot>\n\t\t\t\t</span>\n\t\t\t\t<h4 class=\"cn-kpi-card__title\" :title=\"title || undefined\">\n\t\t\t\t\t{{ title || t('nextcloud-vue', 'Objects') }}\n\t\t\t\t</h4>\n\t\t\t</div>\n\n\t\t\t<div v-if=\"hasError\" class=\"cn-stats-block__count cn-stats-block__count--error\">\n\t\t\t\t<span class=\"cn-stats-block__count-value\">—</span>\n\t\t\t\t<span class=\"cn-stats-block__count-label\">{{ errorLabel }}</span>\n\t\t\t</div>\n\t\t\t<div v-else-if=\"hasValueSlot || count > 0 || (showZeroCount && count === 0)\" class=\"cn-kpi-card__value-row cn-stats-block__count\">\n\t\t\t\t<span class=\"cn-kpi-card__value cn-stats-block__count-value\">\n\t\t\t\t\t<!-- @slot Override the prominently-displayed value — render a pre-formatted string (currency, percent, a \"—\" placeholder, …). `count` stays the raw number; this is presentation only. Defaults to the localized count. -->\n\t\t\t\t\t<!-- @binding {number} count The raw numeric count. -->\n\t\t\t\t\t<!-- @binding {string} formatted The default localized count string. -->\n\t\t\t\t\t<slot name=\"value\" :count=\"count\" :formatted=\"formattedCount\">{{ formattedCount }}</slot>\n\t\t\t\t</span>\n\t\t\t\t<span class=\"cn-kpi-card__label cn-stats-block__count-label\">{{ countLabel }}</span>\n\t\t\t</div>\n\t\t\t<div v-else-if=\"loading\" class=\"cn-stats-block__loading\">\n\t\t\t\t<NcLoadingIcon :size=\"16\" />\n\t\t\t\t{{ loadingLabel }}\n\t\t\t</div>\n\t\t\t<div v-else class=\"cn-stats-block__empty\">\n\t\t\t\t{{ emptyLabel }}\n\t\t\t</div>\n\n\t\t\t<!-- Breakdown details -->\n\t\t\t<div\n\t\t\t\tv-if=\"hasError === false && breakdown && (hasValueSlot || count > 0 || showZeroCount)\"\n\t\t\t\tclass=\"cn-stats-block__breakdown\">\n\t\t\t\t<div\n\t\t\t\t\tv-for=\"(value, key) in breakdown\"\n\t\t\t\t\t:key=\"key\"\n\t\t\t\t\tclass=\"cn-stats-block__breakdown-item\">\n\t\t\t\t\t<span class=\"cn-stats-block__breakdown-label\">{{ formatBreakdownLabel(key) }}</span>\n\t\t\t\t\t<span\n\t\t\t\t\t\tclass=\"cn-stats-block__breakdown-value\"\n\t\t\t\t\t\t:class=\"'cn-stats-block__breakdown-value--' + key\">\n\t\t\t\t\t\t{{ value }}\n\t\t\t\t\t</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</component>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport { normalizeLook } from '../../composables/useLook.js'\n\n// The canonical KPI look, shared with CnStatWidget. Imported here so the\n// card is styled even when the consuming app has not pulled in the\n// library's global css/index.css.\nimport '../../css/kpi-card.css'\n\n/**\n * CnStatsBlock — Statistics display card with icon, count, and optional breakdown.\n *\n * Renders the canonical KPI card: a large circular icon beside a left-aligned\n * number, drawing no box of its own because the wrapper around it already\n * draws one. Supports colour variants, icons and clickable state. Use in a\n * CnKpiGrid for responsive dashboard layouts.\n *\n * Basic (canonical horizontal card)\n * ```vue\n * <CnStatsBlock title=\"Cases\" :count=\"42\" count-label=\"open cases\" />\n * ```\n *\n * With icon and variant\n * ```vue\n * <CnStatsBlock\n * title=\"Open Cases\"\n * :count=\"42\"\n * :icon=\"BriefcaseOutline\"\n * variant=\"primary\"\n * clickable\n * @click=\"goToCases\" />\n * ```\n *\n * Stacked and boxed — for a block mounted with no wrapper around it\n * ```vue\n * <CnStatsBlock title=\"Cases\" :count=\"42\" vertical filled />\n * ```\n *\n * With route-based navigation (renders as <router-link>)\n * ```vue\n * <CnStatsBlock\n * title=\"Open Cases\"\n * :count=\"42\"\n * :icon=\"BriefcaseOutline\"\n * variant=\"primary\"\n * :route=\"{ name: 'Cases', query: { status: 'open' } }\" />\n * ```\n *\n * With breakdown\n * ```vue\n * <CnStatsBlock\n * title=\"Cases\"\n * :count=\"42\"\n * :breakdown=\"{ total: 100, invalid: 3, deleted: 5, published: 92 }\" />\n * ```\n *\n * Custom icon slot\n * ```vue\n * <CnStatsBlock title=\"Files\" :count=\"128\">\n * <template #icon>\n * <FileDocumentOutline :size=\"24\" />\n * </template>\n * </CnStatsBlock>\n * ```\n */\nexport default {\n\tname: 'CnStatsBlock',\n\n\tcomponents: {\n\t\tNcLoadingIcon,\n\t},\n\n\tinject: {\n\t\t/**\n\t\t * The look the app is drawn in, provided by CnAppRoot.\n\t\t *\n\t\t * @spec openspec/changes/screens-dashboard-parity/specs/dashboard-page/spec.md#requirement-the-board-kpi-tile\n\t\t */\n\t\tcnLook: { default: 'nextcloud' },\n\t},\n\n\tprops: {\n\t\t/** Block title */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The main count number to display prominently */\n\t\tcount: {\n\t\t\ttype: Number,\n\t\t\tdefault: 0,\n\t\t},\n\n\t\t/** Label displayed next to the count */\n\t\tcountLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'objects'),\n\t\t},\n\n\t\t/** Detailed breakdown object (key-value pairs) */\n\t\tbreakdown: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Whether data is currently loading */\n\t\tloading: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Text shown while loading */\n\t\tloadingLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Loading…'),\n\t\t},\n\n\t\t/** Text shown when count is 0 */\n\t\temptyLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'No items found'),\n\t\t},\n\n\t\t/**\n\t\t * The tile could not load its number. Anything truthy counts — pass the\n\t\t * caught error itself, or just `true`.\n\t\t *\n\t\t * The tile then shows a dash and `errorLabel` INSTEAD of a count, and\n\t\t * tints itself `error`. This takes precedence over `count`, `loading`\n\t\t * and `emptyLabel` deliberately: a stale or defaulted number rendered\n\t\t * during a failure is the exact thing this prop exists to stop. A\n\t\t * dashboard showing 0 because the backend is down is worse than one\n\t\t * showing nothing, because 0 is a number a reader will believe.\n\t\t *\n\t\t * @type {boolean|string|Error|null}\n\t\t */\n\t\terror: {\n\t\t\t// TYPE ORDER IS SEMANTIC HERE. Vue casts an empty value to `true`\n\t\t\t// when Boolean appears BEFORE String in the union, so\n\t\t\t// `[Boolean, String]` would read `error=\"\"` as an error. A caller\n\t\t\t// clearing its message back to '' is reporting RECOVERY, and that\n\t\t\t// would have pinned the tile in its error state after the fetch\n\t\t\t// succeeded again. String first keeps '' falsy.\n\t\t\t// eslint-disable-next-line vue/prefer-prop-type-boolean-first -- Boolean first would make `error=\"\"` truthy, which is the recovery signal.\n\t\t\ttype: [String, Boolean, Object],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Text shown in place of the count when `error` is set. */\n\t\terrorLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Unavailable'),\n\t\t},\n\n\t\t/** Icon component (e.g., imported MDI icon) */\n\t\ticon: {\n\t\t\ttype: [Object, Function],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Icon size in pixels */\n\t\ticonSize: {\n\t\t\ttype: Number,\n\t\t\tdefault: 24,\n\t\t},\n\n\t\t/** Color variant: 'default', 'primary', 'success', 'warning', 'error' */\n\t\tvariant: {\n\t\t\ttype: String,\n\t\t\tdefault: 'default',\n\t\t\tvalidator: (v) => ['default', 'primary', 'success', 'warning', 'error'].includes(v),\n\t\t},\n\n\t\t/**\n\t\t * Stack the icon above a centred number instead of placing it beside\n\t\t * one. The canonical KPI card is horizontal, so this is the opt-out;\n\t\t * `horizontal` below is kept only for callers that already pass it.\n\t\t */\n\t\tvertical: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * `stacked` is the board's tile, as CnStatWidget `content.layout:\n\t\t * \"stacked\"`: no icon circle, a 14px muted title, the number at\n\t\t * 34px/700 in the text colour. Empty (the default) keeps the KPI card.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-stats-block-can-take-the-stacked-board-look\n\t\t */\n\t\tlayout: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Lay the icon left of the content. No longer needed — this is the\n\t\t * canonical card's own layout — and kept so existing callers that pass\n\t\t * `horizontal` keep working. Pass `vertical` to stack instead.\n\t\t *\n\t\t * @deprecated since 2.25.0, the horizontal layout is the default.\n\t\t */\n\t\thorizontal: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Draw the card's own grey box. Off by default: a stats block is\n\t\t * normally rendered inside a wrapper that already draws a card, and a\n\t\t * second box reads as a card inside a card. Turn it on for a block\n\t\t * mounted with no wrapper around it.\n\t\t */\n\t\tfilled: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Whether the card is clickable */\n\t\tclickable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Whether to display 0 as a count value instead of the empty label */\n\t\tshowZeroCount: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Vue Router location object for declarative navigation.\n\t\t * When set, the card renders as a <router-link> and clickable styles are implied.\n\t\t * { name: 'Cases', query: { status: 'open' } }\n\t\t * { path: '/catalogi' }\n\t\t */\n\t\troute: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: ['click'],\n\n\tcomputed: {\n\t\t/**\n\t\t * Whether the app takes the board look.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tisBoardLook() {\n\t\t\treturn normalizeLook(this.cnLook) === 'board'\n\t\t},\n\n\t\thasIcon() {\n\t\t\treturn this.icon !== null || this.$slots.icon || this.$slots.icon\n\t\t},\n\n\t\t/**\n\t\t * Whether a consumer provided the `value` slot. When set, the value\n\t\t * area always renders (the slot decides what to show, even at count 0).\n\t\t */\n\t\thasValueSlot() {\n\t\t\treturn !!this.$slots.value || !!this.$slots.value\n\t\t},\n\n\t\t/**\n\t\t * Whether the tile is in its error state.\n\t\t *\n\t\t * Anything truthy counts, so a caller can pass the caught error itself\n\t\t * rather than converting it to a boolean first. An empty string is NOT\n\t\t * an error — that is a caller who cleared the message, not one\n\t\t * reporting a failure.\n\t\t *\n\t\t * @return {boolean} True when the tile should show `errorLabel`.\n\t\t */\n\t\thasError() {\n\t\t\treturn Boolean(this.error)\n\t\t},\n\n\t\tformattedCount() {\n\t\t\treturn this.count.toLocaleString()\n\t\t},\n\n\t\t/**\n\t\t * Whether the card is interactive (clickable or has a route).\n\t\t * Used for applying hover/focus styles.\n\t\t */\n\t\tisInteractive() {\n\t\t\treturn !!this.route || this.clickable\n\t\t},\n\n\t\t/**\n\t\t * Determines which HTML element or component to render.\n\t\t * - route set → 'router-link' (SPA navigation)\n\t\t * - clickable → 'a' (app handles click via event)\n\t\t * - neither → 'div' (static display)\n\t\t */\n\t\tcomponentTag() {\n\t\t\tif (this.route) {\n\t\t\t\treturn 'router-link'\n\t\t\t}\n\t\t\tif (this.clickable) {\n\t\t\t\treturn 'a'\n\t\t\t}\n\t\t\treturn 'div'\n\t\t},\n\n\t\t/**\n\t\t * Dynamic attributes for the root element based on rendering mode.\n\t\t */\n\t\tcomponentAttrs() {\n\t\t\tif (this.route) {\n\t\t\t\treturn { to: this.route, tabindex: '0' }\n\t\t\t}\n\t\t\tif (this.clickable) {\n\t\t\t\treturn { href: '#', role: 'button', tabindex: '0' }\n\t\t\t}\n\t\t\treturn {}\n\t\t},\n\n\t\t/**\n\t\t * Whether the block takes the stacked board look (`layout`).\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-stats-block-can-take-the-stacked-board-look\n\t\t * @return {boolean}\n\t\t */\n\t\tisStacked() {\n\t\t\treturn this.layout === 'stacked'\n\t\t},\n\n\t\trootClasses() {\n\t\t\treturn {\n\t\t\t\t// Canonical + legacy, in pairs: the canonical class carries the\n\t\t\t\t// look, the legacy one keeps existing app CSS matching.\n\t\t\t\t// The canonical card is already horizontal, so the legacy pair\n\t\t\t\t// is emitted for app CSS but carries no look of its own.\n\t\t\t\t'cn-kpi-card--horizontal': !this.isStacked && (this.horizontal || !this.vertical),\n\t\t\t\t'cn-kpi-card--stacked': this.isStacked,\n\t\t\t\t'cn-kpi-card--board': this.isBoardLook,\n\t\t\t\t'cn-stats-block--horizontal': this.horizontal || !this.vertical,\n\t\t\t\t'cn-kpi-card--vertical': this.vertical,\n\t\t\t\t'cn-stats-block--vertical': this.vertical,\n\t\t\t\t'cn-kpi-card--filled': this.filled,\n\t\t\t\t'cn-kpi-card--clickable': this.isInteractive,\n\t\t\t\t'cn-stats-block--clickable': this.isInteractive,\n\t\t\t\t'cn-kpi-card--error': this.hasError,\n\t\t\t\t[`cn-kpi-card--${this.variant}`]: this.variant !== 'default' && this.hasError === false,\n\t\t\t\t// An errored tile tints itself. Requiring the consumer to also\n\t\t\t\t// pass `variant=\"error\"` would mean every caller remembering\n\t\t\t\t// two props to express one state, and forgetting the second is\n\t\t\t\t// invisible — a tile that reads \"Unavailable\" in the default\n\t\t\t\t// colour looks like ordinary content.\n\t\t\t\t'cn-stats-block--error': this.hasError,\n\t\t\t\t[`cn-stats-block--${this.variant}`]: this.variant !== 'default' && this.hasError === false,\n\t\t\t}\n\t\t},\n\n\t\ticonClasses() {\n\t\t\treturn {\n\t\t\t\t[`cn-stats-block__icon--${this.variant}`]: this.variant !== 'default',\n\t\t\t}\n\t\t},\n\n\t},\n\n\tmethods: {\n\t\tformatBreakdownLabel(key) {\n\t\t\treturn key.charAt(0).toUpperCase() + key.slice(1) + ':'\n\t\t},\n\n\t\tonClick(event) {\n\t\t\t// When route is set, router-link handles navigation — do not emit click\n\t\t\tif (this.route) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.clickable) {\n\t\t\t\tevent.preventDefault()\n\t\t\t\tthis.$emit('click', event)\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n/*\n * The card's own look — box, body stack, icon circle, title, value, variants,\n * hover — is the CANONICAL KPI card in src/css/kpi-card.css, shared with\n * CnStatWidget. The rules that lived here duplicated it in different numbers\n * (a 1.2rem count row, a 40px icon, hardcoded rgba() tints that ignored a\n * re-themed palette), and that duplication IS how the two KPI looks drifted.\n * What is left below is this component's own extras: the breakdown block, the\n * loading line and the empty line.\n *\n * Those extras are secondary KPI text, so they read the shared\n * `--cn-kpi-label-*` scale rather than carrying sizes of their own — an empty\n * state that is a different size from the label it replaces is the same drift\n * in miniature.\n */\n\n.cn-stats-block__header {\n\tmax-width: 100%;\n\tmin-width: 0;\n}\n\n.cn-stats-block__loading {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: flex-start;\n\tgap: 6px;\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n}\n\n.cn-stats-block__empty {\n\ttext-align: start;\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n\tfont-style: italic;\n}\n\n.cn-stats-block__breakdown {\n\tmargin-top: 4px;\n\tpadding: 8px;\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tbackground: var(--color-background-hover);\n\tborder-radius: var(--border-radius);\n\tborder: 1px solid var(--color-border);\n\twidth: 100%;\n\ttext-align: start;\n}\n\n.cn-stats-block__breakdown-item {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\talign-items: center;\n\tgap: 8px;\n\tmargin-bottom: 4px;\n}\n\n.cn-stats-block__breakdown-item:last-child {\n\tmargin-bottom: 0;\n}\n\n.cn-stats-block__breakdown-label {\n\tfont-weight: 500;\n\tcolor: var(--color-main-text);\n}\n\n/* The value takes the card's accent, so a breakdown reads as part of the tile\n it sits in rather than as a table that happened to land there. It used to\n carry a `--color-background-hover` chip — on a `--color-background-hover`\n breakdown box, which is to say an invisible chip drawn at the cost of two\n extra declarations and a differently-shaped hit area. The per-key modifiers\n below still win: `invalid`, `deleted` and `published` mean something\n specific and outrank the tile's own colour. */\n.cn-stats-block__breakdown-value {\n\tfont-weight: var(--cn-kpi-title-weight, 600);\n\tcolor: var(--cn-kpi-accent, var(--color-primary-element));\n}\n\n.cn-stats-block__breakdown-value--invalid { color: var(--color-element-warning); }\n\n.cn-stats-block__breakdown-value--deleted { color: var(--color-element-error); }\n\n.cn-stats-block__breakdown-value--published { color: var(--color-element-success); }\n</style>\n"],"names":["_createElementBlock","_normalizeClass","_renderSlot","_openBlock","_createBlock","_resolveDynamicComponent","_createElementVNode","_toDisplayString","_createVNode","_createTextVNode"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnStatsBlock.vue3.js","sources":["../../../../src/components/CnStatsBlock/CnStatsBlock.vue?vue&type=template&id=722d9aef&lang.js"],"sourcesContent":["<!--\n - CnStatsBlock renders the CANONICAL KPI card (src/css/kpi-card.css) — the\n - same markup and the same stylesheet CnStatWidget renders, so OpenCatalogi's\n - KPIs and dossiq's are one design rather than two. This component's own\n - `cn-stats-block*` classes stay on the same elements for backwards\n - compatibility with app CSS that targets them; the LOOK comes from the\n - shared sheet.\n -\n - The comment lives OUT here rather than inside <template>: a comment node\n - beside the root element makes the component multi-root in Vue 3, and a\n - multi-root component has no root to put class attributes on — `rootClasses`\n - silently stopped reaching the DOM.\n-->\n<template>\n\t<component\n\t\t:is=\"componentTag\"\n\t\tclass=\"cn-kpi-card cn-stats-block\"\n\t\t:class=\"rootClasses\"\n\t\tv-bind=\"componentAttrs\"\n\t\t@click=\"onClick\">\n\t\t<!-- Icon -->\n\t\t<div v-if=\"hasIcon && !isStacked && !isBoardLook\" class=\"cn-kpi-card__icon cn-stats-block__icon\" :class=\"iconClasses\">\n\t\t\t<slot name=\"icon\">\n\t\t\t\t<component :is=\"icon\" v-if=\"icon\" :size=\"iconSize\" />\n\t\t\t</slot>\n\t\t</div>\n\n\t\t<!-- Content -->\n\t\t<div class=\"cn-kpi-card__body cn-stats-block__content\">\n\t\t\t<div class=\"cn-stats-block__header\">\n\t\t\t\t<!-- Board look: an 18px glyph before the label, only on a link tile. -->\n\t\t\t\t<span\n\t\t\t\t\tv-if=\"isBoardLook && isInteractive && hasIcon\"\n\t\t\t\t\tclass=\"cn-kpi-card__glyph cn-stats-block__glyph\"\n\t\t\t\t\tdata-testid=\"cn-stats-block-glyph\"\n\t\t\t\t\taria-hidden=\"true\">\n\t\t\t\t\t<slot name=\"icon\">\n\t\t\t\t\t\t<component :is=\"icon\" v-if=\"icon\" :size=\"18\" />\n\t\t\t\t\t</slot>\n\t\t\t\t</span>\n\t\t\t\t<h4 class=\"cn-kpi-card__title\" :title=\"title || undefined\">\n\t\t\t\t\t{{ title ? tl(title) : t('nextcloud-vue', 'Objects') }}\n\t\t\t\t</h4>\n\t\t\t</div>\n\n\t\t\t<div v-if=\"hasError\" class=\"cn-stats-block__count cn-stats-block__count--error\">\n\t\t\t\t<span class=\"cn-stats-block__count-value\">—</span>\n\t\t\t\t<span class=\"cn-stats-block__count-label\">{{ errorLabel }}</span>\n\t\t\t</div>\n\t\t\t<div v-else-if=\"hasValueSlot || count > 0 || (showZeroCount && count === 0)\" class=\"cn-kpi-card__value-row cn-stats-block__count\">\n\t\t\t\t<span class=\"cn-kpi-card__value cn-stats-block__count-value\">\n\t\t\t\t\t<!-- @slot Override the prominently-displayed value — render a pre-formatted string (currency, percent, a \"—\" placeholder, …). `count` stays the raw number; this is presentation only. Defaults to the localized count. -->\n\t\t\t\t\t<!-- @binding {number} count The raw numeric count. -->\n\t\t\t\t\t<!-- @binding {string} formatted The default localized count string. -->\n\t\t\t\t\t<slot name=\"value\" :count=\"count\" :formatted=\"formattedCount\">{{ formattedCount }}</slot>\n\t\t\t\t</span>\n\t\t\t\t<span class=\"cn-kpi-card__label cn-stats-block__count-label\">{{ countLabel ? tl(countLabel) : countLabel }}</span>\n\t\t\t</div>\n\t\t\t<div v-else-if=\"loading\" class=\"cn-stats-block__loading\">\n\t\t\t\t<NcLoadingIcon :size=\"16\" />\n\t\t\t\t{{ loadingLabel }}\n\t\t\t</div>\n\t\t\t<div v-else class=\"cn-stats-block__empty\">\n\t\t\t\t{{ emptyLabel }}\n\t\t\t</div>\n\n\t\t\t<!-- Breakdown details -->\n\t\t\t<div\n\t\t\t\tv-if=\"hasError === false && breakdown && (hasValueSlot || count > 0 || showZeroCount)\"\n\t\t\t\tclass=\"cn-stats-block__breakdown\">\n\t\t\t\t<div\n\t\t\t\t\tv-for=\"(value, key) in breakdown\"\n\t\t\t\t\t:key=\"key\"\n\t\t\t\t\tclass=\"cn-stats-block__breakdown-item\">\n\t\t\t\t\t<span class=\"cn-stats-block__breakdown-label\">{{ formatBreakdownLabel(key) }}</span>\n\t\t\t\t\t<span\n\t\t\t\t\t\tclass=\"cn-stats-block__breakdown-value\"\n\t\t\t\t\t\t:class=\"'cn-stats-block__breakdown-value--' + key\">\n\t\t\t\t\t\t{{ value }}\n\t\t\t\t\t</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</component>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport { normalizeLook } from '../../composables/useLook.js'\n\n// The canonical KPI look, shared with CnStatWidget. Imported here so the\n// card is styled even when the consuming app has not pulled in the\n// library's global css/index.css.\nimport '../../css/kpi-card.css'\n\n/**\n * CnStatsBlock — Statistics display card with icon, count, and optional breakdown.\n *\n * Renders the canonical KPI card: a large circular icon beside a left-aligned\n * number, drawing no box of its own because the wrapper around it already\n * draws one. Supports colour variants, icons and clickable state. Use in a\n * CnKpiGrid for responsive dashboard layouts.\n *\n * Basic (canonical horizontal card)\n * ```vue\n * <CnStatsBlock title=\"Cases\" :count=\"42\" count-label=\"open cases\" />\n * ```\n *\n * With icon and variant\n * ```vue\n * <CnStatsBlock\n * title=\"Open Cases\"\n * :count=\"42\"\n * :icon=\"BriefcaseOutline\"\n * variant=\"primary\"\n * clickable\n * @click=\"goToCases\" />\n * ```\n *\n * Stacked and boxed — for a block mounted with no wrapper around it\n * ```vue\n * <CnStatsBlock title=\"Cases\" :count=\"42\" vertical filled />\n * ```\n *\n * With route-based navigation (renders as <router-link>)\n * ```vue\n * <CnStatsBlock\n * title=\"Open Cases\"\n * :count=\"42\"\n * :icon=\"BriefcaseOutline\"\n * variant=\"primary\"\n * :route=\"{ name: 'Cases', query: { status: 'open' } }\" />\n * ```\n *\n * With breakdown\n * ```vue\n * <CnStatsBlock\n * title=\"Cases\"\n * :count=\"42\"\n * :breakdown=\"{ total: 100, invalid: 3, deleted: 5, published: 92 }\" />\n * ```\n *\n * Custom icon slot\n * ```vue\n * <CnStatsBlock title=\"Files\" :count=\"128\">\n * <template #icon>\n * <FileDocumentOutline :size=\"24\" />\n * </template>\n * </CnStatsBlock>\n * ```\n */\nexport default {\n\tname: 'CnStatsBlock',\n\n\tcomponents: {\n\t\tNcLoadingIcon,\n\t},\n\n\tinject: {\n\t\t/**\n\t\t * The look the app is drawn in, provided by CnAppRoot.\n\t\t *\n\t\t * @spec openspec/changes/screens-dashboard-parity/specs/dashboard-page/spec.md#requirement-the-board-kpi-tile\n\t\t */\n\t\tcnLook: { default: 'nextcloud' },\n\t\t/**\n\t\t * The host's label lookup (CnAppRoot provides it); identity without one, so a\n\t\t * label that is not a key renders as written.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/** Block title */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The main count number to display prominently */\n\t\tcount: {\n\t\t\ttype: Number,\n\t\t\tdefault: 0,\n\t\t},\n\n\t\t/** Label displayed next to the count */\n\t\tcountLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'objects'),\n\t\t},\n\n\t\t/** Detailed breakdown object (key-value pairs) */\n\t\tbreakdown: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Whether data is currently loading */\n\t\tloading: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Text shown while loading */\n\t\tloadingLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Loading…'),\n\t\t},\n\n\t\t/** Text shown when count is 0 */\n\t\temptyLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'No items found'),\n\t\t},\n\n\t\t/**\n\t\t * The tile could not load its number. Anything truthy counts — pass the\n\t\t * caught error itself, or just `true`.\n\t\t *\n\t\t * The tile then shows a dash and `errorLabel` INSTEAD of a count, and\n\t\t * tints itself `error`. This takes precedence over `count`, `loading`\n\t\t * and `emptyLabel` deliberately: a stale or defaulted number rendered\n\t\t * during a failure is the exact thing this prop exists to stop. A\n\t\t * dashboard showing 0 because the backend is down is worse than one\n\t\t * showing nothing, because 0 is a number a reader will believe.\n\t\t *\n\t\t * @type {boolean|string|Error|null}\n\t\t */\n\t\terror: {\n\t\t\t// TYPE ORDER IS SEMANTIC HERE. Vue casts an empty value to `true`\n\t\t\t// when Boolean appears BEFORE String in the union, so\n\t\t\t// `[Boolean, String]` would read `error=\"\"` as an error. A caller\n\t\t\t// clearing its message back to '' is reporting RECOVERY, and that\n\t\t\t// would have pinned the tile in its error state after the fetch\n\t\t\t// succeeded again. String first keeps '' falsy.\n\t\t\t// eslint-disable-next-line vue/prefer-prop-type-boolean-first -- Boolean first would make `error=\"\"` truthy, which is the recovery signal.\n\t\t\ttype: [String, Boolean, Object],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Text shown in place of the count when `error` is set. */\n\t\terrorLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Unavailable'),\n\t\t},\n\n\t\t/** Icon component (e.g., imported MDI icon) */\n\t\ticon: {\n\t\t\ttype: [Object, Function],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Icon size in pixels */\n\t\ticonSize: {\n\t\t\ttype: Number,\n\t\t\tdefault: 24,\n\t\t},\n\n\t\t/** Color variant: 'default', 'primary', 'success', 'warning', 'error' */\n\t\tvariant: {\n\t\t\ttype: String,\n\t\t\tdefault: 'default',\n\t\t\tvalidator: (v) => ['default', 'primary', 'success', 'warning', 'error'].includes(v),\n\t\t},\n\n\t\t/**\n\t\t * Stack the icon above a centred number instead of placing it beside\n\t\t * one. The canonical KPI card is horizontal, so this is the opt-out;\n\t\t * `horizontal` below is kept only for callers that already pass it.\n\t\t */\n\t\tvertical: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * `stacked` is the board's tile, as CnStatWidget `content.layout:\n\t\t * \"stacked\"`: no icon circle, a 14px muted title, the number at\n\t\t * 34px/700 in the text colour. Empty (the default) keeps the KPI card.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-stats-block-can-take-the-stacked-board-look\n\t\t */\n\t\tlayout: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Lay the icon left of the content. No longer needed — this is the\n\t\t * canonical card's own layout — and kept so existing callers that pass\n\t\t * `horizontal` keep working. Pass `vertical` to stack instead.\n\t\t *\n\t\t * @deprecated since 2.25.0, the horizontal layout is the default.\n\t\t */\n\t\thorizontal: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Draw the card's own grey box. Off by default: a stats block is\n\t\t * normally rendered inside a wrapper that already draws a card, and a\n\t\t * second box reads as a card inside a card. Turn it on for a block\n\t\t * mounted with no wrapper around it.\n\t\t */\n\t\tfilled: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Whether the card is clickable */\n\t\tclickable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Whether to display 0 as a count value instead of the empty label */\n\t\tshowZeroCount: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Vue Router location object for declarative navigation.\n\t\t * When set, the card renders as a <router-link> and clickable styles are implied.\n\t\t * { name: 'Cases', query: { status: 'open' } }\n\t\t * { path: '/catalogi' }\n\t\t */\n\t\troute: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: ['click'],\n\n\tcomputed: {\n\t\t/**\n\t\t * Whether the app takes the board look.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tisBoardLook() {\n\t\t\treturn normalizeLook(this.cnLook) === 'board'\n\t\t},\n\n\t\thasIcon() {\n\t\t\treturn this.icon !== null || this.$slots.icon || this.$slots.icon\n\t\t},\n\n\t\t/**\n\t\t * Whether a consumer provided the `value` slot. When set, the value\n\t\t * area always renders (the slot decides what to show, even at count 0).\n\t\t */\n\t\thasValueSlot() {\n\t\t\treturn !!this.$slots.value || !!this.$slots.value\n\t\t},\n\n\t\t/**\n\t\t * Whether the tile is in its error state.\n\t\t *\n\t\t * Anything truthy counts, so a caller can pass the caught error itself\n\t\t * rather than converting it to a boolean first. An empty string is NOT\n\t\t * an error — that is a caller who cleared the message, not one\n\t\t * reporting a failure.\n\t\t *\n\t\t * @return {boolean} True when the tile should show `errorLabel`.\n\t\t */\n\t\thasError() {\n\t\t\treturn Boolean(this.error)\n\t\t},\n\n\t\tformattedCount() {\n\t\t\treturn this.count.toLocaleString()\n\t\t},\n\n\t\t/**\n\t\t * Whether the card is interactive (clickable or has a route).\n\t\t * Used for applying hover/focus styles.\n\t\t */\n\t\tisInteractive() {\n\t\t\treturn !!this.route || this.clickable\n\t\t},\n\n\t\t/**\n\t\t * Determines which HTML element or component to render.\n\t\t * - route set → 'router-link' (SPA navigation)\n\t\t * - clickable → 'a' (app handles click via event)\n\t\t * - neither → 'div' (static display)\n\t\t */\n\t\tcomponentTag() {\n\t\t\tif (this.route) {\n\t\t\t\treturn 'router-link'\n\t\t\t}\n\t\t\tif (this.clickable) {\n\t\t\t\treturn 'a'\n\t\t\t}\n\t\t\treturn 'div'\n\t\t},\n\n\t\t/**\n\t\t * Dynamic attributes for the root element based on rendering mode.\n\t\t */\n\t\tcomponentAttrs() {\n\t\t\tif (this.route) {\n\t\t\t\treturn { to: this.route, tabindex: '0' }\n\t\t\t}\n\t\t\tif (this.clickable) {\n\t\t\t\treturn { href: '#', role: 'button', tabindex: '0' }\n\t\t\t}\n\t\t\treturn {}\n\t\t},\n\n\t\t/**\n\t\t * Whether the block takes the stacked board look (`layout`).\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-stats-block-can-take-the-stacked-board-look\n\t\t * @return {boolean}\n\t\t */\n\t\tisStacked() {\n\t\t\treturn this.layout === 'stacked'\n\t\t},\n\n\t\trootClasses() {\n\t\t\treturn {\n\t\t\t\t// Canonical + legacy, in pairs: the canonical class carries the\n\t\t\t\t// look, the legacy one keeps existing app CSS matching.\n\t\t\t\t// The canonical card is already horizontal, so the legacy pair\n\t\t\t\t// is emitted for app CSS but carries no look of its own.\n\t\t\t\t'cn-kpi-card--horizontal': !this.isStacked && (this.horizontal || !this.vertical),\n\t\t\t\t'cn-kpi-card--stacked': this.isStacked,\n\t\t\t\t'cn-kpi-card--board': this.isBoardLook,\n\t\t\t\t'cn-stats-block--horizontal': this.horizontal || !this.vertical,\n\t\t\t\t'cn-kpi-card--vertical': this.vertical,\n\t\t\t\t'cn-stats-block--vertical': this.vertical,\n\t\t\t\t'cn-kpi-card--filled': this.filled,\n\t\t\t\t'cn-kpi-card--clickable': this.isInteractive,\n\t\t\t\t'cn-stats-block--clickable': this.isInteractive,\n\t\t\t\t'cn-kpi-card--error': this.hasError,\n\t\t\t\t[`cn-kpi-card--${this.variant}`]: this.variant !== 'default' && this.hasError === false,\n\t\t\t\t// An errored tile tints itself. Requiring the consumer to also\n\t\t\t\t// pass `variant=\"error\"` would mean every caller remembering\n\t\t\t\t// two props to express one state, and forgetting the second is\n\t\t\t\t// invisible — a tile that reads \"Unavailable\" in the default\n\t\t\t\t// colour looks like ordinary content.\n\t\t\t\t'cn-stats-block--error': this.hasError,\n\t\t\t\t[`cn-stats-block--${this.variant}`]: this.variant !== 'default' && this.hasError === false,\n\t\t\t}\n\t\t},\n\n\t\ticonClasses() {\n\t\t\treturn {\n\t\t\t\t[`cn-stats-block__icon--${this.variant}`]: this.variant !== 'default',\n\t\t\t}\n\t\t},\n\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * A manifest string through the host's label lookup (`cnTranslate`), so a key\n\t\t * such as \"Mine\" reads in the user's language.\n\t\t *\n\t\t * @param {string} text The text as written in the manifest.\n\t\t * @return {string} The translated text.\n\t\t */\n\t\ttl(text) {\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text\n\t\t},\n\n\t\tformatBreakdownLabel(key) {\n\t\t\treturn key.charAt(0).toUpperCase() + key.slice(1) + ':'\n\t\t},\n\n\t\tonClick(event) {\n\t\t\t// When route is set, router-link handles navigation — do not emit click\n\t\t\tif (this.route) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.clickable) {\n\t\t\t\tevent.preventDefault()\n\t\t\t\tthis.$emit('click', event)\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n/*\n * The card's own look — box, body stack, icon circle, title, value, variants,\n * hover — is the CANONICAL KPI card in src/css/kpi-card.css, shared with\n * CnStatWidget. The rules that lived here duplicated it in different numbers\n * (a 1.2rem count row, a 40px icon, hardcoded rgba() tints that ignored a\n * re-themed palette), and that duplication IS how the two KPI looks drifted.\n * What is left below is this component's own extras: the breakdown block, the\n * loading line and the empty line.\n *\n * Those extras are secondary KPI text, so they read the shared\n * `--cn-kpi-label-*` scale rather than carrying sizes of their own — an empty\n * state that is a different size from the label it replaces is the same drift\n * in miniature.\n */\n\n.cn-stats-block__header {\n\tmax-width: 100%;\n\tmin-width: 0;\n}\n\n.cn-stats-block__loading {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: flex-start;\n\tgap: 6px;\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n}\n\n.cn-stats-block__empty {\n\ttext-align: start;\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tcolor: var(--cn-kpi-label-color, var(--color-text-maxcontrast));\n\tfont-style: italic;\n}\n\n.cn-stats-block__breakdown {\n\tmargin-top: 4px;\n\tpadding: 8px;\n\tfont-size: var(--cn-kpi-label-size, 13px);\n\tbackground: var(--color-background-hover);\n\tborder-radius: var(--border-radius);\n\tborder: 1px solid var(--color-border);\n\twidth: 100%;\n\ttext-align: start;\n}\n\n.cn-stats-block__breakdown-item {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\talign-items: center;\n\tgap: 8px;\n\tmargin-bottom: 4px;\n}\n\n.cn-stats-block__breakdown-item:last-child {\n\tmargin-bottom: 0;\n}\n\n.cn-stats-block__breakdown-label {\n\tfont-weight: 500;\n\tcolor: var(--color-main-text);\n}\n\n/* The value takes the card's accent, so a breakdown reads as part of the tile\n it sits in rather than as a table that happened to land there. It used to\n carry a `--color-background-hover` chip — on a `--color-background-hover`\n breakdown box, which is to say an invisible chip drawn at the cost of two\n extra declarations and a differently-shaped hit area. The per-key modifiers\n below still win: `invalid`, `deleted` and `published` mean something\n specific and outrank the tile's own colour. */\n.cn-stats-block__breakdown-value {\n\tfont-weight: var(--cn-kpi-title-weight, 600);\n\tcolor: var(--cn-kpi-accent, var(--color-primary-element));\n}\n\n.cn-stats-block__breakdown-value--invalid { color: var(--color-element-warning); }\n\n.cn-stats-block__breakdown-value--deleted { color: var(--color-element-error); }\n\n.cn-stats-block__breakdown-value--published { color: var(--color-element-success); }\n</style>\n"],"names":["_createElementBlock","_normalizeClass","_renderSlot","_openBlock","_createBlock","_resolveDynamicComponent","_createElementVNode","_toDisplayString","_createVNode","_createTextVNode"],"mappings":"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"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* `v-cn-select-aria`: puts `aria-invalid`, `aria-describedby` and
|
|
6
|
+
* `aria-required` on the search input of an NcSelect.
|
|
7
|
+
*
|
|
8
|
+
* NcSelect forwards no attributes to its input: an attribute bound on it lands
|
|
9
|
+
* on the wrapping `.v-select` div, where a screen reader never reads it. The
|
|
10
|
+
* input is the combobox, so the attributes belong there. This directive sets
|
|
11
|
+
* them on that input after every render, and removes the ones that go away
|
|
12
|
+
* (a field that becomes valid loses `aria-invalid`).
|
|
13
|
+
*
|
|
14
|
+
* Usage: `<NcSelect v-cn-select-aria="{ 'aria-invalid': 'true', 'aria-describedby': 'err-id' }" />`
|
|
15
|
+
* A `null` or `undefined` value does nothing, so a caller can bind it
|
|
16
|
+
* unconditionally.
|
|
17
|
+
*
|
|
18
|
+
* @spec openspec/changes/screens-form-parity/tasks.md
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const ARIA_ATTRIBUTES = ['aria-invalid', 'aria-describedby', 'aria-required'];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Apply the bound attributes to the combobox input inside the element.
|
|
25
|
+
*
|
|
26
|
+
* @param {HTMLElement} el The element the directive sits on (the select root).
|
|
27
|
+
* @param {{value: object|null|undefined}} binding The directive binding.
|
|
28
|
+
* @return {void}
|
|
29
|
+
*/
|
|
30
|
+
function apply(el, binding) {
|
|
31
|
+
const attributes = binding.value;
|
|
32
|
+
if (!attributes || typeof el.querySelector !== 'function') {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
const input = el.querySelector('input.vs__search') || el.querySelector('input');
|
|
36
|
+
if (!input) {
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
for (const name of ARIA_ATTRIBUTES) {
|
|
40
|
+
const value = attributes[name];
|
|
41
|
+
if (value === undefined || value === null || value === false) {
|
|
42
|
+
input.removeAttribute(name);
|
|
43
|
+
} else {
|
|
44
|
+
input.setAttribute(name, String(value));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const cnSelectAria = {
|
|
50
|
+
mounted: apply,
|
|
51
|
+
updated: apply,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export { cnSelectAria, cnSelectAria as default };
|
|
55
|
+
//# sourceMappingURL=cnSelectAria.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cnSelectAria.js","sources":["../../../src/directives/cnSelectAria.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * `v-cn-select-aria`: puts `aria-invalid`, `aria-describedby` and\n * `aria-required` on the search input of an NcSelect.\n *\n * NcSelect forwards no attributes to its input: an attribute bound on it lands\n * on the wrapping `.v-select` div, where a screen reader never reads it. The\n * input is the combobox, so the attributes belong there. This directive sets\n * them on that input after every render, and removes the ones that go away\n * (a field that becomes valid loses `aria-invalid`).\n *\n * Usage: `<NcSelect v-cn-select-aria=\"{ 'aria-invalid': 'true', 'aria-describedby': 'err-id' }\" />`\n * A `null` or `undefined` value does nothing, so a caller can bind it\n * unconditionally.\n *\n * @spec openspec/changes/screens-form-parity/tasks.md\n */\n\nconst ARIA_ATTRIBUTES = ['aria-invalid', 'aria-describedby', 'aria-required']\n\n/**\n * Apply the bound attributes to the combobox input inside the element.\n *\n * @param {HTMLElement} el The element the directive sits on (the select root).\n * @param {{value: object|null|undefined}} binding The directive binding.\n * @return {void}\n */\nfunction apply(el, binding) {\n\tconst attributes = binding.value\n\tif (!attributes || typeof el.querySelector !== 'function') {\n\t\treturn\n\t}\n\tconst input = el.querySelector('input.vs__search') || el.querySelector('input')\n\tif (!input) {\n\t\treturn\n\t}\n\tfor (const name of ARIA_ATTRIBUTES) {\n\t\tconst value = attributes[name]\n\t\tif (value === undefined || value === null || value === false) {\n\t\t\tinput.removeAttribute(name)\n\t\t} else {\n\t\t\tinput.setAttribute(name, String(value))\n\t\t}\n\t}\n}\n\nexport const cnSelectAria = {\n\tmounted: apply,\n\tupdated: apply,\n}\n\nexport default cnSelectAria\n"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAM,eAAe,GAAG,CAAC,cAAc,EAAE,kBAAkB,EAAE,eAAe,EAAC;AAC7E;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS,KAAK,CAAC,EAAE,EAAE,OAAO,EAAE;AAC5B,CAAC,MAAM,UAAU,GAAG,OAAO,CAAC,MAAK;AACjC,CAAC,IAAI,CAAC,UAAU,IAAI,OAAO,EAAE,CAAC,aAAa,KAAK,UAAU,EAAE;AAC5D,EAAE,MAAM;AACR,EAAE;AACF,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC,aAAa,CAAC,kBAAkB,CAAC,IAAI,EAAE,CAAC,aAAa,CAAC,OAAO,EAAC;AAChF,CAAC,IAAI,CAAC,KAAK,EAAE;AACb,EAAE,MAAM;AACR,EAAE;AACF,CAAC,KAAK,MAAM,IAAI,IAAI,eAAe,EAAE;AACrC,EAAE,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,EAAC;AAChC,EAAE,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,KAAK,EAAE;AAChE,GAAG,KAAK,CAAC,eAAe,CAAC,IAAI,EAAC;AAC9B,GAAG,MAAM;AACT,GAAG,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,EAAC;AAC1C,GAAG;AACH,EAAE;AACF,CAAC;AACD;AACY,MAAC,YAAY,GAAG;AAC5B,CAAC,OAAO,EAAE,KAAK;AACf,CAAC,OAAO,EAAE,KAAK;AACf;;;;"}
|
|
@@ -795,7 +795,7 @@
|
|
|
795
795
|
}
|
|
796
796
|
|
|
797
797
|
/* An invalid field: 2px error border on the control, 4px edge on the group. */
|
|
798
|
-
.cn-look-board .cn-form-field--invalid input,
|
|
798
|
+
.cn-look-board .cn-form-field--invalid input:not([type='checkbox']):not([type='radio']),
|
|
799
799
|
.cn-look-board .cn-form-field--invalid textarea,
|
|
800
800
|
.cn-look-board .cn-form-field--invalid .select {
|
|
801
801
|
border: var(--cn-field-error-border) solid var(--color-error);
|
|
@@ -38840,6 +38840,71 @@ a.cn-integration-card__title[data-v-f7e9bbfa]:hover {
|
|
|
38840
38840
|
}
|
|
38841
38841
|
|
|
38842
38842
|
|
|
38843
|
+
/* The host is one flex item at the start of Nextcloud's header. */
|
|
38844
|
+
.cn-brand-bar-host {
|
|
38845
|
+
flex: none;
|
|
38846
|
+
display: flex;
|
|
38847
|
+
align-items: center;
|
|
38848
|
+
height: 100%;
|
|
38849
|
+
margin-inline-end: 12px;
|
|
38850
|
+
}
|
|
38851
|
+
.cn-brand-bar {
|
|
38852
|
+
box-sizing: border-box;
|
|
38853
|
+
display: flex;
|
|
38854
|
+
align-items: center;
|
|
38855
|
+
gap: 12px;
|
|
38856
|
+
width: var(--cn-board-brand-width, 237px);
|
|
38857
|
+
}
|
|
38858
|
+
.cn-brand-bar__brand {
|
|
38859
|
+
flex: 1 1 0;
|
|
38860
|
+
box-sizing: border-box;
|
|
38861
|
+
display: flex;
|
|
38862
|
+
align-items: center;
|
|
38863
|
+
gap: 10px;
|
|
38864
|
+
min-width: 0;
|
|
38865
|
+
min-height: 48px;
|
|
38866
|
+
max-height: 100%;
|
|
38867
|
+
padding: 0 6px;
|
|
38868
|
+
border-radius: 8px;
|
|
38869
|
+
color: var(--color-background-plain-text, var(--color-main-text));
|
|
38870
|
+
}
|
|
38871
|
+
.cn-brand-bar__emblem {
|
|
38872
|
+
flex: none;
|
|
38873
|
+
height: var(--cn-nav-emblem-size, 34px);
|
|
38874
|
+
width: auto;
|
|
38875
|
+
object-fit: contain;
|
|
38876
|
+
}
|
|
38877
|
+
.cn-brand-bar__emblem--theme {
|
|
38878
|
+
display: inline-block;
|
|
38879
|
+
width: var(--cn-nav-emblem-size, 34px);
|
|
38880
|
+
background: var(--nldesign-emblem-url) center / contain no-repeat;
|
|
38881
|
+
}
|
|
38882
|
+
.cn-brand-bar__text {
|
|
38883
|
+
display: flex;
|
|
38884
|
+
flex-direction: column;
|
|
38885
|
+
line-height: 1.15;
|
|
38886
|
+
min-width: 0;
|
|
38887
|
+
}
|
|
38888
|
+
.cn-brand-bar__caption {
|
|
38889
|
+
font-size: 13px;
|
|
38890
|
+
white-space: nowrap;
|
|
38891
|
+
opacity: 0.8;
|
|
38892
|
+
}
|
|
38893
|
+
.cn-brand-bar__name {
|
|
38894
|
+
font-size: 18px;
|
|
38895
|
+
font-weight: 700;
|
|
38896
|
+
white-space: nowrap;
|
|
38897
|
+
}
|
|
38898
|
+
.cn-brand-bar__divider {
|
|
38899
|
+
flex: none;
|
|
38900
|
+
width: 1px;
|
|
38901
|
+
height: 28px;
|
|
38902
|
+
background: currentColor;
|
|
38903
|
+
opacity: 0.25;
|
|
38904
|
+
color: var(--color-background-plain-text, var(--color-main-text));
|
|
38905
|
+
}
|
|
38906
|
+
|
|
38907
|
+
|
|
38843
38908
|
.cn-notif-prefs__intro[data-v-72cc0056] {
|
|
38844
38909
|
margin-bottom: 12px;
|
|
38845
38910
|
color: var(--color-text-maxcontrast);
|
|
@@ -60,11 +60,26 @@ function configureCnFetch(config = {}) {
|
|
|
60
60
|
}
|
|
61
61
|
transport = {
|
|
62
62
|
host,
|
|
63
|
-
baseUrl: typeof config.baseUrl === 'string' ? config.baseUrl
|
|
63
|
+
baseUrl: typeof config.baseUrl === 'string' ? stripTrailingSlashes(config.baseUrl) : '',
|
|
64
64
|
credential: config.credential || null,
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Drop trailing `/` characters in one linear pass (a `/\/+$/` pattern
|
|
70
|
+
* backtracks quadratically on a long run of slashes).
|
|
71
|
+
*
|
|
72
|
+
* @param {string} value The string to trim.
|
|
73
|
+
* @return {string} The string without trailing slashes.
|
|
74
|
+
*/
|
|
75
|
+
function stripTrailingSlashes(value) {
|
|
76
|
+
let end = value.length;
|
|
77
|
+
while (end > 0 && value.charCodeAt(end - 1) === 47) {
|
|
78
|
+
end--;
|
|
79
|
+
}
|
|
80
|
+
return value.slice(0, end)
|
|
81
|
+
}
|
|
82
|
+
|
|
68
83
|
/**
|
|
69
84
|
* The bearer credential, resolved.
|
|
70
85
|
*
|
|
@@ -206,5 +221,5 @@ async function cnFetchJson(url, options = {}) {
|
|
|
206
221
|
return parsed
|
|
207
222
|
}
|
|
208
223
|
|
|
209
|
-
export { CN_HOSTS, CnHttpError, cnFetch, cnFetchJson, configureCnFetch };
|
|
224
|
+
export { CN_HOSTS, CnHttpError, cnFetch, cnFetchJson, configureCnFetch, stripTrailingSlashes };
|
|
210
225
|
//# sourceMappingURL=cnFetch.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cnFetch.js","sources":["../../../src/utils/cnFetch.js"],"sourcesContent":["/**\n * The one HTTP client for Conduction Nextcloud apps (ADR-071 Decision 1).\n *\n * ADR-071 names `cnFetch` / `cnFetchJson` as library-owned and says raw\n * `fetch()` with a hand-set requesttoken in app code is review-blocking once\n * shipped. Until now the library did not actually ship them, so every app was\n * left hand-rolling the same four steps — and an ADR that mandates an\n * abstraction nobody provides produces \"violations\" the app cannot fix.\n *\n * This composes helpers this library already had, rather than inventing a\n * parallel idiom:\n *\n * - {@link prefixUrl} — Nextcloud may be served with or without\n * `/index.php`; an API call must use the SAME\n * prefix as the page or the request is rejected.\n * - {@link buildHeaders} — the single blessed CSRF idiom\n * (`requesttoken: OC.requestToken`), plus\n * Content-Type, the OpenRegister organisation\n * header and the translation-target header.\n * - {@link buildQueryString} — consistent query serialisation.\n *\n * The shape is exactly what `visibleWhen.js` (and a dozen call sites like it)\n * already wrote by hand:\n *\n * const response = await fetch(prefixUrl(url), { headers: buildHeaders() })\n * if (!response.ok) throw new Error(`endpoint returned ${response.status}`)\n * const data = await response.json()\n *\n * @module utils/cnFetch\n */\n\nimport { trackedFetch } from './diagnostics.js'\nimport { buildHeaders, buildQueryString, prefixUrl } from './headers.js'\n\n/** The hosts the runtime can boot under. */\nexport const CN_HOSTS = ['nextcloud', 'public']\n\nlet transport = { host: 'nextcloud', baseUrl: '', credential: null }\n\n/**\n * Choose where `cnFetch` runs. The default, `'nextcloud'`, is today's behaviour:\n * the Nextcloud URL prefix and the `requesttoken` header. Under `'public'`\n * (a public origin with no Nextcloud behind it) URLs resolve from `baseUrl`,\n * the request carries `Authorization: Bearer <credential>` and no\n * `requesttoken`, and the credential is stripped from any error `cnFetchJson`\n * throws. An unrecognised host throws; it never falls back to either mode.\n *\n * @spec openspec/changes/public-manifest-runtime/tasks.md#task-2\n * @param {object} [config] The transport.\n * @param {string} [config.host] `'nextcloud'` (default) or `'public'`.\n * @param {string} [config.baseUrl] Public mode: the API base, e.g. `https://portal.example/api`.\n * @param {string|Function} [config.credential] Public mode: the bearer credential, or a function returning it.\n * @throws {Error} When `host` is not one of the accepted values.\n */\nexport function configureCnFetch(config = {}) {\n\tconst host = config.host === undefined ? 'nextcloud' : config.host\n\tif (!CN_HOSTS.includes(host)) {\n\t\tthrow new Error(`cnFetch: unknown host \"${String(host)}\". Accepted values: ${CN_HOSTS.join(', ')}.`)\n\t}\n\ttransport = {\n\t\thost,\n\t\tbaseUrl: typeof config.baseUrl === 'string' ? config.baseUrl.replace(/\\/+$/, '') : '',\n\t\tcredential: config.credential || null,\n\t}\n}\n\n/**\n * The bearer credential, resolved.\n *\n * @return {string} The credential, or ''.\n */\nfunction currentCredential() {\n\tconst c = transport.credential\n\tconst value = typeof c === 'function' ? c() : c\n\treturn typeof value === 'string' ? value : ''\n}\n\n/**\n * Remove the credential from a value before it reaches a log or an error.\n *\n * @param {unknown} value A string, or an object or array holding strings.\n * @return {unknown} The same shape with the credential replaced.\n */\nfunction redact(value) {\n\tconst secret = currentCredential()\n\tif (secret === '' || value === null || value === undefined) {\n\t\treturn value\n\t}\n\tif (typeof value === 'string') {\n\t\treturn value.split(secret).join('[redacted]')\n\t}\n\tif (typeof value === 'object') {\n\t\ttry {\n\t\t\treturn JSON.parse(JSON.stringify(value).split(secret).join('[redacted]'))\n\t\t} catch {\n\t\t\treturn value\n\t\t}\n\t}\n\treturn value\n}\n\n/**\n * An HTTP error carrying the status and the parsed body, so a caller can\n * branch on the status without re-reading a consumed stream.\n *\n * A bare `Error(message)` loses the status, which is the single most useful\n * fact about a failed request — callers then re-parse the message string to\n * recover it, and that is how a 404-vs-403 distinction gets lost.\n */\nexport class CnHttpError extends Error {\n\t/**\n\t * @param {string} message Human-readable message.\n\t * @param {number} status HTTP status code.\n\t * @param {unknown} body Parsed response body, or the raw text when it was not JSON.\n\t * @param {string} url The requested URL, for logs.\n\t */\n\tconstructor(message, status, body, url) {\n\t\tsuper(message)\n\t\tthis.name = 'CnHttpError'\n\t\tthis.status = status\n\t\tthis.body = body\n\t\tthis.url = url\n\t}\n}\n\n/**\n * `fetch` with Nextcloud's URL prefix and headers applied.\n *\n * Returns the raw `Response` untouched — it does NOT throw on a non-2xx, so a\n * caller that legitimately expects a 404 (a probe) can read `response.status`\n * without exception handling. Use {@link cnFetchJson} for the common case.\n *\n * @param {string} url App-absolute path, e.g. `/apps/openregister/api/objects`.\n * @param {object} [options] Standard `fetch` options.\n * @param {object} [options.query] Serialised via {@link buildQueryString} and appended.\n * @param {object|string|null} [options.headerOptions] Passed to {@link buildHeaders}.\n * @return {Promise<Response>} The raw response.\n */\nexport async function cnFetch(url, options = {}) {\n\tconst { query, headerOptions, headers, ...rest } = options\n\tconst qs = query ? buildQueryString(query) : ''\n\t// Caller-supplied headers win, so a one-off Content-Type or Accept does not\n\t// require bypassing this helper entirely — bypassing is how apps end up\n\t// hand-setting requesttoken again.\n\tif (transport.host === 'public') {\n\t\t// No Nextcloud here: bearer auth, the configured base, no requesttoken.\n\t\tconst contentType = (headerOptions && typeof headerOptions === 'object' && Object.hasOwn(headerOptions, 'contentType'))\n\t\t\t? headerOptions.contentType\n\t\t\t: (typeof headerOptions === 'string' ? headerOptions : 'application/json')\n\t\tconst base = { Accept: 'application/json', ...(contentType ? { 'Content-Type': contentType } : {}) }\n\t\tconst credential = currentCredential()\n\t\tconst publicHeaders = { ...base, ...(credential ? { Authorization: `Bearer ${credential}` } : {}), ...(headers || {}) }\n\t\tconst target = /^[a-z][a-z0-9+.-]*:/i.test(url) ? url : `${transport.baseUrl}${url}`\n\t\treturn trackedFetch(`${target}${qs}`, { ...rest, headers: publicHeaders }, 'cnFetch')\n\t}\n\tconst merged = { ...buildHeaders(headerOptions), ...(headers || {}) }\n\treturn trackedFetch(prefixUrl(`${url}${qs}`), { ...rest, headers: merged }, 'cnFetch')\n}\n\n/**\n * `cnFetch` + JSON parsing + error normalisation.\n *\n * Throws {@link CnHttpError} on a non-2xx, carrying the status and the parsed\n * body. Returns the parsed body on success.\n *\n * A 204 (and any empty body) resolves to `null` rather than throwing on an\n * empty JSON parse — DELETE endpoints in this stack answer 204, and making the\n * happy path throw is how callers learn to wrap everything in try/catch and\n * swallow real errors along the way.\n *\n * @param {string} url App-absolute path.\n * @param {object} [options] As {@link cnFetch}.\n * @throws {CnHttpError} When the response status is not 2xx.\n * @return {Promise<unknown>} Parsed JSON body, or null for an empty body.\n */\nexport async function cnFetchJson(url, options = {}) {\n\tconst response = await cnFetch(url, options)\n\n\t// Read the body ONCE. A Response body is a stream and can only be consumed\n\t// a single time, so the error path must reuse what was already read rather\n\t// than calling .json() again on a spent stream.\n\tconst raw = await response.text()\n\tlet parsed = null\n\tif (raw !== '') {\n\t\ttry {\n\t\t\tparsed = JSON.parse(raw)\n\t\t} catch {\n\t\t\tparsed = raw\n\t\t}\n\t}\n\n\tif (!response.ok) {\n\t\tconst detail\n\t\t\t= parsed && typeof parsed === 'object' && typeof parsed.error === 'string'\n\t\t\t\t? parsed.error\n\t\t\t\t: response.statusText || 'request failed'\n\t\tthrow new CnHttpError(\n\t\t\tredact(`${url} returned ${response.status}: ${detail}`),\n\t\t\tresponse.status,\n\t\t\tredact(parsed),\n\t\t\turl,\n\t\t)\n\t}\n\n\treturn parsed\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cnFetch.js","sources":["../../../src/utils/cnFetch.js"],"sourcesContent":["/**\n * The one HTTP client for Conduction Nextcloud apps (ADR-071 Decision 1).\n *\n * ADR-071 names `cnFetch` / `cnFetchJson` as library-owned and says raw\n * `fetch()` with a hand-set requesttoken in app code is review-blocking once\n * shipped. Until now the library did not actually ship them, so every app was\n * left hand-rolling the same four steps — and an ADR that mandates an\n * abstraction nobody provides produces \"violations\" the app cannot fix.\n *\n * This composes helpers this library already had, rather than inventing a\n * parallel idiom:\n *\n * - {@link prefixUrl} — Nextcloud may be served with or without\n * `/index.php`; an API call must use the SAME\n * prefix as the page or the request is rejected.\n * - {@link buildHeaders} — the single blessed CSRF idiom\n * (`requesttoken: OC.requestToken`), plus\n * Content-Type, the OpenRegister organisation\n * header and the translation-target header.\n * - {@link buildQueryString} — consistent query serialisation.\n *\n * The shape is exactly what `visibleWhen.js` (and a dozen call sites like it)\n * already wrote by hand:\n *\n * const response = await fetch(prefixUrl(url), { headers: buildHeaders() })\n * if (!response.ok) throw new Error(`endpoint returned ${response.status}`)\n * const data = await response.json()\n *\n * @module utils/cnFetch\n */\n\nimport { trackedFetch } from './diagnostics.js'\nimport { buildHeaders, buildQueryString, prefixUrl } from './headers.js'\n\n/** The hosts the runtime can boot under. */\nexport const CN_HOSTS = ['nextcloud', 'public']\n\nlet transport = { host: 'nextcloud', baseUrl: '', credential: null }\n\n/**\n * Choose where `cnFetch` runs. The default, `'nextcloud'`, is today's behaviour:\n * the Nextcloud URL prefix and the `requesttoken` header. Under `'public'`\n * (a public origin with no Nextcloud behind it) URLs resolve from `baseUrl`,\n * the request carries `Authorization: Bearer <credential>` and no\n * `requesttoken`, and the credential is stripped from any error `cnFetchJson`\n * throws. An unrecognised host throws; it never falls back to either mode.\n *\n * @spec openspec/changes/public-manifest-runtime/tasks.md#task-2\n * @param {object} [config] The transport.\n * @param {string} [config.host] `'nextcloud'` (default) or `'public'`.\n * @param {string} [config.baseUrl] Public mode: the API base, e.g. `https://portal.example/api`.\n * @param {string|Function} [config.credential] Public mode: the bearer credential, or a function returning it.\n * @throws {Error} When `host` is not one of the accepted values.\n */\nexport function configureCnFetch(config = {}) {\n\tconst host = config.host === undefined ? 'nextcloud' : config.host\n\tif (!CN_HOSTS.includes(host)) {\n\t\tthrow new Error(`cnFetch: unknown host \"${String(host)}\". Accepted values: ${CN_HOSTS.join(', ')}.`)\n\t}\n\ttransport = {\n\t\thost,\n\t\tbaseUrl: typeof config.baseUrl === 'string' ? stripTrailingSlashes(config.baseUrl) : '',\n\t\tcredential: config.credential || null,\n\t}\n}\n\n/**\n * Drop trailing `/` characters in one linear pass (a `/\\/+$/` pattern\n * backtracks quadratically on a long run of slashes).\n *\n * @param {string} value The string to trim.\n * @return {string} The string without trailing slashes.\n */\nexport function stripTrailingSlashes(value) {\n\tlet end = value.length\n\twhile (end > 0 && value.charCodeAt(end - 1) === 47) {\n\t\tend--\n\t}\n\treturn value.slice(0, end)\n}\n\n/**\n * The bearer credential, resolved.\n *\n * @return {string} The credential, or ''.\n */\nfunction currentCredential() {\n\tconst c = transport.credential\n\tconst value = typeof c === 'function' ? c() : c\n\treturn typeof value === 'string' ? value : ''\n}\n\n/**\n * Remove the credential from a value before it reaches a log or an error.\n *\n * @param {unknown} value A string, or an object or array holding strings.\n * @return {unknown} The same shape with the credential replaced.\n */\nfunction redact(value) {\n\tconst secret = currentCredential()\n\tif (secret === '' || value === null || value === undefined) {\n\t\treturn value\n\t}\n\tif (typeof value === 'string') {\n\t\treturn value.split(secret).join('[redacted]')\n\t}\n\tif (typeof value === 'object') {\n\t\ttry {\n\t\t\treturn JSON.parse(JSON.stringify(value).split(secret).join('[redacted]'))\n\t\t} catch {\n\t\t\treturn value\n\t\t}\n\t}\n\treturn value\n}\n\n/**\n * An HTTP error carrying the status and the parsed body, so a caller can\n * branch on the status without re-reading a consumed stream.\n *\n * A bare `Error(message)` loses the status, which is the single most useful\n * fact about a failed request — callers then re-parse the message string to\n * recover it, and that is how a 404-vs-403 distinction gets lost.\n */\nexport class CnHttpError extends Error {\n\t/**\n\t * @param {string} message Human-readable message.\n\t * @param {number} status HTTP status code.\n\t * @param {unknown} body Parsed response body, or the raw text when it was not JSON.\n\t * @param {string} url The requested URL, for logs.\n\t */\n\tconstructor(message, status, body, url) {\n\t\tsuper(message)\n\t\tthis.name = 'CnHttpError'\n\t\tthis.status = status\n\t\tthis.body = body\n\t\tthis.url = url\n\t}\n}\n\n/**\n * `fetch` with Nextcloud's URL prefix and headers applied.\n *\n * Returns the raw `Response` untouched — it does NOT throw on a non-2xx, so a\n * caller that legitimately expects a 404 (a probe) can read `response.status`\n * without exception handling. Use {@link cnFetchJson} for the common case.\n *\n * @param {string} url App-absolute path, e.g. `/apps/openregister/api/objects`.\n * @param {object} [options] Standard `fetch` options.\n * @param {object} [options.query] Serialised via {@link buildQueryString} and appended.\n * @param {object|string|null} [options.headerOptions] Passed to {@link buildHeaders}.\n * @return {Promise<Response>} The raw response.\n */\nexport async function cnFetch(url, options = {}) {\n\tconst { query, headerOptions, headers, ...rest } = options\n\tconst qs = query ? buildQueryString(query) : ''\n\t// Caller-supplied headers win, so a one-off Content-Type or Accept does not\n\t// require bypassing this helper entirely — bypassing is how apps end up\n\t// hand-setting requesttoken again.\n\tif (transport.host === 'public') {\n\t\t// No Nextcloud here: bearer auth, the configured base, no requesttoken.\n\t\tconst contentType = (headerOptions && typeof headerOptions === 'object' && Object.hasOwn(headerOptions, 'contentType'))\n\t\t\t? headerOptions.contentType\n\t\t\t: (typeof headerOptions === 'string' ? headerOptions : 'application/json')\n\t\tconst base = { Accept: 'application/json', ...(contentType ? { 'Content-Type': contentType } : {}) }\n\t\tconst credential = currentCredential()\n\t\tconst publicHeaders = { ...base, ...(credential ? { Authorization: `Bearer ${credential}` } : {}), ...(headers || {}) }\n\t\tconst target = /^[a-z][a-z0-9+.-]*:/i.test(url) ? url : `${transport.baseUrl}${url}`\n\t\treturn trackedFetch(`${target}${qs}`, { ...rest, headers: publicHeaders }, 'cnFetch')\n\t}\n\tconst merged = { ...buildHeaders(headerOptions), ...(headers || {}) }\n\treturn trackedFetch(prefixUrl(`${url}${qs}`), { ...rest, headers: merged }, 'cnFetch')\n}\n\n/**\n * `cnFetch` + JSON parsing + error normalisation.\n *\n * Throws {@link CnHttpError} on a non-2xx, carrying the status and the parsed\n * body. Returns the parsed body on success.\n *\n * A 204 (and any empty body) resolves to `null` rather than throwing on an\n * empty JSON parse — DELETE endpoints in this stack answer 204, and making the\n * happy path throw is how callers learn to wrap everything in try/catch and\n * swallow real errors along the way.\n *\n * @param {string} url App-absolute path.\n * @param {object} [options] As {@link cnFetch}.\n * @throws {CnHttpError} When the response status is not 2xx.\n * @return {Promise<unknown>} Parsed JSON body, or null for an empty body.\n */\nexport async function cnFetchJson(url, options = {}) {\n\tconst response = await cnFetch(url, options)\n\n\t// Read the body ONCE. A Response body is a stream and can only be consumed\n\t// a single time, so the error path must reuse what was already read rather\n\t// than calling .json() again on a spent stream.\n\tconst raw = await response.text()\n\tlet parsed = null\n\tif (raw !== '') {\n\t\ttry {\n\t\t\tparsed = JSON.parse(raw)\n\t\t} catch {\n\t\t\tparsed = raw\n\t\t}\n\t}\n\n\tif (!response.ok) {\n\t\tconst detail\n\t\t\t= parsed && typeof parsed === 'object' && typeof parsed.error === 'string'\n\t\t\t\t? parsed.error\n\t\t\t\t: response.statusText || 'request failed'\n\t\tthrow new CnHttpError(\n\t\t\tredact(`${url} returned ${response.status}: ${detail}`),\n\t\t\tresponse.status,\n\t\t\tredact(parsed),\n\t\t\turl,\n\t\t)\n\t}\n\n\treturn parsed\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* Reads a declared brand (`nav.brand`, or the `brand` prop of CnAppNav and
|
|
6
|
+
* CnAppRoot) into the one shape the brand block and the brand bar draw.
|
|
7
|
+
*
|
|
8
|
+
* @spec openspec/changes/screens-brand-block-top-bar/tasks.md
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Resolve a declared brand.
|
|
13
|
+
*
|
|
14
|
+
* @param {object|null|undefined} declared The declared brand.
|
|
15
|
+
* @param {(text: string) => string} translate Label lookup for name, caption and alt.
|
|
16
|
+
* @return {{logo: string, emblem: (string|boolean), name: string, caption: string, alt: string, placement: string}|null}
|
|
17
|
+
* The brand, or null when nothing is declared that can be drawn.
|
|
18
|
+
*/
|
|
19
|
+
function resolveBrand(declared, translate) {
|
|
20
|
+
if (!declared || typeof declared !== 'object') {
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
const text = (value) => (typeof value === 'string' && value !== '' ? translate(value) : '');
|
|
24
|
+
const brand = {
|
|
25
|
+
logo: typeof declared.logo === 'string' ? declared.logo : '',
|
|
26
|
+
// A URL, or `true` for the theme's emblem (--nldesign-emblem-url).
|
|
27
|
+
emblem: declared.emblem === true ? true : (typeof declared.emblem === 'string' ? declared.emblem : ''),
|
|
28
|
+
name: text(declared.name),
|
|
29
|
+
caption: text(declared.caption),
|
|
30
|
+
alt: text(declared.alt),
|
|
31
|
+
// '' means "not declared": the caller decides the default from the look.
|
|
32
|
+
placement: declared.placement === 'nav' || declared.placement === 'header' ? declared.placement : '',
|
|
33
|
+
};
|
|
34
|
+
return (brand.logo || brand.emblem || brand.name || brand.caption) ? brand : null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export { resolveBrand as default, resolveBrand };
|
|
38
|
+
//# sourceMappingURL=resolveBrand.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolveBrand.js","sources":["../../../src/utils/resolveBrand.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * Reads a declared brand (`nav.brand`, or the `brand` prop of CnAppNav and\n * CnAppRoot) into the one shape the brand block and the brand bar draw.\n *\n * @spec openspec/changes/screens-brand-block-top-bar/tasks.md\n */\n\n/**\n * Resolve a declared brand.\n *\n * @param {object|null|undefined} declared The declared brand.\n * @param {(text: string) => string} translate Label lookup for name, caption and alt.\n * @return {{logo: string, emblem: (string|boolean), name: string, caption: string, alt: string, placement: string}|null}\n * The brand, or null when nothing is declared that can be drawn.\n */\nexport function resolveBrand(declared, translate) {\n\tif (!declared || typeof declared !== 'object') {\n\t\treturn null\n\t}\n\tconst text = (value) => (typeof value === 'string' && value !== '' ? translate(value) : '')\n\tconst brand = {\n\t\tlogo: typeof declared.logo === 'string' ? declared.logo : '',\n\t\t// A URL, or `true` for the theme's emblem (--nldesign-emblem-url).\n\t\temblem: declared.emblem === true ? true : (typeof declared.emblem === 'string' ? declared.emblem : ''),\n\t\tname: text(declared.name),\n\t\tcaption: text(declared.caption),\n\t\talt: text(declared.alt),\n\t\t// '' means \"not declared\": the caller decides the default from the look.\n\t\tplacement: declared.placement === 'nav' || declared.placement === 'header' ? declared.placement : '',\n\t}\n\treturn (brand.logo || brand.emblem || brand.name || brand.caption) ? brand : null\n}\n\nexport default resolveBrand\n"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,YAAY,CAAC,QAAQ,EAAE,SAAS,EAAE;AAClD,CAAC,IAAI,CAAC,QAAQ,IAAI,OAAO,QAAQ,KAAK,QAAQ,EAAE;AAChD,EAAE,OAAO,IAAI;AACb,EAAE;AACF,CAAC,MAAM,IAAI,GAAG,CAAC,KAAK,MAAM,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,EAAE,GAAG,SAAS,CAAC,KAAK,CAAC,GAAG,EAAE,EAAC;AAC5F,CAAC,MAAM,KAAK,GAAG;AACf,EAAE,IAAI,EAAE,OAAO,QAAQ,CAAC,IAAI,KAAK,QAAQ,GAAG,QAAQ,CAAC,IAAI,GAAG,EAAE;AAC9D;AACA,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,KAAK,IAAI,GAAG,IAAI,IAAI,OAAO,QAAQ,CAAC,MAAM,KAAK,QAAQ,GAAG,QAAQ,CAAC,MAAM,GAAG,EAAE,CAAC;AACxG,EAAE,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;AAC3B,EAAE,OAAO,EAAE,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;AACjC,EAAE,GAAG,EAAE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;AACzB;AACA,EAAE,SAAS,EAAE,QAAQ,CAAC,SAAS,KAAK,KAAK,IAAI,QAAQ,CAAC,SAAS,KAAK,QAAQ,GAAG,QAAQ,CAAC,SAAS,GAAG,EAAE;AACtG,GAAE;AACF,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,OAAO,IAAI,KAAK,GAAG,IAAI;AAClF;;;;"}
|