@conduction/nextcloud-vue 2.56.0-beta.35 → 2.56.0-beta.36

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 (37) hide show
  1. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue.js +1 -1
  2. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +114 -0
  3. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
  4. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js +2 -2
  5. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
  6. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
  7. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +95 -0
  8. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
  9. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js +24 -2
  10. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
  11. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue.js +1 -1
  12. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js +2 -1
  13. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js.map +1 -1
  14. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js.map +1 -1
  15. package/dist/esm/index.js +1 -0
  16. package/dist/esm/index.js.map +1 -1
  17. package/dist/esm/library-style.js +1 -1
  18. package/dist/esm/nextcloud-vue.css +120 -65
  19. package/dist/esm/utils/gridStack.js +40 -1
  20. package/dist/esm/utils/gridStack.js.map +1 -1
  21. package/dist/esm/utils/humaniseKey.js +41 -0
  22. package/dist/esm/utils/humaniseKey.js.map +1 -0
  23. package/dist/esm/utils/notificationRuleLabel.js +6 -23
  24. package/dist/esm/utils/notificationRuleLabel.js.map +1 -1
  25. package/dist/nextcloud-vue.cjs.js +546 -256
  26. package/dist/nextcloud-vue.cjs.js.map +2 -2
  27. package/dist/nextcloud-vue.css +120 -65
  28. package/package.json +1 -1
  29. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +116 -2
  30. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +123 -3
  31. package/src/components/CnWeekStripWidget/CnWeekStripWidget.vue +2 -1
  32. package/src/css/kpi-card.css +13 -0
  33. package/src/css/look-board.css +19 -0
  34. package/src/index.js +2 -0
  35. package/src/utils/gridStack.js +39 -0
  36. package/src/utils/humaniseKey.js +38 -0
  37. package/src/utils/notificationRuleLabel.js +5 -23
@@ -781,6 +781,25 @@
781
781
  .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-record-list__chevron {
782
782
  display: none;
783
783
  }
784
+
785
+ /* ---- Week strip (DqMijnWerk "Termijnen deze week") ---------------------- */
786
+ /* The days wrap inside the card instead of scrolling sideways: a row holds
787
+ as many days of at least 120px as the card fits, and the rest go to the
788
+ next row (dossiq at 1100 wide held 680px of days in a 494px card and
789
+ scrolled; now three days and two). 120px keeps a case title's words whole;
790
+ five 88px days fitted one row but broke "Bezwaar" mid-word. A card wide
791
+ enough for the week draws it as before, one row of equal columns. The
792
+ day's head wraps its "today" badge under the label when the day is
793
+ narrow.
794
+ @spec openspec/changes/board-week-strip-fits/specs/week-strip-board-look/spec.md#requirement-a-board-week-strip-fits-its-card */
795
+ .cn-look-board .cn-week-strip:not(.cn-look-nextcloud *) .cn-week-strip__days {
796
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 120px), 1fr));
797
+ min-width: 0;
798
+ }
799
+
800
+ .cn-look-board .cn-week-strip:not(.cn-look-nextcloud *) .cn-week-strip__head {
801
+ flex-wrap: wrap;
802
+ }
784
803
  /*
785
804
  * The board dialog (screens-dialog-parity, canon section 5).
786
805
  *
@@ -3949,6 +3968,19 @@ a.cn-kpi-card--stacked:focus-visible {
3949
3968
  color: var(--color-main-text);
3950
3969
  }
3951
3970
 
3971
+ /* A board tile whose label wraps grows into its cell instead of clipping its
3972
+ own last line. dossiq's "Afgehandeld deze maand" at 1100 wide held 129px of
3973
+ content in a 114px tile and lost its caption's bottom. The tile still fills
3974
+ a cell it fits (`min-height: 100%`), so every tile that fits keeps its
3975
+ size; one that does not takes the height of its content, and the card-fit
3976
+ cell's `safe center` keeps its top in view.
3977
+ @spec openspec/changes/board-stat-tile-grows/specs/stat-tile-board-look/spec.md#requirement-a-board-stat-tile-shows-all-its-lines */
3978
+ .cn-kpi-card--board.cn-kpi-card--stacked {
3979
+ flex: 0 0 auto;
3980
+ height: auto;
3981
+ min-height: 100%;
3982
+ }
3983
+
3952
3984
  .cn-kpi-card--board.cn-kpi-card--stacked .cn-kpi-card__caption-line,
