@geekity/cms 0.10.0 → 0.11.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 (64) hide show
  1. package/README.md +15 -3
  2. package/admin/components/fields.njk +55 -12
  3. package/admin/layouts/base.njk +5 -1
  4. package/admin/layouts/settings-page.njk +7 -2
  5. package/admin/layouts/shell.njk +1 -1
  6. package/admin/pages/account/forgot.njk +1 -1
  7. package/admin/pages/account/login.njk +1 -1
  8. package/admin/pages/account/reset.njk +4 -2
  9. package/admin/pages/account/setup.njk +1 -1
  10. package/admin/pages/dashboard/home.njk +1 -0
  11. package/admin/pages/documents/list.njk +1 -0
  12. package/admin/pages/documents/taxonomy.njk +1 -0
  13. package/admin/pages/federation/followers.njk +3 -0
  14. package/admin/pages/federation/settings.njk +5 -0
  15. package/admin/pages/media/library.njk +39 -0
  16. package/admin/pages/navigation/menus.njk +6 -1
  17. package/admin/pages/settings/discussion.njk +7 -0
  18. package/admin/pages/settings/email.njk +10 -0
  19. package/admin/pages/settings/general.njk +7 -0
  20. package/admin/pages/settings/permalinks.njk +6 -0
  21. package/admin/pages/settings/reading.njk +7 -0
  22. package/admin/pages/tools/content-index.njk +1 -0
  23. package/admin/pages/tools/personal-data.njk +5 -0
  24. package/admin/pages/users/edit.njk +7 -5
  25. package/admin/pages/users/list.njk +1 -0
  26. package/admin/pages/users/new.njk +5 -1
  27. package/admin/static/admin-bar.css +16 -0
  28. package/admin/static/admin.css +103 -3
  29. package/dist/admin/documents.d.ts.map +1 -1
  30. package/dist/admin/documents.js +18 -0
  31. package/dist/admin/documents.js.map +1 -1
  32. package/dist/admin/media.d.ts +13 -2
  33. package/dist/admin/media.d.ts.map +1 -1
  34. package/dist/admin/media.js +58 -3
  35. package/dist/admin/media.js.map +1 -1
  36. package/dist/admin/store.d.ts +1 -1
  37. package/dist/admin/store.d.ts.map +1 -1
  38. package/dist/admin/store.js +1 -1
  39. package/dist/admin/store.js.map +1 -1
  40. package/dist/admin/uploads.d.ts +5 -2
  41. package/dist/admin/uploads.d.ts.map +1 -1
  42. package/dist/admin/uploads.js +6 -2
  43. package/dist/admin/uploads.js.map +1 -1
  44. package/dist/config.d.ts +10 -0
  45. package/dist/config.d.ts.map +1 -1
  46. package/dist/config.js +1 -0
  47. package/dist/config.js.map +1 -1
  48. package/dist/federation/article.d.ts.map +1 -1
  49. package/dist/federation/article.js +37 -1
  50. package/dist/federation/article.js.map +1 -1
  51. package/dist/images/alt-text.d.ts +51 -0
  52. package/dist/images/alt-text.d.ts.map +1 -0
  53. package/dist/images/alt-text.js +113 -0
  54. package/dist/images/alt-text.js.map +1 -0
  55. package/dist/images/markup.d.ts +11 -0
  56. package/dist/images/markup.d.ts.map +1 -1
  57. package/dist/images/markup.js +12 -0
  58. package/dist/images/markup.js.map +1 -1
  59. package/package.json +8 -8
  60. package/themes/default/README.md +33 -0
  61. package/themes/default/layouts/post.njk +10 -1
  62. package/themes/default/partials/comment-form.njk +33 -14
  63. package/themes/default/partials/contact-form.njk +32 -14
  64. package/themes/default/static/style.css +109 -24
package/README.md CHANGED
@@ -1676,6 +1676,7 @@ shadow the login form.
1676
1676
  | `/admin/media` | Everything under `content/uploads`, with the URL, the Markdown and what uses it. |
1677
1677
  | `/admin/media/upload` | `POST` only. Stores one file by the rules the editor's upload enforces. |
1678
1678
  | `/admin/media/delete` | `POST` only. Deletes one upload, asking first when a document points at it. |
1679
+ | `/admin/media/alt` | `POST` only. Sets a picture's alt text, or marks it decorative, in `media.json`. |
1679
1680
  | `/admin/comments` | Pending, approved and spam, with approve, spam, delete and reply. |
