@conduction/nextcloud-vue 2.140.0 → 2.141.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 (74) hide show
  1. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue.js +1 -1
  2. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js +10 -2
  3. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js.map +1 -1
  4. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js.map +1 -1
  5. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue.js +1 -1
  6. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +11 -3
  7. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  8. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  9. package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
  10. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +12 -0
  11. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  12. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js +57 -40
  13. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  14. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  15. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +1 -0
  16. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  17. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  18. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +5 -3
  19. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  20. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +1 -0
  21. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  22. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js +65 -1
  23. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
  24. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js +120 -69
  25. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
  26. package/dist/esm/components/CnMapWidget/CnMapWidget.vue.js +1 -1
  27. package/dist/esm/components/CnMapWidget/CnMapWidget.vue2.js +47 -9
  28. package/dist/esm/components/CnMapWidget/CnMapWidget.vue2.js.map +1 -1
  29. package/dist/esm/components/CnMapWidget/CnMapWidget.vue3.js.map +1 -1
  30. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
  31. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +3 -3
  32. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  33. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  34. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  35. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +2 -0
  36. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  37. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +1 -1
  38. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  39. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue.js +1 -1
  40. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue2.js +10 -0
  41. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue2.js.map +1 -1
  42. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue3.js +34 -26
  43. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue3.js.map +1 -1
  44. package/dist/esm/l10n/en.json.js +2 -1
  45. package/dist/esm/l10n/en.json.js.map +1 -1
  46. package/dist/esm/l10n/nl.json.js +2 -1
  47. package/dist/esm/l10n/nl.json.js.map +1 -1
  48. package/dist/esm/library-style.js +1 -1
  49. package/dist/esm/nextcloud-vue.css +652 -391
  50. package/dist/esm/utils/groupSublines.js +120 -0
  51. package/dist/esm/utils/groupSublines.js.map +1 -0
  52. package/dist/nextcloud-vue.cjs.js +974 -638
  53. package/dist/nextcloud-vue.cjs.js.map +2 -2
  54. package/dist/nextcloud-vue.css +652 -391
  55. package/l10n/en.json +2 -1
  56. package/l10n/nl.json +2 -1
  57. package/package.json +1 -1
  58. package/src/components/CnAssignDialog/CnAssignDialog.vue +10 -2
  59. package/src/components/CnCellRenderer/CnCellRenderer.vue +11 -3
  60. package/src/components/CnChartWidget/CnChartWidget.md +1 -0
  61. package/src/components/CnChartWidget/CnChartWidget.vue +49 -4
  62. package/src/components/CnDashboardPage/CnDashboardPage.vue +1 -0
  63. package/src/components/CnDataTable/CnDataTable.vue +6 -3
  64. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +101 -3
  65. package/src/components/CnMapWidget/CnMapWidget.vue +47 -9
  66. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +3 -3
  67. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +20 -1
  68. package/src/components/CnRecordListWidget/CnRecordNoteRows.vue +33 -9
  69. package/src/css/dialog.css +13 -1
  70. package/src/css/look-board-detail.css +12 -9
  71. package/src/css/look-board-index.css +90 -7
  72. package/src/css/look-board.css +92 -0
  73. package/src/css/table.css +12 -0
  74. package/src/utils/groupSublines.js +125 -0
package/l10n/en.json CHANGED
@@ -2381,7 +2381,8 @@
2381
2381
  "me": "me",
2382
2382
  "Open profile": "Open profile",
2383
2383
  "Link a contact from your address book.": "Link a contact from your address book.",
2384
- "The value could not be shown.": "The value could not be shown."
2384
+ "The value could not be shown.": "The value could not be shown.",
2385
+ "No value": "No value"
2385
2386
  },
