@conduction/nextcloud-vue 2.56.0-beta.12 → 2.56.0-beta.13

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 (84) hide show
  1. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +99 -3
  2. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +23 -7
  4. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  5. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue2.js +13 -0
  6. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue2.js.map +1 -1
  7. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue3.js +2 -1
  8. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue3.js.map +1 -1
  9. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +125 -4
  10. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  11. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +439 -381
  12. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  13. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js +9 -3
  14. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js.map +1 -1
  15. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js +1 -1
  16. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js.map +1 -1
  17. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +142 -26
  18. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  19. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +195 -124
  20. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  21. package/dist/esm/components/CnFkResolveCell/CnFkResolveCell.vue2.js +3 -1
  22. package/dist/esm/components/CnFkResolveCell/CnFkResolveCell.vue2.js.map +1 -1
  23. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +144 -4
  24. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  25. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  26. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +84 -11
  27. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
  28. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js +3 -2
  29. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
  30. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js +11 -0
  31. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js.map +1 -1
  32. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js +2 -1
  33. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js.map +1 -1
  34. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
  35. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
  36. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +22 -3
  37. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  38. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +10 -3
  39. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  40. package/dist/esm/l10n/en.json.js +3 -0
  41. package/dist/esm/l10n/en.json.js.map +1 -1
  42. package/dist/esm/l10n/nl.json.js +3 -0
  43. package/dist/esm/l10n/nl.json.js.map +1 -1
  44. package/dist/esm/mixins/pageViews.js +400 -0
  45. package/dist/esm/mixins/pageViews.js.map +1 -0
  46. package/dist/esm/nextcloud-vue.css +182 -0
  47. package/dist/esm/store/useObjectStore.js +118 -33
  48. package/dist/esm/store/useObjectStore.js.map +1 -1
  49. package/dist/esm/utils/actionsDispatcher.js +25 -2
  50. package/dist/esm/utils/actionsDispatcher.js.map +1 -1
  51. package/dist/esm/utils/columnFilters.js +27 -3
  52. package/dist/esm/utils/columnFilters.js.map +1 -1
  53. package/dist/esm/utils/schema.js +40 -2
  54. package/dist/esm/utils/schema.js.map +1 -1
  55. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  56. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  57. package/dist/nextcloud-vue.cjs.js +2001 -684
  58. package/dist/nextcloud-vue.cjs.js.map +2 -2
  59. package/dist/nextcloud-vue.css +182 -0
  60. package/l10n/en.json +3 -0
  61. package/l10n/nl.json +3 -0
  62. package/package.json +1 -1
  63. package/src/components/CnAppNav/CnAppNav.vue +148 -7
  64. package/src/components/CnBreadcrumbs/CnBreadcrumbs.vue +14 -0
  65. package/src/components/CnDashboardPage/CnDashboardPage.vue +194 -10
  66. package/src/components/CnDataTable/CnColumnFilterPopover.vue +11 -5
  67. package/src/components/CnDetailPage/CnDetailPage.md +5 -1
  68. package/src/components/CnDetailPage/CnDetailPage.vue +208 -11
  69. package/src/components/CnFkResolveCell/CnFkResolveCell.vue +3 -1
  70. package/src/components/CnFormDialog/CnFormDialog.vue +144 -4
  71. package/src/components/CnHeaderWidget/CnHeaderWidget.vue +100 -13
  72. package/src/components/CnSegmentedControl/CnSegmentedControl.md +1 -1
  73. package/src/components/CnSegmentedControl/CnSegmentedControl.vue +12 -0
  74. package/src/components/CnStagesWidget/CnStagesWidget.vue +9 -0
  75. package/src/components/CnStatWidget/CnStatWidget.vue +33 -5
  76. package/src/css/detail-page.css +27 -0
  77. package/src/css/kpi-card.css +47 -0
  78. package/src/mixins/pageViews.js +396 -0
  79. package/src/schemas/app-manifest-v2.schema.json +51 -2
  80. package/src/store/useObjectStore.js +118 -33
  81. package/src/utils/actionsDispatcher.js +25 -2
  82. package/src/utils/columnFilters.js +29 -2
  83. package/src/utils/schema.js +40 -2
  84. package/src/utils/validateManifestV2.compiled.js +2 -2
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: package/dist/nextcloud-vue.cjs.js.map
3
- size: 35197196 bytes
4
- sha256: 893c086c8da03029e46dfe50d98a24e639f212abee995c1345acd72a41ab45be
3
+ size: 35355767 bytes
4
+ sha256: e048d3627c1eba03f90228c9f300e9341baf3086530119fe40b4af2106f5b7b2
@@ -1283,6 +1283,53 @@
1283
1283
  font-size: var(--cn-kpi-value-size-stacked, 2rem);
