@conduction/nextcloud-vue 2.92.0 → 2.93.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 (121) hide show
  1. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +57 -4
  2. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +44 -17
  4. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  5. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue.js +1 -1
  6. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js +44 -2
  7. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
  8. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +140 -59
  9. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
  10. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  11. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +20 -6
  12. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  13. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +3 -2
  14. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  15. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +2 -36
  16. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  17. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  18. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +5 -3
  19. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  20. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  21. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  22. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +6 -4
  23. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  24. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  25. package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue2.js +46 -0
  26. package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue2.js.map +1 -1
  27. package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue3.js +6 -2
  28. package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue3.js.map +1 -1
  29. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue.js +1 -1
  30. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js +82 -9
  31. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js.map +1 -1
  32. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js +104 -90
  33. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js.map +1 -1
  34. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue.js +9 -0
  35. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue.js.map +1 -0
  36. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js +355 -0
  37. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js.map +1 -0
  38. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js +462 -0
  39. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js.map +1 -0
  40. package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
  41. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +21 -0
  42. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  43. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +402 -391
  44. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  45. package/dist/esm/components/CnIcon/semanticIcons.js +2 -2
  46. package/dist/esm/components/CnIcon/semanticIcons.js.map +1 -1
  47. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue.js +1 -1
  48. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js +22 -1
  49. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js.map +1 -1
  50. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js +20 -14
  51. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js.map +1 -1
  52. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue.js +1 -1
  53. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue2.js +2 -0
  54. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue2.js.map +1 -1
  55. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue3.js +15 -5
  56. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue3.js.map +1 -1
  57. package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue.js +1 -1
  58. package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue2.js +8 -30
  59. package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue2.js.map +1 -1
  60. package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue3.js.map +1 -1
  61. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js +9 -0
  62. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js.map +1 -0
  63. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js +164 -0
  64. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js.map +1 -0
  65. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js +93 -0
  66. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js.map +1 -0
  67. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue.js +1 -1
  68. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +12 -0
  69. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
  70. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
  71. package/dist/esm/l10n/en.json.js +24 -1
  72. package/dist/esm/l10n/en.json.js.map +1 -1
  73. package/dist/esm/l10n/nl.json.js +24 -1
  74. package/dist/esm/l10n/nl.json.js.map +1 -1
  75. package/dist/esm/library-style.js +1 -1
  76. package/dist/esm/nextcloud-vue.css +887 -247
  77. package/dist/esm/utils/featureCatalog.js +135 -0
  78. package/dist/esm/utils/featureCatalog.js.map +1 -0
  79. package/dist/esm/utils/fillTemplate.js +74 -3
  80. package/dist/esm/utils/fillTemplate.js.map +1 -1
  81. package/dist/esm/utils/headerMeta.js +62 -12
  82. package/dist/esm/utils/headerMeta.js.map +1 -1
  83. package/dist/esm/utils/roadmapIssues.js +86 -0
  84. package/dist/esm/utils/roadmapIssues.js.map +1 -0
  85. package/dist/esm/utils/userDisplayName.js +47 -0
  86. package/dist/esm/utils/userDisplayName.js.map +1 -0
  87. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  88. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  89. package/dist/nextcloud-vue.cjs.js +14820 -12966
  90. package/dist/nextcloud-vue.cjs.js.map +2 -2
  91. package/dist/nextcloud-vue.css +887 -247
  92. package/l10n/en.json +24 -1
  93. package/l10n/nl.json +24 -1
  94. package/package.json +1 -1
  95. package/src/components/CnAppRoot/CnAppRoot.md +2 -0
  96. package/src/components/CnAppRoot/CnAppRoot.vue +66 -5
  97. package/src/components/CnChoiceCards/CnChoiceCards.md +27 -0
  98. package/src/components/CnChoiceCards/CnChoiceCards.vue +168 -3
  99. package/src/components/CnDashboardPage/CnDashboardPage.vue +45 -6
  100. package/src/components/CnDataTable/CnDataTable.vue +2 -36
  101. package/src/components/CnDetailPage/CnDetailPage.vue +5 -3
  102. package/src/components/CnFactsWidget/CnFactsWidget.vue +6 -4
  103. package/src/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue +51 -1
  104. package/src/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue +98 -13
  105. package/src/components/CnFeaturesBoard/CnFeaturesBoard.vue +1053 -0
  106. package/src/components/CnFormDialog/CnFormDialog.vue +606 -562
  107. package/src/components/CnNcWidgetWidget/CnNcWidgetWidget.vue +23 -2
  108. package/src/components/CnNotificationPreferences/CnNotificationPreferences.vue +5 -0
  109. package/src/components/CnRoadmapTab/CnRoadmapTab.vue +6 -28
  110. package/src/components/CnUserSettingsDialog/CnUserSettingsDialog.vue +305 -0
  111. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +12 -0
  112. package/src/css/dialog.css +47 -0
  113. package/src/css/look-board-detail.css +8 -0
  114. package/src/css/look-board.css +33 -0
  115. package/src/schemas/app-manifest-v2.schema.json +9 -3
  116. package/src/utils/featureCatalog.js +132 -0
  117. package/src/utils/fillTemplate.js +73 -2
  118. package/src/utils/headerMeta.js +62 -12
  119. package/src/utils/roadmapIssues.js +82 -0
  120. package/src/utils/userDisplayName.js +54 -0
  121. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -5,7 +5,7 @@