2386
2387
  "plurals": {
2387
2388
  "{count} member": [
package/l10n/nl.json CHANGED
@@ -2634,7 +2634,8 @@
2634
2634
  "The value could not be shown.": "De waarde kon niet worden getoond.",
2635
2635
  "Save as draft": "Opslaan als concept",
2636
2636
  "Draft saved at {time}": "Concept opgeslagen om {time}",
2637
- "The draft could not be saved": "Het concept kon niet worden opgeslagen"
2637
+ "The draft could not be saved": "Het concept kon niet worden opgeslagen",
2638
+ "No value": "Geen waarde"
2638
2639
  },
2639
2640
  "plurals": {
2640
2641
  "{count} member": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduction/nextcloud-vue",
3
- "version": "2.140.0",
3
+ "version": "2.141.0",
4
4
  "description": "Shared Vue component library for Conduction Nextcloud apps — complements @nextcloud/vue with higher-level components, OpenRegister integration, and NL Design System support",
5
5
  "homepage": "https://github.com/ConductionNL/nextcloud-vue#readme",
6
6
  "bugs": {
@@ -89,6 +89,7 @@ import { generateUrl } from '@nextcloud/router'
89
89
  import { NcButton, NcCheckboxRadioSwitch, NcTextArea } from '@nextcloud/vue'
90
90
  import { fetchEndpointSource, interpolateUrlTokens } from '../../composables/useEndpointSource.js'
91
91
  import { fillTemplate } from '../../utils/fillTemplate.js'
92
+ import { groupSublines } from '../../utils/groupSublines.js'
92
93
  import { readPath } from '../../utils/readPath.js'
93
94
  import { searchNextcloudGroups, searchNextcloudUsers } from '../../utils/userAutocomplete.js'
94
95
  import { CnDialog } from '../CnDialog/index.js'
@@ -134,7 +135,10 @@ export function withTypedAssignee(found, typed, label) {
134
135
  * `eyebrow` a template filled from the record.
135
136
  * - `source`: an endpointSource to read the record from, else `object`.
136
137
  * - `assignee`: `{ kinds?, label?, placeholder?, hint?, currentPath?,
137
- * currentHint?, currentAs?, allowTyped?, field?, kindField? }`. `kinds` is `["group"]`
138
+ * currentHint?, currentAs?, allowTyped?, field?, kindField?, describe? }`.
139
+ * `describe` (`{ register, schema, groupField?, textField? }`) names the
140
+ * records that hold a team's description; each team option then reads
141
+ * "{count} members · {description}", the count where the viewer may read it. `kinds` is `["group"]`
138
142
  * (default), `["user"]` or both; `currentPath` reads who has it now from the
139
143
  * record and `currentHint` ("Now with {current}.", the placeholder named
140
144
  * by `currentAs`) says so, in place of `hint`; `allowTyped`
@@ -398,8 +402,12 @@ export default {
398
402
  if (seq !== this.searchSeq) {
399
403
  return
400
404
  }
405
+ const lines = await groupSublines(groups.map((g) => g.id), typed, this.assigneeConfig.describe || null)
406
+ if (seq !== this.searchSeq) {
407
+ return
408
+ }
401
409
  const found = [
402
- ...groups.map((g) => ({ id: g.id, label: g.label, secondary: g.subline || '', icon: 'AccountGroupOutline', kind: 'group' })),
410
+ ...groups.map((g) => ({ id: g.id, label: g.label, secondary: lines[g.id] || g.subline || '', icon: 'AccountGroupOutline', kind: 'group' })),
403
411
  ...users.map((u) => ({ id: u.id, label: u.label, secondary: u.subline || '', avatar: true, kind: 'user' })),
404
412
  ]
405
413
  this.options = this.assigneeConfig.allowTyped ? withTypedAssignee(found, typed, t('nextcloud-vue', 'Use "{name}"', { name: typed })) : found
@@ -1737,8 +1737,9 @@ export default {
1737
1737
 
1738
1738
  /**
1739
1739
  * The swatch colour — read from the sibling row field named by
1740
- * `format.colorField` (default `'color'`). Returns null when no usable
1741
- * colour string is present (the dot is then omitted).
1740
+ * `format.colorField` (default `'color'`), through `format.colorMap`
1741
+ * when given (a stored name such as `blue` to a CSS colour). Returns
1742
+ * null when no usable colour is present (the dot is then omitted).
1742
1743
  *
1743
1744
  * @return {string|null}
1744
1745
  */
@@ -1748,7 +1749,14 @@ export default {
1748
1749
  }
1749
1750
  const field = (this.format && this.format.colorField) || 'color'
1750
1751
  const c = this.row && this.row[field]
1751
- return (typeof c === 'string' && c.trim() !== '') ? c : null
1752
+ if (typeof c !== 'string' || c.trim() === '') {
1753
+ return null
1754
+ }
1755
+ const map = this.format && this.format.colorMap
1756
+ if (map && typeof map === 'object') {
1757
+ return typeof map[c] === 'string' && map[c] !== '' ? map[c] : null
1758
+ }
1759
+ return c
1752
1760
  },
1753
1761
 
1754
1762
  formattedValue() {
@@ -135,3 +135,4 @@ The `dataSource` OBJECT-form `aggregate` (Wave 3, #91) — `{ groupBy, metric?:
135
135
  | Prop | Type | Default | Description |
136
136
  |------|------|---------|-------------|
137
137
  | `widgetId` | String | `''` | Matches `cn:widget:refresh` event-bus events; on a matching id the chart re-queries its `dataSource`. Also exposes a ref-callable `refresh()`. |
138
+ | `barValueAtEnd` | Boolean | `false` | A `bar-list` without a track: each bar as long as its share and its value right after the bar's end (DqProcesanalyse "Doorlooptijd per status"). False keeps the track and the value column. |
@@ -65,7 +65,7 @@
65
65
  share of `barMax` (default the largest value), the value at the
66
66
  end. Rows navigate when the source declares a drilldown. -->
67
67
  <div v-else-if="type === 'bar-list'" class="cn-chart-widget__html" data-testid="cn-chart-widget-bar-list">
68
- <ul class="cn-chart-widget__bar-list">
68
+ <ul class="cn-chart-widget__bar-list" :class="{ 'cn-chart-widget__bar-list--value-at-end': barValueAtEnd }">
69
69
  <li v-for="row in barListRows" :key="row.index" class="cn-chart-widget__bar-row">
70
70
  <component
71
71
  :is="row.drill ? 'button' : 'div'"
@@ -75,10 +75,16 @@
75
75
  data-testid="cn-chart-widget-bar-row"
76
76
  @click="row.drill ? onDataPointSelection(null, null, { dataPointIndex: row.index }) : null">
77
77
  <span class="cn-chart-widget__bar-label">{{ row.label }}</span>
78
- <span class="cn-chart-widget__bar-track" aria-hidden="true">
79
- <span class="cn-chart-widget__bar-fill" :style="{ width: `${row.share}%` }" />
78
+ <span v-if="barValueAtEnd" class="cn-chart-widget__bar-track">
79
+ <span class="cn-chart-widget__bar-fill" aria-hidden="true" :style="{ flexBasis: `${row.share}%` }" />
80
+ <span class="cn-chart-widget__bar-value">{{ row.text }}</span>
80
81
  </span>
81
- <span class="cn-chart-widget__bar-value">{{ row.text }}</span>
82
+ <template v-else>
83
+ <span class="cn-chart-widget__bar-track" aria-hidden="true">
84
+ <span class="cn-chart-widget__bar-fill" :style="{ width: `${row.share}%` }" />
85
+ </span>
86
+ <span class="cn-chart-widget__bar-value">{{ row.text }}</span>
87
+ </template>
82
88
  </component>
83
89
  </li>
84
90
  </ul>
@@ -468,6 +474,18 @@ export default {
468
474
  default: null,
469
475
  },
470
476
 
477
+ /**
478
+ * A `bar-list` without a track: each bar as long as its share and its
479
+ * value right after the bar's end (DqProcesanalyse "Doorlooptijd per
480
+ * status"). False, the default, keeps the track and the value column.
481
+ *
482
+ * @spec openspec/changes/chart-bar-list-value-at-end/specs/chart-bar-list-value-at-end/spec.md#requirement-a-bar-list-can-put-each-value-at-its-bars-end
483
+ */
484
+ barValueAtEnd: {
485
+ type: Boolean,
486
+ default: false,
487
+ },
488
+
471
489
  /**
472
490
  * A `matrix`'s row-label column width in px. Null, the default, sizes
473
491
  * the column to its longest label (at least 120px); a width wraps a
@@ -3006,6 +3024,33 @@ export default {
3006
3024
  white-space: nowrap;
3007
3025
  }
3008
3026
 
3027
+ /* `barValueAtEnd`: no track; the bar takes its share of the row and gives way
3028
+ to the value right after its end, so the longest bar still fits. */
3029
+ .cn-chart-widget__bar-list--value-at-end .cn-chart-widget__bar-line {
3030
+ grid-template-columns: minmax(80px, 130px) minmax(0, 1fr);
3031
+ }
3032
+
3033
+ .cn-chart-widget__bar-list--value-at-end .cn-chart-widget__bar-track {
3034
+ align-items: center;
3035
+ background: none;
3036
+ display: flex;
3037
+ gap: 6px;
3038
+ height: auto;
3039
+ overflow: visible;
3040
+ }
3041
+
3042
+ .cn-chart-widget__bar-list--value-at-end .cn-chart-widget__bar-fill {
3043
+ border-radius: 4px;
3044
+ flex: 0 1 auto;
3045
+ height: 18px;
3046
+ min-width: 2px;
3047
+ }
3048
+
3049
+ .cn-chart-widget__bar-list--value-at-end .cn-chart-widget__bar-value {
3050
+ flex: none;
3051
+ text-align: start;
3052
+ }
3053
+
3009
3054
  .cn-chart-widget__footnote {
3010
3055
  color: var(--color-text-maxcontrast);
3011
3056
  margin: 0;
@@ -1150,6 +1150,7 @@ const CHART_PROP_KEYS = [
1150
1150
  'endpointSource',
1151
1151
  // `bar-list` / `matrix` presentation keys.
1152
1152
  'barMax',
1153
+ 'barValueAtEnd',
1153
1154
  'matrixLabelWidth',
1154
1155
  'footnote',
1155
1156
  'highlight',
@@ -6,6 +6,7 @@
6
6
  'cn-table-container--board': isBoardLook,
7
7
  'cn-table-container--title-plain': isBoardLook && rowTitle === 'plain',
8
8
  'cn-table-container--title-text': isBoardLook && rowTitle === 'text',
9
+ 'cn-table-container--title-strong': isBoardLook && rowTitle === 'strong',
9
10
  'cn-table-container--scrollable': scrollable,
10
11
  'cn-table-container--borderless': borderless,
11
12
  'cn-table-container--fill': fillHeight,
@@ -856,8 +857,10 @@ export default {
856
857
  * the 15px bold title without an underline and a 13px muted secondary
857
858
  * line, as the PqTickets and OcPublicaties boards draw it; `text` is an
858
859
  * ordinary cell, for a log table whose first column is a date
859
- * (DqZaakContact). The row stays clickable either way. The Nextcloud
860
- * look ignores it.
860
+ * (DqZaakContact); `strong` is the cell's own size at weight 600 in the
861
+ * main ink without an underline, the name column of a card's list
862
+ * (DqZaaktypeStatussen). The row stays clickable either way. The
863
+ * Nextcloud look ignores it.
861
864
  *
862
865
  * @spec openspec/changes/screens-table-rows-parity/specs/index-list-board-look/spec.md#requirement-a-row-title-can-be-plain-text
863
866
  * @spec openspec/changes/board-log-table-plain-cells/specs/board-log-table-plain-cells/spec.md#requirement-a-tables-title-column-can-read-as-plain-text
@@ -865,7 +868,7 @@ export default {
865
868
  rowTitle: {
866
869
  type: String,
867
870
  default: 'link',
868
- validator: (value) => ['link', 'plain', 'text'].includes(value),
871
+ validator: (value) => ['link', 'plain', 'text', 'strong'].includes(value),
869
872
  },
870
873
 
871
874
  /**
@@ -103,7 +103,7 @@
103
103
  </p>
104
104
  </div>
105
105
 
106
- <div v-if="hasActiveFilters" class="cn-index-sidebar__section">
106
+ <div v-if="hasActiveFilters && !isBoardLook" class="cn-index-sidebar__section">
107
107
  <NcButton variant="tertiary" @click="onClearAll">
108
108
  <template #icon>
109
109
  <FilterRemoveOutline :size="20" />
@@ -113,7 +113,20 @@
113
113
  </div>
114
114
 
115
115
  <div v-if="schemaFilters.length > 0" class="cn-index-sidebar__section">
116
- <h3>{{ filtersLabel }}</h3>
116
+ <!-- The board look: "Alles wissen" beside the heading (DqZakenSorteren). -->
117
+ <div v-if="isBoardLook" class="cn-index-sidebar__filters-head" data-testid="cn-index-sidebar-filters-head">
118
+ <h3>{{ filtersLabel }}</h3>
119
+ <button
120
+ v-if="hasActiveFilters"
121
+ type="button"
122
+ class="cn-index-sidebar__filters-clear"
123
+ @click="onClearAll">
124
+ {{ clearAllLabel }}
125
+ </button>
126
+ </div>
127
+ <h3 v-else>
128
+ {{ filtersLabel }}
129
+ </h3>
117
130
  <div
118
131
  v-for="filter in schemaFilters"
119
132
  :key="filter.key"
@@ -155,6 +168,27 @@
155
168
  :label="filter.label"
156
169
  :placeholder="filter.label"
157
170
  @update:modelValue="onTextChange(filter.key, $event)" />
171
+ <!-- The board look: a short field as checkbox rows with counts. -->
172
+ <fieldset
173
+ v-else-if="checklistOptions(filter)"
174
+ class="cn-index-sidebar__checklist"
175
+ :data-testid="`cn-index-sidebar-checklist-${filter.key}`">
176
+ <legend class="hidden-visually">
177
+ {{ filter.label }}
178
+ </legend>
179
+ <label
180
+ v-for="option in checklistOptions(filter)"
181
+ :key="`${filter.key}-${option.id}`"
182
+ class="cn-index-sidebar__checklist-row"
183
+ data-testid="cn-index-sidebar-checklist-row">
184
+ <input
185
+ type="checkbox"
186
+ :checked="isChecked(filter, option.id)"
187
+ @change="onChecklistToggle(filter, option.id, $event.target.checked)">
188
+ <span class="cn-index-sidebar__checklist-label">{{ option.label }}</span>
189
+ <span v-if="option.count !== null" class="cn-index-sidebar__checklist-count">{{ option.count }}</span>
190
+ </label>
191
+ </fieldset>
158
192
  <NcSelect
159
193
  v-else
160
194
  class="cn-index-sidebar__select"
@@ -369,7 +403,7 @@ import CnSavedViewsPanel from '../CnSavedViewsPanel/CnSavedViewsPanel.vue'
369
403
  import CnSegmentedControl from '../CnSegmentedControl/CnSegmentedControl.vue'
370
404
  import { normalizeLook } from '../../composables/useLook.js'
371
405
  import { METADATA_COLUMNS } from '../../constants/metadata.js'
372
- import { facetOptions } from '../../utils/facets.js'
406
+ import { facetOptions, MISSING_VALUE } from '../../utils/facets.js'
373
407
  import { labelLang } from '../../utils/manifestTranslate.js'
374
408
  import { columnsFromSchema, filtersFromSchema } from '../../utils/schema.js'
375
409
  /**
@@ -389,6 +423,9 @@ import { labelSidePanelClose } from '../../utils/sidePanelCloseLabel.js'
389
423
  import { reverseSortKeys, sameSortKeys } from '../../utils/sortOrder.js'
390
424
  import { CnDateRangePicker, DEFAULT_DATE_RANGE_PRESETS } from '../CnDateRangePicker/index.js'
391
425
  import { CnIcon } from '../CnIcon/index.js'
426
+ /** The most values a filter shows as checkbox rows in the board look; a longer field keeps its picker. */
427
+ const CHECKLIST_MAX = 12
428
+
392
429
  export default {
393
430
  name: 'CnIndexSidebar',
394
431
 
@@ -1411,6 +1448,67 @@ export default {
1411
1448
  return values.map((v) => options.find((o) => o.id === v) || { id: v, label: String(v) })
1412
1449
  },
1413
1450
 
1451
+ /**
1452
+ * A field's values as checkbox rows, in the board look and for a short
1453
+ * list only: label and count apart, from the facet when there is one,
1454
+ * else the field's own options. Null where the picker stays (the
1455
+ * Nextcloud look, a single choice, a reference, more than
1456
+ * CHECKLIST_MAX values).
1457
+ *
1458
+ * @spec openspec/changes/board-sidebar-filters-as-checkbox-lists/specs/index-sidebar/spec.md#requirement-the-board-look-draws-a-short-filter-as-a-checkbox-list-with-counts
1459
+ * @param {object} filter The filter definition.
1460
+ * @return {Array<{id: (string|number|boolean), label: string, count: (number|null)}>|null} The rows, or null.
1461
+ */
1462
+ checklistOptions(filter) {
1463
+ if (!this.isBoardLook || filter.multiple === false || filter.type === 'reference') {
1464
+ return null
1465
+ }
1466
+ const facet = this.facetData[filter.key]
1467
+ let rows
1468
+ if (facet && Array.isArray(facet.values) && facet.values.length > 0) {
1469
+ rows = facet.values.map((bucket) => ({
1470
+ id: bucket.value,
1471
+ label: String(bucket.label !== undefined && bucket.label !== null ? bucket.label : bucket.value),
1472
+ count: typeof bucket.count === 'number' ? bucket.count : null,
1473
+ }))
1474
+ if (typeof facet.missing === 'number' && facet.missing > 0) {
1475
+ rows.push({ id: MISSING_VALUE, label: t('nextcloud-vue', 'No value'), count: facet.missing })
1476
+ }
1477
+ } else {
1478
+ rows = (filter.options || []).map((option) => ({ id: option.id, label: String(option.label), count: null }))
1479
+ }
1480
+ return rows.length > 0 && rows.length <= CHECKLIST_MAX ? rows : null
1481
+ },
1482
+
1483
+ /**
1484
+ * Whether a value of a field is among its active filter values.
1485
+ *
1486
+ * @param {object} filter The filter definition.
1487
+ * @param {string|number|boolean} id The value.
1488
+ * @return {boolean}
1489
+ */
1490
+ isChecked(filter, id) {
1491
+ const value = this.activeFilters[filter.key]
1492
+ const values = Array.isArray(value) ? value : (value === undefined || value === null || value === '' ? [] : [value])
1493
+ return values.map(String).includes(String(id))
1494
+ },
1495
+
1496
+ /**
1497
+ * A checkbox row was toggled: the field's values with it added or taken out.
1498
+ *
1499
+ * @spec openspec/changes/board-sidebar-filters-as-checkbox-lists/specs/index-sidebar/spec.md#requirement-the-board-look-draws-a-short-filter-as-a-checkbox-list-with-counts
1500
+ * @param {object} filter The filter definition.
1501
+ * @param {string|number|boolean} id The value.
1502
+ * @param {boolean} checked Whether it is now ticked.
1503
+ * @return {void}
1504
+ */
1505
+ onChecklistToggle(filter, id, checked) {
1506
+ const value = this.activeFilters[filter.key]
1507
+ const current = Array.isArray(value) ? [...value] : (value === undefined || value === null || value === '' ? [] : [value])
1508
+ const rest = current.filter((v) => String(v) !== String(id))
1509
+ this.$emit('filter-change', { key: filter.key, values: checked ? [...rest, id] : rest })
1510
+ },
1511
+
1414
1512
  /**
1415
1513
  * Handle filter select change
1416
1514
  *
@@ -226,6 +226,25 @@ function controlIcon(path) {
226
226
  * @slot popup Per-marker popup body override. Scoped props: `{ feature, properties }`. When omitted, the widget renders the value of `markers.popupField` (or no popup at all when unset).
227
227
  * @slot fallback Replaces the default "map unavailable" message when Leaflet fails to load (test environments, CSP-blocked builds, offline dev). No scoped props.
228
228
  */
229
+ /**
230
+ * The count line a header spec gives (`{ one, other }` or one string, with
231
+ * `{total}`), once the marker source answered with markers.
232
+ *
233
+ * @param {object|string|undefined} spec The declared line.
234
+ * @param {object} vm The map: `markersLoaded`, `markersFailed`, `markerCount`, `cnTranslate`.
235
+ * @return {string} The line, or ''.
236
+ */
237
+ function countLine(spec, vm) {
238
+ if (!spec || !vm.markersLoaded || vm.markersFailed || vm.markerCount === 0) {
239
+ return ''
240
+ }
241
+ const text = typeof spec === 'string' ? spec : (vm.markerCount === 1 ? spec.one : spec.other)
242
+ if (typeof text !== 'string' || text === '') {
243
+ return ''
244
+ }
245
+ return vm.cnTranslate(text).replace(/\{total\}/g, String(vm.markerCount))
246
+ }
247
+
229
248
  export default {
230
249
  name: 'CnMapWidget',
231
250
 
@@ -577,15 +596,19 @@ export default {
577
596
  * @spec openspec/changes/map-widget-header-count/specs/map-widget-header-count/spec.md#requirement-a-map-card-can-say-how-many-places-it-shows
578
597
  */
579
598
  headerSubtitleText() {
580
- const spec = this.content?.headerSubtitle
581
- if (!spec || !this.markersLoaded || this.markersFailed || this.markerCount === 0) {
582
- return ''
583
- }
584
- const text = typeof spec === 'string' ? spec : (this.markerCount === 1 ? spec.one : spec.other)
585
- if (typeof text !== 'string' || text === '') {
586
- return ''
587
- }
588
- return this.cnTranslate(text).replace(/\{total\}/g, String(this.markerCount))
599
+ return countLine(this.content?.headerSubtitle, this)
600
+ },
601
+
602
+ /**
603
+ * The same count as a note at the right of the card's title row
604
+ * (`content.headerNote`, the same shape as `headerSubtitle`), as the
605
+ * case boards draw "Locaties ... 1 locatie".
606
+ *
607
+ * @return {string}
608
+ * @spec openspec/changes/map-count-note/specs/map-widget/spec.md
609
+ */
610
+ headerNoteText() {
611
+ return countLine(this.content?.headerNote, this)
589
612
  },
590
613
 
591
614
  markerStatusText() {
@@ -672,6 +695,21 @@ export default {
672
695
  }
673
696
  },
674
697
 
698
+ /**
699
+ * Report the count note to the card around the map.
700
+ *
701
+ * @param {string} note The note, or ''.
702
+ */
703
+ headerNoteText(note) {
704
+ if (this.content?.headerNote) {
705
+ /**
706
+ * @event header The card header's note at the right of the title.
707
+ * @type {{note: string}}
708
+ */
709
+ this.$emit('header', { note })
710
+ }
711
+ },
712
+
675
713
  // Keyed, never `deep: true` — see `layersKey`.
676
714
  layersKey() {
677
715
  if (this.map) {
@@ -669,14 +669,14 @@ export default {
669
669
  },
670
670
 
671
671
  /**
672
- * How the table draws its title column (`content.rowTitle`: link, plain
673
- * or text), link when unset or unknown.
672
+ * How the table draws its title column (`content.rowTitle`: link, plain,
673
+ * text or strong), link when unset or unknown.
674
674
  *
675
675
  * @return {string}
676
676
  * @spec openspec/changes/board-log-table-plain-cells/specs/board-log-table-plain-cells/spec.md#requirement-a-tables-title-column-can-read-as-plain-text
677
677
  */
678
678
  rowTitle() {
679
- return ['plain', 'text'].includes(this.content.rowTitle) ? this.content.rowTitle : 'link'
679
+ return ['plain', 'text', 'strong'].includes(this.content.rowTitle) ? this.content.rowTitle : 'link'
680
680
  },
681
681
 
682
682
  /**
@@ -6,7 +6,7 @@
6
6
  <template>
7
7
  <div
8
8
  class="cn-record-list"
9
- :class="[`cn-record-list--${presentation}`, { 'cn-record-list--compact': content.density === 'compact', 'cn-record-list--tiles': presentation === 'rows' && content.tiles === true, 'cn-record-list--rule-above': content.ruleAbove === true, 'cn-record-list--data-table': presentation === 'table' && content.tableLook === 'data' }]"
9
+ :class="[`cn-record-list--${presentation}`, { 'cn-record-list--compact': content.density === 'compact', 'cn-record-list--stacked': content.density === 'stacked', 'cn-record-list--tiles': presentation === 'rows' && content.tiles === true, 'cn-record-list--rule-above': content.ruleAbove === true, 'cn-record-list--data-table': presentation === 'table' && content.tableLook === 'data' }]"
10
10
  data-testid="cn-record-list">
11
11
  <p v-if="intro && !absent" class="cn-record-list__intro" data-testid="cn-record-list-intro">
12
12
  {{ intro }}
@@ -713,6 +713,8 @@ export default {
713
713
  * without a value.
714
714
  * `density: "compact"` draws the rows 10px above and below, at least
715
715
  * 56px tall, with the title at 500 (DqProcesanalyse).
716
+ * `density: "stacked"` stands the rows 10px apart with no rule and no
717
+ * row padding (DqDagAfsluiten "Morgen eerst").
716
718
  * In `rows`, `text` is a line under the meta line ("12 dagen stil"),
717
719
  * and `chevron: false` leaves the chevron off a linked row
718
720
  * (DqDashboard).
@@ -3103,6 +3105,23 @@ export default {
3103
3105
  font-weight: 500;
3104
3106
  }
3105
3107
 
3108
+ /* `density: "stacked"`: the rows stand 10px apart with no rule and no row
3109
+ padding, a short side list (DqDagAfsluiten "Morgen eerst"). */
3110
+ .cn-record-list--stacked .cn-record-list__list {
3111
+ display: flex;
3112
+ flex-direction: column;
3113
+ gap: 10px;
3114
+ }
3115
+
3116
+ .cn-record-list--stacked .cn-record-list__row {
3117
+ min-height: 0;
3118
+ padding: 0;
3119
+ }
3120
+
3121
+ .cn-record-list--stacked .cn-record-list__item + .cn-record-list__item {
3122
+ border-top: 0;
3123
+ }
3124
+
3106
3125
  .cn-record-list__meta {
3107
3126
  color: var(--color-text-maxcontrast);
3108
3127
  font-size: 13px;
@@ -37,15 +37,20 @@
37
37
  data-testid="cn-record-list-due">{{ item.due.text }}</span>
38
38
  </div>
39
39
  <div class="cn-record-notes__fields">
40
- <NcTextField
41
- class="cn-record-notes__field"
42
- :modelValue="drafts[item.key] || ''"
43
- :label="fieldLabel"
44
- :labelOutside="false"
45
- :placeholder="placeholder"
46
- :disabled="busy"
47
- :data-testid="`cn-record-list-note-${item.key}`"
48
- @update:modelValue="setDraft(item.key, $event)" />
40
+ <!-- The label stands above the field, as the leaf's own beside it
41
+ (humaniq "Uren"), so both fields share one line. -->
42
+ <div class="cn-record-notes__field">
43
+ <label :for="fieldId(item.key)" class="cn-record-notes__field-label">{{ fieldLabel }}</label>
44
+ <NcTextField
45
+ :id="fieldId(item.key)"
46
+ :modelValue="drafts[item.key] || ''"
47
+ :label="fieldLabel"
48
+ labelOutside
49
+ :placeholder="placeholder"
50
+ :disabled="busy"
51
+ :data-testid="`cn-record-list-note-${item.key}`"
52
+ @update:modelValue="setDraft(item.key, $event)" />
53
+ </div>
49
54
  <div v-if="leaf && leafProvider" class="cn-record-notes__leaf" :data-testid="`cn-record-list-leaf-${item.key}`">
50
55
  <CnLeafMountHost
51
56
  v-if="leafIsMount"
@@ -301,6 +306,16 @@ export default {
301
306
  this.drafts = { ...this.drafts, [key]: String(value ?? '') }
302
307
  },
303
308
 
309
+ /**
310
+ * The id of a row's note field, for its label.
311
+ *
312
+ * @param {string} key The row key.
313
+ * @return {string} The id.
314
+ */
315
+ fieldId(key) {
316
+ return `cn-record-notes-${this.$.uid}-${String(key).replace(/[^A-Za-z0-9_-]/g, '-')}`
317
+ },
318
+
304
319
  /**
305
320
  * @param {string} key The row key.
306
321
  * @return {boolean} Whether the row has a note typed.
@@ -472,7 +487,16 @@ export default {
472
487
  }
473
488
 
474
489
  .cn-record-notes__field {
490
+ display: flex;
475
491
  flex: 1 1 260px;
492
+ flex-direction: column;
493
+ gap: 6px;
494
+ }
495
+
496
+ .cn-record-notes__field-label {
497
+ color: var(--color-main-text);
498
+ font-size: 14px;
499
+ font-weight: 600;
476
500
  }
477
501
 
478
502
  .cn-record-notes__leaf {
@@ -238,8 +238,11 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .modal-mask {
238
238
  padding-inline-end: 24px;
239
239
  }
240
240
 
241
+ /* The body starts 22px under the header, as most dialog boards draw it
242
+ (header padding-bottom 4px + form padding-top 18px; DqBerichtVersturen,
243
+ DqOverdragen, DqNieuweZaak, DqBesluit): the 18px gap plus 4px. */
241
244
  .cn-dialog-board .dialog__content > .cn-dialog-header {
242
- margin-bottom: -2px;
245
+ margin-bottom: 4px;
243
246
  }
244
247
 
245
248
  /* Footer: 16px 24px, hairline on top, right aligned, 40px buttons, 10px gap. */
@@ -344,6 +347,15 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .modal-mask {
344
347
  margin-block-start: 26px;
345
348
  }
346
349
 
350
+ /* A select in a form dialog sits under its label and over its help line
351
+ as a text field does (DqNieuweZaak "Zaaktype", "Intakekanaal"): the
352
+ form's label stands outside the select, so NcSelect's own 6px/4px margins
353
+ only pushed the control 6px below the text fields beside it and its help
354
+ line 4px further down. */
355
+ .cn-dialog-board .dialog__content .cn-form-dialog__select-wrapper > .v-select.select:not(:has(.input-field__label)) {
356
+ margin-block: 0;
357
+ }
358
+
347
359
  /* A multiple select is the board's 40px field too (DqFilterDelen "Delen
348
360
  met"). The edge goes on the toggle, which holds the chips and the search,
349
361
  so the chosen people and groups sit inside the field; the search inside it
@@ -721,6 +721,13 @@
721
721
  padding-top: calc(14px - var(--cn-board-first-line-leading));
722
722
  }
723
723
 
724
+ /* A table (an object list) starts 12px below the title's box: its head has
725
+ a fill and no leading, so the glyph-to-glyph gap above left it 2px short
726
+ (DqZaaktypeStatussen "Statussen", "Resultaten"). */
727
+ .cn-look-board .cn-detail-page .cn-widget-wrapper__content:has(> .cn-object-list-widget):not(.cn-detail-page__side *) {
728
+ padding-top: 12px;
729
+ }
730
+
724
731
  /* Running text (DqZaak "Het verzoek") sits 12px below the title's box and
725
732
  keeps its own half-leading, at 1.6 as drawn.
726
733
  @spec openspec/changes/board-card-title-gap-variants/specs/board-card-title-gap-variants/spec.md#requirement-a-board-card-starts-its-body-as-drawn-for-its-kind-of-body */
@@ -1527,12 +1534,12 @@
1527
1534
  }
1528
1535
 
1529
1536
  /* ---- A card's Add ------------------------------------------------------ */
1530
- /* The board draws a list card's Add as the neutral secondary at the 40px
1531
- control height, right-aligned in the title row (DqZaakGerelateerd
1532
- "Deelzaak aanmaken", DqZaakTaken "Toevoegen"). CnDetailWidgetHost renders
1537
+ /* The board draws a list card's Add as the neutral secondary right-aligned
1538
+ in the title row, at the title row's 34px (DqZaaktypeStatussen "Status
1539
+ toevoegen", DqZaaktype "Taak toevoegen", DqCatalogus "Objecttype
1540
+ toevoegen": 13 of the 15 boards that draw one). CnDetailWidgetHost renders
1533
1541
  that button in the board look, so the list's footer Add bar, which ADR-062
1534
- draws in the Nextcloud look, is hidden in the card. The size outranks a
1535
- card title row's 34px buttons.
1542
+ draws in the Nextcloud look, is hidden in the card.
1536
1543
  @spec openspec/changes/board-object-list-add-button/specs/board-object-list-add-button/spec.md#requirement-the-board-look-draws-a-cards-add-as-a-title-row-button */
1537
1544
  .cn-look-board .cn-detail-page__catalog-card:not(.cn-look-nextcloud *) .cn-object-list-widget__add {
1538
1545
  display: none;
@@ -1544,10 +1551,6 @@
1544
1551
  display: none;
1545
1552
  }
1546
1553
 
1547
- .cn-look-board .cn-widget-wrapper__actions .button-vue.cn-detail-page__catalog-add:has(> .button-vue__wrapper > .button-vue__text:not(:empty)):where(:not(.cn-look-nextcloud *)) {
1548
- --button-size: var(--cn-board-control-height);
1549
- }
1550
-
1551
1554
  /* A map in a board card has the board's control corners (DqZaak, DqZaakMeer
1552
1555
  "Locaties"); overflow keeps the tiles inside them. */
1553
1556
  .cn-look-board .cn-map-widget:not(.cn-map-widget--fullscreen) {