1680
1681
  | `/admin/comments/moderate` | `POST` only. Approves one comment, files it as spam, or deletes it. |
1681
1682
  | `/admin/comments/reply` | `POST` only. Posts an approved reply under the comment it answers. |
@@ -1930,8 +1931,9 @@ written by an Eleventy build is on the screen without a restart, and one
1930
1931
  removed the same way is off it. The only question the index is asked is which
1931
1932
  documents mention a URL, which is the one thing a directory cannot answer.
1932
1933
 
1933
- Each row carries the public URL and the ready-made Markdown — `![name](url)`
1934
- for a picture and `[name](url)` for anything else, from the same function the
1934
+ Each row carries the public URL and the ready-made Markdown — `![alt](url)`
1935
+ for a picture, with its alt text from the library or empty when it has none or
1936
+ is decorative, and `[name](url)` for anything else, from the same function the
1935
1937
  editor's upload control pastes from — as readonly text fields, which select and
1936
1938
  copy on their own. `admin/static/copy.js` reveals a Copy button beside each and
1937
1939
  does nothing else, so the screen is complete before it loads and a browser
@@ -1956,7 +1958,17 @@ name a file elsewhere in the content directory.
1956
1958
  content-relative path once the original is gone. The original under
1957
1959
  `content/uploads` is the only source of truth (decision-10) and anything
1958
1960
  generated from it is derived state that must not outlive it; the hook defaults
1959
- to `removeImageVariants`, which takes the file's derived images with it.
1961
+ to `removeImageVariants`, which takes the file's derived images with it. The
1962
+ file's entry in `content/_data/media.json` goes too.
1963
+
1964
+ A picture's row also carries its alt text: a text field, a Decorative checkbox
1965
+ that wins over the text, and a line saying which of the two the library holds
1966
+ or that it holds neither. The form posts to `/admin/media/alt`, which writes
1967
+ `content/_data/media.json` atomically, keeping every other entry. The file is
1968
+ the truth (decision-9) and is read on each request, so an entry written by hand
1969
+ shows up without a restart. `readAltTexts(contentDir)` and
1970
+ `undescribedImages(html, library)` in `src/images/alt-text.ts` are what the
1971
+ editor's publish check and the federated `Image` attachments read.
1960
1972
 
1961
1973
  ### Image variants
1962
1974
 
@@ -34,9 +34,9 @@
34
34
  with. `errorHtml` is the rare second thing a field has to say — the Reading
35
35
  page's "that page is no longer published" — already written as its own
36
36
  paragraph. #}
37
- {% macro _below(hint, error, errorHtml) %}
37
+ {% macro _below(id, hint, error, errorHtml) %}
38
38
  {% if error %}
39
- <p class="admin-field-error">{{ error }}</p>
39
+ <p class="admin-field-error" id="{{ id }}-error">{{ error }}</p>
40
40
  {% endif %}
41
41
  {% if errorHtml %}
42
42
  {{ errorHtml | safe }}
@@ -46,6 +46,14 @@
46
46
  {% endif %}
47
47
  {% endmacro %}
48
48
 
49
+ {# What a control says about itself when the form refused it: that it is
50
+ invalid, and which paragraph says why, so a screen reader reads the message
51
+ with the field rather than leaving it to be found. `errorHtml` is not named
52
+ here: it is a standing note about the stored value, not a refusal. #}
53
+ {% macro _invalid(id, error) -%}
54
+ {% if error %} aria-invalid="true" aria-describedby="{{ id }}-error"{% endif %}
55
+ {%- endmacro %}
56
+
49
57
  {# A box somebody types into: text, email, number, or whatever `type` says.
50
58
 
51
59
  `value` is always written, even empty, so the control's state is on the page
@@ -71,8 +79,8 @@
71
79
  disabled=false
72
80
  ) %}
73
81
  <label for="{{ id }}">{{ label | safe }}</label>
