@conduction/nextcloud-vue 2.144.0 → 2.146.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 (133) hide show
  1. package/dist/esm/components/CnActionDialog/CnActionDialog.vue.js +1 -1
  2. package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js.map +1 -1
  3. package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js +1 -0
  4. package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js.map +1 -1
  5. package/dist/esm/components/CnAiCompanion/CnAiChatPanel.vue2.js.map +1 -1
  6. package/dist/esm/components/CnAiCompanion/CnAiChatPanel.vue3.js.map +1 -1
  7. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
  8. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +55 -2
  9. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
  10. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
  11. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue.js +1 -1
  12. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js +16 -0
  13. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js.map +1 -1
  14. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js +3 -1
  15. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js.map +1 -1
  16. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue.js +1 -1
  17. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js.map +1 -1
  18. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js.map +1 -1
  19. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +12 -2
  20. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  21. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +42 -30
  22. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  23. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +22 -0
  24. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  25. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +2 -1
  26. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  27. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue.js +1 -1
  28. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue2.js +1 -1
  29. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue2.js.map +1 -1
  30. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue3.js +3 -3
  31. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue3.js.map +1 -1
  32. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  33. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +46 -8
  34. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  35. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +3 -3
  36. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  37. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +1 -1
  38. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +2 -2
  39. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
  40. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
  41. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +6 -3
  42. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  43. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  44. package/dist/esm/components/CnMailTab/CnMailTab.vue.js +1 -1
  45. package/dist/esm/components/CnMailTab/CnMailTab.vue2.js.map +1 -1
  46. package/dist/esm/components/CnMailTab/CnMailTab.vue3.js +2 -2
  47. package/dist/esm/components/CnMailTab/CnMailTab.vue3.js.map +1 -1
  48. package/dist/esm/components/CnNotesCard/CnNotesCard.vue.js +1 -1
  49. package/dist/esm/components/CnNotesCard/CnNotesCard.vue2.js.map +1 -1
  50. package/dist/esm/components/CnNotesCard/CnNotesCard.vue3.js +1 -0
  51. package/dist/esm/components/CnNotesCard/CnNotesCard.vue3.js.map +1 -1
  52. package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue.js +1 -1
  53. package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue2.js +21 -2
  54. package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue2.js.map +1 -1
  55. package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue3.js.map +1 -1
  56. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  57. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +73 -12
  58. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  59. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +165 -126
  60. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  61. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue.js +1 -1
  62. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue2.js.map +1 -1
  63. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue3.js.map +1 -1
  64. package/dist/esm/components/CnSettingsField/CnSettingsField.vue.js +1 -1
  65. package/dist/esm/components/CnSettingsField/CnSettingsField.vue2.js +14 -0
  66. package/dist/esm/components/CnSettingsField/CnSettingsField.vue2.js.map +1 -1
  67. package/dist/esm/components/CnSettingsField/CnSettingsField.vue3.js +2 -1
  68. package/dist/esm/components/CnSettingsField/CnSettingsField.vue3.js.map +1 -1
  69. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  70. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  71. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  72. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
  73. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +13 -8
  74. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  75. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  76. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue.js +1 -1
  77. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js +10 -2
  78. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js.map +1 -1
  79. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js.map +1 -1
  80. package/dist/esm/composables/indexSources.js +35 -9
  81. package/dist/esm/composables/indexSources.js.map +1 -1
  82. package/dist/esm/l10n/nl.json.js +2 -0
  83. package/dist/esm/l10n/nl.json.js.map +1 -1
  84. package/dist/esm/library-style.js +1 -1
  85. package/dist/esm/nextcloud-vue.css +1204 -467
  86. package/dist/esm/utils/builtInFormatters.js +18 -1
  87. package/dist/esm/utils/builtInFormatters.js.map +1 -1
  88. package/dist/esm/utils/detailObjectContext.js +7 -2
  89. package/dist/esm/utils/detailObjectContext.js.map +1 -1
  90. package/dist/esm/utils/localHeaderRows.js +95 -0
  91. package/dist/esm/utils/localHeaderRows.js.map +1 -0
  92. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  93. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  94. package/dist/esm/utils/visibleWhen.js +6 -0
  95. package/dist/esm/utils/visibleWhen.js.map +1 -1
  96. package/dist/nextcloud-vue.cjs.js +686 -239
  97. package/dist/nextcloud-vue.cjs.js.map +2 -2
  98. package/dist/nextcloud-vue.css +1204 -467
  99. package/l10n/nl.json +2 -0
  100. package/package.json +1 -1
  101. package/src/components/CnActionDialog/CnActionDialog.vue +8 -1
  102. package/src/components/CnAiCompanion/CnAiChatPanel.vue +3 -0
  103. package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +55 -2
  104. package/src/components/CnAuditTrailWidget/CnAuditTrailWidget.vue +18 -0
  105. package/src/components/CnChecklistRunWidget/CnChecklistRunWidget.vue +4 -2
  106. package/src/components/CnDataTable/CnDataTable.vue +19 -2
  107. package/src/components/CnDetailPage/CnDetailPage.md +1 -0
  108. package/src/components/CnDetailPage/CnDetailPage.vue +23 -0
  109. package/src/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue +3 -1
  110. package/src/components/CnFactsWidget/CnFactsWidget.vue +74 -9
  111. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +2 -2
  112. package/src/components/CnIndexPage/CnIndexPage.vue +6 -3
  113. package/src/components/CnMailTab/CnMailTab.vue +6 -4
  114. package/src/components/CnNotesCard/CnNotesCard.vue +7 -2
  115. package/src/components/CnNotificationMatrix/CnNotificationMatrix.vue +25 -3
  116. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +132 -20
  117. package/src/components/CnRetentionWidget/CnRetentionWidget.vue +2 -0
  118. package/src/components/CnSettingsField/CnSettingsField.vue +15 -0
  119. package/src/components/CnTabsWidget/CnTabsWidget.vue +6 -1
  120. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +13 -8
  121. package/src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue +10 -2
  122. package/src/composables/indexSources.js +35 -9
  123. package/src/css/dialog.css +25 -9
  124. package/src/css/look-board-detail.css +370 -30
  125. package/src/css/look-board-index.css +116 -4
  126. package/src/css/look-board.css +168 -8
  127. package/src/css/table.css +17 -1
  128. package/src/schemas/app-manifest-v2.schema.json +2 -2
  129. package/src/utils/builtInFormatters.js +17 -0
  130. package/src/utils/detailObjectContext.js +7 -2
  131. package/src/utils/localHeaderRows.js +91 -0
  132. package/src/utils/validateManifestV2.compiled.js +2 -2
  133. package/src/utils/visibleWhen.js +6 -0
