@geekity/cms 0.21.0 → 0.22.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 (99) hide show
  1. package/README.md +23 -0
  2. package/admin/components/admin-bar.njk +27 -6
  3. package/admin/components/alert.njk +28 -0
  4. package/admin/components/badge.njk +58 -0
  5. package/admin/components/button.njk +56 -0
  6. package/admin/components/card.njk +40 -0
  7. package/admin/components/collapse.njk +36 -0
  8. package/admin/components/copy.njk +20 -0
  9. package/admin/components/dropdown.njk +32 -0
  10. package/admin/components/fields.njk +81 -45
  11. package/admin/components/flash.njk +12 -5
  12. package/admin/components/menu.njk +51 -0
  13. package/admin/components/navbar.njk +23 -0
  14. package/admin/components/pagination.njk +47 -0
  15. package/admin/components/public-admin-bar.njk +4 -41
  16. package/admin/components/stat.njk +35 -0
  17. package/admin/components/table.njk +74 -0
  18. package/admin/components/tabs.njk +33 -0
  19. package/admin/components/theme-choice.njk +18 -0
  20. package/admin/layouts/base.njk +9 -16
  21. package/admin/layouts/settings-page.njk +15 -10
  22. package/admin/layouts/shell.njk +23 -31
  23. package/admin/pages/account/forgot.njk +39 -33
  24. package/admin/pages/account/login.njk +22 -16
  25. package/admin/pages/account/reset.njk +34 -25
  26. package/admin/pages/account/setup.njk +21 -15
  27. package/admin/pages/appearance/themes.njk +50 -44
  28. package/admin/pages/comments/all.njk +88 -106
  29. package/admin/pages/dashboard/home.njk +50 -62
  30. package/admin/pages/documents/conflict.njk +29 -24
  31. package/admin/pages/documents/editor.njk +155 -275
  32. package/admin/pages/documents/list.njk +60 -69
  33. package/admin/pages/documents/syndication.njk +98 -60
  34. package/admin/pages/documents/taxonomy.njk +36 -32
  35. package/admin/pages/error.njk +8 -5
  36. package/admin/pages/federation/followers.njk +91 -97
  37. package/admin/pages/federation/settings.njk +10 -11
  38. package/admin/pages/indieauth/consent.njk +60 -44
  39. package/admin/pages/indieauth/refused.njk +6 -3
  40. package/admin/pages/media/library.njk +86 -107
  41. package/admin/pages/messages/all.njk +71 -77
  42. package/admin/pages/navigation/menus.njk +90 -85
  43. package/admin/pages/placeholder.njk +13 -8
  44. package/admin/pages/settings/discussion.njk +30 -29
  45. package/admin/pages/settings/email.njk +26 -28
  46. package/admin/pages/settings/general.njk +10 -4
  47. package/admin/pages/settings/permalinks.njk +8 -9
  48. package/admin/pages/settings/privacy.njk +3 -3
  49. package/admin/pages/settings/reading.njk +3 -3
  50. package/admin/pages/tools/content-index.njk +79 -73
  51. package/admin/pages/tools/personal-data.njk +61 -55
  52. package/admin/pages/users/activity-entry.njk +12 -12
  53. package/admin/pages/users/activity.njk +13 -19
  54. package/admin/pages/users/apps.njk +112 -100
  55. package/admin/pages/users/edit.njk +65 -58
  56. package/admin/pages/users/list.njk +26 -24
  57. package/admin/pages/users/new.njk +11 -7
  58. package/admin/src/admin.css +174 -0
  59. package/admin/static/admin-bar.css +33 -31
  60. package/admin/static/admin-bar.js +22 -0
  61. package/admin/static/admin.css +7388 -1195
  62. package/admin/static/copy.js +1 -1
  63. package/admin/static/editor.js +18 -18
  64. package/dist/admin/accounts.d.ts +20 -0
  65. package/dist/admin/accounts.d.ts.map +1 -1
  66. package/dist/admin/accounts.js +25 -0
  67. package/dist/admin/accounts.js.map +1 -1
  68. package/dist/admin/admin-theme.d.ts +157 -0
  69. package/dist/admin/admin-theme.d.ts.map +1 -0
  70. package/dist/admin/admin-theme.js +52 -0
  71. package/dist/admin/admin-theme.js.map +1 -0
  72. package/dist/admin/assets.d.ts +4 -3
  73. package/dist/admin/assets.d.ts.map +1 -1
  74. package/dist/admin/assets.js +4 -3
  75. package/dist/admin/assets.js.map +1 -1
  76. package/dist/admin/documents.js +1 -1
  77. package/dist/admin/documents.js.map +1 -1
  78. package/dist/admin/headers.js +1 -1
  79. package/dist/admin/headers.js.map +1 -1
  80. package/dist/admin/routes.d.ts.map +1 -1
  81. package/dist/admin/routes.js +17 -8
  82. package/dist/admin/routes.js.map +1 -1
  83. package/dist/admin/templates.d.ts +6 -3
  84. package/dist/admin/templates.d.ts.map +1 -1
  85. package/dist/admin/templates.js +25 -3
  86. package/dist/admin/templates.js.map +1 -1
  87. package/dist/admin/users.d.ts +4 -0
  88. package/dist/admin/users.d.ts.map +1 -1
  89. package/dist/admin/users.js +37 -1
  90. package/dist/admin/users.js.map +1 -1
  91. package/dist/content/post-type.d.ts.map +1 -1
  92. package/dist/content/post-type.js +7 -2
  93. package/dist/content/post-type.js.map +1 -1
  94. package/dist/web/admin-bar.d.ts +4 -4
  95. package/dist/web/admin-bar.d.ts.map +1 -1
  96. package/dist/web/admin-bar.js +11 -34
  97. package/dist/web/admin-bar.js.map +1 -1
  98. package/package.json +8 -4
  99. package/themes/default/README.md +3 -1
@@ -7,48 +7,33 @@
7
7
  {% block title %}{{ heading }}{% endblock %}
8
8
 
9
9
  {% import "components/fields.njk" as field %}
10
+ {% from "components/alert.njk" import alert %}
11
+ {% from "components/button.njk" import button, buttonLink %}
12
+ {% from "components/card.njk" import card, cardActions %}
13
+ {% from "components/collapse.njk" import collapse %}
10
14
 