1284
1284
  }
1285
1285
 
1286
+ /* Stacked: the board's tile (`content.layout: 'stacked'`). A plain muted
1287
+ label, the number at 34px/700 in the text colour, and the caption on a line
1288
+ of its own; CnStatWidget draws no icon circle in this layout. The padding
1289
+ adds up with a dashboard card-fit cell's 8px 14px to the board's 18px 20px.
1290
+ Every value is a token so a theme can move it. */
1291
+ .cn-kpi-card--stacked {
1292
+ flex-direction: column;
1293
+ align-items: stretch;
1294
+ justify-content: center;
1295
+ gap: 0;
1296
+ padding: var(--cn-kpi-stacked-padding, 10px 6px);
1297
+ }
1298
+
1299
+ .cn-kpi-card--stacked .cn-kpi-card__body {
1300
+ gap: var(--cn-kpi-stacked-gap, 6px);
1301
+ }
1302
+
1303
+ .cn-kpi-card--stacked .cn-kpi-card__title {
1304
+ font-size: var(--cn-kpi-stacked-title-size, 14px);
1305
+ font-weight: var(--cn-kpi-stacked-title-weight, 400);
1306
+ color: var(--color-text-maxcontrast);
1307
+ }
1308
+
1309
+ .cn-kpi-card--stacked .cn-kpi-card__value {
1310
+ font-size: var(--cn-kpi-stacked-value-size, 34px);
1311
+ font-weight: var(--cn-kpi-value-weight, 700);
1312
+ line-height: 1;
1313
+ letter-spacing: -0.01em;
1314
+ color: var(--cn-kpi-stacked-value-color, var(--color-main-text));
1315
+ }
1316
+
1317
+ .cn-kpi-card--stacked .cn-kpi-card__caption-line {
1318
+ flex: 0 0 auto;
1319
+ max-width: 100%;
1320
+ font-size: var(--cn-kpi-label-size, 13px);
1321
+ font-weight: 500;
1322
+ }
1323
+
1324
+ /* A linked stacked tile reads as a tile, not as underlined link text. The
1325
+ NL Design theme underlines every `a` with `!important`, so the opt-out
1326
+ needs it too; it only applies to this opt-in layout. */
1327
+ a.cn-kpi-card--stacked,
1328
+ a.cn-kpi-card--stacked:hover,
1329
+ a.cn-kpi-card--stacked:focus-visible {
1330
+ text-decoration: none !important;
1331
+ }
1332
+
1286
1333
  /* Filled: the card draws its own grey box, for a KPI that is NOT already
1287
1334
  inside a wrapper that draws one. Opt-in via `content.flat: false`. */
1288
1335
  .cn-kpi-card--filled {
@@ -2790,6 +2837,28 @@
2790
2837
  min-width: 0;
2791
2838
  }
2792
2839
 
