@simmalugnt-se/payload-content-health 0.5.2 → 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,15 +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
- --content-health-control-height: calc(var(--base) * 1.6);
5
3
  padding-bottom: calc(var(--base) * 2);
6
4
  }
7
5
 
8
- html[data-theme="dark"] .content-health-list {
9
- --content-health-accent: #60a5fa;
10
- }
11
-
12
6
  .content-health-list__header {
7
+ display: flex;
8
+ align-items: center;
9
+ gap: calc(var(--base) * 1.1);
13
10
  padding: calc(var(--base) * 1.5) 0 var(--base);
14
11
  }
15
12
 
@@ -17,22 +14,121 @@ html[data-theme="dark"] .content-health-list {
17
14
  margin: 0;
18
15
  }
19
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
+
20
39
  .content-health-list__intro {
21
- margin: calc(var(--base) * 0.5) 0 0;
22
- 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);
23
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);
24
119
  }
25
120
 
26
121
  .content-health-list__filters {
27
122
  display: flex;
28
123
  flex-wrap: wrap;
29
- align-items: flex-end;
124
+ justify-content: flex-end;
30
125
  gap: calc(var(--base) * 0.75);
126
+ margin-bottom: var(--base);
31
127
  }
32
128
 
33
129
  .content-health-list__filter {
34
130
  display: flex;
35
- flex: 1 1 10rem;
131
+ flex: 0 1 12rem;
36
132
  min-width: 0;
37
133
  flex-direction: column;
38
134
  gap: calc(var(--base) * 0.25);
@@ -46,97 +142,90 @@ html[data-theme="dark"] .content-health-list {
46
142
 
47
143
  .content-health-list__filter .rs__control {
48
144
  box-sizing: border-box;
49
- min-height: var(--content-health-control-height);
50
- height: var(--content-health-control-height);
145
+ min-height: calc(var(--base) * 1.6);
146
+ height: calc(var(--base) * 1.6);
51
147
  padding: 0 calc(var(--base) * 0.5);
52
148
  }
53
149
 
54
- .content-health-list__filters .btn,
55
- .content-health-list__summary .btn,
56
- .content-health-list__actions .btn {
57
- display: inline-flex;
58
- align-items: center;
59
- justify-content: center;
60
- box-sizing: border-box;
61
- min-height: var(--content-health-control-height);
62
- padding-inline: calc(var(--base) * 0.6);
150
+ /* One section per check inside the selected category. */
151
+ .content-health-list__panel {
152
+ border: 1px solid var(--theme-elevation-150);
153
+ border-radius: var(--style-radius-m);
154
+ background: var(--theme-elevation-0);
155
+ padding: 0 calc(var(--base) * 1.25);
63
156
  }
64
157
 
65
- .content-health-list__clear {
66
- flex-shrink: 0;
67
- margin-inline-start: auto;
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);
68
164
  }
69
165
 
70
- .content-health-list__summary {
166
+ .content-health-list__section-head {
71
167
  display: flex;
72
168
  flex-wrap: wrap;
73
- gap: calc(var(--base) * 0.5) var(--base);
74
169
  align-items: center;
75
- justify-content: space-between;
76
- min-height: 1.5em;
77
- margin: var(--base) 0 calc(var(--base) * 0.75);
78
- color: var(--theme-elevation-600);
170
+ gap: calc(var(--base) * 0.5);
171
+ margin-bottom: calc(var(--base) * 0.4);
79
172
  }
80
173
 
81
- .content-health-list__summary p {
174
+ .content-health-list__section-head h2 {
82
175
  margin: 0;
176
+ font-size: 1rem;
177
+ font-weight: 600;
83
178
  }
84
179
 
85
- /* One grid for all rows, so the columns line up; each row takes its tracks through subgrid. */
86
- .content-health-list__rows {
87
- display: grid;
88
- grid-template-columns: minmax(12rem, 1fr) minmax(12rem, 1.5fr) auto;
89
- margin: 0;
90
- padding: 0;
91
- list-style: none;
92
- border: 1px solid var(--theme-elevation-150);
93
- border-radius: var(--style-radius-m);
94
- background: var(--theme-elevation-0);
180
+ .content-health-list__batch {
181
+ margin-inline-start: auto;
95
182
  }
96
183
 
97
- .content-health-list__row {
98
- display: grid;
99
- grid-column: 1 / -1;
100
- grid-template-columns: subgrid;
184
+ .content-health-list__section-head .btn {
185
+ display: inline-flex;
101
186
  align-items: center;
102
- gap: calc(var(--base) * 0.75);
103
- 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);
104
190
  }
105
191
 
106
- .content-health-list__row + .content-health-list__row {
107
- border-top: 1px solid var(--theme-elevation-100);
192
+ .content-health-list__items {
193
+ margin: 0;
194
+ padding: 0;
195
+ list-style: none;
108
196
  }
109
197
 
110
- .content-health-list__what,
111
- .content-health-list__document {
198
+ .content-health-list__item {
112
199
  display: flex;
113
200
  flex-wrap: wrap;
114
201
  align-items: baseline;
115
202
  gap: calc(var(--base) * 0.25) calc(var(--base) * 0.5);
116
- min-width: 0;
203
+ padding: calc(var(--base) * 0.25) 0;
117
204
  }
118
205
 
119
- .content-health-list__what {
120
- flex-direction: column;
121
- align-items: flex-start;
206
+ .content-health-list__item a {
207
+ color: var(--content-health-accent-text);
208
+ text-decoration: none;
122
209
  }
123
210
 
124
- .content-health-list__check {
125
- font-weight: 500;
211
+ .content-health-list__item a:hover {
212
+ text-decoration: underline;
126
213
  }
127
214
 
128
- .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;
129
218
  color: var(--theme-elevation-600);
130
219
  font-size: 0.9em;
131
220
  }
132
221
 
133
- .content-health-list__document a {
134
- color: var(--content-health-accent);
135
- text-decoration: none;
222
+ .content-health-list__item a.content-health-list__fix:hover {
223
+ color: var(--content-health-accent-text);
136
224
  }
137
225
 
138
- .content-health-list__document a:hover {
139
- text-decoration: underline;
226
+ .content-health-list__detail {
227
+ color: var(--theme-elevation-600);
228
+ font-size: 0.9em;
140
229
  }
141
230
 
142
231
  .content-health-list__pill {
@@ -152,11 +241,9 @@ html[data-theme="dark"] .content-health-list {
152
241
  text-transform: none;
153
242
  }
154
243
 
155
- .content-health-list__actions {
156
- justify-self: end;
157
- }
158
-
159
244
  .content-health-list__empty {
245
+ margin: 0;
246
+ padding: var(--base) 0;
160
247
  color: var(--theme-elevation-600);
161
248
  }
162
249
 
@@ -166,11 +253,7 @@ html[data-theme="dark"] .content-health-list {
166
253
  }
167
254
 
168
255
  @media (max-width: 768px) {
169
- .content-health-list__rows {
170
- grid-template-columns: 1fr;
171
- }
172
-
173
- .content-health-list__actions {
174
- justify-self: start;
256
+ .content-health-list__panel {
257
+ padding: 0 var(--base);
175
258
  }
176
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,10 +60,26 @@ 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
85
  label: label(config.collections.find((collection) => collection.slug === slug)?.labels?.plural, language) ?? slug,
@@ -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;