74
- <input id="{{ id }}" name="{{ name }}" type="{{ type }}"{% if min %} min="{{ min }}"{% endif %}{% if step %} step="{{ step }}"{% endif %}{% if inputmode %} inputmode="{{ inputmode }}"{% endif %}{% if autocomplete %} autocomplete="{{ autocomplete }}"{% endif %} value="{{ value }}"{% if placeholder %} placeholder="{{ placeholder }}"{% endif %}{% if spellcheck %} spellcheck="{{ spellcheck }}"{% endif %}{% if required %} required{% endif %}{% if autofocus %} autofocus{% endif %}{% if readonly %} readonly{% endif %}{% if disabled %} disabled{% endif %} />
75
- {{ _below(hint, error, errorHtml) }}
82
+ <input id="{{ id }}" name="{{ name }}" type="{{ type }}"{% if min %} min="{{ min }}"{% endif %}{% if step %} step="{{ step }}"{% endif %}{% if inputmode %} inputmode="{{ inputmode }}"{% endif %}{% if autocomplete %} autocomplete="{{ autocomplete }}"{% endif %} value="{{ value }}"{% if placeholder %} placeholder="{{ placeholder }}"{% endif %}{% if spellcheck %} spellcheck="{{ spellcheck }}"{% endif %}{% if required %} required{% endif %}{% if autofocus %} autofocus{% endif %}{% if readonly %} readonly{% endif %}{% if disabled %} disabled{% endif %}{{ _invalid(id, error) }} />
83
+ {{ _below(id, hint, error, errorHtml) }}
76
84
  {%- endmacro %}
77
85
 
78
86
  {# A password box. It carries `value=""` and no way to set one: nothing typed
@@ -93,8 +101,8 @@
93
101
  autofocus=false
94
102
  ) %}
95
103
  <label for="{{ id }}">{{ label | safe }}</label>
96
- <input id="{{ id }}" name="{{ name }}" type="password" value=""{% if autocomplete %} autocomplete="{{ autocomplete }}"{% endif %}{% if spellcheck %} spellcheck="{{ spellcheck }}"{% endif %}{% if placeholder %} placeholder="{{ placeholder }}"{% endif %}{% if required %} required{% endif %}{% if autofocus %} autofocus{% endif %} />
97
- {{ _below(hint, error, errorHtml) }}
104
+ <input id="{{ id }}" name="{{ name }}" type="password" value=""{% if autocomplete %} autocomplete="{{ autocomplete }}"{% endif %}{% if spellcheck %} spellcheck="{{ spellcheck }}"{% endif %}{% if placeholder %} placeholder="{{ placeholder }}"{% endif %}{% if required %} required{% endif %}{% if autofocus %} autofocus{% endif %}{{ _invalid(id, error) }} />
105
+ {{ _below(id, hint, error, errorHtml) }}
98
106
  {%- endmacro %}
99
107
 
100
108
  {# A box of several lines. #}
@@ -114,8 +122,8 @@
114
122
  disabled=false
115
123
  ) %}
116
124
  <label for="{{ id }}">{{ label | safe }}</label>
117
- <textarea id="{{ id }}" name="{{ name }}"{% if rows %} rows="{{ rows }}"{% endif %}{% if spellcheck %} spellcheck="{{ spellcheck }}"{% endif %}{% if required %} required{% endif %}{% if autofocus %} autofocus{% endif %}{% if readonly %} readonly{% endif %}{% if disabled %} disabled{% endif %}>{{ value }}</textarea>
118
- {{ _below(hint, error, errorHtml) }}
125
+ <textarea id="{{ id }}" name="{{ name }}"{% if rows %} rows="{{ rows }}"{% endif %}{% if spellcheck %} spellcheck="{{ spellcheck }}"{% endif %}{% if required %} required{% endif %}{% if autofocus %} autofocus{% endif %}{% if readonly %} readonly{% endif %}{% if disabled %} disabled{% endif %}{{ _invalid(id, error) }}>{{ value }}</textarea>
126
+ {{ _below(id, hint, error, errorHtml) }}
119
127
  {%- endmacro %}
120
128
 
121
129
  {# A list to pick from. The options are the call's body, because which of them
@@ -137,10 +145,10 @@
137
145
  disabled=false
138
146
  ) %}
139
147
  <label for="{{ id }}">{{ label | safe }}</label>
140
- <select id="{{ id }}" name="{{ name }}"{% if required %} required{% endif %}{% if autofocus %} autofocus{% endif %}{% if disabled %} disabled{% endif %}>
148
+ <select id="{{ id }}" name="{{ name }}"{% if required %} required{% endif %}{% if autofocus %} autofocus{% endif %}{% if disabled %} disabled{% endif %}{{ _invalid(id, error) }}>
141
149
  {{ caller() }}
142
150
  </select>