@@ -77,7 +77,7 @@
77
77
  grid-column: 1 / -1;
78
78
  grid-row: 2;
79
79
  justify-self: start;
80
- margin-block: 6px 3px;
80
+ margin-block: 6px 0;
81
81
  }
82
82
 
83
83
  .cn-look-board.cn-index-page--sidebar-open .cn-page-header .cn-header-trail,
@@ -90,6 +90,13 @@
90
90
  grid-row: 4;
91
91
  }
92
92
 
93
+ /* Without a breadcrumb trail the count line takes the trail's row, so no
94
+ empty row adds a second gap (DqZakenSorteren: 58px under the title). */
95
+ .cn-look-board.cn-index-page--sidebar-open .cn-page-header:not(:has(.cn-header-trail)) .cn-page-header__text > .cn-page-header__description,
96
+ .cn-look-board .cn-index-page--sidebar-open .cn-page-header:not(:has(.cn-header-trail)) .cn-page-header__text > .cn-page-header__description {
97
+ grid-row: 3;
98
+ }
99
+
93
100
  /* The header buttons keep the board's button under a theme that styles every
94
101
  secondary or primary button with `!important` (the nldesign theme: a
95
102
  primary border, 8px 16px padding): Download and the Actions menu are 40px,
@@ -190,6 +197,15 @@
190
197
  gap: 8px;
191
198
  }
192
199
 
200
+ /* With the side panel open the boards keep Filter on row 1 after Save view
201
+ and wrap the view switch alone to row 2 (DqFilterBeheren, DqZakenSorteren):
202
+ the unit dissolves into the bar's flex row.
203
+ @spec openspec/changes/board-filter-row-one-beside-side-panel/specs/board-filter-row-one-beside-side-panel/spec.md */
204
+ .cn-look-board.cn-index-page--sidebar-open .cn-actions-bar--board .cn-actions-bar__view-controls,
205
+ .cn-look-board .cn-index-page--sidebar-open .cn-actions-bar--board .cn-actions-bar__view-controls {
206
+ display: contents;
207
+ }
208
+
193
209
  /* `quickFilterPlacement: "toolbar"` (DqTeamwachtrij): the chips take the room
194
210
  the Filter button and the view switch leave on row 1, so the three stay on
195
211
  one row in the column beside a side panel; the chips that do not fit move
@@ -891,6 +907,16 @@
891
907
  max-width: calc(100% - 26px);
892
908
  }
893
909
 
910
+ /* A plain (unsortable) label wraps by word beside its funnel, as a sort
911
+ button does, instead of the funnel dropping onto a line of its own
912
+ ("Lopende / zaken", DqOrganisaties: head 64 -> 59, board 58.5).
913
+ @spec openspec/changes/board-head-label-beside-funnel/specs/data-table/spec.md#requirement-a-board-heads-plain-label-wraps-beside-its-filter */
914
+ .cn-look-board .cn-index-page__main--table .cn-table-container--board .cn-data-table th > span:first-child:has(~ .cn-table-header__filter) {
915
+ display: inline-block;
916
+ max-width: calc(100% - 26px);
917
+ vertical-align: middle;
918
+ }
919
+
894
920
  /* The board's funnel: a 22px button 4px after the sort chevron. */