11
- {# The legend repeats the summary so every box in it is still announced with
12
- the name of its group. #}
13
- {% macro group(name, open, class='admin-editor-group') %}
14
- <details class="{{ class }}"{% if open %} open{% endif %}>
15
- <summary>{{ name }}</summary>
16
- <fieldset>
17
- <legend class="admin-visually-hidden">{{ name }}</legend>
18
- {{ caller() }}
19
- </fieldset>
20
- </details>
21
- {% endmacro %}
22
-
23
- {% macro invalid(id, hint='') -%}
24
- {% if errorField == id %} aria-invalid="true" aria-describedby="{{ id }}-error{% if hint %} {{ hint }}{% endif %}"{% elif hint %} aria-describedby="{{ hint }}"{% endif %}
25
- {%- endmacro %}
26
-
27
- {% macro problem(id) %}
28
- {% if errorField == id %}<p class="admin-field-error" id="{{ id }}-error">{{ error }}</p>{% endif %}
29
- {% endmacro %}
15
+ {% set ACTION_COLORS = { 'publish': 'primary', 'update': 'primary', 'trash': 'error' } %}
16
+ {% set ACTION_STYLES = { 'trash': 'ghost' } %}
30
17
 
31
18
  {% macro citedCard(property) %}
32
- {% set card = citedPreviews[property] %}
33
- {% if card %}
34
- <div class="admin-cited-card">
35
- <img src="{{ card.picture.src }}" alt="" width="{{ card.picture.width }}" height="{{ card.picture.height }}" loading="lazy">
36
- <p class="admin-cited-title">{{ card.name }}</p>
37
- <input type="checkbox" id="editor-preview-{{ property }}" name="preview" value="hide"{% if form.previewHidden %} checked{% endif %}>
38
- <label class="admin-cited-remove" for="editor-preview-{{ property }}"><span aria-hidden="true">×</span><span class="admin-visually-hidden">Remove the preview of {{ card.name }}</span></label>
39
- <p class="admin-cited-removed">Removed. The post shows the plain citation until you select the X again.</p>
40
- {%- if card.described %}
41
- <label for="editor-cited-alt">Alt text of the reposted image</label>
42
- <input id="editor-cited-alt" name="cited-alt" type="text" value="{{ form.citedAlt or form.title }}"{{ invalid('editor-cited-alt', 'editor-cited-alt-hint') }} />
43
- {{ problem('editor-cited-alt') }}
44
- <p class="admin-hint" id="editor-cited-alt-hint">What the image shows, for readers who cannot see it. It starts as the post’s title.</p>
45
- {%- endif %}
46
- </div>
19
+ {% set cited = citedPreviews[property] %}
20
+ {% if cited %}
21
+ {% call card(size='sm', style='border') %}
22
+ {# First: Tailwind's `peer-*` variants reach later siblings only. #}
23
+ <input class="peer sr-only" type="checkbox" id="editor-preview-{{ property }}" name="preview" value="hide"{% if form.previewHidden %} checked{% endif %}>
24
+ <label class="btn btn-circle btn-xs absolute end-2 top-2 peer-checked:btn-neutral peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-base-content" for="editor-preview-{{ property }}"><span aria-hidden="true">×</span><span class="sr-only">Remove the preview of {{ cited.name }}</span></label>
25
+ <img class="h-auto max-h-32 w-full rounded-box object-cover peer-checked:opacity-40" src="{{ cited.picture.src }}" alt="" width="{{ cited.picture.width }}" height="{{ cited.picture.height }}" loading="lazy">
26
+ <p class="break-words pe-8 peer-checked:opacity-40">{{ cited.name }}</p>
27
+ <p class="label hidden whitespace-normal peer-checked:block">Removed. The post shows the plain citation until you select the X again.</p>
28
+ {% if cited.described %}
29
+ {{ field.text('editor-cited-alt', 'cited-alt', 'Alt text of the reposted image', value=(form.citedAlt or form.title), error=(error if errorField == 'editor-cited-alt' else ''), hint='What the image shows, for readers who cannot see it. It starts as the post’s title.', hintId='editor-cited-alt-hint') }}
30
+ {% endif %}
31
+ {% endcall %}
47
32
  {% endif %}
48
33
  {% endmacro %}
49
34
 
50
35
  {% block content %}
51
- <h1>{{ heading }}</h1>
36
+ <h1 class="mb-6 text-2xl font-bold">{{ heading }}</h1>
52
37
 
53
38
  {% if error and errorField %}
54
39
  {% call field.summary('Nothing was saved. Fix the field marked below and try again.') %}
@@ -58,15 +43,22 @@
58
43
  {{ field.summary(error) }}
59
44
  {% endif %}
60
45
 
61
- {% if trashed %}
62
- <p class="admin-note">This {{ kind.singular }} is in the trash. The public site is not serving it.</p>
63
- {% endif %}
64
-
65
- {% if scheduledFor %}
66
- <p class="admin-note">Scheduled for {{ scheduledFor }}. The public site starts serving it then, and the followers are told then.</p>
46
+ {% if trashed or scheduledFor %}
47
+ <div class="my-6 flex flex-col gap-2">
48
+ {% if trashed %}
49
+ {% call alert(color='warning', polite=true) %}
50
+ <span>This {{ kind.singular }} is in the trash. The public site is not serving it.</span>
51
+ {% endcall %}
52
+ {% endif %}
53
+ {% if scheduledFor %}
54
+ {% call alert(color='info', polite=true) %}
55
+ <span>Scheduled for {{ scheduledFor }}. The public site starts serving it then, and the followers are told then.</span>
56
+ {% endcall %}
57
+ {% endif %}
58
+ </div>
67
59
  {% endif %}
68
60
 
69
- <form class="admin-editor" method="post" action="{{ saveUrl }}">
61
+ <form class="grid items-start gap-6 xl:grid-cols-[minmax(0,1fr)_20rem]" method="post" action="{{ saveUrl }}">
70
62
  <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
71
63
  {# The hash of the file this form was filled in from. A save whose hash no
72
64
  longer matches what is on disk is refused rather than overwriting
@@ -75,54 +67,42 @@
75
67
  {# Which layout the preview renders through. The save handler ignores it. #}
76
68
  <input type="hidden" name="type" value="{{ kind.type }}" />
77
69
 
78
- <div class="admin-editor-main">
79
- <label for="editor-title">Title</label>
80
- {#- A post may go without one, and is then a note; a page is always named. -#}
81
- <input id="editor-title" name="title" type="text" value="{{ form.title }}"{% if kind.type == "page" %} required{% endif %} autofocus{{ invalid('editor-title') }} />
82
- {{ problem('editor-title') }}
83
- {% if kind.type == "post" %}
84
- <p class="admin-hint">Leave it empty to write a note: the post is then its words, and takes its slug from the first of them.</p>
85
- {% endif %}
86
-
87
- <label for="editor-body">Body</label>
88
- <textarea id="editor-body" name="body" rows="24">{{ form.body }}</textarea>
89
-
90
- {# Everything the editor's own script needs, on the element it hangs off.
91
- With JavaScript off this is one button that posts the form to the
92
- preview in a new tab, which is a preview. With it on, editor.js takes
93
- the button over, puts CodeMirror on the textarea above and adds the
94
- tabs and the upload control beside it. #}
95
- <div
96
- class="admin-editor-tools"
97
- id="editor-enhance"
98
- data-preview-url="{{ previewUrl }}"
99
- data-upload-url="{{ uploadUrl }}"
100
- data-kind="{{ kind.type }}"
101
- >
102
- <button
103
- type="submit"
104
- id="editor-preview-fallback"
105
- class="admin-button-quiet"
106
- formaction="{{ previewUrl }}"
107
- formtarget="_blank"
108
- formnovalidate
109
- >Preview</button>
110
- </div>
70
+ <div class="flex min-w-0 flex-col gap-4">
71
+ {% call card() %}
72
+ {% set titleHint %}{% if kind.type == "post" %}Leave it empty to write a note: the post is then its words, and takes its slug from the first of them.{% endif %}{% endset %}
73
+ {#- A post may go without one, and is then a note; a page is always named. -#}
74
+ {{ field.text('editor-title', 'title', 'Title', value=form.title, required=(kind.type == "page"), autofocus=true, error=(error if errorField == 'editor-title' else ''), hint=titleHint) }}
75
+
76
+ {{ field.textarea('editor-body', 'body', 'Body', value=form.body, rows=24) }}
77
+
78
+ {# Everything the editor's own script needs, on the element it hangs
79
+ off. With JavaScript off this is one button that posts the form to
80
+ the preview in a new tab, which is a preview. With it on, editor.js
81
+ takes the button over, puts CodeMirror on the textarea above and
82
+ adds the Write and Preview tabs, the upload control and its status
83
+ line here. #}
84
+ <div
85
+ class="flex flex-wrap items-center gap-2"
86
+ id="editor-enhance"
87
+ data-preview-url="{{ previewUrl }}"
88
+ data-upload-url="{{ uploadUrl }}"
89
+ data-kind="{{ kind.type }}"
90
+ >
91
+ {{ button('Preview', type='submit', id='editor-preview-fallback', style='soft', formaction=previewUrl, formtarget='_blank', formnovalidate=true) }}
92
+ </div>
93
+ {% endcall %}
111
94
 
112
95
  {% if kind.type == "post" %}
113
- {% call group('Photos', open.photos) %}
114
- <p class="admin-hint">
96
+ {% call collapse('Photos', open.photos, fields=true) %}
97
+ <p class="label block whitespace-normal text-xs">
115
98
  Each is an image in the media library or an address starting https://. Leave the alt text
116
99
  empty to use the media library’s. Empty an address to remove that photo.
117
100
  </p>
118
101
  {% for row in photoRows %}
119
102
  {% set n = loop.index0 %}
120
- {% call group('Photo ' + loop.index if row.url else 'Add a photo', row.url, 'admin-editor-group admin-editor-row') %}
121
- <label for="editor-photo-url-{{ n }}">Address</label>
122
- <input id="editor-photo-url-{{ n }}" name="{{ photoFields.url }}{{ n }}" type="text" value="{{ row.url }}" list="editor-photo-uploads" autocomplete="off"{{ invalid('editor-photo-url-' + n) }} />
123
- {{ problem('editor-photo-url-' + n) }}
124
- <label for="editor-photo-alt-{{ n }}">Alt text</label>
125
- <input id="editor-photo-alt-{{ n }}" name="{{ photoFields.alt }}{{ n }}" type="text" value="{{ row.alt }}"{% if row.libraryAlt %} placeholder="{{ row.libraryAlt }}"{% endif %} />
103
+ {% call collapse(('Photo ' + loop.index) if row.url else 'Add a photo', row.url, fields=true) %}
104
+ {{ field.text('editor-photo-url-' + n, photoFields.url + n, 'Address', value=row.url, list='editor-photo-uploads', autocomplete='off', error=(error if errorField == ('editor-photo-url-' + n) else '')) }}
105
+ {{ field.text('editor-photo-alt-' + n, photoFields.alt + n, 'Alt text', value=row.alt, placeholder=row.libraryAlt) }}
126
106
  {% endcall %}
127
107
  {% endfor %}
128
108
  <datalist id="editor-photo-uploads">
@@ -132,100 +112,61 @@
132
112
  </datalist>
133
113
  {% endcall %}
134
114
 
135
- {% call group('Location', open.location) %}
136
- <p class="admin-hint">
115
+ {% call collapse('Location', open.location, fields=true) %}
116
+ <p class="label block whitespace-normal text-xs">
137
117
  Kept in <code>data/locations.json</code>, not in the post’s file. What readers see of it
138
118
  is chosen on Settings &gt; Privacy. Empty every box to remove it.
139
119
  </p>
140
- <button type="button" id="editor-location-here" class="admin-location-here" hidden>Use my location</button>
141
- <p id="editor-location-status" class="admin-hint" role="status"></p>
142
- <label for="editor-location-geo">Coordinates</label>
143
- <input id="editor-location-geo" name="{{ locationFields.geo }}" type="text" value="{{ form.location.geo }}" placeholder="48.85837, 2.29448" autocomplete="off" spellcheck="false"{{ invalid('editor-location-geo', 'editor-location-geo-hint') }} />
144
- {{ problem('editor-location-geo') }}
145
- <p class="admin-hint" id="editor-location-geo-hint">Latitude, longitude, as decimal degrees. A <code>geo:</code> URI works too.</p>
146
- <label for="editor-location-accuracy">Accuracy</label>
147
- <input id="editor-location-accuracy" name="{{ locationFields.accuracy }}" type="text" inputmode="numeric" value="{{ form.location.accuracy }}" autocomplete="off"{{ invalid('editor-location-accuracy', 'editor-location-accuracy-hint') }} />
148
- {{ problem('editor-location-accuracy') }}
149
- <p class="admin-hint" id="editor-location-accuracy-hint">Metres. Leave it empty if you do not know.</p>
150
- <label for="editor-location-name">Place</label>
151
- <input id="editor-location-name" name="{{ locationFields.name }}" type="text" value="{{ form.location.name }}" />
152
- <label for="editor-location-locality">Locality</label>
153
- <input id="editor-location-locality" name="{{ locationFields.locality }}" type="text" value="{{ form.location.locality }}" />
154
- <label for="editor-location-region">Region</label>
155
- <input id="editor-location-region" name="{{ locationFields.region }}" type="text" value="{{ form.location.region }}" />
156
- <label for="editor-location-country">Country</label>
157
- <input id="editor-location-country" name="{{ locationFields.country }}" type="text" value="{{ form.location.country }}" />
158
- <p class="admin-check">
159
- <input id="editor-location-checkin" name="{{ locationFields.checkin }}" type="checkbox" value="1"{% if form.location.checkin %} checked{% endif %} aria-describedby="editor-location-checkin-hint" />
160
- <label for="editor-location-checkin">A check-in</label>
161
- </p>
162
- <p class="admin-hint" id="editor-location-checkin-hint">A Micropub app sent it as a checkin, and reads it back as one. Readers see it as any other location.</p>
120
+ <div>
121
+ {{ button('Use my location', id='editor-location-here', style='soft', hidden=true) }}
122
+ </div>
123
+ <p id="editor-location-status" class="label block whitespace-normal text-xs" role="status"></p>
124
+ {{ field.text('editor-location-geo', locationFields.geo, 'Coordinates', value=form.location.geo, placeholder='48.85837, 2.29448', autocomplete='off', spellcheck='false', error=(error if errorField == 'editor-location-geo' else ''), hint='Latitude, longitude, as decimal degrees. A <code>geo:</code> URI works too.', hintId='editor-location-geo-hint') }}
125
+ {{ field.text('editor-location-accuracy', locationFields.accuracy, 'Accuracy', value=form.location.accuracy, inputmode='numeric', autocomplete='off', error=(error if errorField == 'editor-location-accuracy' else ''), hint='Metres. Leave it empty if you do not know.', hintId='editor-location-accuracy-hint') }}
126
+ {{ field.text('editor-location-name', locationFields.name, 'Place', value=form.location.name) }}
127
+ {{ field.text('editor-location-locality', locationFields.locality, 'Locality', value=form.location.locality) }}
128
+ {{ field.text('editor-location-region', locationFields.region, 'Region', value=form.location.region) }}
129
+ {{ field.text('editor-location-country', locationFields.country, 'Country', value=form.location.country) }}
130
+ {{ field.checkbox('editor-location-checkin', locationFields.checkin, 'A check-in', checked=form.location.checkin, hint='A Micropub app sent it as a checkin, and reads it back as one. Readers see it as any other location.', hintId='editor-location-checkin-hint') }}
163
131
  {% endcall %}
164
132
 
165
- {% call group('Recording', open.recording) %}
166
- <label for="editor-enclosure-url">Audio or video file</label>
167
- <select id="editor-enclosure-url" name="{{ enclosureFields.url }}"{{ invalid('editor-enclosure-url', 'editor-enclosure-url-hint') }}>
133
+ {% call collapse('Recording', open.recording, fields=true) %}
134
+ {% call field.select('editor-enclosure-url', enclosureFields.url, 'Audio or video file', error=(error if errorField == 'editor-enclosure-url' else ''), hint='Upload the file to the media library first. Feeds carry it as the post’s enclosure and the post page plays it. None removes the recording, its transcript and its other versions.', hintId='editor-enclosure-url-hint') %}
168
135
  <option value=""{% if form.enclosure.url == "" %} selected{% endif %}>None</option>
169
136
  {% for choice in enclosureChoices.media %}
170
137
  <option value="{{ choice.url }}"{% if choice.url == form.enclosure.url %} selected{% endif %}>{{ choice.label }}</option>
171
138
  {% endfor %}
172
- </select>
173
- {{ problem('editor-enclosure-url') }}
174
- <p class="admin-hint" id="editor-enclosure-url-hint">
175
- Upload the file to the media library first. Feeds carry it as the post’s enclosure and the
176
- post page plays it. None removes the recording, its transcript and its other versions.
177
- </p>
139
+ {% endcall %}
178
140
 
179
- <label for="editor-enclosure-duration">Duration</label>
180
- <input id="editor-enclosure-duration" name="{{ enclosureFields.duration }}" type="text" inputmode="numeric" value="{{ form.enclosure.duration }}" autocomplete="off"{{ invalid('editor-enclosure-duration', 'editor-enclosure-duration-hint') }} />
181
- {{ problem('editor-enclosure-duration') }}
182
- <p class="admin-hint" id="editor-enclosure-duration-hint">Seconds, or 30:34, or 1:02:03. Leave it empty if you do not know.</p>
141
+ {{ field.text('editor-enclosure-duration', enclosureFields.duration, 'Duration', value=form.enclosure.duration, inputmode='numeric', autocomplete='off', error=(error if errorField == 'editor-enclosure-duration' else ''), hint='Seconds, or 30:34, or 1:02:03. Leave it empty if you do not know.', hintId='editor-enclosure-duration-hint') }}
183
142
 
184
- <label for="editor-enclosure-transcript">Transcript or captions</label>
185
- <input id="editor-enclosure-transcript" name="{{ enclosureFields.transcriptUrl }}" type="text" value="{{ form.enclosure.transcriptUrl }}" list="editor-transcript-uploads" autocomplete="off"{{ invalid('editor-enclosure-transcript', 'editor-enclosure-transcript-hint') }} />
186
- {{ problem('editor-enclosure-transcript') }}
143
+ {{ field.text('editor-enclosure-transcript', enclosureFields.transcriptUrl, 'Transcript or captions', value=form.enclosure.transcriptUrl, list='editor-transcript-uploads', autocomplete='off', error=(error if errorField == 'editor-enclosure-transcript' else ''), hintId='editor-enclosure-transcript-hint') }}
187
144
  <datalist id="editor-transcript-uploads">
188
145
  {% for url in enclosureChoices.transcripts %}
189
146
  <option value="{{ url }}"></option>
190
147
  {% endfor %}
191
148
  </datalist>
192
- <label for="editor-enclosure-transcript-type">Transcript type</label>
193
- <select id="editor-enclosure-transcript-type" name="{{ enclosureFields.transcriptType }}"{{ invalid('editor-enclosure-transcript-type', 'editor-enclosure-transcript-hint') }}>
149
+ {% call field.select('editor-enclosure-transcript-type', enclosureFields.transcriptType, 'Transcript type', error=(error if errorField == 'editor-enclosure-transcript-type' else ''), hint='A .vtt, .srt or .txt file in the media library, or an address starting https://. A linked transcript needs its type.', hintId='editor-enclosure-transcript-hint') %}
194
150
  <option value=""{% if form.enclosure.transcriptType == "" %} selected{% endif %}>From the file</option>
195
151
  {% for type in transcriptTypes %}
196
152
  <option value="{{ type }}"{% if type == form.enclosure.transcriptType %} selected{% endif %}>{{ type }}</option>
197
153
  {% endfor %}
198
- </select>
199
- {{ problem('editor-enclosure-transcript-type') }}
200
- <p class="admin-hint" id="editor-enclosure-transcript-hint">
201
- A .vtt, .srt or .txt file in the media library, or an address starting https://. A linked
202
- transcript needs its type.
203
- </p>
154
+ {% endcall %}
204
155
 
205
- {% call group('Other versions', open.versions) %}
206
- <p class="admin-hint">
156
+ {% call collapse('Other versions', open.versions, fields=true) %}
157
+ <p class="label block whitespace-normal text-xs">
207
158
  A video of an audio episode, a smaller file, another language. Each is a file in the media
208
159
  library or an address starting https://, which needs its type. Empty an address to remove
209
160
  that version.
210
161
  </p>
211
162
  {% for row in alternateRows %}
212
163
  {% set n = loop.index0 %}
213
- {% call group('Version ' + loop.index if row.url else 'Add a version', row.url, 'admin-editor-group admin-editor-row') %}
214
- <label for="editor-alternate-url-{{ n }}">Address</label>
215
- <input id="editor-alternate-url-{{ n }}" name="{{ enclosureFields.alternate.url }}{{ n }}" type="text" value="{{ row.url }}" list="editor-media-uploads" autocomplete="off"{{ invalid('editor-alternate-url-' + n) }} />
216
- {{ problem('editor-alternate-url-' + n) }}
217
- <label for="editor-alternate-type-{{ n }}">Type</label>
218
- <input id="editor-alternate-type-{{ n }}" name="{{ enclosureFields.alternate.type }}{{ n }}" type="text" value="{{ row.type }}" placeholder="video/mp4" autocomplete="off" spellcheck="false"{{ invalid('editor-alternate-type-' + n) }} />
219
- {{ problem('editor-alternate-type-' + n) }}
220
- <label for="editor-alternate-title-{{ n }}">Title</label>
221
- <input id="editor-alternate-title-{{ n }}" name="{{ enclosureFields.alternate.title }}{{ n }}" type="text" value="{{ row.title }}" maxlength="32"{{ invalid('editor-alternate-title-' + n) }} />
222
- {{ problem('editor-alternate-title-' + n) }}
223
- <label for="editor-alternate-height-{{ n }}">Height</label>
224
- <input id="editor-alternate-height-{{ n }}" name="{{ enclosureFields.alternate.height }}{{ n }}" type="text" inputmode="numeric" value="{{ row.height }}" autocomplete="off"{{ invalid('editor-alternate-height-' + n) }} />
225
- {{ problem('editor-alternate-height-' + n) }}
226
- <label for="editor-alternate-lang-{{ n }}">Language</label>
227
- <input id="editor-alternate-lang-{{ n }}" name="{{ enclosureFields.alternate.lang }}{{ n }}" type="text" value="{{ row.lang }}" autocomplete="off" spellcheck="false"{{ invalid('editor-alternate-lang-' + n) }} />
228
- {{ problem('editor-alternate-lang-' + n) }}
164
+ {% call collapse(('Version ' + loop.index) if row.url else 'Add a version', row.url, fields=true) %}
165
+ {{ field.text('editor-alternate-url-' + n, enclosureFields.alternate.url + n, 'Address', value=row.url, list='editor-media-uploads', autocomplete='off', error=(error if errorField == ('editor-alternate-url-' + n) else '')) }}
166
+ {{ field.text('editor-alternate-type-' + n, enclosureFields.alternate.type + n, 'Type', value=row.type, placeholder='video/mp4', autocomplete='off', spellcheck='false', error=(error if errorField == ('editor-alternate-type-' + n) else '')) }}
167
+ {{ field.text('editor-alternate-title-' + n, enclosureFields.alternate.title + n, 'Title', value=row.title, maxlength='32', error=(error if errorField == ('editor-alternate-title-' + n) else '')) }}
168
+ {{ field.text('editor-alternate-height-' + n, enclosureFields.alternate.height + n, 'Height', value=row.height, inputmode='numeric', autocomplete='off', error=(error if errorField == ('editor-alternate-height-' + n) else '')) }}
169
+ {{ field.text('editor-alternate-lang-' + n, enclosureFields.alternate.lang + n, 'Language', value=row.lang, autocomplete='off', spellcheck='false', error=(error if errorField == ('editor-alternate-lang-' + n) else '')) }}
229
170
  {% endcall %}
230
171
  {% endfor %}
231
172
  <datalist id="editor-media-uploads">
@@ -238,31 +179,29 @@
238
179
  {% endif %}
239
180
  </div>
240
181
 
241
- <div class="admin-editor-side">
242
- {% call group('Publishing', open.publishing) %}
243
- <p class="admin-check">
244
- <input id="editor-draft" name="draft" type="checkbox" value="1"{% if form.draft %} checked{% endif %} />
245
- <label for="editor-draft">Draft</label>
246
- </p>
182
+ <div class="flex min-w-0 flex-col gap-4">
183
+ {% call card(size='sm') %}
184
+ {% call cardActions() %}
185
+ {% for action in actions %}
186
+ {{ button(action.label, type='submit', name='action', value=action.value, color=ACTION_COLORS[action.value], style=ACTION_STYLES[action.value]) }}
187
+ {% endfor %}
188
+ {% if viewUrl %}
189
+ {{ buttonLink('View', viewUrl, style='ghost') }}
190
+ {% endif %}
191
+ {% endcall %}
192
+ {% endcall %}
193
+
194
+ {% call collapse('Publishing', open.publishing, fields=true) %}
195
+ {{ field.checkbox('editor-draft', 'draft', 'Draft', checked=form.draft) }}
247
196
 
248
197
  {% if kind.dated %}
249
- <label for="editor-date">Date</label>
250
- <input id="editor-date" name="date" type="text" value="{{ form.date }}"{{ invalid('editor-date') }} />
251
- {{ problem('editor-date') }}
252
198
  {# The file holds a UTC instant; this field is that instant as the
253
199
  site's own clock reads it, so the zone has to be on the screen. #}
254
- <p class="admin-hint">Wall-clock time in {{ dateZone }}. Stored as UTC.</p>
200
+ {% set dateHint %}Wall-clock time in {{ dateZone }}. Stored as UTC.{% endset %}
201
+ {{ field.text('editor-date', 'date', 'Date', value=form.date, error=(error if errorField == 'editor-date' else ''), hint=dateHint) }}
255
202
  {% endif %}
256
203
 
257
- <label for="editor-visibility">Visibility</label>
258
- <select id="editor-visibility" name="visibility" aria-describedby="editor-visibility-hint">
259
- {% if form.visibility.unrecognized is defined %}
260
- <option value="{{ form.visibility.unrecognized }}" selected>{{ form.visibility.unrecognized }} (not recognized)</option>
261
- {% endif %}
262
- <option value="public"{% if form.visibility == "public" %} selected{% endif %}>Public</option>
263
- <option value="unlisted"{% if form.visibility == "unlisted" %} selected{% endif %}>Unlisted</option>
264
- </select>
265
- <p class="admin-hint" id="editor-visibility-hint">
204
+ {% set visibilityHint %}
266
205
  {% if form.visibility.unrecognized is defined %}
267
206
  The front matter says <code>visibility: {{ form.visibility.unrecognized }}</code>, which this
268
207
  site does not recognize. The {{ kind.singular }} is hidden until you choose Public or Unlisted:
@@ -271,20 +210,19 @@
271
210
  Unlisted writes <code>visibility: unlisted</code> into the front matter. The {{ kind.singular }}
272
211
  keeps its page and still goes to your followers, but is left off the home page, archives,
273
212
  feeds, the sitemap, search and llms.txt, and asks search engines not to index it.
274
- </p>
213
+ {% endset %}
214
+ {% call field.select('editor-visibility', 'visibility', 'Visibility', hint=visibilityHint, hintId='editor-visibility-hint') %}
215
+ {% if form.visibility.unrecognized is defined %}
216
+ <option value="{{ form.visibility.unrecognized }}" selected>{{ form.visibility.unrecognized }} (not recognized)</option>
217
+ {% endif %}
218
+ <option value="public"{% if form.visibility == "public" %} selected{% endif %}>Public</option>
219
+ <option value="unlisted"{% if form.visibility == "unlisted" %} selected{% endif %}>Unlisted</option>
220
+ {% endcall %}
275
221
 
276
222
  {% if kind.type == "post" %}
277
223
  {# TASK-207: shown at the top of the author's fediverse profile, from
278
224
  the featured collection their actor links. #}
279
- <p class="admin-check">
280
- <input id="editor-pinned" name="pinned" type="checkbox" value="1"{% if form.pinned %} checked{% endif %}{{ invalid('editor-pinned') }} />
281
- <label for="editor-pinned">Pinned</label>
282
- </p>
283
- {{ problem('editor-pinned') }}
284
- <p class="admin-hint">
285
- Writes <code>pinned:</code> and the moment you pinned it into the front matter. Pinned posts
286
- show first on your fediverse profile, newest pin first, up to five.
287
- </p>
225
+ {{ field.checkbox('editor-pinned', 'pinned', 'Pinned', checked=form.pinned, error=(error if errorField == 'editor-pinned' else ''), hint='Writes <code>pinned:</code> and the moment you pinned it into the front matter. Pinned posts show first on your fediverse profile, newest pin first, up to five.') }}
288
226
  {% endif %}
289
227
 
290
228
  {# Who the front matter says wrote this (TASK-67). A select of the site's
@@ -293,65 +231,47 @@
293
231
  and, once the actors land, whose followers hear about it. A file that
294
232
  still names somebody who has no account keeps an option of its own, so
295
233
  opening the editor cannot silently reattribute it. #}
296
- <label for="editor-author">Author</label>
297
- <select id="editor-author" name="author">
234
+ {% call field.select('editor-author', 'author', 'Author') %}
298
235
  {% for choice in authors %}
299
236
  <option value="{{ choice.value }}"{% if choice.chosen %} selected{% endif %}>{{ choice.label }}</option>
300
237
  {% endfor %}
301
- </select>
238
+ {% endcall %}
302
239
  {% endcall %}
303
240
 
304
241
  {% if kind.tagged or kind.categorised %}
305
- {% call group('Tags and categories', open.taxonomy) %}
242
+ {% call collapse('Tags and categories', open.taxonomy, fields=true) %}
306
243
  {% if kind.tagged %}
307
- <label for="editor-tags">Tags</label>
308
- <input id="editor-tags" name="tags" type="text" value="{{ form.tags }}" />
309
- <p class="admin-hint">Separated by commas.</p>
244
+ {{ field.text('editor-tags', 'tags', 'Tags', value=form.tags, hint='Separated by commas.') }}
310
245
  {% endif %}
311
-
312
246
  {% if kind.categorised %}
313
- <label for="editor-categories">Categories</label>
314
- <input id="editor-categories" name="categories" type="text" value="{{ form.categories }}" />
315
- <p class="admin-hint">Separated by commas.</p>
247
+ {{ field.text('editor-categories', 'categories', 'Categories', value=form.categories, hint='Separated by commas.') }}
316
248
  {% endif %}
317
249
  {% endcall %}
318
250
  {% endif %}
319
251
 
320
- {% call group('Address', open.address) %}
321
- <label for="editor-slug">Slug</label>
322
- <input id="editor-slug" name="slug" type="text" value="{{ form.slug }}"{{ invalid('editor-slug') }} />
323
- {{ problem('editor-slug') }}
324
-
325
- <label for="editor-permalink">Permalink</label>
326
- <input id="editor-permalink" name="permalink" type="text" value="{{ form.permalink }}" />
327
- <p class="admin-hint">Leave it empty to let the {{ kind.singular }} take the URL its slug implies.</p>
252
+ {% call collapse('Address', open.address, fields=true) %}
253
+ {{ field.text('editor-slug', 'slug', 'Slug', value=form.slug, error=(error if errorField == 'editor-slug' else '')) }}
254
+ {% set permalinkHint %}Leave it empty to let the {{ kind.singular }} take the URL its slug implies.{% endset %}
255
+ {{ field.text('editor-permalink', 'permalink', 'Permalink', value=form.permalink, hint=permalinkHint) }}
328
256
  {% endcall %}
329
257
 
330
258
  {% if kind.type == "post" %}
331
- {% call group('Responding to', open.responding) %}
259
+ {% call collapse('Responding to', open.responding, fields=true) %}
332
260
  {#- The mf2 `in-reply-to`: a URL here makes the post a reply, threads
333
261
  it in the fediverse and sends that page a webmention. -#}
334
- <label for="editor-in-reply-to">In reply to</label>
335
- <input id="editor-in-reply-to" name="in-reply-to" type="url" value="{{ form.inReplyTo }}"{{ invalid('editor-in-reply-to') }} />
336
- {{ problem('editor-in-reply-to') }}
262
+ {{ field.text('editor-in-reply-to', 'in-reply-to', 'In reply to', value=form.inReplyTo, type='url', error=(error if errorField == 'editor-in-reply-to' else '')) }}
337
263
  {{ citedCard('in-reply-to') }}
338
- <p class="admin-hint">The address of the post this one answers. Leave it empty for a post that answers nobody.</p>
264
+ <p class="label block whitespace-normal text-xs">The address of the post this one answers. Leave it empty for a post that answers nobody.</p>
339
265
 
340
266
  {#- The mf2 `like-of`, `repost-of` and `bookmark-of` (TASK-169): each
341
267
  cites a page the way a reply does, and sends it a webmention. -#}
342
- <label for="editor-like-of">Like of</label>
343
- <input id="editor-like-of" name="like-of" type="url" value="{{ form.likeOf }}"{{ invalid('editor-like-of') }} />
344
- {{ problem('editor-like-of') }}
268
+ {{ field.text('editor-like-of', 'like-of', 'Like of', value=form.likeOf, type='url', error=(error if errorField == 'editor-like-of' else '')) }}
345
269
  {{ citedCard('like-of') }}
346
- <label for="editor-repost-of">Repost of</label>
347
- <input id="editor-repost-of" name="repost-of" type="url" value="{{ form.repostOf }}"{{ invalid('editor-repost-of') }} />
348
- {{ problem('editor-repost-of') }}
270
+ {{ field.text('editor-repost-of', 'repost-of', 'Repost of', value=form.repostOf, type='url', error=(error if errorField == 'editor-repost-of' else '')) }}
349
271
  {{ citedCard('repost-of') }}
350
- <label for="editor-bookmark-of">Bookmark of</label>
351
- <input id="editor-bookmark-of" name="bookmark-of" type="url" value="{{ form.bookmarkOf }}"{{ invalid('editor-bookmark-of') }} />
352
- {{ problem('editor-bookmark-of') }}
272
+ {{ field.text('editor-bookmark-of', 'bookmark-of', 'Bookmark of', value=form.bookmarkOf, type='url', error=(error if errorField == 'editor-bookmark-of' else '')) }}
353
273
  {{ citedCard('bookmark-of') }}
354
- <p class="admin-hint">The address of a post this one likes, reposts or bookmarks. Leave them empty for a post that cites nothing.</p>
274
+ <p class="label block whitespace-normal text-xs">The address of a post this one likes, reposts or bookmarks. Leave them empty for a post that cites nothing.</p>
355
275
  {% if form.previewHidden and not (citedPreviews | length) %}
356
276
  <input type="hidden" name="preview" value="hide">
357
277
  {% endif %}
@@ -360,9 +280,8 @@
360
280
  {% endif %}
361
281
  {% endcall %}
362
282
 
363
- {% call group('Read', open.read) %}
364
- <label for="editor-read-status">Read status</label>
365
- <select id="editor-read-status" name="{{ readFields.status }}"{{ invalid('editor-read-status') }}>
283
+ {% call collapse('Read', open.read, fields=true) %}
284
+ {% call field.select('editor-read-status', readFields.status, 'Read status', error=(error if errorField == 'editor-read-status' else '')) %}
366
285
  <option value=""{% if form.readStatus == "" %} selected{% endif %}>Not a read post</option>
367
286
  {% for status in readStatuses %}
368
287
  <option value="{{ status.value }}"{% if form.readStatus == status.value %} selected{% endif %}>{{ status.label }}</option>
@@ -370,47 +289,33 @@
370
289
  {% if unrecognizedReadStatus %}
371
290
  <option value="{{ unrecognizedReadStatus }}" selected>{{ unrecognizedReadStatus }} (not recognized)</option>
372
291
  {% endif %}
373
- </select>
374
- {{ problem('editor-read-status') }}
375
- <label for="editor-read-of-name">Title</label>
376
- <input id="editor-read-of-name" name="{{ readFields.name }}" type="text" value="{{ form.readOf.name }}"{{ invalid('editor-read-of-name') }} />
377
- {{ problem('editor-read-of-name') }}
378
- <label for="editor-read-of-author">Author</label>
379
- <input id="editor-read-of-author" name="{{ readFields.author }}" type="text" value="{{ form.readOf.author }}" />
380
- <label for="editor-read-of-uid">Identifier</label>
381
- <input id="editor-read-of-uid" name="{{ readFields.uid }}" type="text" value="{{ form.readOf.uid }}" placeholder="isbn:9780441478125" autocomplete="off" spellcheck="false" />
382
- <label for="editor-read-of-url">Address</label>
383
- <input id="editor-read-of-url" name="{{ readFields.url }}" type="url" value="{{ form.readOf.url }}"{{ invalid('editor-read-of-url') }} />
384
- {{ problem('editor-read-of-url') }}
385
- <p class="admin-hint">A book or paper this post says its author read. An identifier is an ISBN or a DOI, written isbn:… or doi:…. Choose Not a read post and empty every box to remove it. A read is described by what it says, so leave its Description empty.</p>
292
+ {% endcall %}
293
+ {{ field.text('editor-read-of-name', readFields.name, 'Title', value=form.readOf.name, error=(error if errorField == 'editor-read-of-name' else '')) }}
294
+ {{ field.text('editor-read-of-author', readFields.author, 'Author', value=form.readOf.author) }}
295
+ {{ field.text('editor-read-of-uid', readFields.uid, 'Identifier', value=form.readOf.uid, placeholder='isbn:9780441478125', autocomplete='off', spellcheck='false') }}
296
+ {{ field.text('editor-read-of-url', readFields.url, 'Address', value=form.readOf.url, type='url', error=(error if errorField == 'editor-read-of-url' else '')) }}
297
+ <p class="label block whitespace-normal text-xs">A book or paper this post says its author read. An identifier is an ISBN or a DOI, written isbn:… or doi:…. Choose Not a read post and empty every box to remove it. A read is described by what it says, so leave its Description empty.</p>
386
298
  {% endcall %}
387
299
  {% endif %}
388
300
 
389
- {% call group('Summary and language', open.summary) %}
390
- <label for="editor-description">Description</label>
391
- <input id="editor-description" name="description" type="text" value="{{ form.description }}"{{ invalid('editor-description') }} />
392
- {{ problem('editor-description') }}
301
+ {% call collapse('Summary and language', open.summary, fields=true) %}
302
+ {{ field.text('editor-description', 'description', 'Description', value=form.description, error=(error if errorField == 'editor-description' else '')) }}
393
303
 
394
304
  {#- The `lang` front matter (TASK-154): a post or page written in another
395
305
  language than the site's says so, for screen readers, translation
396
306
  and fediverse language filters. -#}
397
- <label for="editor-lang">Language</label>
398
- <input id="editor-lang" name="lang" type="text" value="{{ form.lang }}" autocomplete="off" spellcheck="false"{{ invalid('editor-lang', 'editor-lang-hint') }} />
399
- {{ problem('editor-lang') }}
400
- <p class="admin-hint" id="editor-lang-hint">A language tag, such as fr or pt-BR. Leave it empty for the site’s language, {{ siteLanguage }}.</p>
307
+ {% set langHint %}A language tag, such as fr or pt-BR. Leave it empty for the site’s language, {{ siteLanguage }}.{% endset %}
308
+ {{ field.text('editor-lang', 'lang', 'Language', value=form.lang, autocomplete='off', spellcheck='false', error=(error if errorField == 'editor-lang' else ''), hint=langHint, hintId='editor-lang-hint') }}
401
309
  {% endcall %}
402
310
 
403
311
  {% if kind.type == "post" and syndicationTargets.length %}
404
312
  {# TASK-155: the targets content/_data/syndicationTargets.json declares.
405
313
  A target whose tag the post carries is selected without a tick. #}
406
- {% call group('Syndicate to', open.syndication) %}
314
+ {% call collapse('Syndicate to', open.syndication, fields=true) %}
407
315
  {% for target in syndicationTargets %}
408
- <p class="admin-check">
409
- <input id="editor-{{ target.field }}" name="{{ target.field }}" type="checkbox" value="1"{% if target.checked %} checked{% endif %} />
410
- <label for="editor-{{ target.field }}">{{ target.name }}</label>
411
- </p>
316
+ {{ field.checkbox('editor-' + target.field, target.field, target.name | escape, checked=target.checked) }}
412
317
  {% endfor %}
413
- <p class="admin-hint">
318
+ <p class="label block whitespace-normal text-xs">
414
319
  Writes <code>syndicate-to:</code> into the front matter. The post links to each target and
415
320
  sends it a webmention when it is published or updated. A post tagged with a target’s tag is
416
321
  sent there without a tick.
@@ -418,55 +323,30 @@
418
323
  {% endcall %}
419
324
  {% endif %}
420
325
 
421
- {% call group('Display and discussion', open.display) %}
326
+ {% call collapse('Display and discussion', open.display, fields=true) %}
422
327
  {# What this document says about comments, if anything. Three answers
423
328
  rather than two: the site's own rules, always open, always closed. #}
424
- <label for="editor-comments">Comments</label>
425
- <select id="editor-comments" name="comments">
329
+ {% call field.select('editor-comments', 'comments', 'Comments', hint='Writes <code>comments: true</code> or <code>comments: false</code> into the front matter, which beats the site’s switch and its closing window in both directions. Fediverse replies are shown either way.') %}
426
330
  <option value="{{ commentSettings.site }}"{% if form.comments == commentSettings.site %} selected{% endif %}>Follow the site settings</option>
427
331
  <option value="{{ commentSettings.open }}"{% if form.comments == commentSettings.open %} selected{% endif %}>Open</option>
428
332
  <option value="{{ commentSettings.closed }}"{% if form.comments == commentSettings.closed %} selected{% endif %}>Closed</option>
429
- </select>
430
- <p class="admin-hint">
431
- Writes <code>comments: true</code> or <code>comments: false</code> into the front matter,
432
- which beats the site's switch and its closing window in both directions. Fediverse replies
433
- are shown either way.
434
- </p>
333
+ {% endcall %}
435
334
 
436
335
  {% if kind.excludable %}
437
336
  {# doc-2's eleventyExcludeFromCollections: still published and still at
438
337
  its URL, but out of every collection a template lists from. #}
439
- <p class="admin-check">
440
- <input id="editor-exclude" name="exclude" type="checkbox" value="1"{% if form.exclude %} checked{% endif %} />
441
- <label for="editor-exclude">Hide from collections</label>
442
- </p>
443
- <p class="admin-hint">Keeps the {{ kind.singular }} out of listings that gather content, without unpublishing it.</p>
338
+ {% set excludeHint %}Keeps the {{ kind.singular }} out of listings that gather content, without unpublishing it.{% endset %}
339
+ {{ field.checkbox('editor-exclude', 'exclude', 'Hide from collections', checked=form.exclude, hint=excludeHint) }}
444
340
  {% endif %}
445
341
 
446
342
  {% if kind.contactable %}
447
343
  {# The page offers a contact form under its content (TASK-56). Where a
448
344
  message goes is the contact address on the Settings screen; it is
449
345
  never on this page or in its HTML. #}
450
- <p class="admin-check">
451
- <input id="editor-contact" name="contact" type="checkbox" value="1"{% if form.contact %} checked{% endif %} />
452
- <label for="editor-contact">Contact form</label>
453
- </p>
454
- <p class="admin-hint">
455
- Writes <code>contact: true</code> into the front matter and puts a name, email, subject
456
- and message form under this {{ kind.singular }}. Messages arrive on the Messages screen
457
- and are emailed to the contact address on Settings.
458
- </p>
346
+ {% set contactHint %}Writes <code>contact: true</code> into the front matter and puts a name, email, subject and message form under this {{ kind.singular }}. Messages arrive on the Messages screen and are emailed to the contact address on Settings.{% endset %}
347
+ {{ field.checkbox('editor-contact', 'contact', 'Contact form', checked=form.contact, hint=contactHint) }}
459
348
  {% endif %}
460
349
  {% endcall %}
461
-
462
- <p class="admin-editor-buttons">
463
- {% for button in actions %}
464
- <button type="submit" name="action" value="{{ button.value }}">{{ button.label }}</button>
465
- {% endfor %}
466
- {% if viewUrl %}
467
- <a class="admin-button admin-button-quiet" href="{{ viewUrl }}">View</a>
468
- {% endif %}
469
- </p>
470
350
  </div>
471
351
  </form>
472
352