143
- {{ _below(hint, error, errorHtml) }}
151
+ {{ _below(id, hint, error, errorHtml) }}
144
152
  {%- endmacro %}
145
153
 
146
154
  {# A switch. The label follows the box rather than heading it, which is what a
@@ -158,8 +166,43 @@
158
166
  disabled=false
159
167
  ) %}
160
168
  <p class="admin-check">
161
- <input id="{{ id }}" name="{{ name }}" type="checkbox" value="{{ value }}"{% if checked %} checked{% endif %}{% if disabled %} disabled{% endif %} />
169
+ <input id="{{ id }}" name="{{ name }}" type="checkbox" value="{{ value }}"{% if checked %} checked{% endif %}{% if disabled %} disabled{% endif %}{{ _invalid(id, error) }} />
162
170
  <label for="{{ id }}">{{ label | safe }}</label>
163
171
  </p>
164
- {{ _below(hint, error, errorHtml) }}
172
+ {{ _below(id, hint, error, errorHtml) }}
173
+ {%- endmacro %}
174
+
175
+ {# The top of a refused form: what went wrong, and a link to every field that
176
+ needs putting right. It is an alert and takes focus when the page loads, so
177
+ a screen reader hears it at once and a keyboard starts from it, with no
178
+ script. It comes before the form, so its `autofocus` is the first on the
179
+ page and wins over any a field carries.
180
+
181
+ A form with one thing to say calls it bare, with the message as the heading.
182
+ A form with fields to fix calls it with a `problem` per field; one given no
183
+ message writes nothing, so a page lists every field it could fault:
184
+
185
+ {% call field.summary('Nothing was saved. Fix the fields marked below.') %}
186
+ {{ field.problem('settings-title', problems.title) }}
187
+ {% endcall %}
188
+
189
+ Only one form on a page is ever refused at a time, which is why the ids are
190
+ fixed. #}
191
+ {% macro summary(heading) %}
192
+ {% set problems = (caller() if caller else '') | trim %}
193
+ <div class="admin-error admin-error-summary" role="alert" tabindex="-1" autofocus aria-labelledby="admin-error-summary-heading">
194
+ <h2 id="admin-error-summary-heading">{{ heading }}</h2>
195
+ {% if problems %}
196
+ <ul>
197
+ {{ problems | safe }}
198
+ </ul>
199
+ {% endif %}
200
+ </div>
201
+ {%- endmacro %}
202
+
203
+ {# One field's line in the summary: its message, linking to the field. #}
204
+ {% macro problem(id, message) %}
205
+ {% if message %}
206
+ <li><a href="#{{ id }}">{{ message }}</a></li>
207
+ {% endif %}
165
208
  {%- endmacro %}
@@ -17,9 +17,13 @@
17
17
  <link rel="stylesheet" href="{{ assetPrefix }}admin.css" />
18
18
  </head>
19
19
  <body>
20
+ {# First on every screen, so a keyboard gets past the bar and the menu
21
+ in one key. It moves no focus of its own: a refused form's summary
22
+ still takes focus on load (TASK-142). #}
23
+ <a class="admin-skip-link" href="#main">Skip to main content</a>
20
24
  {% block chrome %}{% endblock %}
21
25
  {% block main %}
22
- <main class="admin-plain">
26
+ <main id="main" class="admin-plain">
23
27
  {% include "components/flash.njk" %}
24
28
  {% block content %}{% endblock %}
25
29
  </main>
@@ -1,7 +1,10 @@
1
1
  {# What every settings page is: a heading, one form that saves the fields that
2
2
  page carries, and whatever panels sit under it. A page fills in the two
3
- blocks; nothing about the chrome is written six times. #}
3
+ blocks, and names in `settingsProblems` each field a save can refuse, so
4
+ the summary at the top links to it; nothing about the chrome is written six
5
+ times. #}
4
6
  {% extends "layouts/shell.njk" %}
7
+ {% import "components/fields.njk" as field %}
5
8
 
6
9
  {% block title %}{{ settingsLabel }} settings{% endblock %}
7
10
 
@@ -9,7 +12,9 @@
9
12
  <h1>{{ settingsLabel }}</h1>
10
13
 
11
14
  {% if hasProblems %}
12
- <p class="admin-error">Nothing was saved. Fix the fields marked below and try again.</p>
15
+ {% call field.summary('Nothing was saved. Fix the fields marked below and try again.') %}
16
+ {% block settingsProblems %}{% endblock %}
17
+ {% endcall %}
13
18
  {% endif %}
14
19
 
15
20
  <form class="admin-settings" method="post" action="{{ settingsUrl }}">
@@ -39,7 +39,7 @@
39
39
  </ul>
40
40
  </nav>
41
41
 
42
- <main class="admin-main">
42
+ <main id="main" class="admin-main">
43
43
  {% include "components/flash.njk" %}
44
44
  {% block content %}{% endblock %}
45
45
  </main>
@@ -30,7 +30,7 @@
30
30
  <p>Type your username or your email address, and a link to set a new password is sent to the address on your account.</p>
31
31
 
32
32
  {% if error %}
33
- <p class="error">{{ error }}</p>
33
+ {{ field.summary(error) }}
34
34
  {% endif %}
35
35
 
36
36
  <form method="post" action="{{ forgotUrl }}">
@@ -7,7 +7,7 @@
7
7
  <h1>Log in</h1>
8
8
 
9
9
  {% if error %}
10
- <p class="error">{{ error }}</p>
10
+ {{ field.summary(error) }}
11
11
  {% endif %}
12
12
 
13
13
  <form method="post" action="{{ loginUrl }}">
@@ -17,14 +17,16 @@
17
17
  <p><a href="{{ forgotUrl }}">Ask for another one</a></p>
18
18
  {% else %}
19
19
  {% if problem %}
20
- <p class="error">{{ problem }}</p>
20
+ {% call field.summary('The password was not set.') %}
21
+ {{ field.problem('password', problem) }}
22
+ {% endcall %}
21
23
  {% endif %}
22
24
 
23
25
  <form method="post" action="{{ resetUrl }}">
24
26
  <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
25
27
  <input type="hidden" name="{{ fields.token }}" value="{{ token }}" />
26
28
 
27
- {{ field.password('password', fields.password, 'New password',
29
+ {{ field.password('password', fields.password, 'New password', error=problem,
28
30
  autocomplete='new-password', required=true, autofocus=true) }}
29
31
 
30
32
  {{ field.password('password_confirmation', fields.passwordConfirmation,
@@ -8,7 +8,7 @@
8
8
  <p>Nobody can sign in to this site yet. Pick a username and a password.</p>
9
9
 
10
10
  {% if error %}
11
- <p class="error">{{ error }}</p>
11
+ {{ field.summary(error) }}
12
12
  {% endif %}
13
13
 
14
14
  <form method="post" action="{{ setupUrl }}">
@@ -40,6 +40,7 @@
40
40
  <h2>Recent posts</h2>
41
41
  {% if recent.length %}
42
42
  <table class="admin-list">
43
+ <caption class="admin-visually-hidden">Recent posts</caption>
43
44
  <thead>
44
45
  <tr>
45
46
  <th scope="col">Title</th>
@@ -26,6 +26,7 @@
26
26
 
27
27
  {% if documents.length %}
28
28
  <table class="admin-list">
29
+ <caption class="admin-visually-hidden">{{ kind.plural }}</caption>
29
30
  <thead>
30
31
  <tr>
31
32
  <th scope="col">Title</th>
@@ -42,6 +42,7 @@
42
42
  <p>Nothing carries a {{ label }} yet.</p>
43
43
  {% else %}
44
44
  <table class="admin-list">
45
+ <caption class="admin-visually-hidden">{{ heading }}</caption>
45
46
  <thead>
46
47
  <tr>
47
48
  <th scope="col">{{ kind.singular | capitalize }}</th>
@@ -40,6 +40,7 @@
40
40
 
41
41
  {% if actor.followers.length %}
42
42
  <table class="admin-list">
43
+ <caption>Followers of {{ actor.handle }}</caption>
43
44
  <thead>
44
45
  <tr>
45
46
  <th scope="col">Follower</th>
@@ -80,6 +81,7 @@
80
81
  <h2>Relays</h2>
81
82
  {% if relays.length %}
82
83
  <table class="admin-list">
84
+ <caption class="admin-visually-hidden">Relays</caption>
83
85
  <thead>
84
86
  <tr>
85
87
  <th scope="col">Relay</th>
@@ -174,6 +176,7 @@
174
176
  <h2>Delivery</h2>
175
177
  {% if posts.length %}
176
178
  <table class="admin-list">
179
+ <caption class="admin-visually-hidden">Delivery</caption>
177
180
  <thead>
178
181
  <tr>
179
182
  <th scope="col">Post</th>
@@ -7,6 +7,10 @@
7
7
  {% extends "layouts/settings-page.njk" %}
8
8
  {% import "components/fields.njk" as field %}
9
9
 
10
+ {% block settingsProblems %}
11
+ {{ field.problem('settings-relays', problems.relays) }}
12
+ {% endblock %}
13
+
10
14
  {% block settingsFields %}
11
15
  {% set relaysHint %}
12
16
  One relay inbox per line, such as
@@ -42,6 +46,7 @@
42
46
 
43
47
  {% if wordpressRoutes.length %}
44
48
  <table class="admin-table">
49
+ <caption class="admin-visually-hidden">WordPress paths</caption>
45
50
  <thead>
46
51
  <tr><th scope="col">Path</th><th scope="col">For</th><th scope="col">Last asked for</th></tr>
47
52
  </thead>
@@ -60,9 +60,11 @@
60
60
  <p class="admin-empty">Nothing has been uploaded yet.</p>
61
61
  {% else %}
62
62
  <table class="admin-list admin-media">
63
+ <caption class="admin-visually-hidden">{{ heading }}</caption>
63
64
  <thead>
64
65
  <tr>
65
66
  <th scope="col">File</th>
67
+ <th scope="col">Alt text</th>
66
68
  <th scope="col">Uploaded</th>
67
69
  <th scope="col">Size</th>
68
70
  <th scope="col">Link</th>
@@ -83,6 +85,43 @@
83
85
  {% endif %}
84
86
  <a href="{{ file.url }}">{{ file.path }}</a>
85
87
  </td>
88
+ <td class="admin-media-alt">
89
+ {% if file.image %}
90
+ <form method="post" action="{{ altUrl }}">
91
+ <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
92
+ <input type="hidden" name="{{ fields.path }}" value="{{ file.path }}" />
93
+ <input type="hidden" name="{{ fields.page }}" value="{{ page }}" />
94
+ <label class="admin-visually-hidden" for="media-alt-{{ loop.index }}">
95
+ Alt text for {{ file.name }}
96
+ </label>
97
+ <input
98
+ id="media-alt-{{ loop.index }}"
99
+ name="{{ fields.alt }}"
100
+ type="text"
101
+ value="{% if file.alt and file.alt.kind == 'described' %}{{ file.alt.text }}{% endif %}"
102
+ aria-describedby="media-alt-state-{{ loop.index }}"
103
+ />
104
+ <span class="admin-check">
105
+ <input
106
+ id="media-decorative-{{ loop.index }}"
107
+ name="{{ fields.decorative }}"
108
+ type="checkbox"
109
+ value="1"
110
+ {% if file.alt and file.alt.kind == 'decorative' %}checked{% endif %}
111
+ />
112
+ <label for="media-decorative-{{ loop.index }}">Decorative</label>
113
+ </span>
114
+ <button type="submit">Save</button>
115
+ <span id="media-alt-state-{{ loop.index }}" class="admin-status{% if not file.alt %} admin-status-missing{% endif %}">
116
+ {%- if not file.alt %}Missing: describe it, or mark it decorative
117
+ {%- elif file.alt.kind == 'decorative' %}Decorative: inserted with an empty alt
118
+ {%- else %}Offered when it is inserted{% endif -%}
119
+ </span>
120
+ </form>
121
+ {% else %}
122
+ <span class="admin-status">Not an image</span>
123
+ {% endif %}
124
+ </td>
86
125
  <td>{{ file.modified | date('html') }}</td>
87
126
  <td>{{ file.bytes }}</td>
88
127
  <td class="admin-media-links">
@@ -60,7 +60,12 @@
60
60
  <h1>{{ heading }}</h1>
61
61
 
62
62
  {% if hasProblems %}
63
- <p class="admin-field-error">Nothing was saved. See below.</p>
63
+ {% call field.summary('Nothing was saved. Fix the box marked below and try again.') %}
64
+ {% for menu in areas.concat(others) %}
65
+ {{ field.problem('menu-' + menu.name, menu.problem) }}
66
+ {% endfor %}
67
+ {{ field.problem('menu-name', nameProblem) }}
68
+ {% endcall %}
64
69
  {% endif %}
65
70
 
66
71
  <h2>Where {{ themeName }} puts a menu</h2>
@@ -13,6 +13,13 @@
13
13
  {%- endif -%}
14
14
  {% endmacro %}
15
15
 
16
+ {% block settingsProblems %}
17
+ {{ field.problem('settings-comments-close', problems.commentsCloseAfterDays) }}
18
+ {{ field.problem('settings-comment-email-retention', problems.commentEmailRetentionDays) }}
19
+ {{ field.problem('settings-address-hash-retention', problems.addressHashRetentionDays) }}
20
+ {{ field.problem('settings-contact-message-retention', problems.contactMessageRetentionDays) }}
21
+ {% endblock %}
22
+
16
23
  {% block settingsFields %}
17
24
  <h2>Comments</h2>
18
25
 
@@ -3,6 +3,16 @@
3
3
  {% extends "layouts/settings-page.njk" %}
4
4
  {% import "components/fields.njk" as field %}
5
5
 
6
+ {% block settingsProblems %}
7
+ {{ field.problem('settings-mail-provider', problems.mailProvider) }}
8
+ {{ field.problem('settings-mail-from-address', problems.mailFromAddress) }}
9
+ {{ field.problem('settings-mail-reply-to', problems.mailReplyTo) }}
10
+ {{ field.problem('settings-contact-email', problems.contactEmail) }}
11
+ {{ field.problem('settings-security-contacts', problems.securityContacts) }}
12
+ {{ field.problem('settings-security-policy', problems.securityPolicy) }}
13
+ {{ field.problem('settings-security-languages', problems.securityLanguages) }}
14
+ {% endblock %}
15
+
6
16
  {% block settingsFields %}
7
17
  {% set providerHint %}
8
18
  The key or password each one needs is a credential and is saved below,
@@ -2,6 +2,13 @@
2
2
  {% extends "layouts/settings-page.njk" %}
3
3
  {% import "components/fields.njk" as field %}
4
4
 
5
+ {% block settingsProblems %}
6
+ {{ field.problem('settings-title', problems.title) }}
7
+ {{ field.problem('settings-base-url', problems.baseUrl) }}
8
+ {{ field.problem('settings-timezone', problems.timezone) }}
9
+ {{ field.problem('settings-language', problems.language) }}
10
+ {% endblock %}
11
+
5
12
  {% block settingsFields %}
6
13
  {{ field.text('settings-title', fields.title, 'Title', form.title,
7
14
  error=problems.title, required=true, autofocus=true) }}
@@ -3,6 +3,11 @@
3
3
  {% extends "layouts/settings-page.njk" %}
4
4
  {% import "components/fields.njk" as field %}
5
5
 
6
+ {% block settingsProblems %}
7
+ {{ field.problem('settings-tag-base', problems.tagBase) }}
8
+ {{ field.problem('settings-category-base', problems.categoryBase) }}
9
+ {% endblock %}
10
+
6
11
  {% block settingsFields %}
7
12
  {% set tagBaseHint %}The tag archives live at <code>/{{ form.tagBase }}/{tag}/</code>. One path segment, no slashes.{% endset %}
8
13
  {{ field.text('settings-tag-base', fields.tagBase, 'Tag base', form.tagBase,
@@ -22,6 +27,7 @@
22
27
 
23
28
  {% if taxonomyRedirects.length %}
24
29
  <table class="admin-list">
30
+ <caption class="admin-visually-hidden">Archive redirects</caption>
25
31
  <thead>
26
32
  <tr><th scope="col">Taxonomy</th><th scope="col">Was</th><th scope="col">Is now</th></tr>
27
33
  </thead>
@@ -4,6 +4,13 @@
4
4
  {% extends "layouts/settings-page.njk" %}
5
5
  {% import "components/fields.njk" as field %}
6
6
 
7
+ {% block settingsProblems %}
8
+ {{ field.problem('settings-homepage', problems.homepage) }}
9
+ {{ field.problem('settings-posts-page', problems.postsPage) }}
10
+ {{ field.problem('settings-posts-per-page', problems.postsPerPage) }}
11
+ {{ field.problem('settings-notify-server', problems.notifyServer) }}
12
+ {% endblock %}
13
+
7
14
  {% block settingsFields %}
8
15
  {# WordPress's "Your homepage displays": the empty option is Your latest
9
16
  posts, so the choice and the page it needs are one control rather than a
@@ -20,6 +20,7 @@
20
20
  </p>
21
21
 
22
22
  <table class="admin-list">
23
+ <caption class="admin-visually-hidden">What the index holds</caption>
23
24
  <thead>
24
25
  <tr>
25
26
  <th scope="col">What the index holds</th>
@@ -49,6 +49,11 @@
49
49
  {% endif %}
50
50
  </form>
51
51
  {% else %}
52
+ {% if problem %}
53
+ {% call field.summary('Nothing was looked up.') %}
54
+ {{ field.problem('personal-data-email', problem) }}
55
+ {% endcall %}
56
+ {% endif %}
52
57
  <form class="admin-settings" method="post" action="{{ actionUrl }}">
53
58
  <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
54
59
  {{ field.text('personal-data-email', fields.email, 'Email address', email,
@@ -80,10 +80,9 @@
80
80
  </p>
81
81
 
82
82
  {% if profileProblems.links %}
83
- <p class="admin-error">
84
- Nothing was saved. Fix the Links box below and save again; every other
85
- box here still holds what you typed.
86
- </p>
83
+ {% call field.summary('Nothing was saved. Fix the Links box below and save again; every other box here still holds what you typed.') %}
84
+ {{ field.problem('profile-links', profileProblems.links) }}
85
+ {% endcall %}
87
86
  {% endif %}
88
87
 
89
88
  {{ field.text('profile-display-name', fields.displayName, 'Display name',
@@ -219,7 +218,10 @@
219
218
  <h2>Change your password</h2>
220
219
 
221
220
  {% if passwordProblems.currentPassword or passwordProblems.newPassword %}
222
- <p class="admin-error">Your password was not changed. Fix the fields marked below and try again.</p>
221
+ {% call field.summary('Your password was not changed. Fix the fields marked below and try again.') %}
222
+ {{ field.problem('current-password', passwordProblems.currentPassword) }}
223
+ {{ field.problem('new-password', passwordProblems.newPassword) }}
224
+ {% endcall %}
223
225
  {% endif %}
224
226
 
225
227
  {{ field.password('current-password', fields.currentPassword, 'Current password',
@@ -13,6 +13,7 @@
13
13
  <h1>Users <a class="admin-button admin-button-small" href="{{ addUserUrl }}">Add new</a></h1>
14
14
 
15
15
  <table class="admin-list">
16
+ <caption class="admin-visually-hidden">Users</caption>
16
17
  <thead>
17
18
  <tr>
18
19
  <th scope="col">Username</th>
@@ -16,7 +16,11 @@
16
16
  <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
17
17
 
18
18
  {% if addProblems.username or addProblems.password or addProblems.email %}
19
- <p class="admin-error">Nobody was added. Fix the fields marked below and try again.</p>
19
+ {% call field.summary('Nobody was added. Fix the fields marked below and try again.') %}
20
+ {{ field.problem('add-username', addProblems.username) }}
21
+ {{ field.problem('add-email', addProblems.email) }}
22
+ {{ field.problem('add-password', addProblems.password) }}
23
+ {% endcall %}
20
24
  {% endif %}
21
25
 
22
26
  {{ field.text('add-username', fields.username, 'Username', addForm.username,
@@ -15,6 +15,7 @@
15
15
  --admin-bar: #1d2327;
16
16
  --admin-nav: #2c3338;
17
17
  --admin-nav-text: #f0f0f1;
18
+ --admin-focus-on-dark: #fff;
18
19
  }
19
20
 
20
21
  .admin-bar,
@@ -53,6 +54,15 @@
53
54
  color: #72aee6;
54
55
  }
55
56
 
57
+ /* The ring where the keyboard is, white on the dark bar. Here rather than in
58
+ admin.css because on the public site this is the only stylesheet the bar
59
+ has. */
60
+
61
+ .admin-bar :focus-visible {
62
+ outline: 2px solid var(--admin-focus-on-dark);
63
+ outline-offset: 2px;
64
+ }
65
+
56
66
  .admin-bar-site {
57
67
  font-weight: 600;
58
68
  }
@@ -128,6 +138,12 @@
128
138
  cursor: pointer;
129
139
  }
130
140
 
141
+ /* A menu entry fills the menu's width, so its ring is drawn inside it. */
142
+
143
+ .admin-bar .admin-account-menu :focus-visible {
144
+ outline-offset: -2px;
145
+ }
146
+
131
147
  .admin-account-menu a:hover,
132
148
  .admin-account-menu a:focus-visible,
133
149
  .admin-account-menu button[type='submit']:hover,