895
921
  .cn-look-board .cn-table-container--board .cn-data-table th .cn-table-header__filter {
896
922
  width: 22px;
@@ -909,16 +935,21 @@
909
935
  `#content[class*="app-"]`, an id selector no class rule outranks, so a plain
910
936
  `content-box` here was ignored and the cell left 12px for the checkbox (the
911
937
  header's checkbox showed as a stray bracket). */
912
- /* The checkbox starts 20px in, as the boards draw a native checkbox (16px of
913
- cell padding and its 4px margin; DqZoekresultaten), in the same 60px. */
938
+ /* The cell pads 16px as the boards do and the checkbox starts 4px further in,
939
+ a native checkbox's margin (DqZoekresultaten, DqPostvak, DqContracten), in
940
+ the same 60px. */
914
941
  .cn-look-board .cn-table-container--board .cn-data-table .cn-table-col--checkbox {
915
942
  box-sizing: border-box !important;
916
943
  width: 60px;
917
944
  min-width: 60px;
918
- padding-inline: 20px 12px;
945
+ padding-inline: 16px;
919
946
  text-align: start;
920
947
  }
921
948
 
949
+ .cn-look-board .cn-table-container--board .cn-data-table .cn-table-col--checkbox .checkbox-radio-switch {
950
+ margin-inline-start: 4px;
951
+ }
952
+
922
953
  .cn-look-board .cn-table-container--board .cn-data-table .checkbox-radio-switch {
923
954
  --icon-size: 18px;
924
955
  }
@@ -968,6 +999,16 @@
968
999
  width: 100%;
969
1000
  }
970
1001
 
1002
+ /* A wrapping title column (`cn-cell--wrap`, DqZoekresultaten) runs its
1003
+ secondary line at a normal line height over its lines, 2px under the
1004
+ title, as the board draws it; a single-line secondary keeps the table's
1005
+ 1.35.
1006
+ @spec openspec/changes/board-wrapped-secondary-line-height/specs/board-wrapped-secondary-line-height/spec.md */
1007
+ .cn-look-board .cn-table-container--board .cn-data-table td.cn-table-col--title.cn-cell--wrap .cn-table-cell__secondary {
1008
+ margin-top: 2px;
1009
+ line-height: normal;
1010
+ }
1011
+
971
1012
  /* `rowTitle: "plain"` (PqTickets, OcPublicaties): the title is bold text in
972
1013
  the main text colour without an underline, the secondary line 13px muted,
973
1014
  2px under it. The row stays clickable through its stretched row link. */
@@ -1076,6 +1117,22 @@
1076
1117
  line-height: 16px;
1077
1118
  }
1078
1119
 
1120
+ /* The grey person chip with a second line (DqContacten): 14px above and
1121
+ under, the name 2px above a 14px address on its normal line, a 66px row.
1122
+ @spec openspec/changes/board-person-chip-row-height/specs/data-table/spec.md#requirement-a-board-person-chip-row-is-66px */
1123
+ .cn-look-board .cn-table-container--board .cn-data-table td:has(.cn-cell-renderer__avatar-initials--neutral + .cn-cell-renderer__avatar-text .cn-cell-renderer__avatar-secondary) {
1124
+ padding-block: 14px;
1125
+ }
1126
+
1127
+ .cn-look-board .cn-table-container--board .cn-data-table .cn-cell-renderer__avatar-initials--neutral + .cn-cell-renderer__avatar-text:has(.cn-cell-renderer__avatar-secondary) {
1128
+ gap: 2px;
1129
+ }
1130
+
1131
+ .cn-look-board .cn-table-container--board .cn-data-table .cn-cell-renderer__avatar-initials--neutral + .cn-cell-renderer__avatar-text .cn-cell-renderer__avatar-secondary {
1132
+ font-size: 14px;
1133
+ line-height: normal;
1134
+ }
1135
+
1079
1136
  /* A status pill: 3px 10px, radius 12, 13px weight 600, 21px tall on a
1080
1137
  normal line (a 1.4 line made it 24.2). The colours stay the status colour
1081
1138
  map's.
@@ -1428,6 +1485,19 @@
1428
1485
  padding-top: 0;
1429
1486
  }
1430
1487
 
1488
+ /* The selection box sits flush where the boards draw it: NcCheckbox pads its
1489
+ content 7px on each side (DqAanMijToegewezen card head, DqTeamwachtrij rows,
1490
+ whose box starts 18px in).
1491
+ @spec openspec/changes/board-selection-box-flush/specs/board-selection-box-flush/spec.md#requirement-a-cards-and-a-rows-selection-box-sits-where-the-board-draws-it */
1492
+ .cn-look-board .cn-object-card__record-head > .cn-object-card__checkbox .checkbox-radio-switch__content,
1493
+ .cn-look-board .cn-object-row__checkbox .checkbox-radio-switch__content {
1494
+ padding-inline: 0;
1495
+ }
1496
+
1497
+ .cn-look-board .cn-object-row__checkbox {
1498
+ margin-inline-start: 6px;
1499
+ }
1500
+
1431
1501
  .cn-look-board .cn-object-card--board .cn-object-card__record-head > .cn-object-card__checkbox ~ .cn-object-card__record-pill {
1432
1502
  margin-inline-start: auto;
1433
1503
  }
@@ -2292,3 +2362,45 @@
2292
2362
  .cn-look-board .cn-data-table th.cn-table-col--icon + th {
2293
2363
  padding-inline-start: 12px;
2294
2364
  }