5
5
 
6
6
  <template>
7
7
  <div class="cn-nc-widget-widget" :class="{ 'cn-nc-widget-widget--horizontal': displayMode === 'horizontal' }">
8
- <header class="cn-nc-widget-widget__header">
8
+ <header v-if="showsHeader" class="cn-nc-widget-widget__header" data-testid="cn-nc-widget-header">
9
9
  <img
10
10
  v-if="widgetIconUrl"
11
11
  class="cn-nc-widget-widget__header-icon"
@@ -130,9 +130,16 @@ function getDashboardGlobal() {
130
130
  export default {
131
131
  name: 'CnNcWidgetWidget',
132
132
 
133
+ inject: {
134
+ /** Whether the hosting card draws a title row (CnWidgetWrapper). */
135
+ cnWidgetTitled: { default: null },
136
+ },
137
+
133
138
  props: {
134
139
  /**
135
- * Persisted widget content `{widgetId, displayMode, limit?, showAllUrl?, showAllLabel?}`.
140
+ * Persisted widget content `{widgetId, displayMode, limit?, showAllUrl?, showAllLabel?, showHeader?}`.
141
+ * `showHeader: false` drops the widget's own header (icon and title);
142
+ * it is dropped too when the hosting card draws a title row.
136
143
  * `limit` is the most items shown (the list then comes from the items
137
144
  * API, so it fits its cell); `showAllUrl` / `showAllLabel` the link to
138
145
  * the rest (default the widget's own page and "Show all").
@@ -207,6 +214,20 @@ export default {
207
214
  return null
208
215
  },
209
216
 
217
+ /**
218
+ * Whether the widget draws its own header: not with
219
+ * `content.showHeader: false`, nor inside a card that draws a title.
220
+ *
221
+ * @spec openspec/changes/nc-widget-header/specs/nc-widget-header/spec.md#requirement-a-nextcloud-widget-draws-no-second-heading
222
+ * @return {boolean}
223
+ */
224
+ showsHeader() {
225
+ if (this.content && this.content.showHeader === false) {
226
+ return false
227
+ }
228
+ return !(typeof this.cnWidgetTitled === 'function' && this.cnWidgetTitled())
229
+ },
230
+
210
231
  /**
211
232
  * The header title.
212
233
  *
@@ -1,5 +1,8 @@
1
1
  <template>
2
2
  <NcAppSettingsSection id="notifications" :name="t('nextcloud-vue', 'Notifications')">
3
+ <template v-if="cnLook === 'board'" #icon>
4
+ <BellOutline :size="18" />
5
+ </template>
3
6
  <NcLoadingIcon v-if="loading" :size="24" />
4
7
 
5
8
  <NcEmptyContent v-else-if="error"
@@ -107,6 +110,8 @@ export default {
107
110
  * than an always-empty pane.
108
111
  */
109
112
  cnAppId: { default: () => '' },
113
+ /** The app's look; `board` gives the section its navigation icon. */
114
+ cnLook: { default: 'nextcloud' },
110
115
  /**
111
116
  * App-supplied labels for its notification rules, provided by the
112
117
  * CnAppRoot ancestor from its `notificationLabels` prop. Keyed
@@ -69,9 +69,7 @@
69
69
  *
70
70
  * Spec: features-roadmap-component — Requirement "RoadmapTab".
71
71
  */
72
- import axios from '@nextcloud/axios'
73
72
  import { translate as t } from '@nextcloud/l10n'
74
- import { generateUrl } from '@nextcloud/router'
75
73
  import { NcButton, NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue'
76
74
  import AlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue'
77
75
  import ClockOutline from 'vue-material-design-icons/ClockOutline.vue'
@@ -79,6 +77,7 @@ import InformationOutline from 'vue-material-design-icons/InformationOutline.vue
79
77
  import KeyOutline from 'vue-material-design-icons/KeyOutline.vue'
80
78
  import RoadVariant from 'vue-material-design-icons/RoadVariant.vue'
81
79
  import CnRoadmapItem from '../CnRoadmapItem/CnRoadmapItem.vue'
80
+ import { fetchRoadmapIssues, sortByReactions } from '../../utils/roadmapIssues.js'
82
81
 
83
82
  export default {
84
83
  name: 'CnRoadmapTab',
@@ -124,7 +123,7 @@ export default {
124
123
 
125
124
  computed: {
126
125
  sortedItems() {
127
- return [...this.items].sort((a, b) => (b.reactions?.total_count || 0) - (a.reactions?.total_count || 0))
126
+ return sortByReactions(this.items)
128
127
  },
129
128
 
130
129
  disabledLabel() {
@@ -175,31 +174,10 @@ export default {
175
174
  this.loading = true
176
175
  this.degraded = null
177
176
  this.items = []
178
- try {
179
- const url = generateUrl('/apps/openregister/api/github/issues')
180
- const response = await axios.get(url, {
181
- params: {
182
- repo: this.repo,
183
- labels: 'enhancement,feature',
184
- },
185
- })
186
- const data = response.data || {}
187
- if (data.hint === 'github_pat_not_configured') {
188
- this.degraded = 'pat_not_configured'
189
- } else if (Array.isArray(data.items)) {
190
- this.items = data.items
191
- }
192
- } catch (e) {
193
- const status = e?.response?.status
194
- const code = e?.response?.data?.error
195
- if (status === 429 || code === 'github_rate_limited' || code === 'rate_limited') {
196
- this.degraded = 'rate_limited'
197
- } else {
198
- this.degraded = 'error'
199
- }
200
- } finally {
201
- this.loading = false
202
- }
177
+ const { items, degraded } = await fetchRoadmapIssues(this.repo)
178
+ this.items = items
179
+ this.degraded = degraded
180
+ this.loading = false
203
181
  },
204
182
  },
205
183
  }
@@ -0,0 +1,305 @@
1
+ <!--
2
+ - SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ - SPDX-License-Identifier: EUPL-1.2
4
+ -->
5
+ <template>
6
+ <CnDialog
7
+ class="cn-user-settings-dialog"
8
+ :name="title"
9
+ look="board"
10
+ width="wizard"
11
+ :eyebrow="eyebrow"
12
+ :subtitle="subtitle"
13
+ :open="open"
14
+ :style="eyebrowStyle"
15
+ data-testid="cn-user-settings-dialog"
16
+ @update:open="onUpdateOpen"
17
+ @closing="close">
18
+ <div class="cn-user-settings-dialog__body">
19
+ <nav
20
+ v-if="sections.length > 0"
21
+ class="cn-user-settings-dialog__nav"
22
+ :aria-label="navLabel"
23
+ data-testid="cn-user-settings-nav">
24
+ <ul>
25
+ <li v-for="section in sections" :key="section.id">
26
+ <a
27
+ :href="`#settings-section_${section.id}`"
28
+ class="cn-user-settings-dialog__nav-link"
29
+ :class="{ 'cn-user-settings-dialog__nav-link--active': section.id === selected }"
30
+ :aria-current="section.id === selected ? 'true' : undefined"
31
+ :data-testid="`cn-user-settings-nav-${section.id}`"
32
+ @click.prevent="selectSection(section.id)">
33
+ <span v-if="section.icon" class="cn-user-settings-dialog__nav-icon" aria-hidden="true">
34
+ <CnVNodes :vnodes="section.icon" />
35
+ </span>
36
+ <span>{{ section.name }}</span>
37
+ </a>
38
+ </li>
39
+ </ul>
40
+ </nav>
41
+ <div class="cn-user-settings-dialog__sections">
42
+ <!-- @slot default NcAppSettingsSection children; each registers itself in the navigation. -->
43
+ <slot />
44
+ </div>
45
+ </div>
46
+ <template #actions>
47
+ <p class="cn-user-settings-dialog__note" data-testid="cn-user-settings-note">
48
+ {{ footerNote }}
49
+ </p>
50
+ <NcButton variant="secondary" data-testid="cn-user-settings-close" @click="close">
51
+ {{ closeLabel }}
52
+ </NcButton>
53
+ </template>
54
+ </CnDialog>
55
+ </template>
56
+
57
+ <script>
58
+ import { translate as t } from '@nextcloud/l10n'
59
+ import { NcButton } from '@nextcloud/vue'
60
+ import { markRaw } from 'vue'
61
+ import CnDialog from '../CnDialog/CnDialog.vue'
62
+
63
+ /**
64
+ * The key NcAppSettingsSection injects to register itself with its dialog
65
+ * (`Symbol.for`, so the same symbol `@nextcloud/vue` provides).
66
+ */
67
+ const REGISTRATION_KEY = Symbol.for('NcAppSettingsDialog:registration')
68
+
69
+ /** Renders a section's icon vnodes as they were registered. */
70
+ const CnVNodes = {
71
+ name: 'CnVNodes',
72
+ props: { vnodes: { type: [Array, Object], default: null } },
73
+ render() {
74
+ return this.vnodes
75
+ },
76
+ }
77
+
78
+ /**
79
+ * CnUserSettingsDialog - the user settings dialog in the board look (board
80
+ * PqPersoonlijk): eyebrow, title and subtitle, a left navigation listing the
81
+ * sections with their icons, the sections themselves, and a footer with a
82
+ * note that changes save straight away and a Close button.
83
+ *
84
+ * The sections stay the app's: any `NcAppSettingsSection` in the default slot
85
+ * registers itself with this dialog exactly as it does with
86
+ * `NcAppSettingsDialog`, with its name, order and `#icon` slot. CnAppRoot
87
+ * renders this dialog instead of `NcAppSettingsDialog` under the board look.
88
+ *
89
+ * @spec openspec/changes/user-settings-board-dialog/specs/user-settings-board-dialog/spec.md#requirement-the-user-settings-dialog-takes-the-board-look
90
+ *
91
+ * @event update:open Emitted with `false` when the dialog closes.
92
+ */
93
+ export default {
94
+ name: 'CnUserSettingsDialog',
95
+
96
+ components: { CnDialog, CnVNodes, NcButton },
97
+
98
+ provide() {
99
+ return {
100
+ [REGISTRATION_KEY]: {
101
+ registerSection: this.registerSection,
102
+ unregisterSection: this.unregisterSection,
103
+ },
104
+ }
105
+ },
106
+
107
+ props: {
108
+ /** Whether the dialog is open. */
109
+ open: { type: Boolean, default: false },
110
+ /** Dialog title; also its accessible name. */
111
+ title: { type: String, required: true },
112
+ /** Context line above the title. */
113
+ eyebrow: { type: String, default: () => t('nextcloud-vue', 'Your account') },
114
+ /** Sentence under the title. */
115
+ subtitle: { type: String, default: () => t('nextcloud-vue', 'Only for you. Colleagues keep their own settings.') },
116
+ /** The app's name, after the footer note. */
117
+ appName: { type: String, default: '' },
118
+ },
119
+
120
+ emits: ['update:open'],
121
+
122
+ data() {
123
+ return {
124
+ sections: [],
125
+ selected: '',
126
+ picked: false,
127
+ }
128
+ },
129
+
130
+ computed: {
131
+ /** @return {object} The board's eyebrow: muted, in sentence case. */
132
+ eyebrowStyle() {
133
+ return { '--cn-dialog-eyebrow-transform': 'none', '--cn-dialog-eyebrow-color': 'var(--color-text-maxcontrast)' }
134
+ },
135
+
136
+ /** @return {string} The navigation's accessible name. */
137
+ navLabel() {
138
+ return t('nextcloud-vue', 'Sections of the settings')
139
+ },
140
+
141
+ /** @return {string} The footer note, with the app's name when known. */
142
+ footerNote() {
143
+ const note = t('nextcloud-vue', 'Changes are saved straight away')
144
+ return this.appName ? `${note} · ${this.appName}` : note
145
+ },
146
+
147
+ /** @return {string} The Close button label. */
148
+ closeLabel() {
149
+ return t('nextcloud-vue', 'Close')
150
+ },
151
+ },
152
+
153
+ methods: {
154
+ /**
155
+ * Add a section to the navigation, kept in `order`; a section
156
+ * without one goes last. Until the reader picks one, the first
157
+ * section is current.
158
+ *
159
+ * @param {string} id The section id.
160
+ * @param {string} name The section name.
161
+ * @param {number} [order] The section's place.
162
+ * @param {Array} [icon] The section's icon vnodes.
163
+ */
164
+ registerSection(id, name, order, icon) {
165
+ const rest = this.sections.filter((section) => section.id !== id)
166
+ const place = typeof order === 'number' ? order : Math.max(0, ...rest.map((section) => section.order)) + 1
167
+ this.sections = [...rest, { id, name, order: place, icon: icon ? markRaw(icon) : null }].sort((a, b) => a.order - b.order)
168
+ if (!this.picked) {
169
+ this.selected = this.sections[0].id
170
+ }
171
+ },
172
+
173
+ /**
174
+ * Remove a section from the navigation.
175
+ *
176
+ * @param {string} id The section id.
177
+ */
178
+ unregisterSection(id) {
179
+ this.sections = this.sections.filter((section) => section.id !== id)
180
+ if (this.selected === id) {
181
+ this.selected = this.sections.length > 0 ? this.sections[0].id : ''
182
+ }
183
+ },
184
+
185
+ /**
186
+ * Mark a section current and scroll it into view.
187
+ *
188
+ * @param {string} id The section id.
189
+ */
190
+ selectSection(id) {
191
+ this.picked = true
192
+ this.selected = id
193
+ const target = typeof document !== 'undefined' ? document.getElementById(`settings-section_${id}`) : null
194
+ if (target && typeof target.scrollIntoView === 'function') {
195
+ target.scrollIntoView({ behavior: 'smooth', block: 'start' })
196
+ }
197
+ },
198
+
199
+ /**
200
+ * Follow NcDialog's own open state.
201
+ *
202
+ * @param {boolean} value Whether NcDialog is open.
203
+ */
204
+ onUpdateOpen(value) {
205
+ if (!value) {
206
+ this.close()
207
+ }
208
+ },
209
+
210
+ /** Close the dialog. */
211
+ close() {
212
+ /**
213
+ * @event update:open Emitted with `false` when the dialog closes (Close button, close icon, Escape).
214
+ */
215
+ this.$emit('update:open', false)
216
+ },
217
+ },
218
+ }
219
+ </script>
220
+
221
+ <style scoped>
222
+ .cn-user-settings-dialog__body {
223
+ display: flex;
224
+ flex-wrap: wrap;
225
+ gap: 0 24px;
226
+ }
227
+
228
+ .cn-user-settings-dialog__nav {
229
+ flex: 0 0 200px;
230
+ width: 200px;
231
+ padding-top: 18px;
232
+ }
233
+
234
+ .cn-user-settings-dialog__nav ul {
235
+ display: flex;
236
+ flex-direction: column;
237
+ gap: 2px;
238
+ margin: 0;
239
+ padding: 0;
240
+ list-style: none;
241
+ }
242
+
243
+ .cn-user-settings-dialog__nav-link {
244
+ display: flex;
245
+ align-items: center;
246
+ gap: 10px;
247
+ min-height: 40px;
248
+ padding: 0 12px;
249
+ border-radius: var(--border-radius-element, 8px);
250
+ font-size: 15px;
251
+ color: var(--color-main-text);
252
+ text-decoration: none;
253
+ }
254
+
255
+ .cn-user-settings-dialog__nav-link:hover {
256
+ background: var(--color-background-hover);
257
+ }
258
+
259
+ .cn-user-settings-dialog__nav-link:focus-visible {
260
+ outline: 2px solid var(--color-primary-element);
261
+ outline-offset: 2px;
262
+ }
263
+
264
+ .cn-user-settings-dialog__nav-link--active,
265
+ .cn-user-settings-dialog__nav-link--active:hover {
266
+ background: var(--color-primary-element-light);
267
+ color: var(--color-primary-element-light-text);
268
+ font-weight: 600;
269
+ }
270
+
271
+ .cn-user-settings-dialog__nav-icon {
272
+ display: inline-flex;
273
+ flex: none;
274
+ width: 18px;
275
+ height: 18px;
276
+ align-items: center;
277
+ justify-content: center;
278
+ }
279
+
280
+ .cn-user-settings-dialog__sections {
281
+ display: flex;
282
+ flex: 1 1 380px;
283
+ flex-direction: column;
284
+ min-width: 0;
285
+ }
286
+
287
+ .cn-user-settings-dialog__sections :deep(.app-settings-section) {
288
+ margin: 0;
289
+ padding: 18px 0;
290
+ border-top: 1px solid var(--color-border);
291
+ }
292
+
293
+ .cn-user-settings-dialog__sections :deep(.app-settings-section__name) {
294
+ margin: 0 0 12px;
295
+ font-size: 17px;
296
+ font-weight: 700;
297
+ }
298
+
299
+ .cn-user-settings-dialog__note {
300
+ flex: 1 1 auto;
301
+ margin: 0;
302
+ font-size: 14px;
303
+ color: var(--color-text-maxcontrast);
304
+ }
305
+ </style>
@@ -225,6 +225,18 @@ export default {
225
225
  CnActionsMenu,
226
226
  },
227
227
 
228
+ provide() {
229
+ return {
230
+ /**
231
+ * Whether this card draws a title row, so a widget inside it can
232
+ * leave out a heading of its own (nc-widget-header).
233
+ *
234
+ * @return {boolean}
235
+ */
236
+ cnWidgetTitled: () => this.titleVisible && Boolean(this.displayTitle),
237
+ }
238
+ },
239
+
228
240
  inject: {
229
241
  /**
230
242
  * Host translate function provided by CnAppRoot as
@@ -203,3 +203,50 @@
203
203
  .cn-dialog__confirm-pending {
204
204
  opacity: 0.45;
205
205
  }
206
+
207
+ /* On a phone the board dialog is a bottom sheet (board PqMobiel): full width,
208
+ anchored to the bottom, as tall as its content up to the screen less the
209
+ header, a 16px top radius and a grab bar. The footer buttons share the row. */
210
+ @media (max-width: 600px) {
211
+ .modal-mask.cn-dialog-board .modal-wrapper .modal-container,
212
+ .modal-mask.cn-dialog-board .modal-wrapper--normal > .modal-container {
213
+ position: absolute;
214
+ inset: auto 0 0;
215
+ width: 100%;
216
+ max-width: 100%;
217
+ height: auto;
218
+ max-height: calc(100% - var(--header-height, 50px));
219
+ border-radius: 16px 16px 0 0;
220
+ }
221
+
222
+ .modal-mask.cn-dialog-board .modal-container::before {
223
+ content: '';
224
+ display: block;
225
+ flex: none;
226
+ width: 40px;
227
+ height: 4px;
228
+ margin: 8px auto 0;
229
+ border-radius: 2px;
230
+ background: var(--color-border-dark);
231
+ }
232
+
233
+ .modal-mask.cn-dialog-board .modal-container {
234
+ flex-direction: column;
235
+ }
236
+
237
+ .cn-dialog-board .dialog {
238
+ height: auto;
239
+ min-height: 0;
240
+ }
241
+
242
+ .cn-dialog-board .dialog__content {
243
+ flex: 0 1 auto;
244
+ overflow-y: auto;
245
+ }
246
+
247
+ .cn-dialog-board .dialog__actions > .button-vue,
248
+ .cn-dialog-board .dialog__actions > * {
249
+ flex: 1 1 0;
250
+ min-height: 44px;
251
+ }
252
+ }
@@ -660,3 +660,11 @@
660
660
  .cn-look-board:has(.cn-detail-page) > .app-sidebar__toggle {
661
661
  display: none;
662
662
  }
663
+
664
+ /* Below 768px the header is a column (detail-page.css), where the 320px basis
665
+ above would be a 320px HEIGHT: the title sat in a band of white on a phone. */
666
+ @media (max-width: 768px) {
667
+ .cn-look-board .cn-detail-page__header--board > :first-child {
668
+ flex: none;
669
+ }
670
+ }
@@ -591,6 +591,39 @@
591
591
  max-width: none;
592
592
  }
593
593
 
594
+ /* A Nextcloud text field or text area in a board settings card has its label
595
+ above the control (14px/600), as the boards draw it (PqBeheer Leadbeheer,
596
+ PqBeheerKassa Btw), instead of floating inside the border. The label stays
597
+ the field's own `<label for>`; it moves out of the control and the field
598
+ makes room for it. With the label outside, the placeholder shows at rest.
599
+ @spec openspec/changes/settings-section-board-fields/specs/settings-board-look/spec.md#requirement-a-settings-card-labels-its-text-fields-above-the-control */
600
+ .cn-look-board .settings-section:not(.cn-look-nextcloud *) .input-field:has(.input-field__label),
601
+ .cn-look-board .settings-section:not(.cn-look-nextcloud *) .textarea:has(.textarea__label) {
602
+ margin-block-start: 26px;
603
+ }
604
+
605
+ .cn-look-board .settings-section:not(.cn-look-nextcloud *) .input-field__input + .input-field__label,
606
+ .cn-look-board .settings-section:not(.cn-look-nextcloud *) .textarea__input + .textarea__label {
607
+ inset-block-start: auto;
608
+ inset-block-end: calc(100% + 4px);
609
+ inset-inline: 0 auto;
610
+ max-width: 100%;
611
+ margin: 0;
612
+ padding: 0;
613
+ background: none;
614
+ color: var(--color-main-text);
615
+ font-size: 14px;
616
+ font-weight: 600;
617
+ line-height: 20px;
618
+ pointer-events: auto;
619
+ transition: none;
620
+ }
621
+
622
+ .cn-look-board .settings-section:not(.cn-look-nextcloud *) .input-field:has(.input-field__label) .input-field__input:not(:focus)::placeholder,
623
+ .cn-look-board .settings-section:not(.cn-look-nextcloud *) .textarea__input:not(:focus):not(.textarea__input--label-outside)::placeholder {
624
+ opacity: 1 !important;
625
+ }
626
+
594
627
  /* The section footer is the save row: the hairline replaces its own rule. */
595
628
  .cn-look-board .settings-section .cn-settings-section__footer {
596
629
  margin: 0;
@@ -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.108.0",
6
+ "version": "2.111.0",
7
7
  "type": "object",
8
8
  "required": ["$schema", "version"],
9
9
  "allOf": [
@@ -4285,10 +4285,16 @@
4285
4285
  "greeting": {
4286
4286
  "oneOf": [
4287
4287
  { "type": "boolean" },
4288
- { "type": "string", "enum": ["first", "full"] }
4288
+ { "type": "string", "enum": ["first", "full", "date"] },
4289
+ {
4290
+ "type": "object",
4291
+ "required": ["label"],
4292
+ "additionalProperties": false,
4293
+ "properties": { "label": { "type": "string", "minLength": 1, "description": "i18n key drawn where the greeting would be." } }
4294
+ }
4289
4295
  ],
4290
4296
  "default": false,
4291
- "description": "type='dashboard': greet the reader in the header subtitle. `true` or \"first\" writes \"Good afternoon, Pieter · Monday 5 October 2026\", \"full\" uses the whole display name; the page `description` follows after another middle dot (screens-dashboard-greeting-header). Maps to CnDashboardPage `greeting`."
4297
+ "description": "type='dashboard': greet the reader in the header subtitle. `true` or \"first\" writes \"Good afternoon, Pieter · Monday 5 October 2026\", \"full\" uses the whole display name; the page `description` follows after another middle dot (screens-dashboard-greeting-header). Since 2.111.0 \"date\" leads with the date alone (\"Tuesday 7 October 2026 · ...\") and `{ \"label\": \"Customer contact\" }` puts the translated label where the greeting would be (dashboard-greeting-label). Maps to CnDashboardPage `greeting`."
4292
4298
  },
4293
4299
  "viewLinks": {
4294
4300
  "type": "array",