@simmalugnt-se/payload-content-health 0.5.1 → 0.6.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.
@@ -1,14 +1,12 @@
1
- /* The work list. Colors come from Payload's theme; the accent is the blue the widget uses. */
1
+ /* The work list. Colors come from Payload's theme; tokens.css has the accent, red and amber. */
2
2
  .content-health-list {
3
- --content-health-accent: #2563eb;
4
3
  padding-bottom: calc(var(--base) * 2);
5
4
  }
6
5
 
7
- html[data-theme="dark"] .content-health-list {
8
- --content-health-accent: #60a5fa;
9
- }
10
-
11
6
  .content-health-list__header {
7
+ display: flex;
8
+ align-items: center;
9
+ gap: calc(var(--base) * 1.1);
12
10
  padding: calc(var(--base) * 1.5) 0 var(--base);
13
11
  }
14
12
 
@@ -16,108 +14,218 @@ html[data-theme="dark"] .content-health-list {
16
14
  margin: 0;
17
15
  }
18
16
 
17
+ .content-health-list__ring {
18
+ flex: none;
19
+ width: 5rem;
20
+ height: 5rem;
21
+ }
22
+
23
+ .content-health-list__ring .content-health__ring-track,
24
+ .content-health-list__ring-track {
25
+ stroke: var(--theme-elevation-100);
26
+ }
27
+
28
+ .content-health-list__ring .content-health__ring-ok {
29
+ stroke: var(--content-health-accent);
30
+ }
31
+
32
+ .content-health-list__ring .content-health__ring-value {
33
+ fill: var(--theme-text);
34
+ font-size: 24px;
35
+ font-weight: 600;
36
+ font-variant-numeric: tabular-nums;
37
+ }
38
+
19
39
  .content-health-list__intro {
20
- margin: calc(var(--base) * 0.5) 0 0;
21
- max-width: 44rem;
40
+ margin: calc(var(--base) * 0.25) 0 0;
41
+ color: var(--theme-elevation-600);
42
+ }
43
+
44
+ /* Category tabs: the selected one is filled with the accent tint and outlined in the accent. */
45
+ .content-health-list__tabs {
46
+ display: grid;
47
+ grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
48
+ gap: calc(var(--base) * 0.6);
49
+ margin-bottom: var(--base);
50
+ }
51
+
52
+ .content-health-list__tab {
53
+ display: flex;
54
+ flex-direction: column;
55
+ align-items: flex-start;
56
+ gap: calc(var(--base) * 0.15);
57
+ padding: calc(var(--base) * 0.6) calc(var(--base) * 0.75);
58
+ border: 1px solid var(--theme-elevation-150);
59
+ border-radius: var(--style-radius-m);
60
+ background: var(--theme-elevation-0);
22
61
  color: var(--theme-elevation-600);
62
+ font: inherit;
63
+ text-align: start;
64
+ cursor: pointer;
65
+ }
66
+
67
+ .content-health-list__tab:hover {
68
+ border-color: var(--theme-elevation-400);
69
+ }
70
+
71
+ .content-health-list__tab:focus-visible {
72
+ outline: 2px solid var(--content-health-accent);
73
+ outline-offset: 2px;
74
+ }
75
+
76
+ .content-health-list__tab--selected,
77
+ .content-health-list__tab--selected:hover {
78
+ border-color: var(--content-health-accent);
79
+ background: var(--content-health-accent-tint);
80
+ color: var(--theme-text);
81
+ }
82
+
83
+ .content-health-list__tab-name {
84
+ display: flex;
85
+ align-items: center;
86
+ gap: calc(var(--base) * 0.3);
87
+ font-size: 0.9em;
88
+ }
89
+
90
+ .content-health-list__tab--selected .content-health-list__tab-name {
91
+ font-weight: 600;
92
+ }
93
+
94
+ .content-health-list__tab-count {
95
+ color: var(--theme-text);
96
+ font-size: 1.6rem;
97
+ font-weight: 600;
98
+ line-height: 1.2;
99
+ font-variant-numeric: tabular-nums;
100
+ }
101
+
102
+ .content-health-list__dot {
103
+ width: 8px;
104
+ height: 8px;
105
+ border-radius: 50%;
106
+ background: var(--content-health-accent);
107
+ }
108
+
109
+ .content-health-list__dot--bad {
110
+ background: var(--content-health-bad);
111
+ }
112
+
113
+ .content-health-list__dot--warn {
114
+ background: var(--content-health-warn);
115
+ }
116
+
117
+ .content-health-list__dot--none {
118
+ background: var(--theme-elevation-400);
23
119
  }