2365
+
2366
+ /* ---- The table beside the open side panel (DqZakenSorteren, DqFilterBeheren) */
2367
+ /* The table takes its column's width: its configured column widths make way
2368
+ for the content, titles and names wrap (a long word hyphenated, an
2369
+ unbreakable token such as an id anywhere), a pill
2370
+ wraps before the row scrolls, and each header keeps its label and icons on
2371
+ one line. No sideways scroll.
2372
+ @spec openspec/changes/board-table-fits-beside-side-panel/specs/board-table-fits-beside-side-panel/spec.md#requirement-the-table-fits-its-column-beside-the-open-side-panel */
2373
+ .cn-look-board .cn-index-page--sidebar-open .cn-table-container--board .cn-data-table {
2374
+ table-layout: auto;
2375
+ width: 100% !important;
2376
+ min-width: 0 !important;
2377
+ }
2378
+
2379
+ .cn-look-board .cn-index-page--sidebar-open .cn-table-container--board .cn-data-table th:not(.cn-table-col--checkbox):not(.cn-table-col--actions) {
2380
+ width: auto !important;
2381
+ min-width: 0 !important;
2382
+ max-width: none !important;
2383
+ white-space: nowrap;
2384
+ }
2385
+
2386
+ .cn-look-board .cn-index-page--sidebar-open .cn-table-container--board .cn-data-table td:not(.cn-table-col--checkbox):not(.cn-table-col--actions) {
2387
+ width: auto !important;
2388
+ min-width: 0 !important;
2389
+ max-width: none !important;
2390
+ white-space: normal;
2391
+ hyphens: auto;
2392
+ overflow-wrap: anywhere;
2393
+ }
2394
+
2395
+ .cn-look-board .cn-index-page--sidebar-open .cn-table-container--board .cn-data-table td :is(.cn-cell-renderer, .cn-cell-renderer__avatar-text, .cn-cell-renderer__avatar-name, .cn-table-row__link) {
2396
+ min-width: 0;
2397
+ white-space: normal;
2398
+ }
2399
+
2400
+ .cn-look-board .cn-index-page--sidebar-open .cn-table-container--board .cn-data-table td .cn-status-badge {
2401
+ white-space: normal;
2402
+ }
2403
+
2404
+ .cn-look-board .cn-index-page--sidebar-open .cn-table-container--board .cn-data-table th .cn-table-header__sort {
2405
+ min-width: max-content;
2406
+ }
@@ -664,12 +664,24 @@
664
664
  }
665
665
 
666
666
  /* Two buttons in a card's title row stand 8px apart (DqZaakContact "Live
667
- gesprek", DqZaakEmail "Mail koppelen" / "Mail opstellen").
667
+ gesprek", DqZaakEmail "Mail koppelen" / "Mail opstellen"), also in an
668
+ integration card's CnDetailCard header (DqZaakTaken "Afspraken", "Kaarten").
668
669
  @spec openspec/changes/board-card-header-actions-gap/specs/board-card-header-actions-gap/spec.md#requirement-a-board-cards-title-row-buttons-stand-8px-apart */
669
670
  .cn-look-board .cn-widget-wrapper__actions:not(.cn-look-nextcloud *) {
670
671
  gap: 8px;
671
672
  }
672
673
 
