@conduction/nextcloud-vue 2.91.0 → 2.93.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +57 -4
  2. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +44 -17
  4. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  5. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue.js +1 -1
  6. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js +44 -2
  7. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
  8. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +140 -59
  9. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
  10. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  11. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +20 -6
  12. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  13. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +3 -2
  14. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  15. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +10 -39
  16. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  17. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  18. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +5 -3
  19. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  20. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  21. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  22. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +6 -4
  23. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  24. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  25. package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue2.js +46 -0
  26. package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue2.js.map +1 -1
  27. package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue3.js +6 -2
  28. package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue3.js.map +1 -1
  29. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue.js +1 -1
  30. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js +82 -9
  31. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js.map +1 -1
  32. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js +104 -90
  33. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js.map +1 -1
  34. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue.js +9 -0
  35. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue.js.map +1 -0
  36. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js +355 -0
  37. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js.map +1 -0
  38. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js +462 -0
  39. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js.map +1 -0
  40. package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
  41. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +39 -10
  42. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  43. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +418 -399
  44. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  45. package/dist/esm/components/CnIcon/semanticIcons.js +2 -2
  46. package/dist/esm/components/CnIcon/semanticIcons.js.map +1 -1
  47. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue.js +1 -1
  48. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js +156 -3
  49. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js.map +1 -1
  50. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js +168 -52
  51. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js.map +1 -1
  52. package/dist/esm/components/CnModuleCard/CnModuleCard.vue.js +9 -0
  53. package/dist/esm/components/CnModuleCard/CnModuleCard.vue.js.map +1 -0
  54. package/dist/esm/components/CnModuleCard/CnModuleCard.vue2.js +83 -0
  55. package/dist/esm/components/CnModuleCard/CnModuleCard.vue2.js.map +1 -0
  56. package/dist/esm/components/CnModuleCard/CnModuleCard.vue3.js +119 -0
  57. package/dist/esm/components/CnModuleCard/CnModuleCard.vue3.js.map +1 -0
  58. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue.js +1 -1
  59. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js +22 -1
  60. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js.map +1 -1
  61. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js +20 -14
  62. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js.map +1 -1
  63. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue.js +1 -1
  64. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue2.js +2 -0
  65. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue2.js.map +1 -1
  66. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue3.js +15 -5
  67. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue3.js.map +1 -1
  68. package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue.js +1 -1
  69. package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue2.js +8 -30
  70. package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue2.js.map +1 -1
  71. package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue3.js.map +1 -1
  72. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  73. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +33 -2
  74. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  75. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  76. package/dist/esm/components/CnTextWidget/CnTextWidget.vue.js +1 -1
  77. package/dist/esm/components/CnTextWidget/CnTextWidget.vue2.js +63 -4
  78. package/dist/esm/components/CnTextWidget/CnTextWidget.vue2.js.map +1 -1
  79. package/dist/esm/components/CnTextWidget/CnTextWidget.vue3.js +19 -2
  80. package/dist/esm/components/CnTextWidget/CnTextWidget.vue3.js.map +1 -1
  81. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js +9 -0
  82. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js.map +1 -0
  83. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js +164 -0
  84. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js.map +1 -0
  85. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js +93 -0
  86. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js.map +1 -0
  87. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue.js +1 -1
  88. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +12 -0
  89. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
  90. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
  91. package/dist/esm/l10n/en.json.js +45 -1
  92. package/dist/esm/l10n/en.json.js.map +1 -1
  93. package/dist/esm/l10n/nl.json.js +44 -1
  94. package/dist/esm/l10n/nl.json.js.map +1 -1
  95. package/dist/esm/library-style.js +1 -1
  96. package/dist/esm/nextcloud-vue.css +1182 -328
  97. package/dist/esm/utils/boardDate.js +35 -1
  98. package/dist/esm/utils/boardDate.js.map +1 -1
  99. package/dist/esm/utils/featureCatalog.js +135 -0
  100. package/dist/esm/utils/featureCatalog.js.map +1 -0
  101. package/dist/esm/utils/fillTemplate.js +84 -4
  102. package/dist/esm/utils/fillTemplate.js.map +1 -1
  103. package/dist/esm/utils/headerMeta.js +62 -12
  104. package/dist/esm/utils/headerMeta.js.map +1 -1
  105. package/dist/esm/utils/roadmapIssues.js +86 -0
  106. package/dist/esm/utils/roadmapIssues.js.map +1 -0
  107. package/dist/esm/utils/userDisplayName.js +47 -0
  108. package/dist/esm/utils/userDisplayName.js.map +1 -0
  109. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  110. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  111. package/dist/nextcloud-vue.cjs.js +15618 -13097
  112. package/dist/nextcloud-vue.cjs.js.map +2 -2
  113. package/dist/nextcloud-vue.css +1182 -328
  114. package/l10n/en.json +45 -1
  115. package/l10n/nl.json +44 -1
  116. package/package.json +1 -1
  117. package/src/components/CnAppRoot/CnAppRoot.md +2 -0
  118. package/src/components/CnAppRoot/CnAppRoot.vue +66 -5
  119. package/src/components/CnChoiceCards/CnChoiceCards.md +27 -0
  120. package/src/components/CnChoiceCards/CnChoiceCards.vue +168 -3
  121. package/src/components/CnDashboardPage/CnDashboardPage.vue +45 -6
  122. package/src/components/CnDataTable/CnDataTable.vue +10 -39
  123. package/src/components/CnDetailPage/CnDetailPage.vue +5 -3
  124. package/src/components/CnFactsWidget/CnFactsWidget.vue +6 -4
  125. package/src/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue +51 -1
  126. package/src/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue +98 -13
  127. package/src/components/CnFeaturesBoard/CnFeaturesBoard.vue +1053 -0
  128. package/src/components/CnFormDialog/CnFormDialog.md +1 -0
  129. package/src/components/CnFormDialog/CnFormDialog.vue +619 -562
  130. package/src/components/CnLinkCardsPage/CnLinkCardsPage.vue +338 -5
  131. package/src/components/CnModuleCard/CnModuleCard.vue +258 -0
  132. package/src/components/CnNcWidgetWidget/CnNcWidgetWidget.vue +23 -2
  133. package/src/components/CnNotificationPreferences/CnNotificationPreferences.vue +5 -0
  134. package/src/components/CnRoadmapTab/CnRoadmapTab.vue +6 -28
  135. package/src/components/CnTabsWidget/CnTabsWidget.vue +33 -2
  136. package/src/components/CnTextWidget/CnTextWidget.vue +89 -4
  137. package/src/components/CnUserSettingsDialog/CnUserSettingsDialog.vue +305 -0
  138. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +12 -0
  139. package/src/css/dialog.css +47 -0
  140. package/src/css/look-board-detail.css +8 -0
  141. package/src/css/look-board.css +33 -0
  142. package/src/schemas/app-manifest-v2.schema.json +67 -4
  143. package/src/utils/boardDate.js +34 -0
  144. package/src/utils/featureCatalog.js +132 -0
  145. package/src/utils/fillTemplate.js +83 -3
  146. package/src/utils/headerMeta.js +62 -12
  147. package/src/utils/roadmapIssues.js +82 -0
  148. package/src/utils/userDisplayName.js +54 -0
  149. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -89,6 +89,40 @@ function formatBoardDate(value, now = new Date(), locale = boardDateLocale()) {
89
89
  }
90
90
  }
91
91
 
