@conduction/nextcloud-vue 2.77.0 → 2.79.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 (159) hide show
  1. package/dist/bundled-packages.json +0 -2
  2. package/dist/esm/bundled-packages.json +0 -2
  3. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  4. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +113 -104
  5. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
  6. package/dist/esm/components/CnAiCompanion/CnAiCompanion.vue2.js +23 -0
  7. package/dist/esm/components/CnAiCompanion/CnAiCompanion.vue2.js.map +1 -1
  8. package/dist/esm/components/CnAiCompanion/CnAiCompanion.vue3.js.map +1 -1
  9. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +12 -2
  10. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  11. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  12. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +2 -1
  13. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
  14. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
  15. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +117 -0
  16. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  17. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +122 -97
  18. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  19. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +37 -0
  20. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  21. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js +2 -2
  22. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  23. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +168 -5
  24. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  25. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +140 -92
  26. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  27. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +84 -2
  28. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  29. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +4 -2
  30. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  31. package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue.js +1 -1
  32. package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue2.js +45 -2
  33. package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue2.js.map +1 -1
  34. package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue3.js.map +1 -1
  35. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +61 -1
  36. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  37. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  38. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +173 -5
  39. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  40. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +181 -99
  41. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  42. package/dist/esm/components/CnFilesBrowser/filesBrowser.js +29 -11
  43. package/dist/esm/components/CnFilesBrowser/filesBrowser.js.map +1 -1
  44. package/dist/esm/components/CnFilesBrowser/openRegisterSource.js +252 -0
  45. package/dist/esm/components/CnFilesBrowser/openRegisterSource.js.map +1 -0
  46. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +8 -2
  47. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  48. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  49. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +4 -1
  50. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
  51. package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
  52. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +31 -2
  53. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  54. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +3 -2
  55. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  56. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +100 -3
  57. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  58. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +4 -2
  59. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  60. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js +3 -2
  61. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js.map +1 -1
  62. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js.map +1 -1
  63. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue2.js.map +1 -1
  64. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js +1 -1
  65. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js.map +1 -1
  66. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +36 -0
  67. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  68. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +35 -23
  69. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  70. package/dist/esm/components/CnStatusBadge/CnStatusBadge.vue2.js +6 -2
  71. package/dist/esm/components/CnStatusBadge/CnStatusBadge.vue2.js.map +1 -1
  72. package/dist/esm/components/CnStatusBadge/CnStatusBadge.vue3.js.map +1 -1
  73. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +30 -1
  74. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  75. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  76. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +13 -0
  77. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  78. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  79. package/dist/esm/composables/useAppManifest.js +8 -3
  80. package/dist/esm/composables/useAppManifest.js.map +1 -1
  81. package/dist/esm/composables/useFormDraft.js +11 -1
  82. package/dist/esm/composables/useFormDraft.js.map +1 -1
  83. package/dist/esm/composables/useObjectPresence.js +15 -4
  84. package/dist/esm/composables/useObjectPresence.js.map +1 -1
  85. package/dist/esm/index.js +1 -0
  86. package/dist/esm/index.js.map +1 -1
  87. package/dist/esm/l10n/en.json.js +68 -1
  88. package/dist/esm/l10n/en.json.js.map +1 -1
  89. package/dist/esm/l10n/nl.json.js +113 -2
  90. package/dist/esm/l10n/nl.json.js.map +1 -1
  91. package/dist/esm/nextcloud-vue.css +526 -16
  92. package/dist/esm/utils/badgeVariants.js +32 -0
  93. package/dist/esm/utils/badgeVariants.js.map +1 -0
  94. package/dist/esm/utils/boardDate.js +193 -0
  95. package/dist/esm/utils/boardDate.js.map +1 -0
  96. package/dist/esm/utils/dynamicProperties.js +5 -1
  97. package/dist/esm/utils/dynamicProperties.js.map +1 -1
  98. package/dist/esm/utils/validateManifestV2.compiled.js +57 -6
  99. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  100. package/dist/nextcloud-vue.cjs.js +2569 -811
  101. package/dist/nextcloud-vue.cjs.js.map +2 -2
  102. package/dist/nextcloud-vue.css +526 -16
  103. package/l10n/en.json +68 -1
  104. package/l10n/nl.json +113 -2
  105. package/package.json +1 -1
  106. package/scripts/build-validators.js +26 -0
  107. package/src/components/CnActionsBar/CnActionsBar.vue +48 -39
  108. package/src/components/CnAiCompanion/CnAiCompanion.vue +23 -0
  109. package/src/components/CnAppRoot/CnAppRoot.vue +12 -2
  110. package/src/components/CnBoardView/CnBoardView.vue +2 -1
  111. package/src/components/CnCellRenderer/CnCellRenderer.md +1 -0
  112. package/src/components/CnCellRenderer/CnCellRenderer.vue +169 -10
  113. package/src/components/CnChartWidget/CnChartWidget.vue +39 -2
  114. package/src/components/CnDashboardPage/CnDashboardPage.vue +248 -8
  115. package/src/components/CnDataTable/CnDataTable.md +2 -0
  116. package/src/components/CnDataTable/CnDataTable.vue +88 -4
  117. package/src/components/CnDateRangePicker/CnDateRangePicker.vue +44 -1
  118. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +61 -1
  119. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +232 -11
  120. package/src/components/CnFilesBrowser/filesBrowser.js +43 -2
  121. package/src/components/CnFilesBrowser/index.js +1 -0
  122. package/src/components/CnFilesBrowser/openRegisterSource.js +248 -0
  123. package/src/components/CnFormDialog/CnFormDialog.vue +8 -2
  124. package/src/components/CnFormPage/CnFormPage.vue +4 -1
  125. package/src/components/CnIndexPage/CnIndexPage.vue +33 -3
  126. package/src/components/CnObjectSidebar/CnFilesTab.vue +103 -4
  127. package/src/components/CnQuickFilterBar/CnQuickFilterBar.vue +3 -2
  128. package/src/components/CnSavedViewsControl/CnSavedViewsControl.vue +1 -1
  129. package/src/components/CnStatWidget/CnStatWidget.vue +52 -2
  130. package/src/components/CnStatusBadge/CnStatusBadge.vue +6 -2
  131. package/src/components/CnTabsWidget/CnTabsWidget.vue +30 -1
  132. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +13 -0
  133. package/src/components/index.js +1 -1
  134. package/src/composables/__tests__/useObjectPresence.spec.js +24 -0
  135. package/src/composables/useAppManifest.js +8 -3
  136. package/src/composables/useFormDraft.js +11 -1
  137. package/src/composables/useObjectPresence.js +15 -4
  138. package/src/css/badge.css +31 -0
  139. package/src/css/kpi-card.css +51 -0
  140. package/src/css/look-board-detail.css +10 -0
  141. package/src/css/look-board-index.css +128 -14
  142. package/src/css/look-board.css +85 -0
  143. package/src/css/table.css +37 -0
  144. package/src/index.js +4 -0
  145. package/src/schemas/app-manifest-v2.schema.json +50 -5
  146. package/src/utils/badgeVariants.js +29 -0
  147. package/src/utils/boardDate.js +189 -0
  148. package/src/utils/dynamicProperties.js +5 -1
  149. package/src/utils/validateManifestV2.compiled.js +57 -2
  150. package/dist/esm/_virtual/equal.js +0 -4
  151. package/dist/esm/_virtual/equal.js.map +0 -1
  152. package/dist/esm/_virtual/ucs2length.js +0 -4
  153. package/dist/esm/_virtual/ucs2length.js.map +0 -1
  154. package/dist/esm/node_modules/ajv/dist/runtime/equal.js +0 -9
  155. package/dist/esm/node_modules/ajv/dist/runtime/equal.js.map +0 -1
  156. package/dist/esm/node_modules/ajv/dist/runtime/ucs2length.js +0 -25
  157. package/dist/esm/node_modules/ajv/dist/runtime/ucs2length.js.map +0 -1
  158. package/dist/esm/node_modules/fast-deep-equal/index.js +0 -47
  159. package/dist/esm/node_modules/fast-deep-equal/index.js.map +0 -1
