@conduction/nextcloud-vue 2.76.0 → 2.78.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 (173) hide show
  1. package/dist/esm/components/CnAiCompanion/CnAiCompanion.vue2.js +23 -0
  2. package/dist/esm/components/CnAiCompanion/CnAiCompanion.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAiCompanion/CnAiCompanion.vue3.js.map +1 -1
  4. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +18 -3
  5. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  6. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +49 -3
  7. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  8. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +2 -1
  9. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
  10. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
  11. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +79 -1
  12. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  13. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +229 -203
  14. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  15. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue.js +9 -0
  16. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue.js.map +1 -0
  17. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue2.js +50 -0
  18. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue2.js.map +1 -0
  19. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue3.js +13 -0
  20. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue3.js.map +1 -0
  21. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +37 -0
  22. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  23. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js +2 -2
  24. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  25. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +29 -4
  26. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  27. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  28. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +48 -6
  29. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  30. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +90 -51
  31. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  32. package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue.js +1 -1
  33. package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue2.js +45 -2
  34. package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue2.js.map +1 -1
  35. package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue3.js.map +1 -1
  36. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +16 -28
  37. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  38. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +12 -24
  39. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  40. package/dist/esm/components/CnFavouriteToggle/CnFavouriteToggle.vue2.js +7 -2
  41. package/dist/esm/components/CnFavouriteToggle/CnFavouriteToggle.vue2.js.map +1 -1
  42. package/dist/esm/components/CnFavouriteToggle/CnFavouriteToggle.vue3.js.map +1 -1
  43. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +114 -1
  44. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  45. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +115 -62
  46. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  47. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue2.js +89 -7
  48. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue2.js.map +1 -1
  49. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue3.js +62 -30
  50. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue3.js.map +1 -1
  51. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +7 -1
  52. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  53. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  54. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +4 -1
  55. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
  56. package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
  57. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +104 -13
  58. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  59. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +13 -9
  60. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  61. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +199 -7
  62. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
  63. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js +155 -119
  64. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
  65. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +6 -0
  66. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  67. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +4 -1
  68. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  69. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +36 -0
  70. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  71. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +35 -23
  72. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  73. package/dist/esm/components/CnStatusBadge/CnStatusBadge.vue2.js +6 -2
  74. package/dist/esm/components/CnStatusBadge/CnStatusBadge.vue2.js.map +1 -1
  75. package/dist/esm/components/CnStatusBadge/CnStatusBadge.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/useFormDraft.js +11 -1
  80. package/dist/esm/composables/useFormDraft.js.map +1 -1
  81. package/dist/esm/composables/useListView.js +4 -0
  82. package/dist/esm/composables/useListView.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 +2 -1
  86. package/dist/esm/index.js.map +1 -1
  87. package/dist/esm/l10n/en.json.js +32 -1
  88. package/dist/esm/l10n/en.json.js.map +1 -1
  89. package/dist/esm/l10n/nl.json.js +32 -1
  90. package/dist/esm/l10n/nl.json.js.map +1 -1
  91. package/dist/esm/nextcloud-vue.css +500 -75
  92. package/dist/esm/store/plugins/interactions.js +20 -10
  93. package/dist/esm/store/plugins/interactions.js.map +1 -1
  94. package/dist/esm/store/useObjectStore.js +17 -0
  95. package/dist/esm/store/useObjectStore.js.map +1 -1
  96. package/dist/esm/utils/badgeVariants.js +32 -0
  97. package/dist/esm/utils/badgeVariants.js.map +1 -0
  98. package/dist/esm/utils/builtInFormatters.js +15 -7
  99. package/dist/esm/utils/builtInFormatters.js.map +1 -1
  100. package/dist/esm/utils/groupAutocomplete.js +63 -1
  101. package/dist/esm/utils/groupAutocomplete.js.map +1 -1
  102. package/dist/esm/utils/lensAvailability.js +90 -0
  103. package/dist/esm/utils/lensAvailability.js.map +1 -0
  104. package/dist/esm/utils/personalLenses.js +13 -6
  105. package/dist/esm/utils/personalLenses.js.map +1 -1
  106. package/dist/esm/utils/recordInteractions.js +31 -13
  107. package/dist/esm/utils/recordInteractions.js.map +1 -1
  108. package/dist/esm/utils/resolveFilterTokens.js +46 -3
  109. package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
  110. package/dist/esm/utils/schema.js +1 -1
  111. package/dist/esm/utils/schema.js.map +1 -1
  112. package/dist/esm/utils/sentinelTokens.js +4 -4
  113. package/dist/esm/utils/sentinelTokens.js.map +1 -1
  114. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  115. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  116. package/dist/esm/utils/widgetVisibility.js +46 -1
  117. package/dist/esm/utils/widgetVisibility.js.map +1 -1
  118. package/dist/nextcloud-vue.cjs.js +4389 -2890
  119. package/dist/nextcloud-vue.cjs.js.map +2 -2
  120. package/dist/nextcloud-vue.css +500 -75
  121. package/l10n/en.json +32 -1
  122. package/l10n/nl.json +32 -1
  123. package/package.json +1 -1
  124. package/src/components/CnAiCompanion/CnAiCompanion.vue +23 -0
  125. package/src/components/CnAppNav/CnAppNav.vue +44 -5
  126. package/src/components/CnBoardView/CnBoardView.vue +2 -1
  127. package/src/components/CnCellRenderer/CnCellRenderer.md +1 -0
  128. package/src/components/CnCellRenderer/CnCellRenderer.vue +106 -9
  129. package/src/components/CnCellRenderer/CnGroupNameCell.vue +61 -0
  130. package/src/components/CnChartWidget/CnChartWidget.vue +39 -2
  131. package/src/components/CnDashboardPage/CnDashboardPage.vue +29 -4
  132. package/src/components/CnDataTable/CnDataTable.md +9 -0
  133. package/src/components/CnDataTable/CnDataTable.vue +87 -22
  134. package/src/components/CnDateRangePicker/CnDateRangePicker.vue +44 -1
  135. package/src/components/CnDetailPage/CnDetailPage.vue +10 -27
  136. package/src/components/CnFavouriteToggle/CnFavouriteToggle.vue +7 -2
  137. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +204 -5
  138. package/src/components/CnFollowToggle/CnFollowToggle.vue +118 -13
  139. package/src/components/CnFormDialog/CnFormDialog.vue +7 -1
  140. package/src/components/CnFormPage/CnFormPage.vue +4 -1
  141. package/src/components/CnIndexPage/CnIndexPage.md +36 -0
  142. package/src/components/CnIndexPage/CnIndexPage.vue +112 -19
  143. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +226 -2
  144. package/src/components/CnObjectSidebar/CnFilesTab.vue +9 -0
  145. package/src/components/CnStatWidget/CnStatWidget.vue +52 -2
  146. package/src/components/CnStatusBadge/CnStatusBadge.vue +6 -2
  147. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +13 -0
  148. package/src/composables/__tests__/useObjectPresence.spec.js +24 -0
  149. package/src/composables/useFormDraft.js +11 -1
  150. package/src/composables/useListView.js +4 -0
  151. package/src/composables/useObjectPresence.js +15 -4
  152. package/src/css/badge.css +31 -0
  153. package/src/css/kpi-card.css +51 -0
  154. package/src/css/look-board-detail.css +40 -2
  155. package/src/css/look-board-index.css +36 -0
  156. package/src/css/look-board.css +8 -0
  157. package/src/css/table.css +82 -17
  158. package/src/index.js +2 -1
  159. package/src/schemas/app-manifest-v2.schema.json +15 -11
  160. package/src/store/plugins/interactions.js +20 -10
  161. package/src/store/useObjectStore.js +17 -0
  162. package/src/utils/badgeVariants.js +29 -0
  163. package/src/utils/builtInFormatters.js +15 -7
  164. package/src/utils/groupAutocomplete.js +75 -0
  165. package/src/utils/index.js +1 -1
  166. package/src/utils/lensAvailability.js +86 -0
  167. package/src/utils/personalLenses.js +13 -6
  168. package/src/utils/recordInteractions.js +31 -13
  169. package/src/utils/resolveFilterTokens.js +45 -2
  170. package/src/utils/schema.js +1 -1
  171. package/src/utils/sentinelTokens.js +4 -4
  172. package/src/utils/validateManifestV2.compiled.js +2 -2
  173. package/src/utils/widgetVisibility.js +45 -0