92
+ /**
93
+ * A date with its short weekday, the way an agenda writes it: "wo 7 okt", or
94
+ * "wo 14 feb 2024" outside the current year. With `today` set, today's date
95
+ * reads "Vandaag, wo 7 okt".
96
+ *
97
+ * @param {unknown} value A Date, a timestamp or an ISO string.
98
+ * @param {Date} [now] The reference moment (defaults to the current time).
99
+ * @param {object} [options] Options.
100
+ * @param {boolean} [options.today] Put "Today, " before today's date.
101
+ * @param {string} [options.locale] BCP 47 tag (defaults to `boardDateLocale()`).
102
+ * @return {string} The date, or '' when the value is not a date.
103
+ * @spec openspec/changes/screens-cell-date-parity/specs/cell-date-age/spec.md#requirement-a-board-date-cell-reads-day-and-short-month-in-the-user-language
104
+ */
105
+ function formatBoardWeekday(value, now = new Date(), options = {}) {
106
+ const date = parseDateValue(value);
107
+ if (date === null) {
108
+ return ''
109
+ }
110
+ const parts = { weekday: 'short', day: 'numeric', month: 'short' };
111
+ if (date.getFullYear() !== now.getFullYear()) {
112
+ parts.year = 'numeric';
113
+ }
114
+ let text;
115
+ try {
116
+ text = new Intl.DateTimeFormat(options.locale || boardDateLocale(), parts).format(date);
117
+ } catch {
118
+ text = new Intl.DateTimeFormat(undefined, parts).format(date);
119
+ }
120
+ if (options.today === true && ageInDays(date, now) === 0) {
121
+ return translate('nextcloud-vue', 'Today, {date}', { date: text })
122
+ }
123
+ return text
124
+ }
125
+
92
126
  /**
93
127
  * Whole calendar days from the date until `now` (0 today, 1 yesterday), or
94
128
  * null when the value is not a date. A future date gives a negative number.
@@ -237,5 +271,5 @@ function resolveAgeVariant(value, rules, now = new Date()) {
237
271
  return ''
238
272
  }
239
273
 
240
- export { AGE_VARIANTS, ageInDays, boardDateLocale, formatAge, formatBoardDate, formatRelativeDate, formatTimeOfDay, resolveAgeVariant, shownAgeDays };
274
+ export { AGE_VARIANTS, ageInDays, boardDateLocale, formatAge, formatBoardDate, formatBoardWeekday, formatRelativeDate, formatTimeOfDay, resolveAgeVariant, shownAgeDays };
241
275
  //# sourceMappingURL=boardDate.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"boardDate.js","sources":["../../../src/utils/boardDate.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * boardDate: the two short time readings the drawn boards put in a table cell.\n *\n * - A date (\"5 okt\", \"30 okt\", \"14 feb 2024\"): day and short month in the\n * user's language, the year only when it is not the current year.\n * - An age (\"4 uur\", \"1 dag\", \"3 dagen\"): how long something has been\n * waiting, in hours under a day and in whole calendar days after that.\n *\n * Both read the Nextcloud user LANGUAGE for the month and unit names. A\n * Nextcloud account keeps its locale (date order) apart from its language,\n * and a Dutch reader whose locale was left at the en_US default saw\n * \"Feb 14, 2024\": the language decides the words, so it decides here, and the\n * locale is only used when it is a region of that same language.\n *\n * @module utils/boardDate\n * @spec openspec/changes/screens-cell-date-parity/specs/cell-date-age/spec.md\n */\n\nimport { getCanonicalLocale, getLanguage, translatePlural as n, translate as t } from '@nextcloud/l10n'\nimport { parseDateValue } from './dateVariant.js'\nimport { compareVisibleWhen } from './visibleWhen.js'\n\nconst MS_PER_HOUR = 3600000\nconst MS_PER_DAY = 86400000\n\n/** Variants an age rule may name. `danger` is accepted as an alias of `error`. */\nexport const AGE_VARIANTS = Object.freeze(['default', 'success', 'warning', 'error'])\n\n/**\n * Read a language or locale tag from an @nextcloud/l10n getter, which throws\n * when Nextcloud has not set one (outside a Nextcloud page, in a test).\n *\n * @param {() => string} getter `getLanguage` or `getCanonicalLocale`.\n * @return {string} The tag with `-` separators, or ''.\n */\nfunction readTag(getter) {\n\ttry {\n\t\treturn String(getter() || '').replace(/_/g, '-')\n\t} catch {\n\t\treturn ''\n\t}\n}\n\n/**\n * The BCP 47 tag the board dates are written in: the user's locale when it is\n * a region of the user's language (`nl-BE` for language `nl`), else the\n * language itself.\n *\n * @return {string|undefined} The tag, or undefined to let Intl choose.\n * @spec openspec/changes/screens-cell-date-parity/specs/cell-date-age/spec.md#requirement-a-board-date-cell-reads-day-and-short-month-in-the-user-language\n */\nexport function boardDateLocale() {\n\tconst language = readTag(getLanguage)\n\tconst locale = readTag(getCanonicalLocale)\n\tconst base = (tag) => tag.split('-')[0].toLowerCase()\n\tif (language && locale && base(language) === base(locale)) {\n\t\treturn locale\n\t}\n\treturn language || locale || undefined\n}\n\n/**\n * A date in the board's short form: \"5 okt\", or \"14 feb 2024\" outside the\n * current year.\n *\n * @param {unknown} value A Date, a timestamp or an ISO string.\n * @param {Date} [now] The reference moment (defaults to the current time).\n * @param {string} [locale] BCP 47 tag (defaults to `boardDateLocale()`).\n * @return {string} The short date, or '' when the value is not a date.\n * @spec openspec/changes/screens-cell-date-parity/specs/cell-date-age/spec.md#requirement-a-board-date-cell-reads-day-and-short-month-in-the-user-language\n */\nexport function formatBoardDate(value, now = new Date(), locale = boardDateLocale()) {\n\tconst date = parseDateValue(value)\n\tif (date === null) {\n\t\treturn ''\n\t}\n\tconst options = { day: 'numeric', month: 'short' }\n\tif (date.getFullYear() !== now.getFullYear()) {\n\t\toptions.year = 'numeric'\n\t}\n\ttry {\n\t\treturn new Intl.DateTimeFormat(locale, options).format(date)\n\t} catch {\n\t\treturn new Intl.DateTimeFormat(undefined, options).format(date)\n\t}\n}\n\n/**\n * Whole calendar days from the date until `now` (0 today, 1 yesterday), or\n * null when the value is not a date. A future date gives a negative number.\n *\n * @param {unknown} value The date.\n * @param {Date} [now] The reference moment.\n * @return {number|null} The day count.\n * @spec openspec/changes/screens-cell-date-parity/specs/cell-date-age/spec.md#requirement-an-age-cell-says-how-long-something-has-waited\n */\nexport function ageInDays(value, now = new Date()) {\n\tconst date = parseDateValue(value)\n\tif (date === null) {\n\t\treturn null\n\t}\n\t// UTC midnights of the two LOCAL days, so a daylight-saving hour never\n\t// turns one day into zero or two.\n\tconst start = Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())\n\tconst today = Date.UTC(now.getFullYear(), now.getMonth(), now.getDate())\n\treturn Math.round((today - start) / MS_PER_DAY)\n}\n\n/**\n * The time of day the boards write: \"09.42\" for a Dutch reader, \"09:42\" for\n * an English one. The separator belongs to the language, so it is one\n * translatable string with two placeholders.\n *\n * @param {Date} date The moment.\n * @return {string} The time of day.\n * @spec openspec/changes/screens-relative-dates/specs/relative-dates/spec.md#requirement-a-relative-date-reads-today-yesterday-then-a-short-date\n */\nexport function formatTimeOfDay(date) {\n\tconst pad = (v) => String(v).padStart(2, '0')\n\treturn t('nextcloud-vue', '{hours}:{minutes}', { hours: pad(date.getHours()), minutes: pad(date.getMinutes()) })\n}\n\n/**\n * A moment the way a list says when something came in: \"today\" or\n * \"yesterday\" (with the time of day on request: \"vandaag, 09.42\"), and from\n * the day before that the board's short date (\"5 okt\", \"14 feb 2024\").\n * Tomorrow reads \"tomorrow\", for a due date. Word and month names follow the\n * user's language through the library's catalogue.\n *\n * @param {unknown} value A Date, a timestamp or an ISO string.\n * @param {object} [options] Options.\n * @param {boolean} [options.withTime] Add the time of day after the day: \"today, 09.42\".\n * @param {Date} [options.now] The reference moment (defaults to the current time).\n * @return {string} The text, or '' when the value is not a date.\n * @spec openspec/changes/screens-relative-dates/specs/relative-dates/spec.md#requirement-a-relative-date-reads-today-yesterday-then-a-short-date\n */\nexport function formatRelativeDate(value, options = {}) {\n\tconst now = options.now || new Date()\n\tconst date = parseDateValue(value)\n\tif (date === null) {\n\t\treturn ''\n\t}\n\tconst days = ageInDays(date, now)\n\tlet day\n\tif (days === 0) {\n\t\tday = t('nextcloud-vue', 'today')\n\t} else if (days === 1) {\n\t\tday = t('nextcloud-vue', 'yesterday')\n\t} else if (days === -1) {\n\t\tday = t('nextcloud-vue', 'tomorrow')\n\t} else {\n\t\tday = formatBoardDate(date, now)\n\t}\n\treturn options.withTime === true ? `${day}, ${formatTimeOfDay(date)}` : day\n}\n\n/**\n * How long something has waited: \"4 uur\" under 24 hours (at least 1), else\n * \"1 dag\" / \"3 dagen\" in whole calendar days. Plural forms come from the\n * library's own catalogue (`nextcloud-vue`). A date in the future reads as\n * waiting no time: \"0 hours\".\n *\n * @param {unknown} value The moment the wait started.\n * @param {Date} [now] The reference moment.\n * @return {string} The age, or '' when the value is not a date.\n * @spec openspec/changes/screens-cell-date-parity/specs/cell-date-age/spec.md#requirement-an-age-cell-says-how-long-something-has-waited\n */\nexport function formatAge(value, now = new Date()) {\n\tconst date = parseDateValue(value)\n\tif (date === null) {\n\t\treturn ''\n\t}\n\tconst elapsed = now.getTime() - date.getTime()\n\tif (elapsed < MS_PER_DAY) {\n\t\tconst hours = elapsed <= 0 ? 0 : Math.max(1, Math.floor(elapsed / MS_PER_HOUR))\n\t\treturn n('nextcloud-vue', '%n hour', '%n hours', hours)\n\t}\n\tconst days = shownAgeDays(date, now)\n\treturn n('nextcloud-vue', '%n day', '%n days', days)\n}\n\n/**\n * The number of days an age cell shows: 0 under 24 hours (the cell then reads\n * hours), else the whole calendar days, at least 1. The threshold rules\n * compare against this number, so the colour always agrees with the text.\n *\n * @param {unknown} value The moment the wait started.\n * @param {Date} [now] The reference moment.\n * @return {number|null} The day count, or null when the value is not a date.\n * @spec openspec/changes/screens-cell-date-parity/specs/cell-date-age/spec.md#requirement-an-age-cell-turns-red-past-its-threshold\n */\nexport function shownAgeDays(value, now = new Date()) {\n\tconst date = parseDateValue(value)\n\tif (date === null) {\n\t\treturn null\n\t}\n\tif (now.getTime() - date.getTime() < MS_PER_DAY) {\n\t\treturn 0\n\t}\n\treturn Math.max(1, ageInDays(date, now))\n}\n\n/**\n * The variant of the first `variantWhen` rule the age in days matches, or ''\n * when none does. Same rule shape as the `date` widget (`{ op, value,\n * variant }`, first match wins), compared against the number of days the cell\n * shows (`shownAgeDays`): `[{ op: \"gte\", value: 3, variant: \"error\" }]` turns a wait of three\n * days or more red.\n *\n * @param {unknown} value The moment the wait started.\n * @param {Array<{op: string, value: number, variant: string}>} rules The rules.\n * @param {Date} [now] The reference moment.\n * @return {string} `success | warning | error | default`, or ''.\n * @spec openspec/changes/screens-cell-date-parity/specs/cell-date-age/spec.md#requirement-an-age-cell-turns-red-past-its-threshold\n */\nexport function resolveAgeVariant(value, rules, now = new Date()) {\n\tif (!Array.isArray(rules) || rules.length === 0) {\n\t\treturn ''\n\t}\n\tconst days = shownAgeDays(value, now)\n\tif (days === null) {\n\t\treturn ''\n\t}\n\tfor (const rule of rules) {\n\t\tif (!rule || typeof rule !== 'object') {\n\t\t\tcontinue\n\t\t}\n\t\tif (compareVisibleWhen(days, rule.op || 'eq', rule.value) === true) {\n\t\t\tconst variant = rule.variant === 'danger' ? 'error' : rule.variant\n\t\t\treturn AGE_VARIANTS.includes(variant) ? variant : ''\n\t\t}\n\t}\n\treturn ''\n}\n"],"names":["t","n"],"mappings":"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"}
1
+ {"version":3,"file":"boardDate.js","sources":["../../../src/utils/boardDate.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * boardDate: the two short time readings the drawn boards put in a table cell.\n *\n * - A date (\"5 okt\", \"30 okt\", \"14 feb 2024\"): day and short month in the\n * user's language, the year only when it is not the current year.\n * - An age (\"4 uur\", \"1 dag\", \"3 dagen\"): how long something has been\n * waiting, in hours under a day and in whole calendar days after that.\n *\n * Both read the Nextcloud user LANGUAGE for the month and unit names. A\n * Nextcloud account keeps its locale (date order) apart from its language,\n * and a Dutch reader whose locale was left at the en_US default saw\n * \"Feb 14, 2024\": the language decides the words, so it decides here, and the\n * locale is only used when it is a region of that same language.\n *\n * @module utils/boardDate\n * @spec openspec/changes/screens-cell-date-parity/specs/cell-date-age/spec.md\n */\n\nimport { getCanonicalLocale, getLanguage, translatePlural as n, translate as t } from '@nextcloud/l10n'\nimport { parseDateValue } from './dateVariant.js'\nimport { compareVisibleWhen } from './visibleWhen.js'\n\nconst MS_PER_HOUR = 3600000\nconst MS_PER_DAY = 86400000\n\n/** Variants an age rule may name. `danger` is accepted as an alias of `error`. */\nexport const AGE_VARIANTS = Object.freeze(['default', 'success', 'warning', 'error'])\n\n/**\n * Read a language or locale tag from an @nextcloud/l10n getter, which throws\n * when Nextcloud has not set one (outside a Nextcloud page, in a test).\n *\n * @param {() => string} getter `getLanguage` or `getCanonicalLocale`.\n * @return {string} The tag with `-` separators, or ''.\n */\nfunction readTag(getter) {\n\ttry {\n\t\treturn String(getter() || '').replace(/_/g, '-')\n\t} catch {\n\t\treturn ''\n\t}\n}\n\n/**\n * The BCP 47 tag the board dates are written in: the user's locale when it is\n * a region of the user's language (`nl-BE` for language `nl`), else the\n * language itself.\n *\n * @return {string|undefined} The tag, or undefined to let Intl choose.\n * @spec openspec/changes/screens-cell-date-parity/specs/cell-date-age/spec.md#requirement-a-board-date-cell-reads-day-and-short-month-in-the-user-language\n */\nexport function boardDateLocale() {\n\tconst language = readTag(getLanguage)\n\tconst locale = readTag(getCanonicalLocale)\n\tconst base = (tag) => tag.split('-')[0].toLowerCase()\n\tif (language && locale && base(language) === base(locale)) {\n\t\treturn locale\n\t}\n\treturn language || locale || undefined\n}\n\n/**\n * A date in the board's short form: \"5 okt\", or \"14 feb 2024\" outside the\n * current year.\n *\n * @param {unknown} value A Date, a timestamp or an ISO string.\n * @param {Date} [now] The reference moment (defaults to the current time).\n * @param {string} [locale] BCP 47 tag (defaults to `boardDateLocale()`).\n * @return {string} The short date, or '' when the value is not a date.\n * @spec openspec/changes/screens-cell-date-parity/specs/cell-date-age/spec.md#requirement-a-board-date-cell-reads-day-and-short-month-in-the-user-language\n */\nexport function formatBoardDate(value, now = new Date(), locale = boardDateLocale()) {\n\tconst date = parseDateValue(value)\n\tif (date === null) {\n\t\treturn ''\n\t}\n\tconst options = { day: 'numeric', month: 'short' }\n\tif (date.getFullYear() !== now.getFullYear()) {\n\t\toptions.year = 'numeric'\n\t}\n\ttry {\n\t\treturn new Intl.DateTimeFormat(locale, options).format(date)\n\t} catch {\n\t\treturn new Intl.DateTimeFormat(undefined, options).format(date)\n\t}\n}\n\n/**\n * A date with its short weekday, the way an agenda writes it: \"wo 7 okt\", or\n * \"wo 14 feb 2024\" outside the current year. With `today` set, today's date\n * reads \"Vandaag, wo 7 okt\".\n *\n * @param {unknown} value A Date, a timestamp or an ISO string.\n * @param {Date} [now] The reference moment (defaults to the current time).\n * @param {object} [options] Options.\n * @param {boolean} [options.today] Put \"Today, \" before today's date.\n * @param {string} [options.locale] BCP 47 tag (defaults to `boardDateLocale()`).\n * @return {string} The date, or '' when the value is not a date.\n * @spec openspec/changes/screens-cell-date-parity/specs/cell-date-age/spec.md#requirement-a-board-date-cell-reads-day-and-short-month-in-the-user-language\n */\nexport function formatBoardWeekday(value, now = new Date(), options = {}) {\n\tconst date = parseDateValue(value)\n\tif (date === null) {\n\t\treturn ''\n\t}\n\tconst parts = { weekday: 'short', day: 'numeric', month: 'short' }\n\tif (date.getFullYear() !== now.getFullYear()) {\n\t\tparts.year = 'numeric'\n\t}\n\tlet text\n\ttry {\n\t\ttext = new Intl.DateTimeFormat(options.locale || boardDateLocale(), parts).format(date)\n\t} catch {\n\t\ttext = new Intl.DateTimeFormat(undefined, parts).format(date)\n\t}\n\tif (options.today === true && ageInDays(date, now) === 0) {\n\t\treturn t('nextcloud-vue', 'Today, {date}', { date: text })\n\t}\n\treturn text\n}\n\n/**\n * Whole calendar days from the date until `now` (0 today, 1 yesterday), or\n * null when the value is not a date. A future date gives a negative number.\n *\n * @param {unknown} value The date.\n * @param {Date} [now] The reference moment.\n * @return {number|null} The day count.\n * @spec openspec/changes/screens-cell-date-parity/specs/cell-date-age/spec.md#requirement-an-age-cell-says-how-long-something-has-waited\n */\nexport function ageInDays(value, now = new Date()) {\n\tconst date = parseDateValue(value)\n\tif (date === null) {\n\t\treturn null\n\t}\n\t// UTC midnights of the two LOCAL days, so a daylight-saving hour never\n\t// turns one day into zero or two.\n\tconst start = Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())\n\tconst today = Date.UTC(now.getFullYear(), now.getMonth(), now.getDate())\n\treturn Math.round((today - start) / MS_PER_DAY)\n}\n\n/**\n * The time of day the boards write: \"09.42\" for a Dutch reader, \"09:42\" for\n * an English one. The separator belongs to the language, so it is one\n * translatable string with two placeholders.\n *\n * @param {Date} date The moment.\n * @return {string} The time of day.\n * @spec openspec/changes/screens-relative-dates/specs/relative-dates/spec.md#requirement-a-relative-date-reads-today-yesterday-then-a-short-date\n */\nexport function formatTimeOfDay(date) {\n\tconst pad = (v) => String(v).padStart(2, '0')\n\treturn t('nextcloud-vue', '{hours}:{minutes}', { hours: pad(date.getHours()), minutes: pad(date.getMinutes()) })\n}\n\n/**\n * A moment the way a list says when something came in: \"today\" or\n * \"yesterday\" (with the time of day on request: \"vandaag, 09.42\"), and from\n * the day before that the board's short date (\"5 okt\", \"14 feb 2024\").\n * Tomorrow reads \"tomorrow\", for a due date. Word and month names follow the\n * user's language through the library's catalogue.\n *\n * @param {unknown} value A Date, a timestamp or an ISO string.\n * @param {object} [options] Options.\n * @param {boolean} [options.withTime] Add the time of day after the day: \"today, 09.42\".\n * @param {Date} [options.now] The reference moment (defaults to the current time).\n * @return {string} The text, or '' when the value is not a date.\n * @spec openspec/changes/screens-relative-dates/specs/relative-dates/spec.md#requirement-a-relative-date-reads-today-yesterday-then-a-short-date\n */\nexport function formatRelativeDate(value, options = {}) {\n\tconst now = options.now || new Date()\n\tconst date = parseDateValue(value)\n\tif (date === null) {\n\t\treturn ''\n\t}\n\tconst days = ageInDays(date, now)\n\tlet day\n\tif (days === 0) {\n\t\tday = t('nextcloud-vue', 'today')\n\t} else if (days === 1) {\n\t\tday = t('nextcloud-vue', 'yesterday')\n\t} else if (days === -1) {\n\t\tday = t('nextcloud-vue', 'tomorrow')\n\t} else {\n\t\tday = formatBoardDate(date, now)\n\t}\n\treturn options.withTime === true ? `${day}, ${formatTimeOfDay(date)}` : day\n}\n\n/**\n * How long something has waited: \"4 uur\" under 24 hours (at least 1), else\n * \"1 dag\" / \"3 dagen\" in whole calendar days. Plural forms come from the\n * library's own catalogue (`nextcloud-vue`). A date in the future reads as\n * waiting no time: \"0 hours\".\n *\n * @param {unknown} value The moment the wait started.\n * @param {Date} [now] The reference moment.\n * @return {string} The age, or '' when the value is not a date.\n * @spec openspec/changes/screens-cell-date-parity/specs/cell-date-age/spec.md#requirement-an-age-cell-says-how-long-something-has-waited\n */\nexport function formatAge(value, now = new Date()) {\n\tconst date = parseDateValue(value)\n\tif (date === null) {\n\t\treturn ''\n\t}\n\tconst elapsed = now.getTime() - date.getTime()\n\tif (elapsed < MS_PER_DAY) {\n\t\tconst hours = elapsed <= 0 ? 0 : Math.max(1, Math.floor(elapsed / MS_PER_HOUR))\n\t\treturn n('nextcloud-vue', '%n hour', '%n hours', hours)\n\t}\n\tconst days = shownAgeDays(date, now)\n\treturn n('nextcloud-vue', '%n day', '%n days', days)\n}\n\n/**\n * The number of days an age cell shows: 0 under 24 hours (the cell then reads\n * hours), else the whole calendar days, at least 1. The threshold rules\n * compare against this number, so the colour always agrees with the text.\n *\n * @param {unknown} value The moment the wait started.\n * @param {Date} [now] The reference moment.\n * @return {number|null} The day count, or null when the value is not a date.\n * @spec openspec/changes/screens-cell-date-parity/specs/cell-date-age/spec.md#requirement-an-age-cell-turns-red-past-its-threshold\n */\nexport function shownAgeDays(value, now = new Date()) {\n\tconst date = parseDateValue(value)\n\tif (date === null) {\n\t\treturn null\n\t}\n\tif (now.getTime() - date.getTime() < MS_PER_DAY) {\n\t\treturn 0\n\t}\n\treturn Math.max(1, ageInDays(date, now))\n}\n\n/**\n * The variant of the first `variantWhen` rule the age in days matches, or ''\n * when none does. Same rule shape as the `date` widget (`{ op, value,\n * variant }`, first match wins), compared against the number of days the cell\n * shows (`shownAgeDays`): `[{ op: \"gte\", value: 3, variant: \"error\" }]` turns a wait of three\n * days or more red.\n *\n * @param {unknown} value The moment the wait started.\n * @param {Array<{op: string, value: number, variant: string}>} rules The rules.\n * @param {Date} [now] The reference moment.\n * @return {string} `success | warning | error | default`, or ''.\n * @spec openspec/changes/screens-cell-date-parity/specs/cell-date-age/spec.md#requirement-an-age-cell-turns-red-past-its-threshold\n */\nexport function resolveAgeVariant(value, rules, now = new Date()) {\n\tif (!Array.isArray(rules) || rules.length === 0) {\n\t\treturn ''\n\t}\n\tconst days = shownAgeDays(value, now)\n\tif (days === null) {\n\t\treturn ''\n\t}\n\tfor (const rule of rules) {\n\t\tif (!rule || typeof rule !== 'object') {\n\t\t\tcontinue\n\t\t}\n\t\tif (compareVisibleWhen(days, rule.op || 'eq', rule.value) === true) {\n\t\t\tconst variant = rule.variant === 'danger' ? 'error' : rule.variant\n\t\t\treturn AGE_VARIANTS.includes(variant) ? variant : ''\n\t\t}\n\t}\n\treturn ''\n}\n"],"names":["t","n"],"mappings":"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"}
@@ -0,0 +1,135 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * featureCatalog: the feature list of the Features and roadmap page, read the
6
+ * way the board look draws it. A feature carries a status (available, beta,
7
+ * in progress or planned), an optional category and optional per-language
8
+ * titles (`title_nl`, `summary_nl`) next to the English ones.
9
+ *
10
+ * @module utils/featureCatalog
11
+ * @spec openspec/changes/screens-roadmap-board-look/specs/features-roadmap-board-look/spec.md
12
+ */
13
+
14
+ /** The statuses, in the order the filter and the captions list them. */
15
+ const FEATURE_STATUSES = Object.freeze(['available', 'beta', 'in_progress', 'planned']);
16
+
17
+ /** The badge variant of each status. */
18
+ const FEATURE_STATUS_VARIANTS = Object.freeze({
19
+ available: 'success',
20
+ beta: 'warning',
21
+ in_progress: 'primary',
22
+ planned: 'purple',
23
+ });
24
+
25
+ const STATUS_ALIASES = Object.freeze({
26
+ available: 'available',
27
+ stable: 'available',
28
+ done: 'available',
29
+ released: 'available',
30
+ shipped: 'available',
31
+ beta: 'beta',
32
+ preview: 'beta',
33
+ in_progress: 'in_progress',
34
+ 'in-progress': 'in_progress',
35
+ inprogress: 'in_progress',
36
+ building: 'in_progress',
37
+ development: 'in_progress',
38
+ planned: 'planned',
39
+ roadmap: 'planned',
40
+ proposed: 'planned',
41
+ soon: 'planned',
42
+ });
43
+
44
+ /**
45
+ * The status key of a feature. A feature without a status is one the app
46
+ * ships, as every entry of the list was before statuses existed.
47
+ *
48
+ * @param {unknown} raw The feature's `status`.
49
+ * @return {string} One of FEATURE_STATUSES.
50
+ * @spec openspec/changes/screens-roadmap-board-look/specs/features-roadmap-board-look/spec.md#requirement-a-feature-has-one-of-four-statuses
51
+ */
52
+ function featureStatus(raw) {
53
+ if (typeof raw !== 'string' || raw.trim() === '') {
54
+ return 'available'
55
+ }
56
+ return STATUS_ALIASES[raw.trim().toLowerCase()] || 'available'
57
+ }
58
+
59
+ /**
60
+ * A text field of a feature in the reader's language: `title_nl` for a Dutch
61
+ * reader when the feature has one, else `title`.
62
+ *
63
+ * @param {object} feature The feature.
64
+ * @param {string} field `title` or `summary`.
65
+ * @param {string} [language] The reader's language tag (`nl`, `nl_NL`, `de-DE`).
66
+ * @return {string} The text, or ''.
67
+ * @spec openspec/changes/screens-roadmap-board-look/specs/features-roadmap-board-look/spec.md#requirement-a-feature-reads-in-the-readers-language
68
+ */
69
+ function featureText(feature, field, language = '') {
70
+ if (!feature || typeof feature !== 'object') {
71
+ return ''
72
+ }
73
+ const base = String(language || '').split(/[-_]/)[0].toLowerCase();
74
+ const own = base ? feature[`${field}_${base}`] : undefined;
75
+ if (typeof own === 'string' && own.trim() !== '') {
76
+ return own
77
+ }
78
+ return typeof feature[field] === 'string' ? feature[field] : ''
79
+ }
80
+
81
+ /**
82
+ * How many features carry each status.
83
+ *
84
+ * @param {Array<{status: string}>} features Features with a normalised status.
85
+ * @return {{all: number, available: number, beta: number, in_progress: number, planned: number}} The counts.
86
+ * @spec openspec/changes/screens-roadmap-board-look/specs/features-roadmap-board-look/spec.md#requirement-the-status-filter-shows-a-count-per-status
87
+ */
88
+ function countByStatus(features) {
89
+ const counts = { all: features.length, available: 0, beta: 0, in_progress: 0, planned: 0 };
90
+ for (const feature of features) {
91
+ counts[feature.status] += 1;
92
+ }
93
+ return counts
94
+ }
95
+
96
+ /**
97
+ * The features in the reader's language, grouped by category. Groups follow
98
+ * the order of `categories`; features without a known category come first,
99
+ * under no caption. Inside a group the features keep their given order.
100
+ *
101
+ * @param {Array<object>} features The raw features.
102
+ * @param {object} [categories] Category key to caption.
103
+ * @param {string} [language] The reader's language tag.
104
+ * @return {Array<{key: string, label: string, features: Array<object>}>} The groups; a group without features is left out.
105
+ * @spec openspec/changes/screens-roadmap-board-look/specs/features-roadmap-board-look/spec.md#requirement-features-are-grouped-by-category
106
+ */
107
+ function groupFeatures(features, categories = {}, language = '') {
108
+ const captions = categories && typeof categories === 'object' && !Array.isArray(categories) ? categories : {};
109
+ const groups = [{ key: '', label: '', features: [] }];
110
+ const byKey = { '': groups[0] };
111
+ for (const [key, label] of Object.entries(captions)) {
112
+ const group = { key, label: String(label), features: [] };
113
+ groups.push(group);
114
+ byKey[key] = group;
115
+ }
116
+ for (const raw of Array.isArray(features) ? features : []) {
117
+ if (!raw || typeof raw !== 'object') {
118
+ continue
119
+ }
120
+ const feature = {
121
+ slug: String(raw.slug || raw.id || ''),
122
+ title: featureText(raw, 'title', language) || String(raw.slug || ''),
123
+ summary: featureText(raw, 'summary', language),
124
+ status: featureStatus(raw.status),
125
+ expected: typeof raw.expected === 'string' ? raw.expected : '',
126
+ docsUrl: typeof raw.docsUrl === 'string' ? raw.docsUrl : '',
127
+ };
128
+ const group = byKey[typeof raw.category === 'string' ? raw.category : ''] || byKey[''];
129
+ group.features.push(feature);
130
+ }
131
+ return groups.filter((group) => group.features.length > 0)
132
+ }
133
+
134
+ export { FEATURE_STATUSES, FEATURE_STATUS_VARIANTS, countByStatus, featureStatus, featureText, groupFeatures };
135
+ //# sourceMappingURL=featureCatalog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"featureCatalog.js","sources":["../../../src/utils/featureCatalog.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * featureCatalog: the feature list of the Features and roadmap page, read the\n * way the board look draws it. A feature carries a status (available, beta,\n * in progress or planned), an optional category and optional per-language\n * titles (`title_nl`, `summary_nl`) next to the English ones.\n *\n * @module utils/featureCatalog\n * @spec openspec/changes/screens-roadmap-board-look/specs/features-roadmap-board-look/spec.md\n */\n\n/** The statuses, in the order the filter and the captions list them. */\nexport const FEATURE_STATUSES = Object.freeze(['available', 'beta', 'in_progress', 'planned'])\n\n/** The badge variant of each status. */\nexport const FEATURE_STATUS_VARIANTS = Object.freeze({\n\tavailable: 'success',\n\tbeta: 'warning',\n\tin_progress: 'primary',\n\tplanned: 'purple',\n})\n\nconst STATUS_ALIASES = Object.freeze({\n\tavailable: 'available',\n\tstable: 'available',\n\tdone: 'available',\n\treleased: 'available',\n\tshipped: 'available',\n\tbeta: 'beta',\n\tpreview: 'beta',\n\tin_progress: 'in_progress',\n\t'in-progress': 'in_progress',\n\tinprogress: 'in_progress',\n\tbuilding: 'in_progress',\n\tdevelopment: 'in_progress',\n\tplanned: 'planned',\n\troadmap: 'planned',\n\tproposed: 'planned',\n\tsoon: 'planned',\n})\n\n/**\n * The status key of a feature. A feature without a status is one the app\n * ships, as every entry of the list was before statuses existed.\n *\n * @param {unknown} raw The feature's `status`.\n * @return {string} One of FEATURE_STATUSES.\n * @spec openspec/changes/screens-roadmap-board-look/specs/features-roadmap-board-look/spec.md#requirement-a-feature-has-one-of-four-statuses\n */\nexport function featureStatus(raw) {\n\tif (typeof raw !== 'string' || raw.trim() === '') {\n\t\treturn 'available'\n\t}\n\treturn STATUS_ALIASES[raw.trim().toLowerCase()] || 'available'\n}\n\n/**\n * A text field of a feature in the reader's language: `title_nl` for a Dutch\n * reader when the feature has one, else `title`.\n *\n * @param {object} feature The feature.\n * @param {string} field `title` or `summary`.\n * @param {string} [language] The reader's language tag (`nl`, `nl_NL`, `de-DE`).\n * @return {string} The text, or ''.\n * @spec openspec/changes/screens-roadmap-board-look/specs/features-roadmap-board-look/spec.md#requirement-a-feature-reads-in-the-readers-language\n */\nexport function featureText(feature, field, language = '') {\n\tif (!feature || typeof feature !== 'object') {\n\t\treturn ''\n\t}\n\tconst base = String(language || '').split(/[-_]/)[0].toLowerCase()\n\tconst own = base ? feature[`${field}_${base}`] : undefined\n\tif (typeof own === 'string' && own.trim() !== '') {\n\t\treturn own\n\t}\n\treturn typeof feature[field] === 'string' ? feature[field] : ''\n}\n\n/**\n * How many features carry each status.\n *\n * @param {Array<{status: string}>} features Features with a normalised status.\n * @return {{all: number, available: number, beta: number, in_progress: number, planned: number}} The counts.\n * @spec openspec/changes/screens-roadmap-board-look/specs/features-roadmap-board-look/spec.md#requirement-the-status-filter-shows-a-count-per-status\n */\nexport function countByStatus(features) {\n\tconst counts = { all: features.length, available: 0, beta: 0, in_progress: 0, planned: 0 }\n\tfor (const feature of features) {\n\t\tcounts[feature.status] += 1\n\t}\n\treturn counts\n}\n\n/**\n * The features in the reader's language, grouped by category. Groups follow\n * the order of `categories`; features without a known category come first,\n * under no caption. Inside a group the features keep their given order.\n *\n * @param {Array<object>} features The raw features.\n * @param {object} [categories] Category key to caption.\n * @param {string} [language] The reader's language tag.\n * @return {Array<{key: string, label: string, features: Array<object>}>} The groups; a group without features is left out.\n * @spec openspec/changes/screens-roadmap-board-look/specs/features-roadmap-board-look/spec.md#requirement-features-are-grouped-by-category\n */\nexport function groupFeatures(features, categories = {}, language = '') {\n\tconst captions = categories && typeof categories === 'object' && !Array.isArray(categories) ? categories : {}\n\tconst groups = [{ key: '', label: '', features: [] }]\n\tconst byKey = { '': groups[0] }\n\tfor (const [key, label] of Object.entries(captions)) {\n\t\tconst group = { key, label: String(label), features: [] }\n\t\tgroups.push(group)\n\t\tbyKey[key] = group\n\t}\n\tfor (const raw of Array.isArray(features) ? features : []) {\n\t\tif (!raw || typeof raw !== 'object') {\n\t\t\tcontinue\n\t\t}\n\t\tconst feature = {\n\t\t\tslug: String(raw.slug || raw.id || ''),\n\t\t\ttitle: featureText(raw, 'title', language) || String(raw.slug || ''),\n\t\t\tsummary: featureText(raw, 'summary', language),\n\t\t\tstatus: featureStatus(raw.status),\n\t\t\texpected: typeof raw.expected === 'string' ? raw.expected : '',\n\t\t\tdocsUrl: typeof raw.docsUrl === 'string' ? raw.docsUrl : '',\n\t\t}\n\t\tconst group = byKey[typeof raw.category === 'string' ? raw.category : ''] || byKey['']\n\t\tgroup.features.push(feature)\n\t}\n\treturn groups.filter((group) => group.features.length > 0)\n}\n"],"names":[],"mappings":"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"}
@@ -1,7 +1,8 @@
1
- import { formatBoardDate, formatRelativeDate, boardDateLocale } from './boardDate.js';
1
+ import { formatBoardDate, formatBoardWeekday, formatTimeOfDay, formatRelativeDate, boardDateLocale } from './boardDate.js';
2
2
  import { parseDateValue } from './dateVariant.js';
3
3
  import { formatMoney } from './formatMetric.js';
4
4
  import { readPath } from './readPath.js';
5
+ import { userDisplayName } from './userDisplayName.js';
5
6
 
6
7
  // SPDX-FileCopyrightText: 2026 Conduction B.V.
7
8
  // SPDX-License-Identifier: EUPL-1.2
@@ -33,11 +34,37 @@ function formatBoardDateTime(value, now = new Date()) {
33
34
  return `${day}, ${time}`
34
35
  }
35
36
 
37
+ /**
38
+ * A date with the month written out: "7 oktober", or "6 oktober 2026"
39
+ * outside the current year or when `withYear` asks for it.
40
+ *
41
+ * @param {unknown} value A Date, a timestamp or an ISO string.
42
+ * @param {Date} now The reference moment.
43
+ * @param {boolean} [withYear] Always write the year.
44
+ * @return {string} The text, or '' when the value is not a date.
45
+ * @spec openspec/changes/screens-header-meta-tokens/specs/detail-page-board-look/spec.md#requirement-the-header-meta-line-formats-users-long-dates-and-enum-labels
46
+ */
47
+ function formatLongDate(value, now = new Date(), withYear = false) {
48
+ const date = parseDateValue(value);
49
+ if (date === null) {
50
+ return ''
51
+ }
52
+ const options = { day: 'numeric', month: 'long' };
53
+ if (withYear || date.getFullYear() !== now.getFullYear()) {
54
+ options.year = 'numeric';
55
+ }
56
+ try {
57
+ return new Intl.DateTimeFormat(boardDateLocale(), options).format(date)
58
+ } catch {
59
+ return new Intl.DateTimeFormat(undefined, options).format(date)
60
+ }
61
+ }
62
+
36
63
  /**
37
64
  * Format one token value.
38
65
  *
39
66
  * @param {unknown} value The raw value.
40
- * @param {string} format `date`, `datetime`, `relative`, `relativetime`, `days` or '' (as is).
67
+ * @param {string} format `date`, `datetime`, `weekday`, `longdate`, `time`, `user`, `currency`, `relative`, `relativetime`, `days` or '' (as is).
41
68
  * @param {Date} now The reference moment.
42
69
  * @param {string} [arg] The format's argument (`currency:EUR`, or a row path holding the code).
43
70
  * @param {object|null} [object] The row, for a currency read from a field.
@@ -54,6 +81,19 @@ function formatValue(value, format, now, arg = '', object = null, config = null)
54
81
  if (format === 'datetime') {
55
82
  return formatBoardDateTime(value, now)
56
83
  }
84
+ if (format === 'weekday') {
85
+ return formatBoardWeekday(value, now, { today: arg === 'today' })
86
+ }
87
+ if (format === 'longdate') {
88
+ return formatLongDate(value, now, arg === 'year')
89
+ }
90
+ if (format === 'user') {
91
+ return userDisplayName(value)
92
+ }
93
+ if (format === 'time') {
94
+ const date = parseDateValue(value);
95
+ return date === null ? '' : formatTimeOfDay(date)
96
+ }
57
97
  if (format === 'currency') {
58
98
  // `{amount|currency:EUR}` names the code, `{amount|currency:price.code}`
59
99
  // reads it from the row, `{amount|currency}` is the app's reporting
@@ -80,7 +120,12 @@ function formatValue(value, format, now, arg = '', object = null, config = null)
80
120
  * `"Termijn {deadline|date}"` from an object.
81
121
  *
82
122
  * Every `{path}` token is a dot path into the object; `{path|date}` and
83
- * `{path|datetime}` format it as the boards write dates, `{path|currency}`,
123
+ * `{path|datetime}` format it as the boards write dates, `{path|weekday}` adds
124
+ * the short weekday ("wo 7 okt"; `{path|weekday:today}` reads "Vandaag, wo 7 okt"
125
+ * for today), `{path|longdate}` writes the month out ("7 oktober";
126
+ * `{path|longdate:year}` always adds the year), `{path|time}` is the time
127
+ * of day ("08:45"), `{path|user}` is a user id's display name (looked up
128
+ * once, '' until it arrives), `{path|currency}`,
84
129
  * `{path|currency:EUR}` and `{path|currency:field}` write an amount in a
85
130
  * currency (a code, or the row field that holds it; without either, or when
86
131
  * the row field is empty, the app's reporting currency), `{path|relative}` and
@@ -140,5 +185,40 @@ function firstFilled(templates, object, translate = (key) => key, now = new Date
140
185
  return ''
141
186
  }
142
187
 
143
- export { fillTemplate, firstFilled, formatBoardDateTime };
188
+ /**
189
+ * Fill a template such as `"{identifier} · {requester}"` field by field
190
+ * (a table column's `secondary` line, a detail page's meta line).
191
+ *
192
+ * A field without a value takes its separator with it: the text between two
193
+ * fields belongs to the field after it, so an empty requester leaves
194
+ * "2026-0002", not "2026-0002 ·", and an empty first field does not leave a
195
+ * leading separator either. Text before the first field and after the last
196
+ * one is kept as written. A template whose fields all have values fills
197
+ * exactly as before.
198
+ *
199
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-an-empty-field-takes-its-separator-with-it
200
+ * @param {string} template The template, with `{field}` placeholders.
201
+ * @param {(key: string) => string} valueOf The text of a field ('' when empty).
202
+ * @return {string} The filled template.
203
+ */
204
+ function joinTemplateParts(template, valueOf) {
205
+ // split() with a capture group alternates literal, field, literal, …
206
+ const parts = template.split(/\{([^}]+)\}/);
207
+ const lead = parts[0];
208
+ const tail = parts[parts.length - 1];
209
+ let body = '';
210
+ for (let i = 1; i < parts.length; i += 2) {
211
+ const value = valueOf(parts[i].trim());
212
+ if (value === '') {
213
+ continue
214
+ }
215
+ // The literal before this field: the separator, unless nothing has
216
+ // been written yet (then there is nothing to separate from).
217
+ const separator = i === 1 ? '' : parts[i - 1];
218
+ body += (body === '' ? '' : separator) + value;
219
+ }
220
+ return lead + body + tail
221
+ }
222
+
223
+ export { fillTemplate, firstFilled, formatBoardDateTime, formatLongDate, joinTemplateParts };
144
224
  //# sourceMappingURL=fillTemplate.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"fillTemplate.js","sources":["../../../src/utils/fillTemplate.js"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 Conduction B.V.\n// SPDX-License-Identifier: EUPL-1.2\n\nimport { boardDateLocale, formatBoardDate, formatRelativeDate } from './boardDate.js'\nimport { parseDateValue } from './dateVariant.js'\nimport { formatMoney } from './formatMetric.js'\nimport { readPath } from './readPath.js'\n\nconst TOKEN = /\\{([^{}\\s|]+)(?:\\|([a-z]+)(?::([^{}\\s]+))?)?\\}/g\n\n/**\n * A moment in the board's short form with the time: \"4 okt, 10:52\", or\n * \"14 feb 2024, 09:00\" outside the current year.\n *\n * @param {unknown} value A Date, a timestamp or an ISO string.\n * @param {Date} [now] The reference moment.\n * @return {string} The text, or '' when the value is not a date.\n * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-record-list-shows-the-objects-that-point-at-this-record\n */\nexport function formatBoardDateTime(value, now = new Date()) {\n\tconst day = formatBoardDate(value, now)\n\tconst date = parseDateValue(value)\n\tif (day === '' || date === null) {\n\t\treturn ''\n\t}\n\tlet time\n\ttry {\n\t\ttime = new Intl.DateTimeFormat(boardDateLocale(), { hour: '2-digit', minute: '2-digit' }).format(date)\n\t} catch {\n\t\ttime = new Intl.DateTimeFormat(undefined, { hour: '2-digit', minute: '2-digit' }).format(date)\n\t}\n\treturn `${day}, ${time}`\n}\n\n/**\n * Format one token value.\n *\n * @param {unknown} value The raw value.\n * @param {string} format `date`, `datetime`, `relative`, `relativetime`, `days` or '' (as is).\n * @param {Date} now The reference moment.\n * @param {string} [arg] The format's argument (`currency:EUR`, or a row path holding the code).\n * @param {object|null} [object] The row, for a currency read from a field.\n * @param {object|null} [config] The app config map, for the reporting currency.\n * @return {string} The text, '' when nothing can be shown.\n */\nfunction formatValue(value, format, now, arg = '', object = null, config = null) {\n\tif (value === null || value === undefined || value === '' || typeof value === 'object') {\n\t\treturn ''\n\t}\n\tif (format === 'date') {\n\t\treturn formatBoardDate(value, now)\n\t}\n\tif (format === 'datetime') {\n\t\treturn formatBoardDateTime(value, now)\n\t}\n\tif (format === 'currency') {\n\t\t// `{amount|currency:EUR}` names the code, `{amount|currency:price.code}`\n\t\t// reads it from the row, `{amount|currency}` is the app's reporting\n\t\t// currency (EUR without one).\n\t\tconst isCode = /^[A-Za-z]{3}$/.test(arg)\n\t\treturn formatMoney(value, { currency: isCode ? arg : undefined, currencyField: !isCode && arg ? arg : undefined }, object, config)\n\t}\n\tif (format === 'relative') {\n\t\treturn formatRelativeDate(value, { now })\n\t}\n\tif (format === 'relativetime') {\n\t\treturn formatRelativeDate(value, { now, withTime: true })\n\t}\n\tif (format === 'days') {\n\t\t// Whole days from the date to now (0 for today; negative for a future date).\n\t\tconst date = parseDateValue(value)\n\t\treturn date === null ? '' : String(Math.floor((now.getTime() - date.getTime()) / 86400000))\n\t}\n\treturn String(value)\n}\n\n/**\n * Fill a row template such as `\"{identifier} · {caseTypeName}\"` or\n * `\"Termijn {deadline|date}\"` from an object.\n *\n * Every `{path}` token is a dot path into the object; `{path|date}` and\n * `{path|datetime}` format it as the boards write dates, `{path|currency}`,\n * `{path|currency:EUR}` and `{path|currency:field}` write an amount in a\n * currency (a code, or the row field that holds it; without either, or when\n * the row field is empty, the app's reporting currency), `{path|relative}` and\n * `{path|relativetime}` say \"today\", \"yesterday\" and then the short date\n * (\"vandaag, 09.42\"), and `{path|days}` is the\n * whole days since that date (\"has waited {createdAt|days} days\"). A token without a\n * value empties the WHOLE template, because \"Termijn \" is worse than nothing.\n * A template without tokens is returned as written (a constant label).\n *\n * @param {string} template The template, already translated.\n * @param {object|null} object The object to read.\n * @param {Date} [now] The reference moment for dates.\n * @param {{config?: object}|null} [context] The app config map, for the reporting currency of a money token.\n * @return {string} The filled text, or ''.\n * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-record-list-shows-the-objects-that-point-at-this-record\n */\nexport function fillTemplate(template, object, now = new Date(), context = null) {\n\tif (typeof template !== 'string' || template.trim() === '') {\n\t\treturn ''\n\t}\n\tlet missing = false\n\tconst text = template.replace(TOKEN, (_match, path, format, arg) => {\n\t\tconst value = object && typeof object === 'object' ? readPath(object, path) : undefined\n\t\tconst shown = formatValue(value, format || '', now, arg || '', object, context && context.config ? context.config : null)\n\t\tif (shown === '') {\n\t\t\tmissing = true\n\t\t}\n\t\treturn shown\n\t})\n\treturn missing ? '' : text.trim()\n}\n\n/**\n * The first of several templates that fills: `[\"Klaar {closedAt|date}\",\n * \"Termijn {deadline|date}\"]` reads \"Klaar ...\" once the case is closed and\n * \"Termijn ...\" while it is open. A single string is one candidate.\n *\n * @param {string|string[]} templates The candidates, in order.\n * @param {object|null} object The object to read.\n * @param {(key: string) => string} [translate] Applied to each template before filling.\n * @param {Date} [now] The reference moment for dates.\n * @param {{config?: object}|null} [context] Passed on to `fillTemplate`.\n * @return {string} The first filled text, or ''.\n * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-record-list-shows-the-objects-that-point-at-this-record\n */\nexport function firstFilled(templates, object, translate = (key) => key, now = new Date(), context = null) {\n\tconst list = Array.isArray(templates) ? templates : [templates]\n\tfor (const template of list) {\n\t\tif (typeof template !== 'string') {\n\t\t\tcontinue\n\t\t}\n\t\tconst text = fillTemplate(translate(template), object, now, context)\n\t\tif (text !== '') {\n\t\t\treturn text\n\t\t}\n\t}\n\treturn ''\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"fillTemplate.js","sources":["../../../src/utils/fillTemplate.js"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 Conduction B.V.\n// SPDX-License-Identifier: EUPL-1.2\n\nimport { boardDateLocale, formatBoardDate, formatBoardWeekday, formatRelativeDate, formatTimeOfDay } from './boardDate.js'\nimport { parseDateValue } from './dateVariant.js'\nimport { formatMoney } from './formatMetric.js'\nimport { readPath } from './readPath.js'\nimport { userDisplayName } from './userDisplayName.js'\n\nconst TOKEN = /\\{([^{}\\s|]+)(?:\\|([a-z]+)(?::([^{}\\s]+))?)?\\}/g\n\n/**\n * A moment in the board's short form with the time: \"4 okt, 10:52\", or\n * \"14 feb 2024, 09:00\" outside the current year.\n *\n * @param {unknown} value A Date, a timestamp or an ISO string.\n * @param {Date} [now] The reference moment.\n * @return {string} The text, or '' when the value is not a date.\n * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-record-list-shows-the-objects-that-point-at-this-record\n */\nexport function formatBoardDateTime(value, now = new Date()) {\n\tconst day = formatBoardDate(value, now)\n\tconst date = parseDateValue(value)\n\tif (day === '' || date === null) {\n\t\treturn ''\n\t}\n\tlet time\n\ttry {\n\t\ttime = new Intl.DateTimeFormat(boardDateLocale(), { hour: '2-digit', minute: '2-digit' }).format(date)\n\t} catch {\n\t\ttime = new Intl.DateTimeFormat(undefined, { hour: '2-digit', minute: '2-digit' }).format(date)\n\t}\n\treturn `${day}, ${time}`\n}\n\n/**\n * A date with the month written out: \"7 oktober\", or \"6 oktober 2026\"\n * outside the current year or when `withYear` asks for it.\n *\n * @param {unknown} value A Date, a timestamp or an ISO string.\n * @param {Date} now The reference moment.\n * @param {boolean} [withYear] Always write the year.\n * @return {string} The text, or '' when the value is not a date.\n * @spec openspec/changes/screens-header-meta-tokens/specs/detail-page-board-look/spec.md#requirement-the-header-meta-line-formats-users-long-dates-and-enum-labels\n */\nexport function formatLongDate(value, now = new Date(), withYear = false) {\n\tconst date = parseDateValue(value)\n\tif (date === null) {\n\t\treturn ''\n\t}\n\tconst options = { day: 'numeric', month: 'long' }\n\tif (withYear || date.getFullYear() !== now.getFullYear()) {\n\t\toptions.year = 'numeric'\n\t}\n\ttry {\n\t\treturn new Intl.DateTimeFormat(boardDateLocale(), options).format(date)\n\t} catch {\n\t\treturn new Intl.DateTimeFormat(undefined, options).format(date)\n\t}\n}\n\n/**\n * Format one token value.\n *\n * @param {unknown} value The raw value.\n * @param {string} format `date`, `datetime`, `weekday`, `longdate`, `time`, `user`, `currency`, `relative`, `relativetime`, `days` or '' (as is).\n * @param {Date} now The reference moment.\n * @param {string} [arg] The format's argument (`currency:EUR`, or a row path holding the code).\n * @param {object|null} [object] The row, for a currency read from a field.\n * @param {object|null} [config] The app config map, for the reporting currency.\n * @return {string} The text, '' when nothing can be shown.\n */\nfunction formatValue(value, format, now, arg = '', object = null, config = null) {\n\tif (value === null || value === undefined || value === '' || typeof value === 'object') {\n\t\treturn ''\n\t}\n\tif (format === 'date') {\n\t\treturn formatBoardDate(value, now)\n\t}\n\tif (format === 'datetime') {\n\t\treturn formatBoardDateTime(value, now)\n\t}\n\tif (format === 'weekday') {\n\t\treturn formatBoardWeekday(value, now, { today: arg === 'today' })\n\t}\n\tif (format === 'longdate') {\n\t\treturn formatLongDate(value, now, arg === 'year')\n\t}\n\tif (format === 'user') {\n\t\treturn userDisplayName(value)\n\t}\n\tif (format === 'time') {\n\t\tconst date = parseDateValue(value)\n\t\treturn date === null ? '' : formatTimeOfDay(date)\n\t}\n\tif (format === 'currency') {\n\t\t// `{amount|currency:EUR}` names the code, `{amount|currency:price.code}`\n\t\t// reads it from the row, `{amount|currency}` is the app's reporting\n\t\t// currency (EUR without one).\n\t\tconst isCode = /^[A-Za-z]{3}$/.test(arg)\n\t\treturn formatMoney(value, { currency: isCode ? arg : undefined, currencyField: !isCode && arg ? arg : undefined }, object, config)\n\t}\n\tif (format === 'relative') {\n\t\treturn formatRelativeDate(value, { now })\n\t}\n\tif (format === 'relativetime') {\n\t\treturn formatRelativeDate(value, { now, withTime: true })\n\t}\n\tif (format === 'days') {\n\t\t// Whole days from the date to now (0 for today; negative for a future date).\n\t\tconst date = parseDateValue(value)\n\t\treturn date === null ? '' : String(Math.floor((now.getTime() - date.getTime()) / 86400000))\n\t}\n\treturn String(value)\n}\n\n/**\n * Fill a row template such as `\"{identifier} · {caseTypeName}\"` or\n * `\"Termijn {deadline|date}\"` from an object.\n *\n * Every `{path}` token is a dot path into the object; `{path|date}` and\n * `{path|datetime}` format it as the boards write dates, `{path|weekday}` adds\n * the short weekday (\"wo 7 okt\"; `{path|weekday:today}` reads \"Vandaag, wo 7 okt\"\n * for today), `{path|longdate}` writes the month out (\"7 oktober\";\n * `{path|longdate:year}` always adds the year), `{path|time}` is the time\n * of day (\"08:45\"), `{path|user}` is a user id's display name (looked up\n * once, '' until it arrives), `{path|currency}`,\n * `{path|currency:EUR}` and `{path|currency:field}` write an amount in a\n * currency (a code, or the row field that holds it; without either, or when\n * the row field is empty, the app's reporting currency), `{path|relative}` and\n * `{path|relativetime}` say \"today\", \"yesterday\" and then the short date\n * (\"vandaag, 09.42\"), and `{path|days}` is the\n * whole days since that date (\"has waited {createdAt|days} days\"). A token without a\n * value empties the WHOLE template, because \"Termijn \" is worse than nothing.\n * A template without tokens is returned as written (a constant label).\n *\n * @param {string} template The template, already translated.\n * @param {object|null} object The object to read.\n * @param {Date} [now] The reference moment for dates.\n * @param {{config?: object}|null} [context] The app config map, for the reporting currency of a money token.\n * @return {string} The filled text, or ''.\n * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-record-list-shows-the-objects-that-point-at-this-record\n */\nexport function fillTemplate(template, object, now = new Date(), context = null) {\n\tif (typeof template !== 'string' || template.trim() === '') {\n\t\treturn ''\n\t}\n\tlet missing = false\n\tconst text = template.replace(TOKEN, (_match, path, format, arg) => {\n\t\tconst value = object && typeof object === 'object' ? readPath(object, path) : undefined\n\t\tconst shown = formatValue(value, format || '', now, arg || '', object, context && context.config ? context.config : null)\n\t\tif (shown === '') {\n\t\t\tmissing = true\n\t\t}\n\t\treturn shown\n\t})\n\treturn missing ? '' : text.trim()\n}\n\n/**\n * The first of several templates that fills: `[\"Klaar {closedAt|date}\",\n * \"Termijn {deadline|date}\"]` reads \"Klaar ...\" once the case is closed and\n * \"Termijn ...\" while it is open. A single string is one candidate.\n *\n * @param {string|string[]} templates The candidates, in order.\n * @param {object|null} object The object to read.\n * @param {(key: string) => string} [translate] Applied to each template before filling.\n * @param {Date} [now] The reference moment for dates.\n * @param {{config?: object}|null} [context] Passed on to `fillTemplate`.\n * @return {string} The first filled text, or ''.\n * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-record-list-shows-the-objects-that-point-at-this-record\n */\nexport function firstFilled(templates, object, translate = (key) => key, now = new Date(), context = null) {\n\tconst list = Array.isArray(templates) ? templates : [templates]\n\tfor (const template of list) {\n\t\tif (typeof template !== 'string') {\n\t\t\tcontinue\n\t\t}\n\t\tconst text = fillTemplate(translate(template), object, now, context)\n\t\tif (text !== '') {\n\t\t\treturn text\n\t\t}\n\t}\n\treturn ''\n}\n\n/**\n * Fill a template such as `\"{identifier} · {requester}\"` field by field\n * (a table column's `secondary` line, a detail page's meta line).\n *\n * A field without a value takes its separator with it: the text between two\n * fields belongs to the field after it, so an empty requester leaves\n * \"2026-0002\", not \"2026-0002 ·\", and an empty first field does not leave a\n * leading separator either. Text before the first field and after the last\n * one is kept as written. A template whose fields all have values fills\n * exactly as before.\n *\n * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-an-empty-field-takes-its-separator-with-it\n * @param {string} template The template, with `{field}` placeholders.\n * @param {(key: string) => string} valueOf The text of a field ('' when empty).\n * @return {string} The filled template.\n */\nexport function joinTemplateParts(template, valueOf) {\n\t// split() with a capture group alternates literal, field, literal, …\n\tconst parts = template.split(/\\{([^}]+)\\}/)\n\tconst lead = parts[0]\n\tconst tail = parts[parts.length - 1]\n\tlet body = ''\n\tfor (let i = 1; i < parts.length; i += 2) {\n\t\tconst value = valueOf(parts[i].trim())\n\t\tif (value === '') {\n\t\t\tcontinue\n\t\t}\n\t\t// The literal before this field: the separator, unless nothing has\n\t\t// been written yet (then there is nothing to separate from).\n\t\tconst separator = i === 1 ? '' : parts[i - 1]\n\t\tbody += (body === '' ? '' : separator) + value\n\t}\n\treturn lead + body + tail\n}\n"],"names":[],"mappings":"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"}