@@ -254,6 +254,11 @@ export default {
254
254
  cnLook: { default: 'nextcloud' },
255
255
  /** The tab list's name under the board look, provided by CnDetailPage (`config.tabsLabel`). */
256
256
  cnDetailTabsLabel: { default: null },
257
+ /**
258
+ * The host's translate (CnAppRoot provides `cnTranslate`), so a tab
259
+ * label written in the manifest reads in the user's language.
260
+ */
261
+ cnTranslate: { default: () => (key) => key },
257
262
  },
258
263
 
259
264
  props: {
@@ -471,7 +476,7 @@ export default {
471
476
  key: `${widgetId || 'tab'}-${index}`,
472
477
  id: (tab && typeof tab === 'object' && tab.id) || widgetId || '',
473
478
  widgetId,
474
- label: (tab && tab.label) || (this.isBoard && isActivityWidget(widget) ? t('nextcloud-vue', 'History') : '') || widgetTitleOf(widget) || widgetId || '',
479
+ label: this.tabLabel(tab, widget, widgetId),
475
480
  icon: (tab && tab.icon) || widget?.icon || '',
476
481
  widget,
477
482
  count,
@@ -652,6 +657,30 @@ export default {
652
657
  },
653
658
 
654
659
  methods: {
660
+ /**
661
+ * The label a tab shows: the authored `label`, else (board look) History
662
+ * for the activity widget, else the child widget's title, else its id.
663
+ * A manifest label or title goes through the host's translate; the
664
+ * library's own History is translated by the library catalogue.
665
+ *
666
+ * @param {object|string} tab The `content.tabs[]` entry.
667
+ * @param {object|null} widget The widget definition it names.
668
+ * @param {string} widgetId The widget id.
669
+ * @return {string} The label.
670
+ * @spec openspec/changes/screens-detail-labels-parity/specs/detail-labels-board-look/spec.md#requirement-detail-labels-read-in-the-users-language
671
+ */
672
+ tabLabel(tab, widget, widgetId) {
673
+ const translate = typeof this.cnTranslate === 'function' ? this.cnTranslate : (key) => key
674
+ if (tab && typeof tab.label === 'string' && tab.label !== '') {
675
+ return translate(tab.label)
676
+ }
677
+ if (this.isBoard && isActivityWidget(widget)) {
678
+ return t('nextcloud-vue', 'History')
679
+ }
680
+ const title = widgetTitleOf(widget)
681
+ return title ? translate(title) : (widgetId || '')
682
+ },
683
+
655
684
  /**
656
685
  * Whether a tab shows: `visible`, `hidden`, or `pending` while its
657
686
  * condition cannot be decided yet (object not loaded, count not back).
@@ -323,6 +323,19 @@ export default {
323
323
  default: false,
324
324
  },
325
325
 
326
+ /**
327
+ * Fit the table to the tile: the title column (a column's `grow: true`,
328
+ * else `title` or `name`, else the first) shrinks and ends in an
329
+ * ellipsis so the trailing columns stay visible. On by default; pass
330
+ * `false` for the table's own auto layout. Forwarded to CnDataTable.
331
+ *
332
+ * @spec openspec/changes/narrow-object-table-widgets/specs/narrow-object-table-widgets/spec.md#requirement-a-narrow-object-table-keeps-its-trailing-columns-in-view
333
+ */
334
+ fitWidth: {
335
+ type: Boolean,
336
+ default: true,
337
+ },
338
+
326
339
  /**
327
340
  * Route NAME for row-click navigation. Mapped onto CnDataTable's
328
341
  * `rowClickRoute` as `{ name: rowRoute, params: { id } }` with the
@@ -109,7 +109,7 @@ export { CnTreeView } from './CnTreeView/index.js'
109
109
  export { CnUserActionMenu } from './CnUserActionMenu/index.js'
110
110
  export { CnNotesCard } from './CnNotesCard/index.js'
111
111
  export { CnTasksCard } from './CnTasksCard/index.js'
112
- export { CnFilesBrowser } from './CnFilesBrowser/index.js'
112
+ export { CnFilesBrowser, createOpenRegisterSource } from './CnFilesBrowser/index.js'
113
113
  export { CnFilesCard } from './CnFilesCard/index.js'
114
114
  export { CnFileManager } from './CnFileManager/index.js'
115
115
  export { CnRelatedFiles } from './CnRelatedFiles/index.js'
@@ -40,6 +40,7 @@ jest.mock('@nextcloud/router', () => ({
40
40
  jest.mock('@nextcloud/auth', () => ({
41
41
  __esModule: true,
42
42
  getCurrentUser: () => ({ uid: 'anna' }),
43
+ getRequestToken: () => 'token-123',
43
44
  }))
44
45
 
45
46
  /** The subscription the composable made, so a test can push into it. */
@@ -246,6 +247,29 @@ describe('useObjectPresence', () => {
246
247
  scope.stop()
247
248
  })
248
249
 
250
+ it('🔴 a closing tab departs with a beacon that carries the request token', async () => {
251
+ // A beacon cannot set headers, and the departure route keeps the CSRF
252
+ // check, so the token has to travel in the form body. The old beacon
253
+ // sent no body at all and could never pass that check.
254
+ const sendBeacon = jest.fn(() => true)
255
+ const original = navigator.sendBeacon
256
+ Object.defineProperty(navigator, 'sendBeacon', { value: sendBeacon, configurable: true, writable: true })
257
+ try {
258
+ await withPresence(async () => {
259
+ await nextTick()
260
+ await Promise.resolve()
261
+ window.dispatchEvent(new Event('beforeunload'))
262
+ })
263
+ } finally {
264
+ Object.defineProperty(navigator, 'sendBeacon', { value: original, configurable: true, writable: true })
265
+ }
266
+
267
+ const call = sendBeacon.mock.calls.find(([url]) => url === '/apps/openregister/api/objects/dossiq/case/case-1/presence?_method=DELETE')
268
+ expect(call).toBeDefined()
269
+ expect(call[1]).toBeInstanceOf(FormData)
270
+ expect(call[1].get('requesttoken')).toBe('token-123')
271
+ })
272
+
249
273
  it('does nothing at all when disabled', async () => {
250
274
  const scope = effectScope()
251
275
  scope.run(() => {
@@ -205,6 +205,7 @@ function loadInMemory(input) {
205
205
  * @param {object} options Configuration options (endpoint / fetcher /
206
206
  * getAppConfigValue overrides).
207
207
  * @return {{ manifest: import('vue').Ref<object>, isLoading: import('vue').Ref<boolean>, validationErrors: import('vue').Ref<string[]|null>, unresolvedSentinels: import('vue').Ref<string[]> }}
208
+ * @spec openspec/changes/manifest-validator-self-contained/specs/manifest-validator-self-contained/spec.md#requirement-a-backend-manifest-delta-reaches-the-navigation
208
209
  */
209
210
  function loadFromBackend(appId, bundledManifest, options) {
210
211
  // Shallow by default (manifest-shallow-reactivity-by-default) — see
@@ -284,9 +285,13 @@ function loadFromBackend(appId, bundledManifest, options) {
284
285
  // The shallowRef reassignment is what re-renders consumers; the new
285
286
  // object stays shallow (not deep-observed) unless CnAppRoot upgrades it.
286
287
  manifest.value = resolved
287
- } catch {
288
- // Defensive: any unexpected error leaves the bundled manifest in
289
- // place. Apps without a backend endpoint keep working.
288
+ } catch (error) {
289
+ // Any unexpected error leaves the bundled manifest in place, so
290
+ // apps keep working. Say so: a silent catch here once hid a
291
+ // validator that threw on every manifest, and the backend delta
292
+ // never reached the navigation.
293
+ // eslint-disable-next-line no-console
294
+ console.warn('[useAppManifest] Could not resolve the manifest; keeping the bundled manifest.', error)
290
295
  } finally {
291
296
  isLoading.value = false
292
297
  }
@@ -127,12 +127,22 @@ export function readDraft(key, { objectUpdated = '', now = Date.now() } = {}) {
127
127
  * What the draft indicator says: "Saving" while a local write is pending,
128
128
  * "Saved just now" / "Saved 3 minutes ago" once it landed, nothing before that.
129
129
  *
130
+ * After a failed save (`options.saveFailed`) it never says Saving or Saved:
131
+ * next to a server error those words read as if the record had been saved.
132
+ * It then says "Draft kept on this device" when a local copy is stored, and
133
+ * nothing otherwise.
134
+ *
130
135
  * @param {string} state `idle`, `saving` or `saved`.
131
136
  * @param {number} [savedAt] When the last write landed, in epoch ms.
132
137
  * @param {number} [now] The current epoch ms, for testing.
138
+ * @param {{saveFailed?: boolean}} [options] `saveFailed`: the form shows a failed save.
133
139
  * @return {string} The text, or '' when it says nothing.
140
+ * @spec openspec/changes/cell-labels-and-draft-indicator/specs/cell-labels-and-draft-indicator/spec.md#requirement-the-draft-indicator-never-reads-as-a-server-save-after-a-failed-save
134
141
  */
135
- export function draftIndicatorText(state, savedAt = 0, now = Date.now()) {
142
+ export function draftIndicatorText(state, savedAt = 0, now = Date.now(), options = {}) {
143
+ if (options && options.saveFailed === true) {
144
+ return state === 'saved' ? t('nextcloud-vue', 'Draft kept on this device') : ''
145
+ }
136
146
  if (state === 'saving') {
137
147
  return t('nextcloud-vue', 'Saving')
138
148
  }
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  // Copyright (C) 2026 Conduction B.V.
3
3
 
4
- import { getCurrentUser } from '@nextcloud/auth'
4
+ import { getCurrentUser, getRequestToken } from '@nextcloud/auth'
5
5
  import axios from '@nextcloud/axios'
6
6
  import { generateUrl } from '@nextcloud/router'
7
7
  import { computed, getCurrentScope, onScopeDispose, ref, unref, watch } from 'vue'
@@ -201,7 +201,15 @@ export function useObjectPresence(register, schema, objectUuid, options = {}) {
201
201
  * the browser and survives the navigation, which is the only mechanism
202
202
  * that does.
203
203
  *
204
+ * 🔴 THE TOKEN RIDES IN THE BODY. A beacon cannot set headers, so the
205
+ * `requesttoken` header axios adds is missing, and the departure route
206
+ * keeps Nextcloud's CSRF check. Nextcloud also reads the token from a
207
+ * POST field, so it goes in a form body. Without it the beacon answers
208
+ * 412; before OpenRegister registered POST on `/presence` it answered 405
209
+ * (seen on cloud.conduction.nl on every dossiq case page).
210
+ *
204
211
  * @return {void}
212
+ * @spec openspec/changes/no-calls-to-missing-apps-and-presence-beacon/specs/object-presence/spec.md#requirement-a-closing-tab-departs-through-a-route-that-exists
205
213
  */
206
214
  function beaconDepart() {
207
215
  const url = presenceUrl()
@@ -209,9 +217,12 @@ export function useObjectPresence(register, schema, objectUuid, options = {}) {
209
217
  return
210
218
  }
211
219
  try {
212
- // No DELETE from a beacon — it is always a POST — so the server's
213
- // own POST release path is used. Both reach one implementation.
214
- navigator.sendBeacon?.(url.replace(/\/presence$/, '/presence?_method=DELETE'))
220
+ // No DELETE from a beacon, it is always a POST, so the server's
221
+ // POST departure route is used, marked `_method=DELETE`. Both reach
222
+ // one implementation.
223
+ const body = new FormData()
224
+ body.append('requesttoken', getRequestToken() || '')
225
+ navigator.sendBeacon?.(url.replace(/\/presence$/, '/presence?_method=DELETE'), body)
215
226
  } catch {
216
227
  // Best effort.
217
228
  }
package/src/css/badge.css CHANGED
@@ -2,6 +2,15 @@
2
2
  @conduction/nextcloud-vue — Status Badge
3
3
  ======================================== */
4
4
 
5
+ /* The two board tones without a Nextcloud colour role (screens-cell-pill-parity).
6
+ A theme may redefine them; the six role variants below use Nextcloud's own. */
7
+ :root {
8
+ --cn-status-purple-bg: #e8e6f6;
9
+ --cn-status-purple-text: #4a3f8f;
10
+ --cn-status-teal-bg: #e3f1f3;
11
+ --cn-status-teal-text: #1d5e66;
12
+ }
13
+
5
14
  .cn-status-badge {
6
15
  display: inline-flex;
7
16
  align-items: center;
@@ -51,6 +60,18 @@
51
60
  color: var(--color-info-text, var(--color-info, #0082c9));
52
61
  }
53
62
 
63
+ /* Work in progress ("In behandeling"), a board tone. */
64
+ .cn-status-badge--purple {
65
+ background-color: var(--cn-status-purple-bg);
66
+ color: var(--cn-status-purple-text);
67
+ }
68
+
69
+ /* Handed on ("Omgezet naar een zaak"), a board tone. */
70
+ .cn-status-badge--teal {
71
+ background-color: var(--cn-status-teal-bg);
72
+ color: var(--cn-status-teal-text);
73
+ }
74
+
54
75
  /* Solid variants — solid background with white text for use on colored backgrounds */
55
76
  .cn-status-badge--solid.cn-status-badge--default {
56
77
  background-color: var(--color-background-dark);
@@ -81,3 +102,13 @@
81
102
  background-color: var(--color-info, #0082c9);
82
103
  color: white;
83
104
  }
105
+
106
+ .cn-status-badge--solid.cn-status-badge--purple {
107
+ background-color: var(--cn-status-purple-text);
108
+ color: white;
109
+ }
110
+
111
+ .cn-status-badge--solid.cn-status-badge--teal {
112
+ background-color: var(--cn-status-teal-text);
113
+ color: white;
114
+ }
@@ -463,6 +463,57 @@ a.cn-kpi-card--stacked:focus-visible {
463
463
  gap: 6px;
464
464
  }
465
465
 
466
+ /* A linked board tile is one link: the label is not underlined link text, in
467
+ any layout (screens-stat-tile-parity). The NL Design theme underlines every
468
+ `a` with `!important`, so the opt-out needs it too. */
469
+ a.cn-kpi-card--board,
470
+ a.cn-kpi-card--board:hover,
471
+ a.cn-kpi-card--board:focus-visible,
472
+ a.cn-kpi-card--board * {
473
+ text-decoration: none !important;
474
+ }
475
+
476
+ /* `content.iconPlacement: "end"` (DqDashboard): label left, the icon in a
477
+ 32px circle right, tinted by the tile's state. */
478
+ .cn-kpi-card--board .cn-kpi-card__title--badge-end {
479
+ justify-content: space-between;
480
+ gap: 8px;
481
+ }
482
+
483
+ .cn-kpi-card__badge {
484
+ display: inline-flex;
485
+ flex: 0 0 auto;
486
+ align-items: center;
487
+ justify-content: center;
488
+ width: 32px;
489
+ height: 32px;
490
+ margin-inline-start: auto;
491
+ border-radius: 50%;
492
+ color: var(--color-primary-element-light-text, var(--color-primary-element));
493
+ background: var(--color-primary-element-light, color-mix(in srgb, var(--color-primary-element) 12%, transparent));
494
+ }
495
+
496
+ .cn-kpi-card__badge svg {
497
+ width: 18px;
498
+ height: 18px;
499
+ fill: currentcolor;
500
+ }
501
+
502
+ .cn-kpi-card__badge--success {
503
+ color: var(--color-text-success, var(--color-success-text));
504
+ background: color-mix(in srgb, var(--color-text-success, var(--color-success-text)) 12%, var(--color-main-background));
505
+ }
506
+
507
+ .cn-kpi-card__badge--warning {
508
+ color: var(--color-element-warning, var(--color-warning-text));
509
+ background: color-mix(in srgb, var(--color-element-warning, var(--color-warning-text)) 12%, var(--color-main-background));
510
+ }
511
+
512
+ .cn-kpi-card__badge--error {
513
+ color: var(--color-text-error, var(--color-error-text));
514
+ background: color-mix(in srgb, var(--color-text-error, var(--color-error-text)) 10%, var(--color-main-background));
515
+ }
516
+
466
517
  .cn-kpi-card__glyph {
467
518
  display: inline-flex;
468
519
  flex: 0 0 auto;
@@ -112,6 +112,16 @@
112
112
  gap: 20px;
113
113
  }
114
114
 
115
+ /* GridStack insets every cell by its 12px margin, the outer edge included, so
116
+ the tab strip started 12px below the top of the body column and stopped 12px
117
+ short of its end, while the side column started at the top. Pulling the grid
118
+ out by that margin puts its first card on the column's edges: the side column
119
+ is level with the strip, and the strip ends where the body column ends. */
120
+ .cn-look-board.cn-detail-page--with-side > .cn-detail-page__body > .cn-detail-page__grid {
121
+ margin: -12px;
122
+ width: auto;
123
+ }
124
+
115
125
  /* ---- Folder tabs ------------------------------------------------------- */
116
126
 
117
127
  /* The strip draws no line of its own (design-system pull request 147). The card under it
@@ -45,6 +45,42 @@
45
45
  margin-inline-start: 0;
46
46
  }
47
47
 
48
+ /* The header buttons keep the board's button under a theme that styles every
49
+ secondary or primary button with `!important` (the nldesign theme: a
50
+ primary border, 8px 16px padding): Download and the Actions menu are 40px,
51
+ a grey border on the main background (PqTickets, DqZaken), the primary
52
+ button is the primary fill without a border. The buildiq square is not a
53
+ direct child button and keeps its own rule. */
54
+ .cn-look-board .cn-index-page .cn-index-page__header-buttons > .button-vue--secondary,
55
+ .cn-look-board .cn-index-page .cn-index-page__header-buttons > .action-item .button-vue {
56
+ height: var(--cn-board-control-height) !important;
57
+ min-height: var(--cn-board-control-height) !important;
58
+ padding: 0 14px !important;
59
+ border: 1px solid var(--color-border-dark) !important;
60
+ border-radius: var(--cn-board-control-radius) !important;
61
+ background: var(--color-main-background) !important;
62
+ color: var(--color-main-text) !important;
63
+ }
64
+
65
+ .cn-look-board .cn-index-page .cn-index-page__header-buttons > .button-vue--secondary:hover,
66
+ .cn-look-board .cn-index-page .cn-index-page__header-buttons > .action-item .button-vue:hover {
67
+ background: var(--color-background-hover) !important;
68
+ }
69
+
70
+ .cn-look-board .cn-index-page .cn-index-page__header-buttons > .button-vue--primary {
71
+ height: var(--cn-board-control-height) !important;
72
+ min-height: var(--cn-board-control-height) !important;
73
+ padding: 0 16px !important;
74
+ border: 0 !important;
75
+ border-radius: var(--cn-board-control-radius) !important;
76
+ background: var(--color-primary-element) !important;
77
+ color: var(--color-primary-element-text) !important;
78
+ }
79
+
80
+ .cn-look-board .cn-index-page .cn-index-page__header-buttons > .button-vue--primary:hover {
81
+ background: var(--color-primary-element-hover) !important;
82
+ }
83
+
48
84
  /* ----------------------------------------------------------------- toolbar */
49
85
 
50
86
  /* No band, no padding, no radius. The DOM is already in reading order (row 1,
@@ -71,16 +107,27 @@
71
107
  margin-inline-start: 0;
72
108
  }
73
109
 
74
- /* The first of Save view, Filter and the view switch takes the free space, so
75
- the three sit at the end of row 1. */
76
- .cn-look-board .cn-actions-bar--board > .cn-actions-bar__row--views > :is(.cn-saved-views__save, .cn-actions-bar__filter-button, .cn-actions-bar__view-toggle) {
110
+ /* The first of Save view and the Filter + view switch unit takes the free
111
+ space (the board's `flex: 1` spacer), so they sit at the end of row 1.
112
+ When the line is full the unit wraps as one to the START of the next line
113
+ while Save view stays at the end of row 1 (DqZaken); without Save view the
114
+ unit sits at the end of row 1 (PqTickets, PqLeads, PtPortals). */
115
+ .cn-look-board .cn-actions-bar--board > .cn-actions-bar__row--views > :is(.cn-saved-views__save, .cn-actions-bar__view-controls) {
77
116
  margin-inline-start: auto;
78
117
  }
79
118
 
80
- .cn-look-board .cn-actions-bar--board > .cn-actions-bar__row--views > :is(.cn-saved-views__save, .cn-actions-bar__filter-button, .cn-actions-bar__view-toggle) ~ :is(.cn-saved-views__save, .cn-actions-bar__filter-button, .cn-actions-bar__view-toggle) {
119
+ .cn-look-board .cn-actions-bar--board > .cn-actions-bar__row--views > .cn-saved-views__save ~ .cn-actions-bar__view-controls {
81
120
  margin-inline-start: 0;
82
121
  }
83
122
 
123
+ .cn-look-board .cn-actions-bar--board .cn-actions-bar__view-controls {
124
+ display: flex;
125
+ flex: 0 0 auto;
126
+ flex-wrap: nowrap;
127
+ align-items: center;
128
+ gap: 8px;
129
+ }
130
+
84
131
  /* Row 2: the search field and the active filters on a line of their own. */
85
132
  .cn-look-board .cn-actions-bar--board > .cn-actions-bar__row--search {
86
133
  display: flex;
@@ -166,16 +213,39 @@
166
213
  color: var(--color-primary-element-text);
167
214
  }
168
215
 
169
- /* Save view: 34px, radius 17, on the light primary tint. */
216
+ /* Save view: a ghost button, 34px, radius 17, no border, on the light primary
217
+ tint, a 14px icon 6px before the label, on one line. */
170
218
  .cn-look-board .cn-saved-views__save .button-vue {
171
219
  min-height: 34px;
172
220
  height: 34px;
173
- padding: 0 14px;
221
+ padding: 0 12px;
222
+ border: 0;
174
223
  border-radius: 17px;
175
224
  background: var(--color-primary-element-light);
176
225
  color: var(--color-primary-element-light-text);
177
226
  font-size: 13px;
178
227
  font-weight: 600;
228
+ white-space: nowrap;
229
+ }
230
+
231
+ .cn-look-board .cn-saved-views__save .button-vue:hover {
232
+ background: var(--color-primary-element-light-hover);
233
+ }
234
+
235
+ .cn-look-board .cn-saved-views__save .button-vue .button-vue__wrapper {
236
+ gap: 6px;
237
+ }
238
+
239
+ .cn-look-board .cn-saved-views__save .button-vue .button-vue__icon {
240
+ width: auto;
241
+ min-width: 0;
242
+ height: auto;
243
+ min-height: 0;
244
+ }
245
+
246
+ .cn-look-board .cn-saved-views__save .button-vue .button-vue__text {
247
+ margin: 0;
248
+ padding: 0;
179
249
  }
180
250
 
181
251
  /* Filter: a labelled secondary button, 36px, radius 8, with the active count. */
@@ -407,6 +477,29 @@
407
477
  text-decoration: none;
408
478
  }
409
479
 
480
+ /* Only the title is underlined (DqZaken). An underline set on the cell
481
+ reaches every in-flow child whatever its own text-decoration, so the
482
+ secondary lines become inline blocks on a line of their own: an atomic
483
+ inline box is the one thing the underline does not reach. */
484
+ .cn-look-board .cn-table-container--board .cn-data-table td.cn-table-col--title .cn-table-cell__secondary {
485
+ display: inline-block;
486
+ width: 100%;
487
+ vertical-align: top;
488
+ }
489
+
490
+ /* `rowTitle: "plain"` (PqTickets, OcPublicaties): the title is bold text in
491
+ the main text colour without an underline, the secondary line 13px muted,
492
+ 2px under it. The row stays clickable through its stretched row link. */
493
+ .cn-look-board .cn-table-container--board.cn-table-container--title-plain .cn-data-table td.cn-table-col--title {
494
+ font-weight: 700;
495
+ text-decoration: none;
496
+ }
497
+
498
+ .cn-look-board .cn-table-container--board.cn-table-container--title-plain .cn-data-table td.cn-table-col--title .cn-table-cell__secondary {
499
+ margin-top: 2px;
500
+ font-size: 13px;
501
+ }
502
+
410
503
  /* A status pill: 3px 10px, radius 12, 13px weight 600. The colours stay the
411
504
  status colour map's. */
412
505
  .cn-look-board .cn-table-container--board .cn-data-table .cn-status-badge {
@@ -429,15 +522,36 @@
429
522
  --default-clickable-area: 34px;
430
523
  }
431
524
 
525
+ /* The row menu: a 34px square, three dots, a grey border on the main
526
+ background (DqZaken, PqTickets). A theme that styles every secondary button
527
+ with `!important` (the nldesign theme: a primary border, 8px 16px padding,
528
+ `min-width: max-content`) would otherwise turn it into a 64px wide button
529
+ with a blue border, so the board values are `!important` too. */
530
+ .cn-look-board .cn-row-actions--board .button-vue,
531
+ .cn-look-board .cn-row-actions--board .button-vue:hover,
532
+ .cn-look-board .cn-row-actions--board .button-vue:focus {
533
+ box-sizing: border-box !important;
534
+ width: 34px !important;
535
+ min-width: 34px !important;
536
+ height: 34px !important;
537
+ min-height: 34px !important;
538
+ padding: 0 !important;
539
+ border: 1px solid var(--color-border-dark) !important;
540
+ border-radius: 8px !important;
541
+ color: var(--cn-board-text-soft) !important;
542
+ }
543
+
432
544
  .cn-look-board .cn-row-actions--board .button-vue {
433
- width: 34px;
434
- min-width: 34px;
435
- height: 34px;
436
- min-height: 34px;
437
- padding: 0;
438
- border: 1px solid var(--color-border-dark);
439
- border-radius: 8px;
440
- background: var(--color-main-background);
545
+ background: var(--color-main-background) !important;
546
+ }
547
+
548
+ .cn-look-board .cn-row-actions--board .button-vue:hover {
549
+ background: var(--color-background-hover) !important;
550
+ }
551
+
552
+ .cn-look-board .cn-row-actions--board .button-vue:focus-visible {
553
+ outline: 2px solid var(--color-main-text);
554
+ outline-offset: 2px;
441
555
  }
442
556
 
443
557
  /* ------------------------------------------------------------------ footer */
@@ -146,6 +146,14 @@
146
146
  margin-inline-start: -264px;
147
147
  }
148
148
 
149
+ /* The navigation draws the line between it and the content. NcContent draws a
150
+ second one on the content while the navigation is open, which doubled the
151
+ line and put the page 1px further from the navigation than the board's 28px.
152
+ Outranks NcContent's rule (six classes) with seven. */
153
+ .cn-look-board .app-navigation[data-testid="cn-nav"]:not(.app-navigation--closed):not(.app-navigation--close) ~ .app-content.app-content {
154
+ border-inline-start: none;
155
+ }
156
+
149
157
  .cn-look-board .app-navigation[data-testid="cn-nav"] .app-navigation__content {
150
158
  box-sizing: border-box;
151
159
  gap: 20px;
@@ -155,6 +163,31 @@
155
163
  .cn-look-board .app-navigation[data-testid="cn-nav"] .app-navigation__list {
156
164
  height: auto;
157
165
  flex: 1 1 auto;
166
+ margin: 0;
167
+ gap: 2px;
168
+ }
169
+
170
+ /* The board's vertical rhythm (AppZijbalk): the primary action 20px from the
171
+ top, the list 20px under it, entries 2px apart. An empty search slot is a
172
+ flex child too and pushed everything 20px down, so it goes. */
173
+ .cn-look-board .app-navigation[data-testid="cn-nav"] .app-navigation__search:empty {
174
+ display: none;
175
+ }
176
+
177
+ /* A caption is a 12px label with 6px under it, 20px after the group above
178
+ and 2px before its first entry. NcAppNavigationCaption gives its name a
179
+ 15px font on a 34px line, which made it 44px high. */
180
+ .cn-look-board .app-navigation[data-testid="cn-nav"] .app-navigation__list > .app-navigation-caption:not(:first-child) {
181
+ margin-top: 18px;
182
+ }
183
+
184
+ .cn-look-board .app-navigation[data-testid="cn-nav"] .app-navigation-caption .app-navigation-caption__name {
185
+ margin: 0;
186
+ padding: 0;
187
+ font-size: inherit;
188
+ font-weight: inherit;
189
+ line-height: normal;
190
+ color: inherit;
158
191
  }
159
192
 
160
193
  .cn-look-board .app-navigation[data-testid="cn-nav"] .app-navigation-entry,
@@ -239,6 +272,43 @@
239
272
  font-size: 15px;
240
273
  }
241
274
 
275
+ /* The footer of the board: Help and Advanced as two 40px rows at weight 400,
276
+ 2px apart. The Help row kept the 42px of a list entry, and the Advanced
277
+ foldout drew a 34px button inside 4px of padding, 20px under Help. */
278
+ .cn-look-board .app-navigation[data-testid="cn-nav"] .cn-app-nav__footer-list {
279
+ gap: 2px;
280
+ }
281
+
282
+ .cn-look-board .app-navigation[data-testid="cn-nav"] .cn-app-nav__footer-list .app-navigation-entry,
283
+ .cn-look-board .app-navigation[data-testid="cn-nav"] .cn-app-nav__footer-list .app-navigation-entry-link {
284
+ min-height: 40px;
285
+ font-weight: 400;
286
+ }
287
+
288
+ .cn-look-board .app-navigation[data-testid="cn-nav"] [data-testid="cn-nav-settings"] {
289
+ padding: 0;
290
+ }
291
+
292
+ .cn-look-board .app-navigation[data-testid="cn-nav"] .cn-app-nav__footer-list + [data-testid="cn-nav-settings"] {
293
+ margin-top: -18px;
294
+ }
295
+
296
+ .cn-look-board .app-navigation[data-testid="cn-nav"] [data-testid="cn-nav-settings"] > :first-child {
297
+ margin: 0;
298
+ }
299
+
300
+ .cn-look-board .app-navigation[data-testid="cn-nav"] [data-testid="cn-nav-settings"] > :first-child .button-vue {
301
+ box-sizing: border-box;
302
+ width: 100%;
303
+ height: 40px;
304
+ min-height: 40px;
305
+ padding: 0 10px;
306
+ border-radius: 8px;
307
+ font-size: 15px;
308
+ font-weight: 400;
309
+ justify-content: flex-start;
310
+ }
311
+
242
312
  /* The card above the footer, pushed down. */
243
313
  .cn-look-board .cn-app-nav__card {
244
314
  order: 0;
@@ -435,3 +505,18 @@
435
505
  color: var(--color-main-text);
436
506
  opacity: 1;
437
507
  }
508
+
509
+ /* ---- Status pills (screens-cell-pill-parity) ---------------------------- */
510
+ /* The board draws a primary pill in the deep primary ink on the light primary
511
+ ground ("Verzoek", "Nieuw"); the default look keeps the brighter
512
+ primary-element text. The other tones already match the board. */
513
+ .cn-look-board .cn-status-badge--primary:not(.cn-status-badge--solid):not(.cn-look-nextcloud *) {
514
+ color: var(--color-primary-element-light-text);
515
+ }
516
+
517
+ /* ---- Date and age cells (screens-cell-date-parity) ---------------------- */
518
+ /* The board sets every waiting time ("3 dagen", "4 uur") in weight 600; a
519
+ late one is red through its variant class. */
520
+ .cn-look-board .cn-cell-renderer__age:not(.cn-look-nextcloud *) {
521
+ font-weight: 600;
522
+ }