674
+ /* A card's own header items (a count note, a status pill) stand before the
675
+ manifest's title-row buttons, as the boards draw them (DqZaakTaken "Wat
676
+ hierna komt": "2 gepland", then "+ Toevoegen"). */
677
+ .cn-look-board .cn-widget-wrapper__actions > .cn-detail-widget-header-items:not(.cn-look-nextcloud *) {
678
+ order: -1;
679
+ }
680
+
681
+ .cn-look-board .cn-detail-card__header-right:not(.cn-look-nextcloud *) {
682
+ gap: 8px;
683
+ }
684
+
673
685
  /* A detail page marked `cardButtons: "regular"` draws them at the 40px
674
686
  control height: the case page's tab cards (DqZaakContact "Live gesprek",
675
687
  DqZaakHistorie, DqZaakBetrokkenen), where setting and case-type pages keep 34.
@@ -1467,6 +1479,103 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .v-popper__popper.action-
1467
1479
  text-decoration: underline;
1468
1480
  }
1469
1481
 
1482
+ /* A plain table an app draws in a settings card (dossiq's ZGW mapping, StUF
1483
+ endpoints and audit log, AWB terms) reads as the board's settings tables:
1484
+ 14px, a near-white head with muted 600 labels, 10px 12px cells, soft rules.
1485
+ The theme paints every th through its table header tokens, so the head
1486
+ hands those tokens the board's values too.
1487
+ @spec openspec/changes/board-settings-plain-tables/specs/settings-board-look/spec.md */
1488
+ .cn-look-board .settings-section table:not(.cn-data-table):not(.cn-look-nextcloud *) {
1489
+ width: 100%;
1490
+ border-collapse: collapse;
1491
+ font-size: 14px;
1492
+ line-height: 1.2;
1493
+ }
1494
+
1495
+ .cn-look-board .settings-section table:not(.cn-data-table):not(.cn-look-nextcloud *) thead th {
1496
+ --nldesign-component-table-header-background-color: var(--cn-board-table-head-bg, color-mix(in srgb, var(--color-background-hover) 40%, var(--color-main-background)));
1497
+ --nldesign-component-table-header-color: var(--color-text-maxcontrast);
1498
+ --nldesign-component-table-header-font-weight: 600;
1499
+ padding: 10px 12px;
1500
+ border-bottom: 1px solid var(--color-border);
1501
+ background: var(--cn-board-table-head-bg, color-mix(in srgb, var(--color-background-hover) 40%, var(--color-main-background)));
1502
+ color: var(--color-text-maxcontrast);
1503
+ font-size: 14px;
1504
+ font-weight: 600;
1505
+ line-height: 1.2;
1506
+ text-align: start;
1507
+ }
1508
+
1509
+ /* The working calendar's month reads as the board draws it: the month name
1510
+ first, at the content edge, with the previous and next month buttons after
1511
+ it, and the buttons add no height to the line, so the month sits 20px under
1512
+ the card title (DqInstellingenAlgemeen "Werkdagen en feestdagen": the
1513
+ arrow put the month 30px in and 28px down).
1514
+ @spec openspec/changes/working-calendar-board-head/specs/settings-board-look/spec.md */
1515
+ .cn-look-board .cn-working-calendar__title:not(.cn-look-nextcloud *) {
1516
+ order: -1;
1517
+ min-width: 0;
1518
+ margin-inline-end: 4px;
1519
+ line-height: 1.2;
1520
+ text-align: start;
1521
+ }
1522
+
1523
+ /* Body rows are 42px as on the boards (10px, a 21px line, 10px, the rule).
1524
+ A row header (dossiq's prerequisites) reads as a strong cell, not as a
1525
+ head: the theme paints every th, so its tokens are handed the cell's. */
1526
+ .cn-look-board .settings-section table:not(.cn-data-table):not(.cn-look-nextcloud *) td,
1527
+ .cn-look-board .settings-section table:not(.cn-data-table):not(.cn-look-nextcloud *) tbody th {
1528
+ padding: 10px 12px;
1529
+ border-bottom: 1px solid var(--color-background-dark);
1530
+ color: var(--color-main-text);
1531
+ font-size: 14px;
1532
+ line-height: 1.5;
1533
+ vertical-align: middle;
1534
+ }
1535
+
1536
+ .cn-look-board .settings-section table:not(.cn-data-table):not(.cn-look-nextcloud *) tbody th {
1537
+ --nldesign-component-table-header-background-color: transparent;
1538
+ --nldesign-component-table-header-color: var(--color-main-text);
1539
+ --nldesign-component-table-header-font-weight: 600;
1540
+ background: transparent;
1541
+ font-weight: 600;
1542
+ text-align: start;
1543
+ }
1544
+
1545
+ /* A strong cell's name in a settings card reads 15px bold over its muted
1546
+ second line, as the boards draw a connection's name (DqInstellingen
1547
+ "Koppelingen": <strong> 15px), not the index tables' 14px 500.
1548
+ @spec openspec/changes/board-settings-strong-cell-name/specs/settings-board-look/spec.md */
1549
+ .cn-look-board .settings-section .cn-data-table td.cn-cell--strong:not(.cn-look-nextcloud *) {
1550
+ font-weight: 400;
1551
+ }
1552
+
1553
+ .cn-look-board .settings-section .cn-data-table td.cn-cell--strong:not(.cn-look-nextcloud *) > .cn-cell-renderer {
1554
+ font-size: 15px;
1555
+ font-weight: 700;
1556
+ }
1557
+
1558
+ .cn-look-board .cn-working-calendar__head:not(.cn-look-nextcloud *) > .button-vue:not(.cn-working-calendar__add) {
1559
+ margin-block: -8px;
1560
+ }
1561
+
1562
+ /* In a one-column settings card a single-line field is at most 560px wide,
1563
+ its hint under it, as the boards draw a field on its own row
1564
+ (DqBeheerGeavanceerd "Financiële integratie", "Storeregister"); a text
1565
+ area, a note or a switch still spans the card.
1566
+ @spec openspec/changes/board-settings-single-field-width/specs/settings-board-look/spec.md */
1567
+ .cn-look-board .cn-settings-fields__grid--1:not(.cn-look-nextcloud *) > .cn-settings-field--text,
1568
+ .cn-look-board .cn-settings-fields__grid--1:not(.cn-look-nextcloud *) > .cn-settings-field--password,
1569
+ .cn-look-board .cn-settings-fields__grid--1:not(.cn-look-nextcloud *) > .cn-settings-field--secret,
1570
+ .cn-look-board .cn-settings-fields__grid--1:not(.cn-look-nextcloud *) > .cn-settings-field--number,
1571
+ .cn-look-board .cn-settings-fields__grid--1:not(.cn-look-nextcloud *) > .cn-settings-field--email,
1572
+ .cn-look-board .cn-settings-fields__grid--1:not(.cn-look-nextcloud *) > .cn-settings-field--url,
1573
+ .cn-look-board .cn-settings-fields__grid--1:not(.cn-look-nextcloud *) > .cn-settings-field--select,
1574
+ .cn-look-board .cn-settings-fields__grid--1:not(.cn-look-nextcloud *) > .cn-settings-field--date,
1575
+ .cn-look-board .cn-settings-fields__grid--1:not(.cn-look-nextcloud *) > .cn-settings-field--datetime-local {
1576
+ max-width: 560px;
1577
+ }
1578
+
1470
1579
  /* Section actions sit at the end of the title row. The row is a grid, so a
1471
1580
  long title wraps beside the buttons instead of running under them. A
1472
1581
  half-width card keeps them there (DqVthInstellingen "Inspectiechecklists",
@@ -1523,6 +1632,24 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .v-popper__popper.action-
1523
1632
  align-self: center;
1524
1633
  }
1525
1634
 
1635
+ /* A switch or radio in a board settings card starts at the card's content
1636
+ edge, its label 12px (switch) or 10px (radio) after the control, as the
1637
+ boards draw them (DqBeheerinstellingen "AI-ondersteunde verwerking").
1638
+ NcCheckboxRadioSwitch pads its content 9px on each side, which set the
1639
+ control 9px in from the fields above and below it.
1640
+ @spec openspec/changes/board-settings-switch-radio-flush/specs/settings-board-look/spec.md */
1641
+ .cn-look-board .cn-settings-fields:not(.cn-look-nextcloud *) .checkbox-content {
1642
+ padding-inline: 0;
1643
+ }
1644
+
1645
+ .cn-look-board .cn-settings-fields:not(.cn-look-nextcloud *) .checkbox-content-switch {
1646
+ gap: 12px;
1647
+ }
1648
+
1649
+ .cn-look-board .cn-settings-fields:not(.cn-look-nextcloud *) .checkbox-content-radio {
1650
+ gap: 10px;
1651
+ }
1652
+
1526
1653
  /* The "Uitleg tonen" toggle is a text link flush with the card's right edge
1527
1654
  (DqBeheerinstellingen Zaak-e-mail), not a padded tertiary button.
1528
1655
  @spec openspec/changes/board-settings-title-centred-and-link-flush/specs/settings-board-look/spec.md */