@@ -1,28 +1,78 @@
1
- import { fillTemplate } from './fillTemplate.js';
1
+ import { joinTemplateParts, fillTemplate } from './fillTemplate.js';
2
+ import { readPath } from './readPath.js';
2
3
 
3
4
  // SPDX-FileCopyrightText: 2026 Conduction B.V.
4
5
  // SPDX-License-Identifier: EUPL-1.2
5
6
 
6
7
 
8
+ /**
9
+ * The text of one plain `{path}` token: an enum field reads as its label
10
+ * (`x-enum-labels` / `enumLabels` on the schema property, through the
11
+ * translate function), anything else as the stored value.
12
+ *
13
+ * @param {object} object The record.
14
+ * @param {string} path The dotted path.
15
+ * @param {object|null} schema The record's schema.
16
+ * @param {(text: string) => string} translate The translate function.
17
+ * @return {string} The text, or '' when the field is empty.
18
+ */
19
+ function plainTokenText(object, path, schema, translate) {
20
+ const value = readPath(object, path);
21
+ if (value === null || value === undefined || value === '' || typeof value === 'object') {
22
+ return ''
23
+ }
24
+ const property = schema && schema.properties && !path.includes('.') ? schema.properties[path] : null;
25
+ const labels = property && (property.enumLabels || property['x-enum-labels']);
26
+ if (labels && typeof labels === 'object' && typeof labels[String(value)] === 'string') {
27
+ return translate(labels[String(value)])
28
+ }
29
+ return String(value)
30
+ }
31
+
7
32
  /**
8
33
  * Fill a header meta template such as `"via {channel}"` or
9
- * `"received {createdAt|relative}"` from a record.
34
+ * `"{amount|currency:EUR} · {cashier|user} · {at|longdate} {at|time}"` from
35
+ * a record.
10
36
  *
11
37
  * Every `{path}` token (a dotted path into the record) is replaced by its
12
- * value; `{path|date}`, `{path|datetime}`, `{path|relative}`,
13
- * `{path|relativetime}` and `{path|days}` format a date as a record-list
14
- * row does (the shared `fillTemplate`). A token with no value drops the
15
- * WHOLE line, because "via " is worse than nothing. A template without
16
- * tokens is returned as written.
38
+ * value, an enum field by its label. `{path|format}` tokens format as a
39
+ * record-list row does (the shared `fillTemplate`: date, datetime, weekday,
40
+ * longdate, time, user, currency, relative, relativetime, days). A token
41
+ * without a value drops itself and the separator before it, so a missing
42
+ * cashier leaves "€ 96,41 · 7 oktober"; a line whose every token is empty
43
+ * is '' ("via " is worse than nothing). A template without tokens is
44
+ * returned as written.
17
45
  *
18
46
  * @param {string} template The template, already translated.
19
47
  * @param {object|null} object The record.
20
- * @param {Date} [now] The reference moment for dates.
21
- * @return {string} The line, or '' when it cannot be filled.
22
- * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md#requirement-the-detail-header-is-two-rows-on-the-ground
48
+ * @param {Date|{now?: Date, schema?: object, translate?: Function, config?: object}} [options] The reference moment, or options: `schema` for enum labels, `translate` for those labels, `config` for the reporting currency.
49
+ * @return {string} The line, or '' when nothing can be filled.
50
+ * @spec openspec/changes/screens-header-meta-tokens/specs/detail-page-board-look/spec.md#requirement-the-header-meta-line-formats-users-long-dates-and-enum-labels
23
51
  */