@@ -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(() => {
@@ -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
  }
@@ -98,6 +98,9 @@ export function useListView(objectTypeOrOptions, options) {
98
98
  // field (a folder pane grouping by it, say) reads them here rather than
99
99
  // deriving them from `objects`, which is only the current page.
100
100
  const facets = computed(() => objectStore.facets[getType()] || {})
101
+ // Personal-lens reports of the latest response (`{ recent: { available, reason } }`).
102
+ // @spec openspec/changes/lens-says-why-it-is-empty/specs/personal-lens-availability/spec.md#requirement-a-list-response-carries-the-report-of-the-lenses-it-was-asked-for
103
+ const lenses = computed(() => (objectStore.lenses && objectStore.lenses[getType()]) || {})
101
104
  // Outcome of the latest `refresh()`: the store's error for it, or null once
102
105
  // it succeeded or a later fetch stored new rows. The store keeps the previous
103
106
  // rows on a failed fetch, so this is how a consumer tells stale rows from
@@ -381,6 +384,7 @@ export function useListView(objectTypeOrOptions, options) {
381
384
  loading,
382
385
  pagination,
383
386
  facets,
387
+ lenses,
384
388
  error,
385
389
  // Local state
386
390
  searchTerm,
@@ -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;
@@ -114,12 +114,26 @@
114
114
 
115
115
  /* ---- Folder tabs ------------------------------------------------------- */
116
116
 
117
+ /* The strip draws no line of its own (design-system pull request 147). The card under it
118
+ carries its full border, top included, so the top-right corner is the
119
+ card's own rounded corner instead of a strip rule running straight past it.
120
+ The open tab overlaps that top border by 1px (the nav's -1px margin) and
121
+ paints over it, so it flows into the card as a folder tab. */
117
122
  .cn-look-board .cn-tabs--board .cn-tabs__bar {
118
- border-bottom: 1px solid var(--color-border-dark);
123
+ border-bottom: none;
119
124
  gap: 4px;
125
+ position: relative;
126
+ }
127
+
128
+ .cn-look-board .cn-tabs.cn-tabs--board .cn-tabs__bar::after {
129
+ content: none;
120
130
  }
121
131
 
132
+ /* Tabs sit on the card's top edge; only the open tab reaches 1px further
133
+ down (min-height 35px below), over the card's top border. Top-aligned so
134
+ the inactive tabs do not stretch to the open tab's height. */
122
135
  .cn-look-board .cn-tabs--board .cn-tabs__nav {
136
+ align-items: flex-start;
123
137
  gap: 4px;
124
138
  }
125
139
 
@@ -140,7 +154,11 @@
140
154
  border-bottom: 1px solid var(--color-main-background);
141
155
  border-top: 1px solid var(--color-border-dark);
142
156
  font-weight: 700;
157
+ min-height: 35px;
158
+ padding-bottom: 9px;
143
159
  padding-top: 8px;
160
+ position: relative;
161
+ z-index: 1;
144
162
  }
145
163
 
146
164
  /* The count: a grey, borderless 20px badge 8px after the label. */
@@ -177,10 +195,30 @@
177
195
  .cn-look-board .cn-tabs--board .cn-tabs__content > .cn-tab > :first-child .cn-widget-wrapper,
178
196
  .cn-look-board .cn-tabs--board .cn-tabs__content > .cn-tab > .cn-widget-wrapper,
179
197
  .cn-look-board .cn-tabs--board .cn-tabs__content > .cn-tab > .cn-detail-card {
180
- border-top: none;
198
+ border-top: 1px solid var(--color-border);
181
199
  border-radius: 0 12px 12px 12px;
182
200
  }
183
201
 
202
+ /* A CnTabsWidget panel is the card itself: the same full border, so it joins
203
+ the open tab the same way. The top-left stays square under the first tab. */
204
+ .cn-look-board .cn-tabs--board.cn-tabs-widget__tabs .cn-tabs__content {
205
+ border: 1px solid var(--color-border);
206
+ border-radius: 0 12px 12px 12px;
207
+ }
208
+
209
+ /* A panel of CnTabsWidget is the content block itself: the open tab flows
210
+ straight into it. The widget in the panel renders chromeless, but the board
211
+ card rule below (and the first-card rule above) used to give its wrapper a
212
+ border and a 12px radius again, so a second card sat inside the panel. Its
213
+ left edge ran down from the open tab and its rounded top-right drew a line
214
+ across the top right of the content block, where DqZaak shows one sheet.
215
+ Same specificity as the first-card rule and later, so it wins on order. */
216
+ .cn-look-board .cn-tabs--board.cn-tabs-widget__tabs .cn-tabs__content .cn-widget-wrapper.cn-widget-wrapper--chromeless {
217
+ background: transparent;
218
+ border: 0;
219
+ border-radius: 0;
220
+ }
221
+
184
222
  /* ---- Body cards and side cards ----------------------------------------- */
185
223
 
186
224
  .cn-look-board .cn-detail-page .cn-widget-wrapper,
@@ -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,
@@ -435,3 +435,11 @@
435
435
  color: var(--color-main-text);
436
436
  opacity: 1;
437
437
  }
438
+
439
+ /* ---- Status pills (screens-cell-pill-parity) ---------------------------- */
440
+ /* The board draws a primary pill in the deep primary ink on the light primary
441
+ ground ("Verzoek", "Nieuw"); the default look keeps the brighter
442
+ primary-element text. The other tones already match the board. */
443
+ .cn-look-board .cn-status-badge--primary:not(.cn-status-badge--solid):not(.cn-look-nextcloud *) {
444
+ color: var(--color-primary-element-light-text);
445
+ }
package/src/css/table.css CHANGED
@@ -284,9 +284,64 @@
284
284
  background-color: var(--color-background-hover);
285
285
  }