2840
+ /* A widget in the header (`config.headerWidget`) takes a row of its own under
2841
+ the title and the actions. */
2842
+ .cn-detail-page__header--with-widget {
2843
+ flex-wrap: wrap;
2844
+ row-gap: var(--cn-detail-header-row-gap, 22px);
2845
+ }
2846
+
2847
+ .cn-detail-page__header-widget {
2848
+ flex: 1 0 100%;
2849
+ min-width: 0;
2850
+ }
2851
+
2852
+ /* The header as a card (`config.headerCard`): the board's case header, a
2853
+ bordered box on the surface colour. The 56px inset that clears the
2854
+ navigation toggle is not needed inside a card that starts below it. */
2855
+ .cn-detail-page__header--card {
2856
+ padding: var(--cn-detail-header-card-padding, 22px 24px);
2857
+ border: 1px solid var(--color-border);
2858
+ border-radius: var(--cn-detail-header-card-radius, 12px);
2859
+ background: var(--color-main-background);
2860
+ }
2861
+
2793
2862
  .cn-detail-page__header-left {
2794
2863
  display: flex;
2795
2864
  align-items: center;
@@ -3210,6 +3279,11 @@
3210
3279
  font-size: 0.9em;
3211
3280
  white-space: nowrap;
3212
3281
  }
3282
+
3283
+ /* A page view the switch shows with nothing to draw (view-switch-containers). */
3284
+ .cn-detail-page .cn-page-view-region--empty {
3285
+ padding: 40px 20px;
3286
+ }
3213
3287
  /* CnReferencePreview — a reference that shows what it points at. */
3214
3288
 