24
- function resolveHeaderMeta(template, object, now = new Date()) {
25
- return fillTemplate(template, object, now)
52
+ function resolveHeaderMeta(template, object, options = {}) {
53
+ const settings = options instanceof Date ? { now: options } : (options || {});
54
+ const now = settings.now || new Date();
55
+ const translate = typeof settings.translate === 'function' ? settings.translate : (text) => text;
56
+ if (typeof template !== 'string' || template.trim() === '') {
57
+ return ''
58
+ }
59
+ if (!/\{[^}]+\}/.test(template)) {
60
+ return template.trim()
61
+ }
62
+ if (!object || typeof object !== 'object') {
63
+ return ''
64
+ }
65
+ let filled = 0;
66
+ const line = joinTemplateParts(template, (token) => {
67
+ const text = token.includes('|')
68
+ ? fillTemplate(`{${token}}`, object, now, settings.config ? { config: settings.config } : null)
69
+ : plainTokenText(object, token, settings.schema || null, translate);
70
+ if (text !== '') {
71
+ filled += 1;
72
+ }
73
+ return text
74
+ });
75
+ return filled === 0 ? '' : line.trim()
26
76
  }
27
77
 
28
78
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"headerMeta.js","sources":["../../../src/utils/headerMeta.js"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 Conduction B.V.\n// SPDX-License-Identifier: EUPL-1.2\n\nimport { fillTemplate } from './fillTemplate.js'\n\n/**\n * Fill a header meta template such as `\"via {channel}\"` or\n * `\"received {createdAt|relative}\"` from a record.\n *\n * Every `{path}` token (a dotted path into the record) is replaced by its\n * value; `{path|date}`, `{path|datetime}`, `{path|relative}`,\n * `{path|relativetime}` and `{path|days}` format a date as a record-list\n * row does (the shared `fillTemplate`). A token with no value drops the\n * WHOLE line, because \"via \" is worse than nothing. A template without\n * tokens is returned as written.\n *\n * @param {string} template The template, already translated.\n * @param {object|null} object The record.\n * @param {Date} [now] The reference moment for dates.\n * @return {string} The line, or '' when it cannot be filled.\n * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md#requirement-the-detail-header-is-two-rows-on-the-ground\n */\nexport function resolveHeaderMeta(template, object, now = new Date()) {\n\treturn fillTemplate(template, object, now)\n}\n\n/**\n * Whether a widget definition is the record's activity (the History tab or\n * card): an audit-trail or timeline widget, or the `activity` integration.\n *\n * @param {object|null} widget The widget definition.\n * @return {boolean} True for an activity widget.\n * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md#requirement-history-is-the-last-tab\n */\nexport function isActivityWidget(widget) {\n\tif (!widget || typeof widget !== 'object') {\n\t\treturn false\n\t}\n\tif (widget.type === 'audit-trail' || widget.type === 'timeline') {\n\t\treturn true\n\t}\n\treturn widget.type === 'integration' && widget.integrationId === 'activity'\n}\n"],"names":[],"mappings":";;AAAA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,GAAG,IAAI,IAAI,EAAE,EAAE;AACtE,CAAC,OAAO,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,CAAC;AAC3C,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,gBAAgB,CAAC,MAAM,EAAE;AACzC,CAAC,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE;AAC5C,EAAE,OAAO,KAAK;AACd,EAAE;AACF,CAAC,IAAI,MAAM,CAAC,IAAI,KAAK,aAAa,IAAI,MAAM,CAAC,IAAI,KAAK,UAAU,EAAE;AAClE,EAAE,OAAO,IAAI;AACb,EAAE;AACF,CAAC,OAAO,MAAM,CAAC,IAAI,KAAK,aAAa,IAAI,MAAM,CAAC,aAAa,KAAK,UAAU;AAC5E;;;;"}
1
+ {"version":3,"file":"headerMeta.js","sources":["../../../src/utils/headerMeta.js"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 Conduction B.V.\n// SPDX-License-Identifier: EUPL-1.2\n\nimport { fillTemplate, joinTemplateParts } from './fillTemplate.js'\nimport { readPath } from './readPath.js'\n\n/**\n * The text of one plain `{path}` token: an enum field reads as its label\n * (`x-enum-labels` / `enumLabels` on the schema property, through the\n * translate function), anything else as the stored value.\n *\n * @param {object} object The record.\n * @param {string} path The dotted path.\n * @param {object|null} schema The record's schema.\n * @param {(text: string) => string} translate The translate function.\n * @return {string} The text, or '' when the field is empty.\n */\nfunction plainTokenText(object, path, schema, translate) {\n\tconst value = readPath(object, path)\n\tif (value === null || value === undefined || value === '' || typeof value === 'object') {\n\t\treturn ''\n\t}\n\tconst property = schema && schema.properties && !path.includes('.') ? schema.properties[path] : null\n\tconst labels = property && (property.enumLabels || property['x-enum-labels'])\n\tif (labels && typeof labels === 'object' && typeof labels[String(value)] === 'string') {\n\t\treturn translate(labels[String(value)])\n\t}\n\treturn String(value)\n}\n\n/**\n * Fill a header meta template such as `\"via {channel}\"` or\n * `\"{amount|currency:EUR} · {cashier|user} · {at|longdate} {at|time}\"` from\n * a record.\n *\n * Every `{path}` token (a dotted path into the record) is replaced by its\n * value, an enum field by its label. `{path|format}` tokens format as a\n * record-list row does (the shared `fillTemplate`: date, datetime, weekday,\n * longdate, time, user, currency, relative, relativetime, days). A token\n * without a value drops itself and the separator before it, so a missing\n * cashier leaves \"€ 96,41 · 7 oktober\"; a line whose every token is empty\n * is '' (\"via \" is worse than nothing). A template without tokens is\n * returned as written.\n *\n * @param {string} template The template, already translated.\n * @param {object|null} object The record.\n * @param {Date|{now?: Date, schema?: object, translate?: Function, config?: object}} [options] The reference moment, or options: `schema` for enum labels, `translate` for those labels, `config` for the reporting currency.\n * @return {string} The line, or '' when nothing can be filled.\n * @spec openspec/changes/screens-header-meta-tokens/specs/detail-page-board-look/spec.md#requirement-the-header-meta-line-formats-users-long-dates-and-enum-labels\n */\nexport function resolveHeaderMeta(template, object, options = {}) {\n\tconst settings = options instanceof Date ? { now: options } : (options || {})\n\tconst now = settings.now || new Date()\n\tconst translate = typeof settings.translate === 'function' ? settings.translate : (text) => text\n\tif (typeof template !== 'string' || template.trim() === '') {\n\t\treturn ''\n\t}\n\tif (!/\\{[^}]+\\}/.test(template)) {\n\t\treturn template.trim()\n\t}\n\tif (!object || typeof object !== 'object') {\n\t\treturn ''\n\t}\n\tlet filled = 0\n\tconst line = joinTemplateParts(template, (token) => {\n\t\tconst text = token.includes('|')\n\t\t\t? fillTemplate(`{${token}}`, object, now, settings.config ? { config: settings.config } : null)\n\t\t\t: plainTokenText(object, token, settings.schema || null, translate)\n\t\tif (text !== '') {\n\t\t\tfilled += 1\n\t\t}\n\t\treturn text\n\t})\n\treturn filled === 0 ? '' : line.trim()\n}\n\n/**\n * Whether a widget definition is the record's activity (the History tab or\n * card): an audit-trail or timeline widget, or the `activity` integration.\n *\n * @param {object|null} widget The widget definition.\n * @return {boolean} True for an activity widget.\n * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md#requirement-history-is-the-last-tab\n */\nexport function isActivityWidget(widget) {\n\tif (!widget || typeof widget !== 'object') {\n\t\treturn false\n\t}\n\tif (widget.type === 'audit-trail' || widget.type === 'timeline') {\n\t\treturn true\n\t}\n\treturn widget.type === 'integration' && widget.integrationId === 'activity'\n}\n"],"names":[],"mappings":"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"}