@@ -1596,8 +1723,12 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .v-popper__popper.action-
1596
1723
  max-width: none;
1597
1724
  }
1598
1725
 
1599
- .cn-look-board .settings-section.cn-settings-section--wide:not(.cn-look-nextcloud *) .settings-section__name,
1600
- .cn-look-board .settings-section.cn-settings-section--wide:not(.cn-look-nextcloud *) .settings-section__desc {
1726
+ /* A board card's title and subtitle run the card's width, wide or not
1727
+ (DqBeheerGeavanceerd "Eerste keer", "StUF-ZKN-endpoints": the subtitle
1728
+ wrapped at NcSettingsSection's 900px measure, the board's does not).
1729
+ @spec openspec/changes/board-settings-desc-full-width/specs/settings-board-look/spec.md */
1730
+ .cn-look-board .settings-section:not(.cn-look-nextcloud *) .settings-section__name,
1731
+ .cn-look-board .settings-section:not(.cn-look-nextcloud *) .settings-section__desc {
1601
1732
  max-width: none;
1602
1733
  }
1603
1734
 
@@ -2156,7 +2287,7 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .v-popper__popper.action-
2156
2287
  12px follows it: the boards' card is title, 6px, rule, row (DqMijnTeam
2157
2288
  "Vastgelopen zaken", DqMijnWerk "Zaken die u volgt").
2158
2289
  @spec openspec/changes/board-card-title-above-ruled-list/specs/board-card-title-above-ruled-list/spec.md#requirement-a-ruled-list-starts-6px-under-the-card-title */
2159
- .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-widget-wrapper:not(.cn-widget-wrapper--nc-dashboard):not(.cn-widget-wrapper--borderless):has(> .cn-widget-wrapper__content .cn-record-list--rule-above, > .cn-widget-wrapper__content .cn-tasks-widget__list, > .cn-widget-wrapper__content .cn-decision-list__rows) .cn-widget-wrapper__header {
2290
+ .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-widget-wrapper:not(.cn-widget-wrapper--nc-dashboard):not(.cn-widget-wrapper--borderless):has(> .cn-widget-wrapper__content .cn-record-list--rule-above, > .cn-widget-wrapper__content .cn-tasks-widget__list, > .cn-widget-wrapper__content .cn-decision-list__rows, > .cn-widget-wrapper__content .cn-nc-widget-widget__body--vertical) .cn-widget-wrapper__header {
2160
2291
  padding-bottom: 6px;
2161
2292
  }
2162
2293
 
@@ -2692,13 +2823,13 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .v-popper__popper.action-
2692
2823
 
2693
2824
  /* A vertical NC widget is the board's activity list (DqMijnWerk "Recente
2694
2825
  activiteit"): hairline rows 12px above and below the text, a 10px dot in
2695
- the icon's place, the title 14px in the main ink and wrapping, the first
2696
- hairline 12px under the title. Thematiq underlines links with a heavier
2697
- selector, hence the !important.
2826
+ the icon's place, the title 14px in the main ink and wrapping. Thematiq underlines links with a heavier
2827
+ selector, hence the !important. The first hairline stands 6px under the
2828
+ title, as on every Dq list card (design-system #432).
2698
2829
  @spec openspec/changes/board-nc-widget-activity-rows/specs/board-nc-widget-activity-rows/spec.md#requirement-a-board-dashboard-nc-widget-draws-its-items-as-activity-rows */
2699
2830
  .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-widget-wrapper:not(.cn-widget-wrapper--borderless) .cn-nc-widget-widget__body--vertical {
2700
2831
  gap: 0;
2701
- padding: 2px var(--cn-board-dashboard-card-inset) 10px;
2832
+ padding: 0 var(--cn-board-dashboard-card-inset) 10px;
2702
2833
  }
2703
2834
 
2704
2835
  .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-nc-widget-widget__body--vertical a.cn-nc-widget-widget__item--vertical {
@@ -3132,6 +3263,25 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .v-popper__popper.action-
3132
3263
  line-height: normal;
3133
3264
  }
3134
3265
 
3266
+ /* A bar list in a framed section of a sections card is the board's small
3267
+ list: 13px rows 8px apart, a 150px label column, a 10px bar and a 48px
3268
+ value column (DqDoorlooptijd "Naleving per zaaktype").
3269
+ @spec openspec/changes/board-section-bar-list-compact/specs/board-section-bar-list-compact/spec.md#requirement-a-bar-list-in-a-sections-card-section-is-compact */
3270
+ .cn-look-board .cn-sections-widget__section .cn-chart-widget__bar-list:not(.cn-look-nextcloud *) {
3271
+ gap: 8px;
3272
+ }
3273
+
3274
+ .cn-look-board .cn-sections-widget__section .cn-chart-widget__bar-list:not(.cn-look-nextcloud *) .cn-chart-widget__bar-line {
3275
+ grid-template-columns: 150px minmax(0, 1fr) 48px;
3276
+ gap: 10px;
3277
+ font-size: 13px;
3278
+ }
3279
+
3280
+ .cn-look-board .cn-sections-widget__section .cn-chart-widget__bar-list:not(.cn-look-nextcloud *) .cn-chart-widget__bar-track {
3281
+ height: 10px;
3282
+ border-radius: 5px;
3283
+ }
3284
+
3135
3285
  /* A bar list with the value at the bar's end (DqProcesanalyse "Doorlooptijd
3136
3286
  per status") sets its rows in 13px, the label in the secondary text, the
3137
3287
  value in the main ink at 600, rows 34px apart.
@@ -3310,3 +3460,13 @@ html :where(.cn-look-board) :is(h2, h3, h5, h6):where(.cn-inner-heading) {
3310
3460
  .cn-look-board .cn-timeline-manage__draft-label:first-child:not(.cn-look-nextcloud *) {
3311
3461
  margin-block-start: 0;
3312
3462
  }
3463
+
3464
+ /* ---- The share dialog's audience rows ----------------------------------- */
3465
+ /* A person or group the view is shared with sits on the page's own ground at
3466
+ radius 8, as DqFilterDelen draws it; Nextcloud's look keeps its hover grey
3467
+ and large radius.
3468
+ @spec openspec/changes/board-share-rows-ground/specs/board-share-rows-ground/spec.md */
3469
+ .cn-look-board .cn-saved-view-share__mode:not(.cn-look-nextcloud *) {
3470
+ border-radius: 8px;
3471
+ background: var(--cn-board-sunken-ground);
3472
+ }
package/src/css/table.css CHANGED
@@ -310,7 +310,7 @@
310
310
  .cn-data-table--fit td.cn-table-col--grow.cn-cell--wrap {
311
311
  min-width: 8em;
312
312
  max-width: none;
313
- overflow-wrap: anywhere;
313
+ overflow-wrap: break-word;
314
314
  white-space: normal;
315
315
  }
316
316
 
@@ -359,6 +359,13 @@
359
359
  font-weight: 500;
360
360
  }