3215
3289
  .cn-reference-preview {
@@ -16875,7 +16949,17 @@ a.cn-xwiki-card__title[data-v-54e85fe3]:hover {
16875
16949
  text-overflow: ellipsis;
16876
16950
  white-space: nowrap;
16877
16951
  }
16952
+
16953
+ /* A clickable label is a <button>, and Nextcloud's core stylesheet gives
16954
+ every `button:not(.button-vue)` a min-height of the clickable area (34 to
16955
+ 44px) with the text centred in it. That pushed each reachable stage's label
16956
+ about 12px below the current one, which is a <span>. Zeroed here so all
16957
+ labels sit 8px under their bar, as the board draws them. The visually
16958
+ hidden "(current step)" is absolutely positioned and takes no space. */
16878
16959
  button.cn-stages-widget__bar-label[data-v-358fde0f] {
16960
+ min-height: 0;
16961
+ height: auto;
16962
+ line-height: inherit;
16879
16963
  cursor: pointer;
16880
16964
  }
16881
16965
  button.cn-stages-widget__bar-label[aria-disabled="true"][data-v-358fde0f] {
@@ -17310,6 +17394,53 @@ button.cn-stages-widget__bar-label[data-v-358fde0f]:focus-visible {
17310
17394
  font-size: var(--cn-kpi-value-size-stacked, 2rem);
17311
17395
  }
17312
17396
 
17397
+ /* Stacked: the board's tile (`content.layout: 'stacked'`). A plain muted
17398
+ label, the number at 34px/700 in the text colour, and the caption on a line
17399
+ of its own; CnStatWidget draws no icon circle in this layout. The padding
17400
+ adds up with a dashboard card-fit cell's 8px 14px to the board's 18px 20px.
17401
+ Every value is a token so a theme can move it. */
17402
+ .cn-kpi-card--stacked {
17403
+ flex-direction: column;
17404
+ align-items: stretch;
17405
+ justify-content: center;
17406
+ gap: 0;
17407
+ padding: var(--cn-kpi-stacked-padding, 10px 6px);
17408
+ }
17409
+
17410
+ .cn-kpi-card--stacked .cn-kpi-card__body {
17411
+ gap: var(--cn-kpi-stacked-gap, 6px);
17412
+ }
17413
+
17414
+ .cn-kpi-card--stacked .cn-kpi-card__title {
17415
+ font-size: var(--cn-kpi-stacked-title-size, 14px);
17416
+ font-weight: var(--cn-kpi-stacked-title-weight, 400);
17417
+ color: var(--color-text-maxcontrast);
17418
+ }
17419
+
17420
+ .cn-kpi-card--stacked .cn-kpi-card__value {
17421
+ font-size: var(--cn-kpi-stacked-value-size, 34px);
17422
+ font-weight: var(--cn-kpi-value-weight, 700);
17423
+ line-height: 1;
17424
+ letter-spacing: -0.01em;
17425
+ color: var(--cn-kpi-stacked-value-color, var(--color-main-text));
17426
+ }
17427
+
17428
+ .cn-kpi-card--stacked .cn-kpi-card__caption-line {
17429
+ flex: 0 0 auto;
17430
+ max-width: 100%;
17431
+ font-size: var(--cn-kpi-label-size, 13px);
17432
+ font-weight: 500;
17433
+ }
17434
+
17435
+ /* A linked stacked tile reads as a tile, not as underlined link text. The
17436
+ NL Design theme underlines every `a` with `!important`, so the opt-out
17437
+ needs it too; it only applies to this opt-in layout. */
17438
+ a.cn-kpi-card--stacked,
17439
+ a.cn-kpi-card--stacked:hover,
17440
+ a.cn-kpi-card--stacked:focus-visible {
17441
+ text-decoration: none !important;
17442
+ }
17443
+
17313
17444
  /* Filled: the card draws its own grey box, for a KPI that is NOT already
17314
17445
  inside a wrapper that draws one. Opt-in via `content.flat: false`. */
17315
17446
  .cn-kpi-card--filled {
@@ -18262,6 +18393,16 @@ to { background-position: -200% 0;
18262
18393
  flex: 0 0 auto;
18263
18394
  padding: 16px;
18264
18395
  }
18396
+
18397
+ /* On the page ground (`content.ground`): no card, so no inset around the
18398
+ view switch, and the heading at the board's 32px. */
18399
+ .cn-header-widget--ground .cn-header-widget__views[data-v-1b97430f] {
18400
+ padding: 0;
18401
+ }
18402
+ .cn-header-widget--ground .cn-header-widget__title[data-v-1b97430f] {
18403
+ font-size: var(--cn-header-ground-title-size, 32px);
18404
+ letter-spacing: -0.01em;
18405
+ }
18265
18406
  .cn-header-widget__date[data-v-1b97430f] {
18266
18407
  font-size: 14px;
18267
18408
  line-height: 1.4;
@@ -30431,6 +30572,19 @@ to {
30431
30572
  .cn-dashboard-page__empty[data-v-824052fe] {
30432
30573
  padding: 60px 20px;
30433
30574
  }
30575
+
30576
+ /* The page's view switch on a page without a header row: at the end of its
30577
+ own row, above the grids, where the header would have put it. */
30578
+ .cn-dashboard-page__view-switch[data-v-824052fe] {
30579
+ display: flex;
30580
+ justify-content: flex-end;
30581
+ margin-bottom: 12px;
30582
+ }
30583
+
30584
+ /* A view the switch shows with nothing to draw. */
30585
+ .cn-page-view-region--empty[data-v-824052fe] {
30586
+ padding: 40px 20px;
30587
+ }
30434
30588
  .cn-dashboard-page__page-filters[data-v-824052fe] {
30435
30589
  display: flex;
30436
30590
  flex-wrap: wrap;
@@ -33247,6 +33401,21 @@ a.cn-integration-card__title[data-v-f7e9bbfa]:hover {
33247
33401
  padding: calc(var(--default-grid-baseline, 4px) * 2);
33248
33402
  }
33249
33403
 
33404
+ /*
33405
+ * The primary action never shrinks out of sight. NcAppNavigation renders it in
33406
+ * `.app-navigation__body`, a flex child with `overflow-y: scroll` (so its
33407
+ * minimum height is 0) beside a list of `height: 100%` and the footer. With a
33408
+ * card and footer entries below the list, the column overflows and the body
33409
+ * gives up its share of the shrink: "New case" showed as a 24px sliver of a
33410
+ * 42px button. All three forms carry `.app-navigation-new` (the solid and
33411
+ * link wrappers mirror NcAppNavigationNew's class), and the rule is scoped to
33412
+ * this component's navigation, so a navigation without a primary action lays
33413
+ * out exactly as before.
33414
+ */
33415
+ .app-navigation[data-testid="cn-nav"] .app-navigation__body:has(> .app-navigation-new) {
33416
+ flex-shrink: 0;
33417
+ }
33418
+
33250
33419
  /*
33251
33420
  * Footer-section items (section: "footer") render in NcAppNavigation's
33252
33421
  * #footer slot, outside the scroll container, so they stay visible above
@@ -33270,6 +33439,19 @@ a.cn-integration-card__title[data-v-f7e9bbfa]:hover {
33270
33439
  overflow: visible !important;
33271
33440
  }
33272
33441
 
33442
+ /*
33443
+ * A declared footer that names `settings` first (`nav.footer`) puts the
33444
+ * foldout above the footer list. The two are siblings in NcAppNavigation's
33445
+ * flex column, so `order` moves them without changing either's markup; the
33446
+ * card above keeps order 0 and stays first.
33447
+ */
33448
+ .cn-app-nav__settings--first {
33449
+ order: 1;
33450
+ }
33451
+ .cn-app-nav__footer-list--after-settings {
33452
+ order: 2;
33453
+ }
33454
+
33273
33455
  /*
33274
33456
  * Align the settings foldout's items with the main/footer list items,
33275
33457
  * whose icons sit at a 16px inset. The foldout's items live in a bare
package/l10n/en.json CHANGED
@@ -49,6 +49,7 @@
49
49
  "This integration is not available.": "This integration is not available.",
50
50
  "Integrations": "Integrations",
51
51
  "View": "View",
52
+ "Views": "Views",
52
53
  "current step": "current step",
53
54
  "{app} not available": "{app} not available",
54
55
  "{app} not configured": "{app} not configured",
@@ -408,6 +409,7 @@
408
409
  "Any": "Any",
409
410
  "To": "To",
410
411
  "Equals": "Equals",
412
+ "Contains": "Contains",
411
413
  "Clear": "Clear",
412
414
  "Apply": "Apply",
413
415
  "This column cannot list its objects.": "This column cannot list its objects.",
@@ -1088,6 +1090,7 @@
1088
1090
  "The actions of an attention card are declared in the manifest.": "The actions of an attention card are declared in the manifest.",
1089
1091
  "An attention card needs a title": "An attention card needs a title",
1090
1092
  "Good morning, {name}": "Good morning, {name}",
1093
+ "This view has no widgets yet.": "This view has no widgets yet.",
1091
1094
  "Good morning": "Good morning",
1092
1095
  "Good afternoon, {name}": "Good afternoon, {name}",
1093
1096
  "Good afternoon": "Good afternoon",
package/l10n/nl.json CHANGED
@@ -49,6 +49,7 @@
49
49
  "This integration is not available.": "Deze integratie is niet beschikbaar.",
50
50
  "Integrations": "Integraties",
51
51
  "View": "Weergave",
52
+ "Views": "Weergaven",
52
53
  "current step": "huidige stap",
53
54
  "{app} not available": "{app} niet beschikbaar",
54
55
  "{app} not configured": "{app} niet geconfigureerd",
@@ -408,6 +409,7 @@
408
409
  "Any": "Alle",
409
410
  "To": "Tot",
410
411
  "Equals": "Is gelijk aan",
412
+ "Contains": "Bevat",
411
413
  "Clear": "Wissen",
412
414
  "Apply": "Toepassen",
413
415
  "This column cannot list its objects.": "Deze kolom kan zijn objecten niet tonen.",
@@ -1132,6 +1134,7 @@
1132
1134
  "The actions of an attention card are declared in the manifest.": "De acties van een aandachtskaart staan in het manifest.",
1133
1135
  "An attention card needs a title": "Een aandachtskaart heeft een titel nodig",
1134
1136
  "Good morning, {name}": "Goedemorgen, {name}",
1137
+ "This view has no widgets yet.": "Deze weergave heeft nog geen widgets.",
1135
1138
  "Good morning": "Goedemorgen",
1136
1139
  "Good afternoon, {name}": "Goedemiddag, {name}",
1137
1140
  "Good afternoon": "Goedemiddag",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduction/nextcloud-vue",
3
- "version": "2.56.0-beta.12",
3
+ "version": "2.56.0-beta.13",
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": {
@@ -246,7 +246,7 @@
246
246
  </NcAppNavigationItem>
247
247
  </template>
248
248
  </template>
249
- <template v-if="footerItems.length > 0 || showSettingsFoldout || resolvedCard || resolvedHelp" #footer>
249
+ <template v-if="footerItems.length > 0 || showSettingsFoldout || resolvedCard || showsHelp" #footer>
250
250
  <!--
251
251
  @slot card
252
252
  @description Replace the card above the footer entries
@@ -282,11 +282,15 @@
282
282
  hover highlight are scoped to itself, so they survive being
283
283
  slotted. The main list's inset comes from NcAppNavigation's
284
284
  scope, which slot content does not carry. -->
285
- <NcAppNavigationList v-if="footerItems.length > 0 || resolvedHelp" class="cn-app-nav__footer-list">
285
+ <NcAppNavigationList
286
+ v-if="footerItems.length > 0 || showsHelp"
287
+ class="cn-app-nav__footer-list"
288
+ :class="{ 'cn-app-nav__footer-list--after-settings': settingsFirst }">
286
289
  <!-- The help entry (`nav.help`): a link to the app's own help
287
- with a help icon, above the footer entries. -->
290
+ with a help icon, above the footer entries (or after them,
291
+ when a declared `nav.footer` names it last). -->
288
292
  <NcAppNavigationItem
289
- v-if="resolvedHelp"
293
+ v-if="showsHelp && !helpAfterItems"
290
294
  :name="resolvedHelp.label"
291
295
  :to="resolvedHelp.to"
292
296
  :href="resolvedHelp.href"
@@ -318,6 +322,17 @@
318
322
  :active="isActive(item)" />
319
323
  </template>
320
324
  </NcAppNavigationItem>
325
+ <NcAppNavigationItem
326
+ v-if="showsHelp && helpAfterItems"
327
+ :name="resolvedHelp.label"
328
+ :to="resolvedHelp.to"
329
+ :href="resolvedHelp.href"
330
+ :target="resolvedHelp.href ? '_blank' : undefined"
331
+ data-testid="cn-nav-help">
332
+ <template #icon>
333
+ <HelpCircleOutline :size="20" />
334
+ </template>
335
+ </NcAppNavigationItem>
321
336
  </NcAppNavigationList>
322
337
  <!-- Settings foldout (section: "settings" items). NC-native
323
338
  gear-icon button that slides open a panel; the first entry
@@ -326,6 +341,7 @@
326
341
  <NcAppNavigationSettings
327
342
  v-if="showSettingsFoldout"
328
343
  :name="settingsFoldoutLabel"
344
+ :class="{ 'cn-app-nav__settings--first': settingsFirst }"
329
345
  data-testid="cn-nav-settings">
330
346
  <ul class="cn-app-nav__settings-list">
331
347
  <NcAppNavigationItem
@@ -958,17 +974,113 @@ export default {
958
974
  * Documentation, Features & Roadmap, About.
959
975
  */
960
976
  footerItems() {
961
- return this.visibleItems.filter((item) => item.section === 'footer')
977
+ const footer = this.visibleItems.filter((item) => item.section === 'footer')
978
+ const declared = this.declaredFooter
979
+ if (declared === null) {
980
+ return footer
981
+ }
982
+ // A declared footer (`nav.footer`) keeps only the entries it names,
983
+ // in the order it names them.
984
+ return declared
985
+ .map((id) => footer.find((item) => item.id === id))
986
+ .filter(Boolean)
987
+ },
988
+
989
+ /**
990
+ * The footer the manifest declares (`nav.footer`): an ordered list of
991
+ * menu entry ids plus the reserved ids `help` (the `nav.help` entry)
992
+ * and `settings` (the settings foldout). Null when the manifest
993
+ * declares none, which keeps today's footer: the help entry, then
994
+ * every `section: "footer"` entry, then the foldout.
995
+ *
996
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-the-navigation-footer-can-be-declared
997
+ * @return {Array<string>|null}
998
+ */
999
+ declaredFooter() {
1000
+ const declared = this.effectiveManifest?.nav?.footer
1001
+ if (!Array.isArray(declared)) {
1002
+ return null
1003
+ }
1004
+ return declared.filter((id) => typeof id === 'string' && id !== '')
1005
+ },
1006
+
1007
+ /**
1008
+ * Footer-section entries a declared footer leaves out. They move into
1009
+ * the settings foldout rather than disappearing, so a page such as
1010
+ * Store or Reports stays reachable. Empty without `nav.footer`.
1011
+ *
1012
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-the-navigation-footer-can-be-declared
1013
+ * @return {Array<object>}
1014
+ */
1015
+ undeclaredFooterItems() {
1016
+ const declared = this.declaredFooter
1017
+ if (declared === null) {
1018
+ return []
1019
+ }
1020
+ return this.visibleItems.filter((item) => item.section === 'footer' && !declared.includes(item.id))
1021
+ },
1022
+
1023
+ /**
1024
+ * Whether the help entry renders: whenever it resolves, unless a
1025
+ * declared footer leaves `help` out.
1026
+ *
1027
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-the-navigation-footer-can-be-declared
1028
+ * @return {boolean}
1029
+ */
1030
+ showsHelp() {
1031
+ if (!this.resolvedHelp) {
1032
+ return false
1033
+ }
1034
+ return this.declaredFooter === null || this.declaredFooter.includes('help')
1035
+ },
1036
+
1037
+ /**
1038
+ * Whether the help entry goes AFTER the footer entries. Only a declared
1039
+ * footer that names an entry before `help` puts it there; by default
1040
+ * it leads the list.
1041
+ *
1042
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-the-navigation-footer-can-be-declared
1043
+ * @return {boolean}
1044
+ */
1045
+ helpAfterItems() {
1046
+ const declared = this.declaredFooter
1047
+ if (declared === null) {
1048
+ return false
1049
+ }
1050
+ const help = declared.indexOf('help')
1051
+ return this.footerItems.some((item) => declared.indexOf(item.id) < help)
1052
+ },
1053
+
1054
+ /**
1055
+ * Whether the settings foldout goes ABOVE the footer list: only when a
1056
+ * declared footer names `settings` before every other entry
1057
+ * (`["settings", "help"]`, the board's "Instellingen" then "Hulp en
1058
+ * uitleg"). Done with CSS `order` on the two siblings, so the markup
1059
+ * of both stays as it is.
1060
+ *
1061
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-the-navigation-footer-can-be-declared
1062
+ * @return {boolean}
1063
+ */
1064
+ settingsFirst() {
1065
+ const declared = this.declaredFooter
1066
+ return declared !== null && declared.indexOf('settings') === 0
962
1067
  },
963
1068
 
964
1069
  /**
965
1070
  * Items that render INSIDE the NcAppNavigationSettings foldout
966
1071
  * (section: "settings"). The foldout is the NC-native gear-icon
967
1072
  * button that slides a panel open; these entries are app-level
968
- * configuration pages (Forms, Pipelines, Automations, …).
1073
+ * configuration pages (Forms, Pipelines, Automations, …). A declared
1074
+ * footer (`nav.footer`) adds the footer entries it leaves out.
1075
+ *
1076
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-the-navigation-footer-can-be-declared
1077
+ * @return {Array<object>}
969
1078
  */
970
1079
  settingsItems() {
971
- return this.visibleItems.filter((item) => item.section === 'settings')
1080
+ return [
1081
+ ...this.visibleItems.filter((item) => item.section === 'settings'),
1082
+ ...this.undeclaredFooterItems,
1083
+ ]
972
1084
  },
973
1085
 
974
1086
  /**
@@ -2294,6 +2406,21 @@ export default {
2294
2406
  padding: calc(var(--default-grid-baseline, 4px) * 2);
2295
2407
  }
2296
2408
 
2409
+ /*
2410
+ * The primary action never shrinks out of sight. NcAppNavigation renders it in
2411
+ * `.app-navigation__body`, a flex child with `overflow-y: scroll` (so its
2412
+ * minimum height is 0) beside a list of `height: 100%` and the footer. With a
2413
+ * card and footer entries below the list, the column overflows and the body
2414
+ * gives up its share of the shrink: "New case" showed as a 24px sliver of a
2415
+ * 42px button. All three forms carry `.app-navigation-new` (the solid and
2416
+ * link wrappers mirror NcAppNavigationNew's class), and the rule is scoped to
2417
+ * this component's navigation, so a navigation without a primary action lays
2418
+ * out exactly as before.
2419
+ */
2420
+ .app-navigation[data-testid="cn-nav"] .app-navigation__body:has(> .app-navigation-new) {
2421
+ flex-shrink: 0;
2422
+ }
2423
+
2297
2424
  /*
2298
2425
  * Footer-section items (section: "footer") render in NcAppNavigation's
2299
2426
  * #footer slot, outside the scroll container, so they stay visible above
@@ -2317,6 +2444,20 @@ export default {
2317
2444
  overflow: visible !important;
2318
2445
  }
2319
2446
 
2447
+ /*
2448
+ * A declared footer that names `settings` first (`nav.footer`) puts the
2449
+ * foldout above the footer list. The two are siblings in NcAppNavigation's
2450
+ * flex column, so `order` moves them without changing either's markup; the
2451
+ * card above keeps order 0 and stays first.
2452
+ */
2453
+ .cn-app-nav__settings--first {
2454
+ order: 1;
2455
+ }
2456
+
2457
+ .cn-app-nav__footer-list--after-settings {
2458
+ order: 2;
2459
+ }
2460
+
2320
2461
  /*
2321
2462
  * Align the settings foldout's items with the main/footer list items,
2322
2463
  * whose icons sit at a 16px inset. The foldout's items live in a bare
@@ -18,6 +18,7 @@
18
18
  <NcBreadcrumbs
19
19
  v-if="crumbs.length > 0"
20
20
  :aria-label="ariaLabel"
21
+ :rootIcon="rootText ? '' : undefined"
21
22
  data-testid="cn-breadcrumbs">
22
23
  <!-- `name` falls back to '' because NcBreadcrumb declares it a REQUIRED
23
24
  String and warns per render on the label-less icon-only crumb. The
@@ -97,6 +98,19 @@ export default {
97
98
  type: String,
98
99
  default: () => t('nextcloud-vue', 'Breadcrumbs'),
99
100
  },
101
+
102
+ /**
103
+ * Draw the FIRST crumb's label as text. NcBreadcrumbs gives the root
104
+ * crumb a home icon and prints its `label` only as the accessible
105
+ * name, so "All cases" read as a house. Off by default, which keeps
106
+ * the home icon. A first crumb with its own `icon` keeps that icon.
107
+ *
108
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-declared-breadcrumb-label-shows-as-text
109
+ */
110
+ rootText: {
111
+ type: Boolean,
112
+ default: false,
113
+ },
100
114
  },
101
115
 
102
116
  methods: {