3953
3985
  .cn-kpi-card--board.cn-kpi-card--stacked .cn-stats-block__count-label {
3954
3986
  font-size: 13px;
@@ -22309,6 +22341,19 @@ a.cn-kpi-card--stacked:focus-visible {
22309
22341
  color: var(--color-main-text);
22310
22342
  }
22311
22343
 
22344
+ /* A board tile whose label wraps grows into its cell instead of clipping its
22345
+ own last line. dossiq's "Afgehandeld deze maand" at 1100 wide held 129px of
22346
+ content in a 114px tile and lost its caption's bottom. The tile still fills
22347
+ a cell it fits (`min-height: 100%`), so every tile that fits keeps its
22348
+ size; one that does not takes the height of its content, and the card-fit
22349
+ cell's `safe center` keeps its top in view.
22350
+ @spec openspec/changes/board-stat-tile-grows/specs/stat-tile-board-look/spec.md#requirement-a-board-stat-tile-shows-all-its-lines */
22351
+ .cn-kpi-card--board.cn-kpi-card--stacked {
22352
+ flex: 0 0 auto;
22353
+ height: auto;
22354
+ min-height: 100%;
22355
+ }
22356
+
22312
22357
  .cn-kpi-card--board.cn-kpi-card--stacked .cn-kpi-card__caption-line,
22313
22358
  .cn-kpi-card--board.cn-kpi-card--stacked .cn-stats-block__count-label {
22314
22359
  font-size: 13px;
@@ -23136,7 +23181,7 @@ a.cn-kpi-card--board * {
23136
23181
  }
23137
23182
 
23138
23183
 
23139
- .cn-object-data-widget__image[data-v-76edf75d] {
23184
+ .cn-object-data-widget__image[data-v-324e1a00] {
23140
23185
  max-width: 100%;
23141
23186
  max-height: 160px;
23142
23187
  border-radius: 8px;
@@ -23144,33 +23189,33 @@ a.cn-kpi-card--board * {
23144
23189
  }
23145
23190
 
23146
23191
  /* Shimmering placeholder while a relation's display name resolves. */
23147
- .cn-object-data-widget__skeleton[data-v-76edf75d] {
23192
+ .cn-object-data-widget__skeleton[data-v-324e1a00] {
23148
23193
  display: inline-block;
23149
23194
  width: 90px;
23150
23195
  height: 1em;
23151
23196
  border-radius: 4px;
23152
23197
  background: linear-gradient(90deg, var(--color-background-dark) 25%, var(--color-background-hover) 50%, var(--color-background-dark) 75%);
23153
23198
  background-size: 200% 100%;
23154
- animation: cn-odw-shimmer-76edf75d 1.2s ease-in-out infinite;
23199
+ animation: cn-odw-shimmer-324e1a00 1.2s ease-in-out infinite;
23155
23200
  }
23156
- @keyframes cn-odw-shimmer-76edf75d {
23201
+ @keyframes cn-odw-shimmer-324e1a00 {
23157
23202
  from { background-position: 200% 0;
23158
23203
  }
23159
23204
  to { background-position: -200% 0;
23160
23205
  }
23161
23206
  }
23162
23207
  @media (prefers-reduced-motion: reduce) {
23163
- .cn-object-data-widget__skeleton[data-v-76edf75d] { animation: none;
23208
+ .cn-object-data-widget__skeleton[data-v-324e1a00] { animation: none;
23164
23209
  }
23165
23210
  }
23166
- .cn-object-data-widget__grid[data-v-76edf75d] {
23211
+ .cn-object-data-widget__grid[data-v-324e1a00] {
23167
23212
  display: grid;
23168
23213
  gap: calc(2 * var(--default-grid-baseline, 4px)) calc(4 * var(--default-grid-baseline, 4px));
23169
23214
  }
23170
- .cn-object-data-widget__toggle[data-v-76edf75d] {
23215
+ .cn-object-data-widget__toggle[data-v-324e1a00] {
23171
23216
  margin-top: 4px;
23172
23217
  }
23173
- .cn-object-data-widget__cell[data-v-76edf75d] {
23218
+ .cn-object-data-widget__cell[data-v-324e1a00] {
23174
23219
  display: flex;
23175
23220
  flex-direction: column;
23176
23221
  gap: 2px;
@@ -23178,12 +23223,12 @@ to { background-position: -200% 0;
23178
23223
  border-bottom: 1px solid var(--color-border-dark);
23179
23224
  min-width: 0;
23180
23225
  }
23181
- .cn-object-data-widget__label[data-v-76edf75d] {
23226
+ .cn-object-data-widget__label[data-v-324e1a00] {
23182
23227
  font-size: 0.85em;
23183
23228
  color: var(--color-text-maxcontrast);
23184
23229
  font-weight: 500;
23185
23230
  }
23186
- .cn-object-data-widget__value[data-v-76edf75d] {
23231
+ .cn-object-data-widget__value[data-v-324e1a00] {
23187
23232
  font-size: 1em;
23188
23233
  color: var(--color-main-text);
23189
23234
  overflow-wrap: anywhere;
@@ -23197,24 +23242,24 @@ to { background-position: -200% 0;
23197
23242
  which drew a horizontal scrollbar under every data widget in a tab (dossiq's
23198
23243
  case page, Data tab). The end padding drops by the same 4px so the text and
23199
23244
  the edit icon sit where they sat. */
23200
- .cn-object-data-widget__value--editable[data-v-76edf75d] {
23245
+ .cn-object-data-widget__value--editable[data-v-324e1a00] {
23201
23246
  cursor: pointer;
23202
23247
  border-radius: var(--border-radius);
23203
23248
  padding: 4px 20px 4px 4px;
23204
23249
  margin: -4px 0 -4px -4px;
23205
23250
  }
23206
- .cn-object-data-widget__value--editable[data-v-76edf75d]:hover {
23251
+ .cn-object-data-widget__value--editable[data-v-324e1a00]:hover {
23207
23252
  background: var(--color-background-dark);
23208
23253
  }
23209
- .cn-object-data-widget__value--editable[data-v-76edf75d]:focus-visible {
23254
+ .cn-object-data-widget__value--editable[data-v-324e1a00]:focus-visible {
23210
23255
  outline: 2px solid var(--color-primary-element);
23211
23256
  outline-offset: 2px;
23212
23257
  }
23213
- .cn-object-data-widget__value--empty[data-v-76edf75d] {
23258
+ .cn-object-data-widget__value--empty[data-v-324e1a00] {
23214
23259
  color: var(--color-text-maxcontrast);
23215
23260
  font-style: italic;
23216
23261
  }
23217
- .cn-object-data-widget__edit-icon[data-v-76edf75d] {
23262
+ .cn-object-data-widget__edit-icon[data-v-324e1a00] {
23218
23263
  position: absolute;
23219
23264
  right: 4px;
23220
23265
  top: 50%;
@@ -23223,21 +23268,21 @@ to { background-position: -200% 0;
23223
23268
  opacity: 0;
23224
23269
  transition: opacity 0.15s ease;
23225
23270
  }
23226
- .cn-object-data-widget__value--editable:hover .cn-object-data-widget__edit-icon[data-v-76edf75d],
23227
- .cn-object-data-widget__value--editable:focus-visible .cn-object-data-widget__edit-icon[data-v-76edf75d] {
23271
+ .cn-object-data-widget__value--editable:hover .cn-object-data-widget__edit-icon[data-v-324e1a00],
23272
+ .cn-object-data-widget__value--editable:focus-visible .cn-object-data-widget__edit-icon[data-v-324e1a00] {
23228
23273
  opacity: 1;
23229
23274
  }
23230
- .cn-object-data-widget__editor[data-v-76edf75d] {
23275
+ .cn-object-data-widget__editor[data-v-324e1a00] {
23231
23276
  display: flex;
23232
23277
  flex-direction: column;
23233
23278
  gap: 4px;
23234
23279
  }
23235
- .cn-object-data-widget__editor-actions[data-v-76edf75d] {
23280
+ .cn-object-data-widget__editor-actions[data-v-324e1a00] {
23236
23281
  display: flex;
23237
23282
  gap: 2px;
23238
23283
  justify-content: flex-end;
23239
23284
  }
23240
- .cn-object-data-widget__textarea[data-v-76edf75d] {
23285
+ .cn-object-data-widget__textarea[data-v-324e1a00] {
23241
23286
  width: 100%;
23242
23287
  min-height: 80px;
23243
23288
  padding: 8px;
@@ -23249,11 +23294,11 @@ to { background-position: -200% 0;
23249
23294
  font-size: inherit;
23250
23295
  resize: vertical;
23251
23296
  }
23252
- .cn-object-data-widget__textarea[data-v-76edf75d]:focus {
23297
+ .cn-object-data-widget__textarea[data-v-324e1a00]:focus {
23253
23298
  border-color: var(--color-primary-element);
23254
23299
  outline: none;
23255
23300
  }
23256
- .cn-object-data-widget__empty[data-v-76edf75d] {
23301
+ .cn-object-data-widget__empty[data-v-324e1a00] {
23257
23302
  color: var(--color-text-maxcontrast);
23258
23303
  font-style: italic;
23259
23304
  padding: calc(2 * var(--default-grid-baseline, 4px));
@@ -23261,16 +23306,16 @@ to { background-position: -200% 0;
23261
23306
 
23262
23307
  /* Compact inline table for array-of-objects values. Dense + unstyled-simple;
23263
23308
  fits the card and participates in the whole-row overflow handling. */
23264
- .cn-object-data-widget__mini-table-wrap[data-v-76edf75d] {
23309
+ .cn-object-data-widget__mini-table-wrap[data-v-324e1a00] {
23265
23310
  max-width: 100%;
23266
23311
  }
23267
- .cn-object-data-widget__mini-table[data-v-76edf75d] {
23312
+ .cn-object-data-widget__mini-table[data-v-324e1a00] {
23268
23313
  width: 100%;
23269
23314
  border-collapse: collapse;
23270
23315
  font-size: 0.9em;
23271
23316
  }
23272
- .cn-object-data-widget__mini-table th[data-v-76edf75d],
23273
- .cn-object-data-widget__mini-table td[data-v-76edf75d] {
23317
+ .cn-object-data-widget__mini-table th[data-v-324e1a00],
23318
+ .cn-object-data-widget__mini-table td[data-v-324e1a00] {
23274
23319
  text-align: start;
23275
23320
  padding: 2px 8px 2px 0;
23276
23321
  border-bottom: 1px solid var(--color-border);
@@ -23281,23 +23326,23 @@ to { background-position: -200% 0;
23281
23326
  cell, and the card 8px wider than its cell (pipelinq's ticket page). */
23282
23327
  white-space: normal;
23283
23328
  }
23284
- .cn-object-data-widget__mini-table th[data-v-76edf75d] {
23329
+ .cn-object-data-widget__mini-table th[data-v-324e1a00] {
23285
23330
  color: var(--color-text-maxcontrast);
23286
23331
  font-weight: 500;
23287
23332
  }
23288
- .cn-object-data-widget__more[data-v-76edf75d] {
23333
+ .cn-object-data-widget__more[data-v-324e1a00] {
23289
23334
  margin-top: 2px;
23290
23335
  font-size: 0.85em;
23291
23336
  color: var(--color-text-maxcontrast);
23292
23337
  }
23293
23338
 
23294
23339
  /* Comma-less chip row for array-of-scalars values. */
23295
- .cn-object-data-widget__chips[data-v-76edf75d] {
23340
+ .cn-object-data-widget__chips[data-v-324e1a00] {
23296
23341
  display: flex;
23297
23342
  flex-wrap: wrap;
23298
23343
  gap: 4px;
23299
23344
  }
23300
- .cn-object-data-widget__chip[data-v-76edf75d] {
23345
+ .cn-object-data-widget__chip[data-v-324e1a00] {
23301
23346
  padding: 1px 8px;
23302
23347
  border-radius: 12px;
23303
23348
  background: var(--color-background-dark);
@@ -23305,27 +23350,37 @@ to { background-position: -200% 0;
23305
23350
  }
23306
23351
 
23307
23352
  /* Key: value definition list for a single plain-object value. */
23308
- .cn-object-data-widget__deflist[data-v-76edf75d] {
23353
+ .cn-object-data-widget__deflist[data-v-324e1a00] {
23309
23354
  display: grid;
23310
23355
  grid-template-columns: auto 1fr;
23311
23356
  gap: 2px 8px;
23312
23357
  margin: 0;
23313
23358
  }
23314
- .cn-object-data-widget__deflist dt[data-v-76edf75d] {
23359
+ .cn-object-data-widget__deflist dt[data-v-324e1a00] {
23315
23360
  color: var(--color-text-maxcontrast);
23316
23361
  font-size: 0.9em;
23317
23362
  }
23318
- .cn-object-data-widget__deflist dd[data-v-76edf75d] {
23363
+ .cn-object-data-widget__deflist dd[data-v-324e1a00] {
23319
23364
  margin: 0;
23320
23365
  overflow-wrap: anywhere;
23321
23366
  }
23322
23367
 
23368
+ /* A long description keeps its own paragraphs. Set on an inline span with no
23369
+ surrounding template whitespace, so only the value's own line breaks show. */
23370
+ .cn-object-data-widget__deflist-text[data-v-324e1a00] {
23371
+ white-space: pre-wrap;
23372
+ }
23373
+ .cn-object-data-widget__deflist--nested[data-v-324e1a00] {
23374
+ padding-inline-start: 8px;
23375
+ border-inline-start: 2px solid var(--color-border);
23376
+ }
23377
+
23323
23378
  /* Responsive: collapse to single column on narrow widths */
23324
23379
  @media (max-width: 600px) {
23325
- .cn-object-data-widget__grid[data-v-76edf75d] {
23380
+ .cn-object-data-widget__grid[data-v-324e1a00] {
23326
23381
  grid-template-columns: 1fr !important;
23327
23382
  }
23328
- .cn-object-data-widget__cell[data-v-76edf75d] {
23383
+ .cn-object-data-widget__cell[data-v-324e1a00] {
23329
23384
  grid-column: span 1 !important;
23330
23385
  grid-row: span 1 !important;
23331
23386
  }
@@ -25216,18 +25271,18 @@ button.cn-stepper__step[data-v-776581d4] {
25216
25271
  }
25217
25272
 
25218
25273
 
25219
- .cn-week-strip[data-v-b6c5e274] {
25274
+ .cn-week-strip[data-v-27a717bd] {
25220
25275
  width: 100%;
25221
25276
  }
25222
25277
 
25223
25278
  /* `content.inset: true`: the board's inset inside the card (zuiddrecht-pixel-
25224
25279
  gaps-3). A dashboard renders its widgets flush, so without it the strip ran
25225
25280
  from card edge to card edge. Theme hook: --cn-widget-board-inset. */
25226
- .cn-week-strip--inset[data-v-b6c5e274] {
25281
+ .cn-week-strip--inset[data-v-27a717bd] {
25227
25282
  box-sizing: border-box;
25228
25283
  padding: var(--cn-widget-board-inset, 16px 24px 22px);
25229
25284
  }
25230
- .cn-week-strip__state[data-v-b6c5e274] {
25285
+ .cn-week-strip__state[data-v-27a717bd] {
25231
25286
  display: flex;
25232
25287
  align-items: center;
25233
25288
  gap: 8px;
@@ -25235,15 +25290,15 @@ button.cn-stepper__step[data-v-776581d4] {
25235
25290
  padding: 12px 4px;
25236
25291
  color: var(--color-text-maxcontrast);
25237
25292
  }
25238
- .cn-week-strip__scroll[data-v-b6c5e274] {
25293
+ .cn-week-strip__scroll[data-v-27a717bd] {
25239
25294
  overflow-x: auto;
25240
25295
  border-radius: var(--border-radius-large, 8px);
25241
25296
  }
25242
- .cn-week-strip__scroll[data-v-b6c5e274]:focus-visible {
25297
+ .cn-week-strip__scroll[data-v-27a717bd]:focus-visible {
25243
25298
  outline: 2px solid var(--color-primary-element);
25244
25299
  outline-offset: 2px;
25245
25300
  }
25246
- .cn-week-strip__days[data-v-b6c5e274] {
25301
+ .cn-week-strip__days[data-v-27a717bd] {
25247
25302
  display: grid;
25248
25303
  grid-template-columns: repeat(var(--cn-week-strip-columns, 5), minmax(128px, 1fr));
25249
25304
  gap: 10px;
@@ -25253,7 +25308,7 @@ button.cn-stepper__step[data-v-776581d4] {
25253
25308
  padding: 0;
25254
25309
  list-style: none;
25255
25310
  }
25256
- .cn-week-strip__day[data-v-b6c5e274] {
25311
+ .cn-week-strip__day[data-v-27a717bd] {
25257
25312
  display: flex;
25258
25313
  flex-direction: column;
25259
25314
  gap: 8px;
@@ -25263,11 +25318,11 @@ button.cn-stepper__step[data-v-776581d4] {
25263
25318
  border-radius: var(--border-radius-large, 8px);
25264
25319
  background: var(--color-background-hover);
25265
25320
  }
25266
- .cn-week-strip__day--today[data-v-b6c5e274] {
25321
+ .cn-week-strip__day--today[data-v-27a717bd] {
25267
25322
  border-color: var(--color-primary-element);
25268
25323
  background: var(--color-primary-element-light);
25269
25324
  }
25270
- .cn-week-strip__head[data-v-b6c5e274] {
25325
+ .cn-week-strip__head[data-v-27a717bd] {
25271
25326
  display: flex;
25272
25327
  align-items: center;
25273
25328
  justify-content: space-between;
@@ -25276,7 +25331,7 @@ button.cn-stepper__step[data-v-776581d4] {
25276
25331
  font-weight: 600;
25277
25332
  color: var(--color-main-text);
25278
25333
  }
25279
- .cn-week-strip__today[data-v-b6c5e274] {
25334
+ .cn-week-strip__today[data-v-27a717bd] {
25280
25335
  padding: 1px 8px;
25281
25336
  border-radius: var(--border-radius-pill, 10px);
25282
25337
  background: var(--color-primary-element);
@@ -25284,7 +25339,7 @@ button.cn-stepper__step[data-v-776581d4] {
25284
25339
  font-size: 0.85em;
25285
25340
  font-weight: 700;
25286
25341
  }
25287
- .cn-week-strip__items[data-v-b6c5e274] {
25342
+ .cn-week-strip__items[data-v-27a717bd] {
25288
25343
  display: flex;
25289
25344
  flex-direction: column;
25290
25345
  gap: 6px;
@@ -25292,7 +25347,7 @@ button.cn-stepper__step[data-v-776581d4] {
25292
25347
  padding: 0;
25293
25348
  list-style: none;
25294
25349
  }
25295
- .cn-week-strip__item[data-v-b6c5e274] {
25350
+ .cn-week-strip__item[data-v-27a717bd] {
25296
25351
  display: flex;
25297
25352
  flex-direction: column;
25298
25353
  gap: 4px;
@@ -25303,41 +25358,41 @@ button.cn-stepper__step[data-v-776581d4] {
25303
25358
  color: var(--color-main-text);
25304
25359
  text-decoration: none;
25305
25360
  }
25306
- .cn-week-strip__item--link[data-v-b6c5e274]:hover {
25361
+ .cn-week-strip__item--link[data-v-27a717bd]:hover {
25307
25362
  border-color: var(--color-primary-element);
25308
25363
  }
25309
- .cn-week-strip__item--link[data-v-b6c5e274]:focus-visible {
25364
+ .cn-week-strip__item--link[data-v-27a717bd]:focus-visible {
25310
25365
  outline: 2px solid var(--color-primary-element);
25311
25366
  outline-offset: 2px;
25312
25367
  }
25313
25368
 
25314
25369
  /* The edge is drawn inside the box, so a late item is as wide as the others. */
25315
- .cn-week-strip__item--late[data-v-b6c5e274] {
25370
+ .cn-week-strip__item--late[data-v-27a717bd] {
25316
25371
  border-color: var(--color-element-error, var(--color-error));
25317
25372
  box-shadow: inset 3px 0 0 0 var(--color-element-error, var(--color-error));
25318
25373
  }
25319
- .cn-week-strip__title[data-v-b6c5e274] {
25374
+ .cn-week-strip__title[data-v-27a717bd] {
25320
25375
  font-size: 0.95em;
25321
25376
  line-height: 1.3;
25322
25377
  overflow-wrap: anywhere;
25323
25378
  }
25324
- .cn-week-strip__meta[data-v-b6c5e274] {
25379
+ .cn-week-strip__meta[data-v-27a717bd] {
25325
25380
  font-size: 0.85em;
25326
25381
  color: var(--color-text-maxcontrast);
25327
25382
  overflow-wrap: anywhere;
25328
25383
  }
25329
- .cn-week-strip__late[data-v-b6c5e274] {
25384
+ .cn-week-strip__late[data-v-27a717bd] {
25330
25385
  font-size: 0.8em;
25331
25386
  font-weight: 600;
25332
25387
  color: var(--color-text-error, var(--color-error-text));
25333
25388
  }
25334
- .cn-week-strip__empty[data-v-b6c5e274] {
25389
+ .cn-week-strip__empty[data-v-27a717bd] {
25335
25390
  font-size: 0.9em;
25336
25391
  color: var(--color-text-maxcontrast);
25337
25392
  }
25338
25393
 
25339
25394
  /* "+N more" under a capped day: a quiet text control, 24px high to click. */
25340
- .cn-week-strip__more[data-v-b6c5e274] {
25395
+ .cn-week-strip__more[data-v-27a717bd] {
25341
25396
  align-self: flex-start;
25342
25397
  min-height: 24px;
25343
25398
  margin: 0;
@@ -25352,10 +25407,10 @@ button.cn-stepper__step[data-v-776581d4] {
25352
25407
  text-decoration: none;
25353
25408
  cursor: pointer;
25354
25409
  }
25355
- .cn-week-strip__more[data-v-b6c5e274]:hover {
25410
+ .cn-week-strip__more[data-v-27a717bd]:hover {
25356
25411
  text-decoration: underline;
25357
25412
  }
25358
- .cn-week-strip__more[data-v-b6c5e274]:focus-visible {
25413
+ .cn-week-strip__more[data-v-27a717bd]:focus-visible {
25359
25414
  outline: 2px solid var(--color-primary-element);
25360
25415
  outline-offset: 2px;
25361
25416
  }
@@ -37533,17 +37588,17 @@ button.cn-object-calendar__overflow[data-v-2735e5ef] {
37533
37588
  }
37534
37589
 
37535
37590
 
37536
- .cn-dashboard-grid[data-v-ca2ae49c] {
37591
+ .cn-dashboard-grid[data-v-a91998fc] {
37537
37592
  width: 100%;
37538
37593
  min-height: 200px;
37539
37594
  }
37540
- .grid-stack[data-v-ca2ae49c] {
37595
+ .grid-stack[data-v-a91998fc] {
37541
37596
  background: transparent;
37542
37597
  }
37543
37598
 
37544
37599
  /* Screen-reader-only text: the keyboard key map and the live region. Kept in
37545
37600
  the layout (not `display: none`) so assistive tech still reads it. */
37546
- .cn-dashboard-grid__sr-only[data-v-ca2ae49c] {
37601
+ .cn-dashboard-grid__sr-only[data-v-a91998fc] {
37547
37602
  position: absolute;
37548
37603
  width: 1px;
37549
37604
  height: 1px;
@@ -37558,7 +37613,7 @@ button.cn-object-calendar__overflow[data-v-2735e5ef] {
37558
37613
  /* Grid items become tab stops in edit mode — give them a visible focus ring
37559
37614
  that satisfies WCAG 2.1 SC 2.4.7 without relying on the widget's own
37560
37615
  styling. `outline-offset` keeps the ring outside the rounded card. */
37561
- .grid-stack-item[data-v-ca2ae49c]:focus-visible {
37616
+ .grid-stack-item[data-v-a91998fc]:focus-visible {
37562
37617
  outline: 2px solid var(--color-primary-element, #0082c9);
37563
37618
  outline-offset: 2px;
37564
37619
  border-radius: var(--border-radius-container-large, 16px);
@@ -37570,19 +37625,19 @@ button.cn-object-calendar__overflow[data-v-2735e5ef] {
37570
37625
  cell never shrank: "Waiting for me" stayed a tall card around one row. In
37571
37626
  such a cell the child takes its own height. A cell without the key keeps
37572
37627
  the filling child. */
37573
- .cn-dashboard-grid__item--fit[data-v-ca2ae49c] > .grid-stack-item-content > * {
37628
+ .cn-dashboard-grid__item--fit[data-v-a91998fc] > .grid-stack-item-content > * {
37574
37629
  height: auto;
37575
37630
  }
37576
- [data-v-ca2ae49c] .grid-stack-item-content {
37631
+ [data-v-a91998fc] .grid-stack-item-content {
37577
37632
  background: var(--color-main-background);
37578
37633
  border: none;
37579
37634
  box-shadow: none;
37580
37635
  overflow: hidden;
37581
37636
  }
37582
- [data-v-ca2ae49c] .grid-stack-item-content:has(.cn-widget-wrapper--borderless) {
37637
+ [data-v-a91998fc] .grid-stack-item-content:has(.cn-widget-wrapper--borderless) {
37583
37638
  background: transparent;
37584
37639
  }
37585
- [data-v-ca2ae49c] .grid-stack-placeholder > .placeholder-content {
37640
+ [data-v-a91998fc] .grid-stack-placeholder > .placeholder-content {
37586
37641
  background: var(--color-primary-element-light);
37587
37642
  border: 2px dashed var(--color-primary-element);
37588
37643
  border-radius: var(--border-radius-large);
@@ -55,5 +55,44 @@ function readGridGeometry(items) {
55
55
  return map
56
56
  }
57
57
 
58
- export { initGridStack, readGridGeometry };
58
+ /**
59
+ * Where the items under a growing cell go so they keep their order: every item
60
+ * the growth would overlap moves down by the growth, and so does every item
61
+ * that move would overlap in turn. Items beside the cell stay put.
62
+ *
63
+ * GridStack resolves the same collision itself, but not in order: when
64
+ * pipelinq's size-to-content banner grew from two rows to three, the
65
+ * full-width card under the stat tiles landed below the lists.
66
+ *
67
+ * @spec openspec/changes/dashboard-growth-keeps-order/specs/dashboard-grid/spec.md#requirement-a-growing-cell-keeps-the-order-of-the-cards-under-it
68
+ * @param {Array<{x: number, y: number, w: number, h: number}>} nodes The grid's items (the growing one among them).
69
+ * @param {{x: number, y: number, w: number, h: number}} node The growing item.
70
+ * @param {number} h Its new height, in grid rows.
71
+ * @return {Array<{node: object, y: number}>} The items to move and their new row, bottom-most first.
72
+ */
73
+ function planGrowth(nodes, node, h) {
74
+ const delta = h - node.h;
75
+ if (!(delta > 0)) {
76
+ return []
77
+ }
78
+ const moved = new Map();
79
+ const overlaps = (a, b) => a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h;
80
+ const queue = [{ x: node.x, y: node.y, w: node.w, h }];
81
+ while (queue.length > 0) {
82
+ const rect = queue.shift();
83
+ for (const other of nodes) {
84
+ if (other === node || moved.has(other) || !overlaps(rect, other)) {
85
+ continue
86
+ }
87
+ const next = { x: other.x, y: other.y + delta, w: other.w, h: other.h };
88
+ moved.set(other, next.y);
89
+ queue.push(next);
90
+ }
91
+ }
92
+ return [...moved.entries()]
93
+ .map(([item, y]) => ({ node: item, y }))
94
+ .sort((a, b) => b.y - a.y)
95
+ }
96
+
97
+ export { initGridStack, planGrowth, readGridGeometry };
59
98
  //# sourceMappingURL=gridStack.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"gridStack.js","sources":["../../../src/utils/gridStack.js"],"sourcesContent":["/**\n * Thin GridStack bootstrap shared by the drag/resize grids (CnDashboardGrid and\n * CnWidgetGrid's editable body slot). Keeps the GridStack option shape in one\n * place so the two engines stay consistent.\n *\n * @module utils/gridStack\n */\n\nimport { GridStack } from 'gridstack'\n// `gridstack` is a peerDependency, not bundled — see the matching comment in\n// CnDashboardGrid.vue. The consumer must install `gridstack` themselves and\n// import its stylesheet (`gridstack/dist/gridstack.min.css`) from that same\n// copy; nc-vue ships no GridStack CSS of its own so the JS driving the grid\n// and the CSS sizing it can never come from two different versions.\n\n/**\n * Initialise a GridStack instance on a `.grid-stack` element.\n *\n * @param {HTMLElement} el The `.grid-stack` container element.\n * @param {object} opts Options.\n * @param {number} opts.columns Column count.\n * @param {boolean} opts.editable Whether drag + resize are enabled.\n * @param {number} [opts.cellHeight] Cell height in px (default 80).\n * @param {number} [opts.margin] Grid margin in px (default 12).\n * @return {import('gridstack').GridStack} The GridStack instance.\n */\nexport function initGridStack(el, opts) {\n\tconst { columns, editable, cellHeight = 80, margin = 12 } = opts\n\treturn GridStack.init({\n\t\tcolumn: columns,\n\t\tcellHeight,\n\t\tmargin,\n\t\tfloat: true,\n\t\tanimate: true,\n\t\tdisableDrag: !editable,\n\t\tdisableResize: !editable,\n\t\tremovable: false,\n\t}, el)\n}\n\n/**\n * Read the geometry GridStack reports for a changed set of items into a plain\n * map keyed by node id.\n *\n * @param {Array<{id: (string|number), x: number, y: number, w: number, h: number}>} items\n * The items GridStack passes to its `change` handler.\n * @return {Map<string, {gridX: number, gridY: number, gridWidth: number, gridHeight: number}>}\n */\nexport function readGridGeometry(items) {\n\tconst map = new Map()\n\tfor (const it of items || []) {\n\t\tmap.set(String(it.id), { gridX: it.x, gridY: it.y, gridWidth: it.w, gridHeight: it.h })\n\t}\n\treturn map\n}\n"],"names":[],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,aAAa,CAAC,EAAE,EAAE,IAAI,EAAE;AACxC,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,GAAG,EAAE,EAAE,MAAM,GAAG,EAAE,EAAE,GAAG,KAAI;AACjE,CAAC,OAAO,SAAS,CAAC,IAAI,CAAC;AACvB,EAAE,MAAM,EAAE,OAAO;AACjB,EAAE,UAAU;AACZ,EAAE,MAAM;AACR,EAAE,KAAK,EAAE,IAAI;AACb,EAAE,OAAO,EAAE,IAAI;AACf,EAAE,WAAW,EAAE,CAAC,QAAQ;AACxB,EAAE,aAAa,EAAE,CAAC,QAAQ;AAC1B,EAAE,SAAS,EAAE,KAAK;AAClB,EAAE,EAAE,EAAE,CAAC;AACP,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,gBAAgB,CAAC,KAAK,EAAE;AACxC,CAAC,MAAM,GAAG,GAAG,IAAI,GAAG,GAAE;AACtB,CAAC,KAAK,MAAM,EAAE,IAAI,KAAK,IAAI,EAAE,EAAE;AAC/B,EAAE,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,EAAE,CAAC,CAAC,EAAE,EAAC;AACzF,EAAE;AACF,CAAC,OAAO,GAAG;AACX;;;;"}
1
+ {"version":3,"file":"gridStack.js","sources":["../../../src/utils/gridStack.js"],"sourcesContent":["/**\n * Thin GridStack bootstrap shared by the drag/resize grids (CnDashboardGrid and\n * CnWidgetGrid's editable body slot). Keeps the GridStack option shape in one\n * place so the two engines stay consistent.\n *\n * @module utils/gridStack\n */\n\nimport { GridStack } from 'gridstack'\n// `gridstack` is a peerDependency, not bundled — see the matching comment in\n// CnDashboardGrid.vue. The consumer must install `gridstack` themselves and\n// import its stylesheet (`gridstack/dist/gridstack.min.css`) from that same\n// copy; nc-vue ships no GridStack CSS of its own so the JS driving the grid\n// and the CSS sizing it can never come from two different versions.\n\n/**\n * Initialise a GridStack instance on a `.grid-stack` element.\n *\n * @param {HTMLElement} el The `.grid-stack` container element.\n * @param {object} opts Options.\n * @param {number} opts.columns Column count.\n * @param {boolean} opts.editable Whether drag + resize are enabled.\n * @param {number} [opts.cellHeight] Cell height in px (default 80).\n * @param {number} [opts.margin] Grid margin in px (default 12).\n * @return {import('gridstack').GridStack} The GridStack instance.\n */\nexport function initGridStack(el, opts) {\n\tconst { columns, editable, cellHeight = 80, margin = 12 } = opts\n\treturn GridStack.init({\n\t\tcolumn: columns,\n\t\tcellHeight,\n\t\tmargin,\n\t\tfloat: true,\n\t\tanimate: true,\n\t\tdisableDrag: !editable,\n\t\tdisableResize: !editable,\n\t\tremovable: false,\n\t}, el)\n}\n\n/**\n * Read the geometry GridStack reports for a changed set of items into a plain\n * map keyed by node id.\n *\n * @param {Array<{id: (string|number), x: number, y: number, w: number, h: number}>} items\n * The items GridStack passes to its `change` handler.\n * @return {Map<string, {gridX: number, gridY: number, gridWidth: number, gridHeight: number}>}\n */\nexport function readGridGeometry(items) {\n\tconst map = new Map()\n\tfor (const it of items || []) {\n\t\tmap.set(String(it.id), { gridX: it.x, gridY: it.y, gridWidth: it.w, gridHeight: it.h })\n\t}\n\treturn map\n}\n\n/**\n * Where the items under a growing cell go so they keep their order: every item\n * the growth would overlap moves down by the growth, and so does every item\n * that move would overlap in turn. Items beside the cell stay put.\n *\n * GridStack resolves the same collision itself, but not in order: when\n * pipelinq's size-to-content banner grew from two rows to three, the\n * full-width card under the stat tiles landed below the lists.\n *\n * @spec openspec/changes/dashboard-growth-keeps-order/specs/dashboard-grid/spec.md#requirement-a-growing-cell-keeps-the-order-of-the-cards-under-it\n * @param {Array<{x: number, y: number, w: number, h: number}>} nodes The grid's items (the growing one among them).\n * @param {{x: number, y: number, w: number, h: number}} node The growing item.\n * @param {number} h Its new height, in grid rows.\n * @return {Array<{node: object, y: number}>} The items to move and their new row, bottom-most first.\n */\nexport function planGrowth(nodes, node, h) {\n\tconst delta = h - node.h\n\tif (!(delta > 0)) {\n\t\treturn []\n\t}\n\tconst moved = new Map()\n\tconst overlaps = (a, b) => a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h\n\tconst queue = [{ x: node.x, y: node.y, w: node.w, h }]\n\twhile (queue.length > 0) {\n\t\tconst rect = queue.shift()\n\t\tfor (const other of nodes) {\n\t\t\tif (other === node || moved.has(other) || !overlaps(rect, other)) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tconst next = { x: other.x, y: other.y + delta, w: other.w, h: other.h }\n\t\t\tmoved.set(other, next.y)\n\t\t\tqueue.push(next)\n\t\t}\n\t}\n\treturn [...moved.entries()]\n\t\t.map(([item, y]) => ({ node: item, y }))\n\t\t.sort((a, b) => b.y - a.y)\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * Make an identifier-shaped key readable for a person. Pure: no Vue.
6
+ */
7
+
8
+ /**
9
+ * Split camelCase, snake_case and kebab-case into words, keep acronyms, and
10
+ * capitalise only the first word.
11
+ *
12
+ * `caseAssigned` → "Case assigned", `object_created` → "Object created",
13
+ * `newBRPRecord` → "New BRP record".
14
+ *
15
+ * @param {string} key The key.
16
+ * @return {string} A readable sentence-case label, or the key when it has no words.
17
+ * @spec openspec/changes/data-widget-nested-objects/specs/data-widget-nested-objects/spec.md#requirement-a-nested-object-reads-as-a-list-per-key
18
+ */
19
+ function humaniseKey(key) {
20
+ const words = String(key || '')
21
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
22
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
23
+ .split(/[\s_\-.]+/)
24
+ .filter(Boolean);
25
+ if (words.length === 0) {
26
+ return String(key || '')
27
+ }
28
+ return words
29
+ .map((word, index) => {
30
+ const isAcronym = word.length > 1 && word === word.toUpperCase() && /[A-Z]/.test(word);
31
+ if (isAcronym) {
32
+ return word
33
+ }
34
+ const lower = word.toLowerCase();
35
+ return index === 0 ? lower.charAt(0).toUpperCase() + lower.slice(1) : lower
36
+ })
37
+ .join(' ')
38
+ }
39
+
40
+ export { humaniseKey };
41
+ //# sourceMappingURL=humaniseKey.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"humaniseKey.js","sources":["../../../src/utils/humaniseKey.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n * SPDX-License-Identifier: EUPL-1.2\n *\n * Make an identifier-shaped key readable for a person. Pure: no Vue.\n */\n\n/**\n * Split camelCase, snake_case and kebab-case into words, keep acronyms, and\n * capitalise only the first word.\n *\n * `caseAssigned` → \"Case assigned\", `object_created` → \"Object created\",\n * `newBRPRecord` → \"New BRP record\".\n *\n * @param {string} key The key.\n * @return {string} A readable sentence-case label, or the key when it has no words.\n * @spec openspec/changes/data-widget-nested-objects/specs/data-widget-nested-objects/spec.md#requirement-a-nested-object-reads-as-a-list-per-key\n */\nexport function humaniseKey(key) {\n\tconst words = String(key || '')\n\t\t.replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n\t\t.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')\n\t\t.split(/[\\s_\\-.]+/)\n\t\t.filter(Boolean)\n\tif (words.length === 0) {\n\t\treturn String(key || '')\n\t}\n\treturn words\n\t\t.map((word, index) => {\n\t\t\tconst isAcronym = word.length > 1 && word === word.toUpperCase() && /[A-Z]/.test(word)\n\t\t\tif (isAcronym) {\n\t\t\t\treturn word\n\t\t\t}\n\t\t\tconst lower = word.toLowerCase()\n\t\t\treturn index === 0 ? lower.charAt(0).toUpperCase() + lower.slice(1) : lower\n\t\t})\n\t\t.join(' ')\n}\n"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,WAAW,CAAC,GAAG,EAAE;AACjC,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC;AAChC,GAAG,OAAO,CAAC,oBAAoB,EAAE,OAAO,CAAC;AACzC,GAAG,OAAO,CAAC,uBAAuB,EAAE,OAAO,CAAC;AAC5C,GAAG,KAAK,CAAC,WAAW,CAAC;AACrB,GAAG,MAAM,CAAC,OAAO,EAAC;AAClB,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE;AACzB,EAAE,OAAO,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC;AAC1B,EAAE;AACF,CAAC,OAAO,KAAK;AACb,GAAG,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,KAAK;AACxB,GAAG,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,KAAK,IAAI,CAAC,WAAW,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,EAAC;AACzF,GAAG,IAAI,SAAS,EAAE;AAClB,IAAI,OAAO,IAAI;AACf,IAAI;AACJ,GAAG,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,GAAE;AACnC,GAAG,OAAO,KAAK,KAAK,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,KAAK;AAC9E,GAAG,CAAC;AACJ,GAAG,IAAI,CAAC,GAAG,CAAC;AACZ;;;;"}
@@ -1,3 +1,5 @@
1
+ import { humaniseKey } from './humaniseKey.js';
2
+
1
3
  /**
2
4
  * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>
3
5
  * SPDX-License-Identifier: EUPL-1.2
@@ -28,6 +30,7 @@
28
30
  * @spec openspec/changes/notification-rule-labels-and-runtime-version/specs/notification-preferences/spec.md
29
31
  */
30
32
 
33
+
31
34
  /**
32
35
  * Pick the string for the current language out of a string or per-locale map.
33
36
  *
@@ -54,34 +57,14 @@ function resolveLocalised(value, language = 'en') {
54
57
  }
55
58
 
56
59
  /**
57
- * Make a rule key readable: split camelCase, snake_case and kebab-case into
58
- * words, keep acronyms, and capitalise only the first word.
59
- *
60
- * `caseAssigned` → "Case assigned", `object_created` → "Object created",
61
- * `newBRPRecord` → "New BRP record".
60
+ * Make a rule key readable (`caseAssigned` → "Case assigned"); see `humaniseKey`.
62
61
  *
63
62
  * @param {string} key The rule key.
64
63
  * @return {string} A readable sentence-case label, or the key when it has no words.
64
+ * @spec openspec/changes/notification-rule-labels-and-runtime-version/specs/notification-preferences/spec.md#requirement-a-notification-rule-reads-as-a-label
65
65
  */
66
66
  function humaniseRuleKey(key) {
67
- const words = String(key || '')
68
- .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
69
- .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
70
- .split(/[\s_\-.]+/)
71
- .filter(Boolean);
72
- if (words.length === 0) {
73
- return String(key || '')
74
- }
75
- return words
76
- .map((word, index) => {
77
- const isAcronym = word.length > 1 && word === word.toUpperCase() && /[A-Z]/.test(word);
78
- if (isAcronym) {
79
- return word
80
- }
81
- const lower = word.toLowerCase();
82
- return index === 0 ? lower.charAt(0).toUpperCase() + lower.slice(1) : lower
83
- })
84
- .join(' ')
67
+ return humaniseKey(key)
85
68
  }
86
69
 
87
70
  /**