24
120
 
25
121
  .content-health-list__filters {
26
122
  display: flex;
27
123
  flex-wrap: wrap;
28
- align-items: flex-end;
124
+ justify-content: flex-end;
29
125
  gap: calc(var(--base) * 0.75);
126
+ margin-bottom: var(--base);
30
127
  }
31
128
 
32
129
  .content-health-list__filter {
33
130
  display: flex;
131
+ flex: 0 1 12rem;
132
+ min-width: 0;
34
133
  flex-direction: column;
35
134
  gap: calc(var(--base) * 0.25);
36
135
  font-size: 0.9em;
136
+ color: var(--theme-text);
137
+ }
138
+
139
+ .content-health-list__filter > label {
37
140
  color: var(--theme-elevation-600);
38
141
  }
39
142
 
40
- .content-health-list__filter select {
41
- min-width: 12rem;
143
+ .content-health-list__filter .rs__control {
144
+ box-sizing: border-box;
145
+ min-height: calc(var(--base) * 1.6);
42
146
  height: calc(var(--base) * 1.6);
43
147
  padding: 0 calc(var(--base) * 0.5);
148
+ }
149
+
150
+ /* One section per check inside the selected category. */
151
+ .content-health-list__panel {
44
152
  border: 1px solid var(--theme-elevation-150);
45
- border-radius: var(--style-radius-s);
46
- background: var(--theme-input-bg, var(--theme-elevation-0));
47
- color: var(--theme-text);
48
- font: inherit;
153
+ border-radius: var(--style-radius-m);
154
+ background: var(--theme-elevation-0);
155
+ padding: 0 calc(var(--base) * 1.25);
156
+ }
157
+
158
+ .content-health-list__section {
159
+ padding: var(--base) 0;
160
+ }
161
+
162
+ .content-health-list__section + .content-health-list__section {
163
+ border-top: 1px solid var(--theme-elevation-100);
49
164
  }
50
165
 
51
- .content-health-list__summary {
166
+ .content-health-list__section-head {
52
167
  display: flex;
53
168
  flex-wrap: wrap;
54
- gap: calc(var(--base) * 0.5) var(--base);
55
169
  align-items: center;
56
- justify-content: space-between;
57
- min-height: 1.5em;
58
- margin: var(--base) 0 calc(var(--base) * 0.5);
59
- color: var(--theme-elevation-600);
170
+ gap: calc(var(--base) * 0.5);
171
+ margin-bottom: calc(var(--base) * 0.4);
60
172
  }
61
173
 
62
- .content-health-list__summary p {
174
+ .content-health-list__section-head h2 {
63
175
  margin: 0;
176
+ font-size: 1rem;
177
+ font-weight: 600;
64
178
  }
65
179
 
66
- /* One grid for all rows, so the columns line up; each row takes its tracks through subgrid. */
67
- .content-health-list__rows {
68
- display: grid;
69
- grid-template-columns: minmax(12rem, 1fr) minmax(12rem, 1.5fr) auto;
70
- margin: 0;
71
- padding: 0;
72
- list-style: none;
73
- border: 1px solid var(--theme-elevation-150);
74
- border-radius: var(--style-radius-m);
75
- background: var(--theme-elevation-0);
180
+ .content-health-list__batch {
181
+ margin-inline-start: auto;
76
182
  }
77
183
 
78
- .content-health-list__row {
79
- display: grid;
80
- grid-column: 1 / -1;
81
- grid-template-columns: subgrid;
184
+ .content-health-list__section-head .btn {
185
+ display: inline-flex;
82
186
  align-items: center;
83
- gap: calc(var(--base) * 0.75);
84
- padding: calc(var(--base) * 0.6) calc(var(--base) * 0.8);
187
+ justify-content: center;
188
+ min-height: calc(var(--base) * 1.6);
189
+ padding-inline: calc(var(--base) * 0.6);
85
190
  }
86
191
 
87
- .content-health-list__row + .content-health-list__row {
88
- border-top: 1px solid var(--theme-elevation-100);
192
+ .content-health-list__items {
193
+ margin: 0;
194
+ padding: 0;
195
+ list-style: none;
89
196
  }
90
197
 
91
- .content-health-list__what,
92
- .content-health-list__document {
198
+ .content-health-list__item {
93
199
  display: flex;
94
200
  flex-wrap: wrap;
95
201
  align-items: baseline;
96
202
  gap: calc(var(--base) * 0.25) calc(var(--base) * 0.5);
97
- min-width: 0;
203
+ padding: calc(var(--base) * 0.25) 0;
98
204
  }
99
205
 
100
- .content-health-list__what {
101
- flex-direction: column;
102
- align-items: flex-start;
206
+ .content-health-list__item a {
207
+ color: var(--content-health-accent-text);
208
+ text-decoration: none;
103
209
  }
104
210
 
105
- .content-health-list__check {
106
- font-weight: 500;
211
+ .content-health-list__item a:hover {
212
+ text-decoration: underline;
107
213
  }
108
214
 
109
- .content-health-list__detail {
215
+ /* The assistant link sits at the end of the row and stays quiet next to the batch button. */
216
+ .content-health-list__item a.content-health-list__fix {
217
+ margin-inline-start: auto;
110
218
  color: var(--theme-elevation-600);
111
219
  font-size: 0.9em;
112
220
  }
113
221
 
114
- .content-health-list__document a {
115
- color: var(--content-health-accent);
116
- text-decoration: none;
222
+ .content-health-list__item a.content-health-list__fix:hover {
223
+ color: var(--content-health-accent-text);
117
224
  }
118
225
 
119
- .content-health-list__document a:hover {
120
- text-decoration: underline;
226
+ .content-health-list__detail {
227
+ color: var(--theme-elevation-600);
228
+ font-size: 0.9em;
121
229
  }
122
230
 
123
231
  .content-health-list__pill {
@@ -133,11 +241,9 @@ html[data-theme="dark"] .content-health-list {
133
241
  text-transform: none;
134
242
  }
135
243
 
136
- .content-health-list__actions {
137
- justify-self: end;
138
- }
139
-
140
244
  .content-health-list__empty {
245
+ margin: 0;
246
+ padding: var(--base) 0;
141
247
  color: var(--theme-elevation-600);
142
248
  }
143
249
 
@@ -147,11 +253,7 @@ html[data-theme="dark"] .content-health-list {
147
253
  }
148
254
 
149
255
  @media (max-width: 768px) {
150
- .content-health-list__rows {
151
- grid-template-columns: 1fr;
152
- }
153
-
154
- .content-health-list__actions {
155
- justify-self: start;
256
+ .content-health-list__panel {
257
+ padding: 0 var(--base);
156
258
  }
157
259
  }
@@ -1,7 +1,5 @@
1
1
  /* Laid out like Payload's collection cards: a section heading, then the content. */
2
- /* The accent is the blue the editor assistant and visual editing use. */
3
2
  .content-health {
4
- --content-health-accent: #2563eb;
5
3
  display: flex;
6
4
  flex-direction: column;
7
5
  gap: var(--base);
@@ -17,7 +15,7 @@
17
15
  .content-health__panel {
18
16
  display: flex;
19
17
  flex-direction: column;
20
- gap: calc(var(--base) * 1.25);
18
+ gap: var(--base);
21
19
  padding: calc(var(--base) * 1.25);
22
20
  border: 1px solid var(--theme-elevation-150);
23
21
  border-radius: var(--style-radius-m);
@@ -27,7 +25,12 @@
27
25
  .content-health__overview {
28
26
  display: flex;
29
27
  align-items: center;
30
- gap: calc(var(--base) * 1.25);
28
+ gap: calc(var(--base) * 1.1);
29
+ }
30
+
31
+ .content-health__overview-text {
32
+ flex: 1;
33
+ min-width: 0;
31
34
  }
32
35
 
33
36
  .content-health__overall {
@@ -41,30 +44,21 @@
41
44
  color: var(--theme-elevation-600);
42
45
  }
43
46
 
44
- /* Rings */
47
+ /* The ring */
45
48
  .content-health__ring {
46
49
  flex: none;
47
- width: 3.75rem;
48
- height: 3.75rem;
49
- }
50
-
51
- .content-health__ring--large {
52
- width: 7.5rem;
53
- height: 7.5rem;
50
+ width: 5rem;
51
+ height: 5rem;
54
52
  }
55
53
 
56
54
  .content-health__ring-track {
57
- stroke: var(--theme-elevation-150);
55
+ stroke: var(--theme-elevation-100);
58
56
  }
59
57
 
60
58
  .content-health__ring-ok {
61
59
  stroke: var(--content-health-accent);
62
60
  }
63
61
 
64
- html[data-theme="dark"] .content-health {
65
- --content-health-accent: #60a5fa;
66
- }
67
-
68
62
  .content-health__ring-value {
69
63
  fill: var(--theme-text);
70
64
  /* viewBox units: scales with the ring. */
@@ -73,98 +67,79 @@ html[data-theme="dark"] .content-health {
73
67
  font-variant-numeric: tabular-nums;
74
68
  }
75
69
 
76
- /* One tile per check, as many columns as fit. */
77
- .content-health__tiles {
78
- display: grid;
79
- grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
80
- gap: 12px;
70
+ /* One row per category: name, bar and status. The whole row links to its part of the work list. */
71
+ .content-health__categories {
81
72
  margin: 0;
82
73
  padding: 0;
83
74
  list-style: none;
84
75
  }
85
76
 
86
- .content-health__tile {
87
- display: flex;
88
- flex-direction: column;
89
- gap: calc(var(--base) * 0.75);
90
- padding: var(--base);
91
- /* Same as Payload's collection cards. */
92
- border: 1px solid var(--theme-elevation-150);
93
- border-radius: var(--style-radius-m);
94
- background: var(--theme-elevation-50);
77
+ .content-health__categories li + li {
78
+ border-top: 1px solid var(--theme-elevation-100);
95
79
  }
96
80
 
97
- .content-health__tile-head {
98
- display: flex;
81
+ .content-health__category {
82
+ display: grid;
83
+ grid-template-columns: 8rem minmax(0, 1fr) 9rem;
99
84
  align-items: center;
100
- gap: calc(var(--base) * 0.75);
85
+ gap: var(--base);
86
+ padding: calc(var(--base) * 0.6) 0;
87
+ color: var(--theme-text);
88
+ text-decoration: none;
101
89
  }
102
90
 
103
- .content-health__tile-title {
104
- margin: 0;
105
- font-size: 1rem;
106
- line-height: 1.3;
91
+ .content-health__category:hover .content-health__category-name {
92
+ text-decoration: underline;
107
93
  }
108
94
 
109
- .content-health__tile-status {
110
- margin: calc(var(--base) * 0.15) 0 0;
111
- color: var(--theme-elevation-600);
112
- font-variant-numeric: tabular-nums;
95
+ .content-health__bar {
96
+ height: 6px;
97
+ overflow: hidden;
98
+ border-radius: 3px;
99
+ background: var(--theme-elevation-100);
113
100
  }
114
101
 
115
- .content-health__rows {
116
- display: flex;
117
- flex-direction: column;
118
- gap: calc(var(--base) * 0.6);
119
- margin: 0;
120
- padding: calc(var(--base) * 0.6) 0 0;
121
- border-top: 1px solid var(--theme-elevation-100);
122
- list-style: none;
102
+ .content-health__bar-fill {
103
+ display: block;
104
+ height: 100%;
105
+ border-radius: 3px;
106
+ background: var(--content-health-accent);
107
+ transition: width 0.4s ease-out;
123
108
  }
124
109
 
125
- .content-health__row {
126
- display: flex;
127
- flex-direction: column;
128
- gap: calc(var(--base) * 0.25);
110
+ .content-health__bar-fill--bad {
111
+ background: var(--content-health-bad);
112
+ /* A bar at 0% is still a mark: the row reads as "nothing in order", not "no bar". */
113
+ min-width: 4px;
129
114
  }
130
115
 
131
- .content-health__locales {
132
- display: flex;
133
- flex-wrap: wrap;
134
- gap: calc(var(--base) * 0.25);
135
- margin: 0;
136
- padding: 0;
137
- list-style: none;
116
+ .content-health__bar-fill--warn {
117
+ background: var(--content-health-warn);
138
118
  }
139
119
 
140
- .content-health__locales--stacked {
141
- flex-direction: column;
120
+ .content-health__bar-fill--none {
121
+ width: 0 !important;
142
122
  }
143
123
 
144
- .content-health__locale {
145
- display: flex;
146
- align-items: baseline;
147
- gap: calc(var(--base) * 0.3);
124
+ .content-health__category-status {
125
+ text-align: end;
126
+ font-variant-numeric: tabular-nums;
148
127
  }
149
128
 
150
- .content-health__pill {
151
- padding: 0 calc(var(--base) * 0.3);
152
- border-radius: var(--style-radius-s);
153
- background: var(--theme-elevation-150);
154
- color: var(--theme-elevation-800);
155
- font-size: 0.75em;
156
- text-decoration: none;
157
- text-transform: uppercase;
129
+ .content-health__tone--ok {
130
+ color: var(--content-health-accent-text);
158
131
  }
159
132
 
160
- .content-health__pill:hover {
161
- background: var(--theme-elevation-250);
133
+ .content-health__tone--warn {
134
+ color: var(--content-health-warn-text);
162
135
  }
163
136
 
164
- .content-health__detail,
165
- .content-health__more {
137
+ .content-health__tone--bad {
138
+ color: var(--content-health-bad-text);
139
+ }
140
+
141
+ .content-health__tone--none {
166
142
  color: var(--theme-elevation-500);
167
- font-size: 0.9em;
168
143
  }
169
144
 
170
145
  .content-health__problems {
@@ -178,12 +153,23 @@ html[data-theme="dark"] .content-health {
178
153
  }
179
154
 
180
155
  @media (max-width: 768px) {
181
- .content-health__ring--large {
182
- width: 5.5rem;
183
- height: 5.5rem;
156
+ .content-health__overview {
157
+ flex-wrap: wrap;
158
+ }
159
+
160
+ .content-health__category {
161
+ grid-template-columns: 1fr auto;
162
+ gap: calc(var(--base) * 0.25) var(--base);
163
+ }
164
+
165
+ .content-health__bar {
166
+ grid-column: 1 / -1;
167
+ grid-row: 2;
184
168
  }
185
169
  }
186
170
 
187
- .content-health__more a {
188
- color: inherit;
171
+ @media (prefers-reduced-motion: reduce) {
172
+ .content-health__bar-fill {
173
+ transition: none;
174
+ }
189
175
  }
@@ -1,9 +1,10 @@
1
1
  import type { AdminViewServerProps } from "payload";
2
+ import "../styles/tokens.css";
2
3
  import "../styles/view.css";
3
4
  /**
4
- * The work list: every finding with filters on check, collection and locale. Each row opens the
5
- * document in the finding's locale with the field selected; where the editor assistant works, a
6
- * second link opens it with a ready prompt too. Scans once per visit; the filters work in the browser.
5
+ * The work list: every finding, one category at a time. Each row opens the document in the
6
+ * finding's locale with the field selected; where the editor assistant works, a second link opens it
7
+ * with a ready prompt too. Scans once per visit; the tabs and filters work in the browser.
7
8
  * Only for visitors who may use Admin: Payload leaves custom views public.
8
9
  */
9
10
  export declare function ContentHealthView({ initPageResult, params, searchParams, }: AdminViewServerProps): Promise<import("react").JSX.Element>;
@@ -2,16 +2,20 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { DefaultTemplate } from "@payloadcms/next/templates";
3
3
  import { Gutter, SetStepNav } from "@payloadcms/ui";
4
4
  import { redirect } from "next/navigation";
5
- import { ASK, BATCH_ASK, CHECK_LABELS, findingDetail, LIST_COPY, uiLanguage } from "../copy.js";
5
+ import { categoryOf, categoryStats, categoryTone, defaultCategory, } from "../categories.js";
6
+ import { ASK, BATCH_ASK, CATEGORY_LABELS, CHECK_LABELS, COPY, findingDetail, LIST_COPY, uiLanguage, } from "../copy.js";
7
+ import { checkStats, countDocuments } from "../group.js";
6
8
  import { accessRedirect, assistantCollections, BATCH_CHECKS, batchCollections, batchTarget, editURL, readFilters, } from "../links.js";
7
9
  import { scanContentHealth } from "../scan.js";
8
10
  import { CHECK_IDS } from "../types.js";
9
- import { WorkList } from "./WorkList.js";
11
+ import { Ring } from "../widget/Ring.js";
12
+ import { WorkList, } from "./WorkList.js";
13
+ import "../styles/tokens.css";
10
14
  import "../styles/view.css";
11
15
  /**
12
- * The work list: every finding with filters on check, collection and locale. Each row opens the
13
- * document in the finding's locale with the field selected; where the editor assistant works, a
14
- * second link opens it with a ready prompt too. Scans once per visit; the filters work in the browser.
16
+ * The work list: every finding, one category at a time. Each row opens the document in the
17
+ * finding's locale with the field selected; where the editor assistant works, a second link opens it
18
+ * with a ready prompt too. Scans once per visit; the tabs and filters work in the browser.
15
19
  * Only for visitors who may use Admin: Payload leaves custom views public.
16
20
  */
17
21
  export async function ContentHealthView({ initPageResult, params, searchParams, }) {
@@ -27,6 +31,7 @@ export async function ContentHealthView({ initPageResult, params, searchParams,
27
31
  }
28
32
  const language = uiLanguage(req.i18n?.language);
29
33
  const copy = LIST_COPY[language];
34
+ const common = COPY[language];
30
35
  const { config } = req.payload;
31
36
  const adminRoute = config.routes.admin;
32
37
  const result = await scanContentHealth({ payload: req.payload, user: req.user });
@@ -55,13 +60,29 @@ export async function ContentHealthView({ initPageResult, params, searchParams,
55
60
  ...(batchable ? { batchTarget: batchTarget(finding) } : {}),
56
61
  };
57
62
  });
58
- const checks = result.checks.map((check) => ({
59
- value: check,
60
- label: CHECK_LABELS[language][check],
63
+ const stats = checkStats(result.checks, result.checked, result.findings);
64
+ const categoryList = categoryStats(stats);
65
+ const categories = categoryList.map((stat) => ({
66
+ id: stat.category,
67
+ label: CATEGORY_LABELS[language][stat.category],
68
+ tone: categoryTone(stat),
61
69
  }));
70
+ // A link may name a category or, as older ones do, a check.
71
+ const requested = readFilters(searchParams);
72
+ const named = requested.category ?? (requested.check ? categoryOf(requested.check) : undefined);
73
+ const category = categories.some((entry) => entry.id === named)
74
+ ? named
75
+ : defaultCategory(categoryList);
76
+ const initialFilters = {
77
+ ...(category ? { category } : {}),
78
+ ...(requested.collection ? { collection: requested.collection } : {}),
79
+ ...(requested.locale ? { locale: requested.locale } : {}),
80
+ };
81
+ const total = stats.reduce((sum, stat) => sum + stat.checked, 0);
82
+ const failing = stats.reduce((sum, stat) => sum + stat.failing, 0);
62
83
  const collections = Object.keys(result.documents).map((slug) => ({
63
84
  value: slug,
64
- label: label(req.payload.collections[slug]?.config.labels?.plural, language) ?? slug,
85
+ label: label(config.collections.find((collection) => collection.slug === slug)?.labels?.plural, language) ?? slug,
65
86
  }));
66
87
  const locales = config.localization
67
88
  ? config.localization.locales.map((locale) => ({
@@ -70,7 +91,9 @@ export async function ContentHealthView({ initPageResult, params, searchParams,
70
91
  }))
71
92
  : [];
72
93
  // Payload wraps only its own views in the Admin shell; a custom view brings nav and header itself.
73
- return (_jsx(DefaultTemplate, { i18n: req.i18n, locale: locale, params: params, payload: req.payload, permissions: permissions, req: req, searchParams: searchParams, user: req.user ?? undefined, viewActions: req.payload.config.admin?.components?.actions, visibleEntities: visibleEntities, children: _jsxs(Gutter, { className: "content-health-list", children: [_jsx(SetStepNav, { nav: [{ label: copy.title }] }), _jsxs("header", { className: "content-health-list__header", children: [_jsx("h1", { children: copy.title }), _jsx("p", { className: "content-health-list__intro", children: copy.intro })] }), _jsx(WorkList, { adminRoute: adminRoute, batchAsk: batchAsk, checks: checks, collections: collections, initialFilters: readFilters(searchParams), language: language, locales: locales, rows: rows }), result.problems.length ? (_jsx("ul", { className: "content-health-list__problems", children: result.problems.map((problem) => (_jsx("li", { children: problem }, problem))) })) : null] }) }));
94
+ return (_jsx(DefaultTemplate, { i18n: req.i18n, locale: locale, params: params, payload: req.payload, permissions: permissions, req: req, searchParams: searchParams, user: req.user ?? undefined, viewActions: req.payload.config.admin?.components?.actions, visibleEntities: visibleEntities, children: _jsxs(Gutter, { className: "content-health-list", children: [_jsx(SetStepNav, { nav: [{ label: copy.title }] }), _jsxs("header", { className: "content-health-list__header", children: [_jsx(Ring, { className: "content-health-list__ring", failing: failing, label: common.ok(total - failing, total), total: total }), _jsxs("div", { children: [_jsx("h1", { children: copy.title }), _jsx("p", { className: "content-health-list__intro", children: result.findings.length
95
+ ? copy.summary(result.findings.length, countDocuments(result.findings))
96
+ : copy.none })] })] }), _jsx(WorkList, { adminRoute: adminRoute, batchAsk: batchAsk, categories: categories, collections: collections, initialFilters: initialFilters, language: language, locales: locales, rows: rows }), result.problems.length ? (_jsx("ul", { className: "content-health-list__problems", children: result.problems.map((problem) => (_jsx("li", { children: problem }, problem))) })) : null] }) }));
74
97
  }
75
98
  /** By check, then document, then locale, so a document's locales sit together. */
76
99
  function sortFindings(findings) {
@@ -1,10 +1,16 @@
1
+ import { type CategoryId, type Tone } from "../categories.ts";
1
2
  import { type UiLanguage } from "../copy.ts";
2
3
  import { type ListFilters } from "../links.ts";
3
- import type { CheckId } from "../types.ts";
4
+ import { type CheckId } from "../types.ts";
4
5
  export type WorkListOption = {
5
6
  value: string;
6
7
  label: string;
7
8
  };
9
+ export type WorkListCategory = {
10
+ id: CategoryId;
11
+ label: string;
12
+ tone: Tone;
13
+ };
8
14
  /** One finding, ready to show: labels and links are made on the server. */
9
15
  export type WorkListRow = {
10
16
  key: string;
@@ -23,14 +29,15 @@ export type WorkListRow = {
23
29
  batchTarget?: string;
24
30
  };
25
31
  /**
26
- * The findings with filters on check, collection and locale. Filters apply in the browser and are
27
- * kept in the address, so the view can be linked to and reloaded with them.
32
+ * The findings, one category at a time and one section per check. The category and the collection
33
+ * and locale filters apply in the browser and are kept in the address, so the view can be linked to
34
+ * and reloaded with them.
28
35
  */
29
- export declare function WorkList({ adminRoute, batchAsk, checks, collections, initialFilters, language, locales, rows, }: {
36
+ export declare function WorkList({ adminRoute, batchAsk, categories, collections, initialFilters, language, locales, rows, }: {
30
37
  adminRoute: string;
31
38
  /** The assistant's prompt per check that it can fix in many documents at once. */
32
39
  batchAsk: Partial<Record<CheckId, string>>;
33
- checks: WorkListOption[];
40
+ categories: WorkListCategory[];
34
41
  collections: WorkListOption[];
35
42
  initialFilters: ListFilters;
36
43
  language: UiLanguage;