286
286
 
287
- .cn-table-sort-indicator {
288
- margin-inline-start: var(--default-grid-baseline);
289
- opacity: 0.6;
287
+ /* The sort control: a real button that looks like the header text, the label
288
+ followed by a 14px chevron 4px after it (the "Wettelijke termijn" header on
289
+ the DqWooVerzoeken board). Selector scoped under the table so it outranks
290
+ Nextcloud's own `button` rule, which would otherwise give it a border, a
291
+ fill, a 34px min-height and a margin. */
292
+ .cn-data-table th .cn-table-header__sort {
293
+ display: inline-flex;
294
+ align-items: center;
295
+ gap: 4px;
296
+ max-width: 100%;
297
+ min-width: 0;
298
+ min-height: 0;
299
+ margin: 0;
300
+ padding: 0;
301
+ border: 0;
302
+ border-radius: var(--border-radius, 4px);
303
+ background: transparent;
304
+ color: inherit;
305
+ font: inherit;
306
+ font-weight: inherit;
307
+ text-align: start;
308
+ vertical-align: middle;
309
+ cursor: pointer;
310
+ }
311
+
312
+ .cn-data-table th .cn-table-header__sort:hover,
313
+ .cn-data-table th .cn-table-header__sort:focus {
314
+ background: transparent;
315
+ }
316
+
317
+ .cn-data-table th .cn-table-header__sort:focus-visible {
318
+ outline: 2px solid var(--color-main-text);
319
+ outline-offset: 2px;
320
+ }
321
+
322
+ .cn-data-table th .cn-table-header__sort > :first-child {
323
+ min-width: 0;
324
+ overflow: hidden;
325
+ text-overflow: ellipsis;
326
+ }
327
+
328
+ /* The chevron. Unsorted columns show a pale down chevron, so every sortable
329
+ column says it sorts; the sorted column shows the direction at full text
330
+ colour. The direction is also in the header's `aria-sort`. */
331
+ .cn-table-header__chevron {
332
+ display: inline-flex;
333
+ flex: none;
334
+ color: var(--color-main-text);
335
+ }
336
+
337
+ .cn-table-header__chevron--idle {
338
+ color: var(--color-text-maxcontrast);
339
+ opacity: 0.5;
340
+ }
341
+
342
+ .cn-data-table th.cn-table-header--sortable:hover .cn-table-header__chevron--idle,
343
+ .cn-data-table th .cn-table-header__sort:focus-visible .cn-table-header__chevron--idle {
344
+ opacity: 1;
290
345
  }
291
346
 
292
347
  /* A column carrying a `description` gets a dotted underline — the long-standing
@@ -302,15 +357,12 @@
302
357
  cursor: help;
303
358
  }
304
359
 
305
- .cn-data-table th.cn-table-header--sortable:focus-visible {
306
- outline: 2px solid var(--color-main-text);
307
- outline-offset: -2px;
308
- }
309
-
310
- /* Header filter button (CnDataTable `filterable`). 24px hit area with the
311
- 16px icon; the filled icon plus the primary colour marks an active filter,
312
- so the state never rests on colour alone. */
313
- .cn-table-header__filter {
360
+ /* Header filter button (CnDataTable `filterable`): a 14px funnel directly
361
+ after the sort chevron, in a 24px hit area. Muted with no filter set; with
362
+ a filter set it takes the dark primary and a dot, so the state never rests
363
+ on colour alone. */
364
+ .cn-data-table th .cn-table-header__filter {
365
+ position: relative;
314
366
  display: inline-flex;
315
367
  align-items: center;
316
368
  justify-content: center;
@@ -318,7 +370,7 @@
318
370
  min-width: 24px;
319
371
  height: 24px;
320
372
  min-height: 24px;
321
- margin: 0 0 0 4px;
373
+ margin: 0 0 0 2px;
322
374
  padding: 0;
323
375
  border: none;
324
376
  border-radius: var(--border-radius, 4px);
@@ -328,18 +380,31 @@
328
380
  cursor: pointer;
329
381
  }
330
382
 
331
- .cn-table-header__filter:hover {
383
+ .cn-data-table th .cn-table-header__filter:hover {
332
384
  background-color: var(--color-background-hover);
333
385
  color: var(--color-main-text);
334
386
  }
335
387
 
336
- .cn-table-header__filter:focus-visible {
388
+ .cn-data-table th .cn-table-header__filter:focus-visible {
337
389
  outline: 2px solid var(--color-main-text);
338
390
  outline-offset: 0;
339
391
  }
340
392
 
341
- .cn-table-header__filter--active {
342
- color: var(--color-primary-element);
393
+ .cn-data-table th .cn-table-header__filter--active,
394
+ .cn-data-table th .cn-table-header__filter--active:hover {
395
+ color: var(--color-primary-element-light-text, var(--color-primary-element));
396
+ }
397
+
398
+ /* The active-filter dot, on the funnel's top right. */
399
+ .cn-table-header__filter-dot {
400
+ position: absolute;
401
+ inset-block-start: 3px;
402
+ inset-inline-end: 3px;
403
+ width: 6px;
404
+ height: 6px;
405
+ border-radius: 50%;
406
+ background-color: var(--color-primary-element);
407
+ pointer-events: none;
343
408
  }
344
409
 
345
410
  /* Active header filters above the table, one removable chip each. */
package/src/index.js CHANGED
@@ -543,6 +543,7 @@ export { cnFetch, cnFetchJson, CnHttpError, configureCnFetch } from './utils/cnF
543
543
  // need the same answer without rendering a padlock.
544
544
  export { isLockedByCurrentUser, isObjectLocked, lockHolder, readLockPayload, resolveObjectLock } from './utils/objectLock.js'
545
545
  export { dispatchObjectsChanged } from './utils/objectSignals.js'
546
+ export { lensUnavailableText, readLensReports } from './utils/lensAvailability.js'
546
547
  export { columnsFromSchema, fieldsFromSchema, filtersFromSchema, findRepeatingWrite, formatValue, journeyItemTargets, validateValue } from './utils/index.js'
547
548
  export { DYNAMIC_KEY_PREFIX, EXTENDS_FORM_KEY, PREFILL_KEY, splitDynamicFormData, usesArrayValues, valueArrayFor, valueRecordsFor } from './utils/index.js'
548
549
  // The OpenRegister schema API contract — shared so Buildiq and OpenRegister cannot
@@ -569,7 +570,7 @@ export {
569
570
  SENTINEL_RESOLVERS,
570
571
  warnIfDeprecated,
571
572
  } from './utils/resolveManifestTokens.js'
572
- export { filterWidgetsByVisibility, getCurrentUserGroups, getCurrentUserId, isWidgetVisible, resetVisibilityCache } from './utils/index.js'
573
+ export { filterWidgetsByVisibility, getCurrentUserGroups, getCurrentUserId, isWidgetVisible, peekCurrentUserGroups, resetVisibilityCache } from './utils/index.js'
573
574
  export { safeHref, safeImageSrc, safeSvgPath } from './utils/index.js'
574
575
  export { followLinkClick, isModifiedClick, isNewTabHandled, isRowMiddleClick, openRowTarget, resolveHref } from './utils/index.js'
575
576
  export { resolveImageUrl } from './utils/index.js'
@@ -3,7 +3,7 @@
3
3
  "$id": "https://raw.githubusercontent.com/ConductionNL/nextcloud-vue/main/src/schemas/app-manifest-v2.schema.json",
4
4
  "title": "Conduction App Manifest v2",
5
5
  "description": "v2 schema for the JSON-driven page and navigation manifest consumed by @conduction/nextcloud-vue. Introduces a uniform widgets[] array on every page type with a per-slot grid coordinate system, a typed actions[] discriminator, and the required $schema field for version detection. v1 manifests continue to validate against app-manifest.schema.json (unchanged).",
6
- "version": "2.73.0",
6
+ "version": "2.76.0",
7
7
  "type": "object",
8
8
  "required": ["$schema", "version"],
9
9
  "allOf": [
@@ -760,8 +760,8 @@
760
760
  },
761
761
  "sentinelFilterToken": {
762
762
  "type": "string",
763
- "pattern": "^@(?:me(?:\\.(?:displayName|email))?|now|today|monthStart|quarterStart|yearStart)$|^@today[+-][0-9]+d$",
764
- "description": "Filter-value context: relative fetch-time tokens resolved by resolveFilterTokens (@me, @me.displayName, @me.email, @now, @today, @today±Nd, @monthStart, @quarterStart, @yearStart). Mirrors SENTINEL_TOKEN_PATTERNS.filter in src/utils/sentinelTokens.js (byte-equality enforced by a unit test)."
763
+ "pattern": "^@(?:me(?:\\.(?:displayName|email))?|myGroups|now|today|monthStart|quarterStart|yearStart)$|^@today[+-][0-9]+d$",
764
+ "description": "Filter-value context: relative fetch-time tokens resolved by resolveFilterTokens (@me, @me.displayName, @me.email, @myGroups (the current user's group ids, an IN list), @now, @today, @today±Nd, @monthStart, @quarterStart, @yearStart). Mirrors SENTINEL_TOKEN_PATTERNS.filter in src/utils/sentinelTokens.js (byte-equality enforced by a unit test)."
765
765
  },
766
766
  "sentinelConfigToken": {
767
767
  "type": "string",
@@ -1094,7 +1094,7 @@
1094
1094
  "pillColors": {
1095
1095
  "type": "object",
1096
1096
  "description": "Pill value to badge variant. A value without an entry takes the default variant.",
1097
- "additionalProperties": { "type": "string", "enum": ["default", "primary", "success", "warning", "error", "info"] }
1097
+ "additionalProperties": { "type": "string", "enum": ["default", "primary", "success", "warning", "error", "info", "purple", "teal"] }
1098
1098
  },
1099
1099
  "due": { "type": "string", "description": "The date field of the footer's due text, marked by `dueRule`." },
1100
1100
  "owner": { "type": "string", "description": "The field of the owner shown as an avatar (initials) at the footer's end." }
@@ -1143,7 +1143,7 @@
1143
1143
  "colorMap": {
1144
1144
  "type": "object",
1145
1145
  "description": "Raw value to badge variant. Keys are matched ignoring case.",
1146
- "additionalProperties": { "type": "string", "enum": ["default", "primary", "success", "warning", "error", "info"] }
1146
+ "additionalProperties": { "type": "string", "enum": ["default", "primary", "success", "warning", "error", "info", "purple", "teal"] }
1147
1147
  },
1148
1148
  "labels": {
1149
1149
  "type": "object",
@@ -1152,7 +1152,7 @@
1152
1152
  },
1153
1153
  "variant": {
1154
1154
  "type": "string",
1155
- "enum": ["default", "primary", "success", "warning", "error", "info"],
1155
+ "enum": ["default", "primary", "success", "warning", "error", "info", "purple", "teal"],
1156
1156
  "default": "default",
1157
1157
  "description": "Badge variant for a value `colorMap` does not name."
1158
1158
  }
@@ -3304,19 +3304,23 @@
3304
3304
  "properties": {
3305
3305
  "favourite": {
3306
3306
  "type": "boolean",
3307
- "description": "Detail pages: the star beside the title. Omitted means automatic: it shows when the object carries @self.favourite. false removes it."
3307
+ "description": "Deprecated and ignored: there is no star any more, a favourite is a follow with notifications off. Kept so existing manifests stay valid; use follow."
3308
3308
  },
3309
3309
  "follow": {
3310
3310
  "type": "boolean",
3311
- "description": "Detail pages: the Follow toggle beside the title, with the follower count for a user who may update the record. Omitted means automatic: it shows when the object carries @self.watching. false removes it, and the object read then no longer asks for @self.can."
3311
+ "description": "Detail pages: the one Follow control beside the title, with a notifications on/off switch while the user follows and the follower count for a user who may update the record. Omitted means automatic: it shows when the object carries @self.watching. false removes it, and the object read then no longer asks for @self.can."
3312
+ },
3313
+ "showFollowColumn": {
3314
+ "type": "boolean",
3315
+ "description": "Index pages: add a first column with a follow toggle per row, bound to each row's @self.watching. Clicking it does not open the row. Default false."
3312
3316
  },
3313
3317
  "showFavouriteColumn": {
3314
3318
  "type": "boolean",
3315
- "description": "Index pages: add a first column with a star per row, bound to each row's @self.favourite. Clicking it does not open the row. Default false."
3319
+ "description": "Deprecated alias of showFollowColumn: the star column became the follow column. Default false."
3316
3320
  },
3317
3321
  "personalLenses": {
3318
3322
  "type": "array",
3319
- "description": "Index pages: personal lenses added to the quick filters, in order. favourite lists the user's favourites, recent the records the user opened last (it owns the order, so column sorting is off), watching the records the user follows, unread the records changed since the user last looked. They combine with every other filter. Default none.",
3323
+ "description": "Index pages: personal lenses added to the quick filters, in order. watching lists the records the user follows (labelled Following), recent the records the user opened last (it owns the order, so column sorting is off), unread the records changed since the user last looked. They combine with every other filter. favourite is a deprecated alias of watching; asking for both gives one tab. Default none.",
3320
3324
  "items": { "enum": ["favourite", "recent", "watching", "unread"] },
3321
3325
  "uniqueItems": true
3322
3326
  },
@@ -3582,7 +3586,7 @@
3582
3586
  },
3583
3587
  "columns": {
3584
3588
  "type": "array",
3585
- "description": "Table columns (for type='index' and type='logs'). Items may be strings (legacy shorthand) or column objects ({ key, label, sortable?, width?, formatter?, formatterOptions?, widget?, widgetProps?, format?, aggregate? }). `formatter` resolves against cnFormatters (built-ins incl. currency / conditionalPhrase — configured via `formatterOptions`); `widget` resolves against cnCellWidgets (built-ins incl. badge / fkResolve / link / avatar / date).",
3589
+ "description": "Table columns (for type='index' and type='logs'). Items may be strings (legacy shorthand) or column objects ({ key, label, sortable?, width?, formatter?, formatterOptions?, widget?, widgetProps?, format?, aggregate?, colorMap? }). `colorMap` maps an enum column's raw values to a pill tone (default, primary, success, warning, error, info, purple, teal) and wins over the schema property's own `colorMap` / `x-color-map`. `formatter` resolves against cnFormatters (built-ins incl. currency / conditionalPhrase — configured via `formatterOptions`); `widget` resolves against cnCellWidgets (built-ins incl. badge / fkResolve / link / avatar / date).",
3586
3590
  "items": {
3587
3591
  "oneOf": [
3588
3592
  { "type": "string" },