361
361
 
362
+ /* A lead cell's value reads 15px, a person's name over their function
363
+ (DqAfdelingen). */
364
+ .cn-data-table td.cn-cell--lead > .cn-cell-renderer,
365
+ .cn-data-table td.cn-cell--lead .cn-table-row__link > .cn-cell-renderer {
366
+ font-size: 15px;
367
+ }
368
+
362
369
  .cn-data-table td.cn-cell--muted {
363
370
  color: var(--color-text-maxcontrast);
364
371
  }
@@ -609,6 +616,15 @@
609
616
  cursor: grab;
610
617
  }
611
618
 
619
+ /* The handle before a reorderable row's name: a muted 16px grip 10px before it. */
620
+ .cn-table-row__handle {
621
+ display: inline-flex;
622
+ vertical-align: middle;
623
+ margin-inline-end: 10px;
624
+ color: var(--color-text-maxcontrast);
625
+ cursor: grab;
626
+ }
627
+
612
628
  .cn-table-row--drop-target {
613
629
  box-shadow: inset 0 2px 0 var(--color-primary-element);
614
630
  }
@@ -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.155.0",
6
+ "version": "2.156.0",
7
7
  "type": "object",
8
8
  "required": ["$schema", "version"],
9
9
  "allOf": [
@@ -5533,7 +5533,7 @@
5533
5533
  "gridFloat": {
5534
5534
  "type": "boolean",
5535
5535
  "default": true,
5536
- "description": "type='dashboard': `false` packs widgets upward (GridStack `float: false`), so a size-to-content widget that shrinks closes the gap under it instead of leaving an empty band (zuiddrecht-pixel-gaps-3). Maps to CnDashboardPage `gridFloat`."
5536
+ "description": "type='dashboard' and type='detail': `false` packs widgets upward (GridStack `float: false`), so a size-to-content widget that shrinks closes the gap under it instead of leaving an empty band (zuiddrecht-pixel-gaps-3). Maps to CnDashboardPage and CnDetailPage `gridFloat`."
5537
5537
  },
5538
5538
  "showTitle": {
5539
5539
  "type": "boolean",
@@ -2,6 +2,7 @@ import { translatePlural as n, translate as t } from '@nextcloud/l10n'
2
2
  import { formatRelativeDate } from './boardDate.js'
3
3
  import { formatMoney } from './formatMetric.js'
4
4
  import { parseDuration } from './isoDuration.js'
5
+ import { userDisplayName } from './userDisplayName.js'
5
6
 
6
7
  /**
7
8
  * @file Built-in formatters merged into the `cnFormatters` registry
@@ -523,6 +524,21 @@ export function formatIsoDuration(value) {
523
524
  return words[period.unit] ? words[period.unit]() : String(value)
524
525
  }
525
526
 
527
+ /**
528
+ * A Nextcloud user id as its display name (`userName` registry key): the id
529
+ * while the name is looked up, and for a user nobody answers for.
530
+ *
531
+ * @spec openspec/changes/user-name-formatter/specs/user-name-formatter/spec.md#requirement-a-user-id-can-read-as-its-display-name
532
+ * @param {unknown} value The user id.
533
+ * @return {string} The display name, the id, or ''.
534
+ */
535
+ export function formatUserName(value) {
536
+ if (value === null || value === undefined || value === '' || typeof value === 'object') {
537
+ return ''
538
+ }
539
+ return userDisplayName(value) || String(value)
540
+ }
541
+
526
542
  export const BUILT_IN_FORMATTERS = {
527
543
  date: formatDate,
528
544
  datetime: formatDateTime,
@@ -538,4 +554,5 @@ export const BUILT_IN_FORMATTERS = {
538
554
  masked: formatMasked,
539
555
  isoDuration: formatIsoDuration,
540
556
  valueMap: formatValueMap,
557
+ userName: formatUserName,
541
558
  }
@@ -47,8 +47,10 @@ function unwrap(v) {
47
47
  *
48
48
  * @param {object|null} objectCtx The `cnObjectContext` inject (ref or plain object).
49
49
  * @param {object|null} detailCtx The `cnDetailObjectContext` inject (`{ value }` holder).
50
- * @return {{objectId: (string|number|null), object: (object|null), register: string, schema: (string|object)}|null}
51
- * The merged context, or null when neither inject resolves.
50
+ * @return {{objectId: (string|number|null), object: (object|null), register: string, schema: (string|object), context?: object}|null}
51
+ * The merged context, or null when neither inject resolves. `context` is the
52
+ * records the page's conditions name (`@context.<name>`), when the primary
53
+ * inject carries them.
52
54
  */
53
55
  export function resolveObjectTokenContext(objectCtx, detailCtx) {
54
56
  const primary = unwrap(objectCtx)
@@ -81,6 +83,9 @@ export function resolveObjectTokenContext(objectCtx, detailCtx) {
81
83
  if (primary.schema) {
82
84
  out.schema = primary.schema
83
85
  }
86
+ if (primary.context && typeof primary.context === 'object') {
87
+ out.context = primary.context
88
+ }
84
89
  }
85
90
  return out
86
91
  }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * Header sort and filter applied to rows the table already holds (an
6
+ * `endpointSource` table): the same `{ field, dir }` sort and
7
+ * `{ paramKey: values[] }` filter map a self-fetching table sends to the
8
+ * server, read here in the browser.
9
+ *
10
+ * @module utils/localHeaderRows
11
+ */
12
+
13
+ import { CONTAINS_OPERATOR } from './columnFilters.js'
14
+
15
+ /**
16
+ * A row value as one comparable scalar: an array counts as its joined items.
17
+ *
18
+ * @param {unknown} value The raw value.
19
+ * @return {string|number|null} The value to compare.
20
+ */
21
+ function scalar(value) {
22
+ if (value === null || value === undefined) {
23
+ return null
24
+ }
25
+ if (Array.isArray(value)) {
26
+ return value.map((item) => (item && typeof item === 'object' ? (item.title || item.name || '') : String(item))).join(', ')
27
+ }
28
+ if (typeof value === 'number' || typeof value === 'boolean') {
29
+ return typeof value === 'boolean' ? String(value) : value
30
+ }
31
+ return typeof value === 'object' ? (value.title || value.name || '') : String(value)
32
+ }
33
+
34
+ /**
35
+ * Whether a row holds a value the filter entry asks for.
36
+ *
37
+ * @param {object} row The row.
38
+ * @param {string} param The filter key (`field`, `field[like]`, `field[gte]`, `field[lte]`).
39
+ * @param {Array<string>} values The chosen values.
40
+ * @return {boolean} True when the row passes.
41
+ */
42
+ function passes(row, param, values) {
43
+ const match = /^(.+)\[(\w+)\]$/.exec(param)
44
+ const field = match ? match[1] : param
45
+ const op = match ? match[2] : ''
46
+ const value = scalar(row[field])
47
+ if (op === CONTAINS_OPERATOR) {
48
+ const term = String(values[0]).toLowerCase()
49
+ return value !== null && String(value).toLowerCase().includes(term)
50
+ }
51
+ if (op === 'gte' || op === 'lte') {
52
+ if (value === null) {
53
+ return false
54
+ }
55
+ const left = typeof value === 'number' ? value : String(value)
56
+ const right = typeof value === 'number' ? Number(values[0]) : String(values[0])
57
+ return op === 'gte' ? left >= right : left <= right
58
+ }
59
+ return value !== null && values.map(String).includes(String(value))
60
+ }
61
+
62
+ /**
63
+ * The rows that pass every active header filter, in the header sort's order.
64
+ *
65
+ * @param {Array<object>} rows The rows.
66
+ * @param {{field: string, dir: string}|null} sort The header sort.
67
+ * @param {object} filters The header filters, `{ paramKey: values[] }`.
68
+ * @return {Array<object>} The rows to show (the same array when nothing applies).
69
+ */
70
+ export function localHeaderRows(rows, sort, filters) {
71
+ if (!Array.isArray(rows)) {
72
+ return []
73
+ }
74
+ const active = Object.entries(filters || {}).filter(([, values]) => Array.isArray(values) && values.length > 0)
75
+ let out = active.length > 0 ? rows.filter((row) => row && active.every(([param, values]) => passes(row, param, values))) : rows
76
+ if (sort && sort.field) {
77
+ const sign = sort.dir === 'desc' ? -1 : 1
78
+ out = [...out].sort((a, b) => {
79
+ const left = scalar(a && a[sort.field])
80
+ const right = scalar(b && b[sort.field])
81
+ if (left === null || right === null) {
82
+ return left === right ? 0 : (left === null ? 1 : -1)
83
+ }
84
+ if (typeof left === 'number' && typeof right === 'number') {
85
+ return sign * (left - right)
86
+ }
87
+ return sign * String(left).localeCompare(String(right), undefined, { numeric: true, sensitivity: 'base' })
88
+ })
89
+ }
90
+ return out
91
+ }