@geekity/cms 0.33.0 → 0.35.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 (196) hide show
  1. package/admin/pages/documents/chooser.njk +30 -0
  2. package/admin/pages/documents/editor.njk +394 -164
  3. package/admin/pages/media/file.njk +142 -0
  4. package/admin/pages/media/library.njk +103 -198
  5. package/admin/pages/settings/general.njk +48 -1
  6. package/admin/pages/settings/privacy.njk +1 -1
  7. package/admin/pages/tools/maintenance.njk +48 -0
  8. package/admin/src/admin.css +1 -0
  9. package/admin/static/admin.css +728 -65
  10. package/admin/static/copy.js +30 -23
  11. package/admin/static/item-lookup.js +179 -0
  12. package/admin/static/media.js +82 -0
  13. package/dist/admin/documents.d.ts.map +1 -1
  14. package/dist/admin/documents.js +141 -22
  15. package/dist/admin/documents.js.map +1 -1
  16. package/dist/admin/editor-actions.d.ts.map +1 -1
  17. package/dist/admin/editor-actions.js +2 -3
  18. package/dist/admin/editor-actions.js.map +1 -1
  19. package/dist/admin/editor-fields.d.ts +20 -8
  20. package/dist/admin/editor-fields.d.ts.map +1 -1
  21. package/dist/admin/editor-fields.js +35 -12
  22. package/dist/admin/editor-fields.js.map +1 -1
  23. package/dist/admin/editor-kinds.d.ts +30 -0
  24. package/dist/admin/editor-kinds.d.ts.map +1 -0
  25. package/dist/admin/editor-kinds.js +136 -0
  26. package/dist/admin/editor-kinds.js.map +1 -0
  27. package/dist/admin/editor-layout.d.ts +10 -1
  28. package/dist/admin/editor-layout.d.ts.map +1 -1
  29. package/dist/admin/editor-layout.js +38 -31
  30. package/dist/admin/editor-layout.js.map +1 -1
  31. package/dist/admin/federation.d.ts +1 -1
  32. package/dist/admin/federation.d.ts.map +1 -1
  33. package/dist/admin/federation.js +2 -1
  34. package/dist/admin/federation.js.map +1 -1
  35. package/dist/admin/formatting.d.ts +2 -0
  36. package/dist/admin/formatting.d.ts.map +1 -1
  37. package/dist/admin/formatting.js +8 -0
  38. package/dist/admin/formatting.js.map +1 -1
  39. package/dist/admin/index.d.ts +1 -1
  40. package/dist/admin/index.d.ts.map +1 -1
  41. package/dist/admin/index.js +1 -1
  42. package/dist/admin/index.js.map +1 -1
  43. package/dist/admin/item-lookup.d.ts +16 -0
  44. package/dist/admin/item-lookup.d.ts.map +1 -0
  45. package/dist/admin/item-lookup.js +192 -0
  46. package/dist/admin/item-lookup.js.map +1 -0
  47. package/dist/admin/maintenance.d.ts +2 -0
  48. package/dist/admin/maintenance.d.ts.map +1 -1
  49. package/dist/admin/maintenance.js +14 -0
  50. package/dist/admin/maintenance.js.map +1 -1
  51. package/dist/admin/media.d.ts +43 -29
  52. package/dist/admin/media.d.ts.map +1 -1
  53. package/dist/admin/media.js +221 -54
  54. package/dist/admin/media.js.map +1 -1
  55. package/dist/admin/preview.d.ts.map +1 -1
  56. package/dist/admin/preview.js +9 -12
  57. package/dist/admin/preview.js.map +1 -1
  58. package/dist/admin/routes.d.ts.map +1 -1
  59. package/dist/admin/routes.js +3 -0
  60. package/dist/admin/routes.js.map +1 -1
  61. package/dist/admin/settings-general.d.ts +6 -0
  62. package/dist/admin/settings-general.d.ts.map +1 -1
  63. package/dist/admin/settings-general.js +46 -1
  64. package/dist/admin/settings-general.js.map +1 -1
  65. package/dist/admin/templates.d.ts +2 -0
  66. package/dist/admin/templates.d.ts.map +1 -1
  67. package/dist/admin/templates.js +4 -0
  68. package/dist/admin/templates.js.map +1 -1
  69. package/dist/cli.d.ts.map +1 -1
  70. package/dist/cli.js +2 -1
  71. package/dist/cli.js.map +1 -1
  72. package/dist/cms/app.d.ts.map +1 -1
  73. package/dist/cms/app.js +1 -0
  74. package/dist/cms/app.js.map +1 -1
  75. package/dist/cms/services.d.ts +2 -0
  76. package/dist/cms/services.d.ts.map +1 -1
  77. package/dist/cms/services.js +13 -0
  78. package/dist/cms/services.js.map +1 -1
  79. package/dist/content/document-form.d.ts +5 -13
  80. package/dist/content/document-form.d.ts.map +1 -1
  81. package/dist/content/document-form.js +2 -3
  82. package/dist/content/document-form.js.map +1 -1
  83. package/dist/content/excerpt.d.ts +1 -3
  84. package/dist/content/excerpt.d.ts.map +1 -1
  85. package/dist/content/excerpt.js +2 -7
  86. package/dist/content/excerpt.js.map +1 -1
  87. package/dist/content/post-fields.d.ts +1 -1
  88. package/dist/content/post-fields.d.ts.map +1 -1
  89. package/dist/content/post-fields.js +30 -35
  90. package/dist/content/post-fields.js.map +1 -1
  91. package/dist/content/post-type.d.ts +2 -3
  92. package/dist/content/post-type.d.ts.map +1 -1
  93. package/dist/content/post-type.js +8 -9
  94. package/dist/content/post-type.js.map +1 -1
  95. package/dist/content/review-form.d.ts +49 -0
  96. package/dist/content/review-form.d.ts.map +1 -0
  97. package/dist/content/review-form.js +282 -0
  98. package/dist/content/review-form.js.map +1 -0
  99. package/dist/content/review.d.ts +57 -0
  100. package/dist/content/review.d.ts.map +1 -0
  101. package/dist/content/review.js +264 -0
  102. package/dist/content/review.js.map +1 -0
  103. package/dist/content/write.d.ts.map +1 -1
  104. package/dist/content/write.js +6 -9
  105. package/dist/content/write.js.map +1 -1
  106. package/dist/exports.d.ts +1 -1
  107. package/dist/exports.d.ts.map +1 -1
  108. package/dist/federation/article.d.ts.map +1 -1
  109. package/dist/federation/article.js +27 -29
  110. package/dist/federation/article.js.map +1 -1
  111. package/dist/federation/federation.d.ts +3 -0
  112. package/dist/federation/federation.d.ts.map +1 -1
  113. package/dist/federation/federation.js +5 -1
  114. package/dist/federation/federation.js.map +1 -1
  115. package/dist/federation/refusals.d.ts +18 -0
  116. package/dist/federation/refusals.d.ts.map +1 -0
  117. package/dist/federation/refusals.js +84 -0
  118. package/dist/federation/refusals.js.map +1 -0
  119. package/dist/images/variants.d.ts +1 -0
  120. package/dist/images/variants.d.ts.map +1 -1
  121. package/dist/images/variants.js +10 -0
  122. package/dist/images/variants.js.map +1 -1
  123. package/dist/item-lookup/item-id.d.ts +33 -0
  124. package/dist/item-lookup/item-id.d.ts.map +1 -0
  125. package/dist/item-lookup/item-id.js +71 -0
  126. package/dist/item-lookup/item-id.js.map +1 -0
  127. package/dist/item-lookup/lookup.d.ts +58 -0
  128. package/dist/item-lookup/lookup.d.ts.map +1 -0
  129. package/dist/item-lookup/lookup.js +532 -0
  130. package/dist/item-lookup/lookup.js.map +1 -0
  131. package/dist/micropub/create.d.ts +1 -1
  132. package/dist/micropub/create.d.ts.map +1 -1
  133. package/dist/micropub/create.js +4 -2
  134. package/dist/micropub/create.js.map +1 -1
  135. package/dist/micropub/endpoint.d.ts.map +1 -1
  136. package/dist/micropub/endpoint.js +3 -10
  137. package/dist/micropub/endpoint.js.map +1 -1
  138. package/dist/micropub/update.d.ts.map +1 -1
  139. package/dist/micropub/update.js +12 -6
  140. package/dist/micropub/update.js.map +1 -1
  141. package/dist/plugin.d.ts +32 -3
  142. package/dist/plugin.d.ts.map +1 -1
  143. package/dist/plugin.js +1 -1
  144. package/dist/plugin.js.map +1 -1
  145. package/dist/plugins/site.d.ts.map +1 -1
  146. package/dist/plugins/site.js +4 -1
  147. package/dist/plugins/site.js.map +1 -1
  148. package/dist/settings/names.d.ts +1 -1
  149. package/dist/settings/names.d.ts.map +1 -1
  150. package/dist/settings/names.js +8 -1
  151. package/dist/settings/names.js.map +1 -1
  152. package/dist/settings/registry.d.ts.map +1 -1
  153. package/dist/settings/registry.js +2 -1
  154. package/dist/settings/registry.js.map +1 -1
  155. package/dist/settings/tmdb.d.ts +5 -0
  156. package/dist/settings/tmdb.d.ts.map +1 -0
  157. package/dist/settings/tmdb.js +22 -0
  158. package/dist/settings/tmdb.js.map +1 -0
  159. package/dist/store/profiles.d.ts +3 -2
  160. package/dist/store/profiles.d.ts.map +1 -1
  161. package/dist/store/profiles.js +2 -4
  162. package/dist/store/profiles.js.map +1 -1
  163. package/dist/web/context.d.ts +9 -21
  164. package/dist/web/context.d.ts.map +1 -1
  165. package/dist/web/context.js +20 -20
  166. package/dist/web/context.js.map +1 -1
  167. package/dist/web/feed-item.d.ts +6 -6
  168. package/dist/web/feed-item.d.ts.map +1 -1
  169. package/dist/web/feed-item.js +7 -3
  170. package/dist/web/feed-item.js.map +1 -1
  171. package/dist/web/plugin-federation.d.ts +2 -0
  172. package/dist/web/plugin-federation.d.ts.map +1 -1
  173. package/dist/web/plugin-federation.js +3 -0
  174. package/dist/web/plugin-federation.js.map +1 -1
  175. package/dist/webmention/cited-picture.d.ts +1 -0
  176. package/dist/webmention/cited-picture.d.ts.map +1 -1
  177. package/dist/webmention/cited-picture.js +7 -4
  178. package/dist/webmention/cited-picture.js.map +1 -1
  179. package/dist/webmention/fetch-public.d.ts +3 -0
  180. package/dist/webmention/fetch-public.d.ts.map +1 -1
  181. package/dist/webmention/fetch-public.js +1 -1
  182. package/dist/webmention/fetch-public.js.map +1 -1
  183. package/dist/webmention/reply-context/json-ld.d.ts +8 -0
  184. package/dist/webmention/reply-context/json-ld.d.ts.map +1 -1
  185. package/dist/webmention/reply-context/json-ld.js +21 -0
  186. package/dist/webmention/reply-context/json-ld.js.map +1 -1
  187. package/dist/webmention/service.d.ts.map +1 -1
  188. package/dist/webmention/service.js +3 -5
  189. package/dist/webmention/service.js.map +1 -1
  190. package/package.json +1 -1
  191. package/themes/default/layouts/post.njk +6 -5
  192. package/themes/default/partials/post-list.njk +15 -20
  193. package/themes/default/partials/review.njk +1 -0
  194. package/themes/default/src/style.css +24 -4
  195. package/themes/default/static/style.css +33 -5
  196. package/themes/default/partials/read.njk +0 -5
@@ -1,7 +1,7 @@
1
1
  {# The editor behind /admin/posts/{slug} and /admin/pages/{slug}, and behind
2
- .../new. One template for both kinds: `kind` decides whether there is a date
3
- and the taxonomy fields, and the handler decides which buttons the form
4
- carries. #}
2
+ .../new. One template for both types: `kind` decides whether there is a
3
+ date and the taxonomy fields, and the handler decides which buttons the
4
+ form carries. #}
5
5
  {% extends "layouts/shell.njk" %}
6
6
 
7
7
  {% block title %}{{ heading }}{% endblock %}
@@ -12,9 +12,46 @@
12
12
  {% from "components/button.njk" import button, buttonLink %}
13
13
  {% from "components/card.njk" import card, cardActions %}
14
14
  {% from "components/collapse.njk" import collapse %}
15
+ {% from "components/dropdown.njk" import dropdown %}
15
16
 
16
17
  {% set ACTION_COLORS = { 'publish': 'primary', 'update': 'primary', 'trash': 'error' } %}
17
18
  {% set ACTION_STYLES = { 'trash': 'ghost' } %}
19
+ {% set SECTION_TITLES = { photos: 'Photos', link: 'Link', rsvp: 'RSVP', event: 'Event', episode: 'Recording', review: 'Review', checkin: 'Check-in' } %}
20
+
21
+ {% set ICON_SIZES = { md: 'size-6', sm: 'size-4' } %}
22
+
23
+ {% macro icon(path, size='md') -%}
24
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.6" stroke="currentColor" class="shrink-0{{ ICON_SIZES | modifier(size) }}" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="{{ path }}"/></svg>
25
+ {%- endmacro %}
26
+
27
+ {% set ITEM_KIND_SHOWS = {
28
+ book: 'group-has-[[name=item-kind][value=book]:checked]/editor:block',
29
+ movie: 'group-has-[[name=item-kind][value=movie]:checked]/editor:block',
30
+ tv: 'group-has-[[name=item-kind][value=tv]:checked]/editor:block',
31
+ album: 'group-has-[[name=item-kind][value=album]:checked]/editor:block',
32
+ place: 'group-has-[[name=item-kind][value=place]:checked]/editor:block',
33
+ other: 'group-has-[[data-as-other]:checked]/editor:block'
34
+ } %}
35
+
36
+ {% set LOOKUP_TONES = { found: 'text-base-content', failed: 'text-error' } %}
37
+
38
+ {% macro forItemKinds(kinds, id='') -%}
39
+ <div class="hidden{% for kind in kinds %}{{ ITEM_KIND_SHOWS | modifier(kind) }}{% endfor %}"{% if id %} id="{{ id }}"{% endif %}>
40
+ {%- endmacro %}
41
+
42
+ {% macro itemWord(key, after='', before='') -%}
43
+ {% for kind in reviewKinds %}{% if kind[key] %}<span class="hidden{{ ITEM_KIND_SHOWS | modifier(kind.value) }}">{{ before }}{{ kind[key] }}{{ after }}</span>{% endif %}{% endfor %}
44
+ {%- endmacro %}
45
+
46
+ {% macro invalidIf(id) -%}
47
+ {% if errorField == id %} aria-invalid="true" aria-describedby="{{ id }}-error"{% endif %}
48
+ {%- endmacro %}
49
+
50
+ {% macro groupError(id) %}
51
+ {% if errorField == id %}
52
+ <p class="text-sm text-error" id="{{ id }}-error">{{ error }}</p>
53
+ {% endif %}
54
+ {% endmacro %}
18
55
 
19
56
  {% macro citedCard(property) %}
20
57
  {% set cited = citedPreviews[property] %}
@@ -75,8 +112,309 @@
75
112
  {% endif %}
76
113
  {% endmacro %}
77
114
 
115
+ {% macro placeFields(checkin) %}
116
+ <div>
117
+ {{ button('Use my location', id='editor-location-here', style='soft', hidden=true) }}
118
+ </div>
119
+ <p id="editor-location-status" class="label block whitespace-normal text-xs" role="status"></p>
120
+ {{ field.text('editor-location-name', locationFields.name, 'Place', value=form.location.name, placeholder='A café, a park, a venue') }}
121
+ <div class="grid gap-x-4 sm:grid-cols-3">
122
+ {{ field.text('editor-location-locality', locationFields.locality, 'Locality', value=form.location.locality) }}
123
+ {{ field.text('editor-location-region', locationFields.region, 'Region', value=form.location.region) }}
124
+ {{ field.text('editor-location-country', locationFields.country, 'Country', value=form.location.country) }}
125
+ </div>
126
+ {{ 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 ''), hintHtml='Latitude, longitude, as decimal degrees. A <code>geo:</code> URI works too.', hintId='editor-location-geo-hint') }}
127
+ {{ 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') }}
128
+ {% if checkin %}
129
+ <input type="hidden" name="{{ locationFields.checkin }}" value="1" />
130
+ {% elif form.location.checkin %}
131
+ {{ field.checkbox('editor-location-checkin', locationFields.checkin, 'A check-in', checked=true, hint='A Micropub app sent it as a check-in, and reads it back as one. Clear it to keep the place as an ordinary location.', hintId='editor-location-checkin-hint') }}
132
+ {% endif %}
133
+ <p class="label block whitespace-normal text-xs">
134
+ Kept in <code>data/locations.json</code>, not in the post’s file. What readers see of it
135
+ is chosen on Settings &gt; Privacy. Empty every box to remove it.
136
+ </p>
137
+ {% endmacro %}
138
+
139
+ {% macro section(name, own) %}
140
+ {% if name == 'photos' %}
141
+ <div class="flex min-w-0 flex-col gap-3{% if postKind == 'review' and not sections.filled.photos %} hidden{{ ITEM_KIND_SHOWS | modifier('place') }}{% endif %}">
142
+ {% if own %}<p class="fieldset-legend">{% if postKind == 'review' %}Your photos{% else %}Photos{% endif %}</p>{% endif %}
143
+ {% for row in photos.rows %}
144
+ {% set n = loop.index0 %}
145
+ <div class="flex gap-3 rounded-box border border-base-300 p-3">
146
+ {% if row.thumbnail %}
147
+ <img class="size-20 shrink-0 rounded-box object-cover" src="{{ row.thumbnail }}" alt="" loading="lazy" />
148
+ {% endif %}
149
+ <div class="flex min-w-0 flex-1 flex-col">
150
+ {{ field.text('editor-photo-alt-' + n, photoFields.alt + n, 'Alt text for photo ' + loop.index, value=row.alt, placeholder=(row.libraryAlt or 'Describe what the photo shows'), hint=('Leave it empty to use the media library’s, shown greyed.' if row.libraryAlt else '')) }}
151
+ {{ 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 ''), hint='Empty it to take the photo out.') }}
152
+ </div>
153
+ </div>
154
+ {% endfor %}
155
+ {% if photos.picks | length %}
156
+ <fieldset class="fieldset">
157
+ <legend class="fieldset-legend">Add from the media library</legend>
158
+ <div class="grid grid-cols-3 gap-2 sm:grid-cols-6">
159
+ {% for pick in photos.picks %}
160
+ <label class="relative cursor-pointer">
161
+ <input class="peer sr-only" type="checkbox" name="{{ photoFields.url }}{{ pick.index }}" value="{{ pick.url }}" />
162
+ <img class="aspect-square w-full rounded-box object-cover ring-primary ring-offset-2 ring-offset-base-100 peer-checked:ring-2 peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-base-content" src="{{ pick.thumbnail }}" alt="{{ pick.alt or pick.url }}" loading="lazy" />
163
+ <span class="badge badge-primary badge-xs absolute end-1 top-1 hidden peer-checked:inline-flex" aria-hidden="true">✓</span>
164
+ </label>
165
+ {% endfor %}
166
+ </div>
167
+ <p class="label block whitespace-normal">A ticked photo is added when you save, with the media library’s alt text. Add them now to write alt text of your own first.</p>
168
+ <div>{{ button('Add the ticked photos', type='submit', name='switch-kind', value=postKind, style='soft', formnovalidate=true) }}</div>
169
+ </fieldset>
170
+ {% endif %}
171
+ <div class="grid gap-x-4 sm:grid-cols-2">
172
+ {{ field.text('editor-photo-url-' + photos.blank, photoFields.url + photos.blank, 'Or a web address', value='', list='editor-photo-uploads', autocomplete='off', error=(error if errorField == ('editor-photo-url-' + photos.blank) else '')) }}
173
+ {{ field.text('editor-photo-alt-' + photos.blank, photoFields.alt + photos.blank, 'Its alt text', value='') }}
174
+ </div>
175
+ </div>
176
+
177
+ {% elif name == 'link' %}
178
+ <fieldset class="fieldset">
179
+ <legend class="fieldset-legend">This post</legend>
180
+ <div class="join flex-wrap">
181
+ {% for verb in link.verbs %}
182
+ <input class="join-item btn btn-sm" type="radio" name="link-verb" value="{{ verb.property }}" aria-label="{{ verb.label }}"{% if verb.property == link.chosen %} checked{% endif %} />
183
+ {% endfor %}
184
+ </div>
185
+ </fieldset>
186
+ {{ field.text('editor-' + link.chosen, 'link-url', 'Address', value=link.url, type='url', error=(error if errorField == ('editor-' + link.chosen) else ''), hint='The page is fetched for its title and picture, and sent a webmention when you publish. Leave it empty for a post that cites nothing.') }}
187
+ {{ citedCard(link.chosen) }}
188
+ {% for other in link.others %}
189
+ {{ field.text('editor-' + other.property, other.property, other.also, value=other.url, type='url', error=(error if errorField == ('editor-' + other.property) else ''), hint='Empty it to take it out.') }}
190
+ {{ citedCard(other.property) }}
191
+ {% endfor %}
192
+
193
+ {% elif name == 'rsvp' %}
194
+ {{ field.text('editor-in-reply-to', 'in-reply-to', 'Event address', value=form.inReplyTo, type='url', error=(error if errorField == 'editor-in-reply-to' else ''), hint='The event’s page. It is sent a webmention when you publish.') }}
195
+ {{ citedCard('in-reply-to') }}
196
+ <fieldset class="fieldset">
197
+ <legend class="fieldset-legend">Going?</legend>
198
+ <div class="join flex-wrap">
199
+ {% for choice in rsvpChoices %}
200
+ <input{% if loop.first %} id="editor-rsvp"{{ invalidIf('editor-rsvp') }}{% endif %} class="join-item btn btn-sm" type="radio" name="rsvp" value="{{ choice.value }}" aria-label="{{ choice.label }}"{% if form.rsvp == choice.value %} checked{% endif %} />
201
+ {% endfor %}
202
+ {% if unrecognizedRsvp %}
203
+ <input class="join-item btn btn-sm" type="radio" name="rsvp" value="{{ unrecognizedRsvp }}" aria-label="{{ unrecognizedRsvp }} (not recognized)" checked />
204
+ {% endif %}
205
+ {% if not own %}
206
+ <input class="join-item btn btn-sm" type="radio" name="rsvp" value="" aria-label="Not an RSVP" />
207
+ {% endif %}
208
+ </div>
209
+ {{ groupError('editor-rsvp') }}
210
+ </fieldset>
211
+
212
+ {% elif name == 'event' %}
213
+ {% set eventTimeHint %}Wall-clock time in {{ dateZone }}. Stored as UTC.{% endset %}
214
+ <div class="grid gap-x-4 sm:grid-cols-2">
215
+ {{ field.text('editor-event-start', eventFields.start, 'Starts', value=form.event.start, placeholder='2026-10-10 09:00', autocomplete='off', error=(error if errorField == 'editor-event-start' else ''), hintHtml=eventTimeHint, hintId='editor-event-time-hint') }}
216
+ {{ field.text('editor-event-end', eventFields.end, 'Ends', value=form.event.end, placeholder='2026-10-10 17:00', autocomplete='off', error=(error if errorField == 'editor-event-end' else ''), hintId='editor-event-time-hint') }}
217
+ </div>
218
+ {{ field.text('editor-event-location', eventFields.location, 'Where', value=form.event.location, error=(error if errorField == 'editor-event-location' else ''), hint='The place, or the address to join an online event at.', hintId='editor-event-location-hint') }}
219
+ {% if not own %}
220
+ <p class="label block whitespace-normal text-xs">Empty every box to make it a post again.</p>
221
+ {% endif %}
222
+
223
+ {% elif name == 'checkin' %}
224
+ {{ placeFields(true) }}
225
+
226
+ {% elif name == 'episode' %}
227
+ {% 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') %}
228
+ <option value=""{% if form.enclosure.url == "" %} selected{% endif %}>None</option>
229
+ {% for choice in enclosureChoices.media %}
230
+ <option value="{{ choice.url }}"{% if choice.url == form.enclosure.url %} selected{% endif %}>{{ choice.label }}</option>
231
+ {% endfor %}
232
+ {% endcall %}
233
+ <div class="grid gap-x-4 sm:grid-cols-2">
234
+ {{ 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') }}
235
+ {{ 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') }}
236
+ </div>
237
+ <datalist id="editor-transcript-uploads">
238
+ {% for url in enclosureChoices.transcripts %}
239
+ <option value="{{ url }}"></option>
240
+ {% endfor %}
241
+ </datalist>
242
+ {% 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 a web address. A linked transcript needs its type.', hintId='editor-enclosure-transcript-hint') %}
243
+ <option value=""{% if form.enclosure.transcriptType == "" %} selected{% endif %}>From the file</option>
244
+ {% for type in transcriptTypes %}
245
+ <option value="{{ type }}"{% if type == form.enclosure.transcriptType %} selected{% endif %}>{{ type }}</option>
246
+ {% endfor %}
247
+ {% endcall %}
248
+
249
+ {% call collapse('Other versions', open.versions, fields=true) %}
250
+ <p class="label block whitespace-normal text-xs">
251
+ A video of an audio episode, a smaller file, another language. Each is a file in the media
252
+ library or a web address, which needs its type. Empty an address to remove
253
+ that version.
254
+ </p>
255
+ {% for row in alternateRows %}
256
+ {% set n = loop.index0 %}
257
+ {% call collapse(('Version ' + loop.index) if row.url else 'Add a version', row.url, fields=true) %}
258
+ {{ 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 '')) }}
259
+ {{ 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 '')) }}
260
+ {{ 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 '')) }}
261
+ {{ 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 '')) }}
262
+ {{ 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 '')) }}
263
+ {% endcall %}
264
+ {% endfor %}
265
+ <datalist id="editor-media-uploads">
266
+ {% for choice in enclosureChoices.media %}
267
+ <option value="{{ choice.url }}"></option>
268
+ {% endfor %}
269
+ </datalist>
270
+ {% endcall %}
271
+
272
+ {% elif name == 'review' %}
273
+ <fieldset class="fieldset">
274
+ <legend class="fieldset-legend">Reviewing a</legend>
275
+ <div class="join flex-wrap">
276
+ {% for kind in reviewKinds %}
277
+ <input{% if loop.first %} id="editor-item-kind"{{ invalidIf('editor-item-kind') }}{% endif %} class="join-item btn btn-sm" type="radio" name="{{ reviewFields.kind }}" value="{{ kind.value }}" aria-label="{{ kind.label }}"{% if kind.value == reviewKindChosen %} checked{% endif %}{% if kind.value == 'other' %} data-as-other{% endif %} />
278
+ {% endfor %}
279
+ {% if unrecognizedReviewKind %}
280
+ <input class="join-item btn btn-sm" type="radio" name="{{ reviewFields.kind }}" value="{{ unrecognizedReviewKind }}" aria-label="{{ unrecognizedReviewKind }} (not recognized)" checked data-as-other />
281
+ {% endif %}
282
+ </div>
283
+ {{ groupError('editor-item-kind') }}
284
+ </fieldset>
285
+
286
+ {#- Nominatim's usage policy forbids autocomplete, so Find searches only on
287
+ Enter or Search. -#}
288
+ {{ forItemKinds(itemSearchKinds) }}
289
+ <div id="editor-item-find" class="fieldset" data-item-search="{{ itemSearchUrl }}" hidden>
290
+ <label class="fieldset-legend" for="editor-item-find-query">{{ itemWord('noun', before='Find the ') }}</label>
291
+ <div class="join w-full">
292
+ <label class="input input-sm join-item w-full">
293
+ {{ icon('m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z', 'sm') }}
294
+ <input id="editor-item-find-query" type="search" class="grow" placeholder="Search by title" autocomplete="off" aria-describedby="editor-item-find-status" />
295
+ </label>
296
+ <button type="button" class="btn btn-sm btn-soft join-item" id="editor-item-find-button">Search</button>
297
+ </div>
298
+ <p class="text-sm" id="editor-item-find-status" role="status"></p>
299
+ <ul class="menu w-full rounded-box border border-base-300 bg-base-100 p-1" id="editor-item-find-matches" aria-label="Matches" hidden>
300
+ <li class="menu-title text-xs font-normal" id="editor-item-find-credit" hidden></li>
301
+ <li class="menu-title text-xs font-normal">Not here? Paste a link in the box below and press Look up.</li>
302
+ </ul>
303
+ <template id="editor-item-find-match">
304
+ <li>
305
+ <button type="button" class="flex items-center gap-3 text-left">
306
+ <img class="h-12 w-9 shrink-0 rounded object-cover" alt="" hidden />
307
+ <span class="flex h-12 w-9 shrink-0 items-center justify-center rounded bg-base-200" data-no-picture>{{ icon('m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z', 'sm') }}</span>
308
+ <span class="min-w-0 flex-1"><span class="block font-semibold" data-name></span><span class="block text-xs opacity-70" data-detail></span></span>
309
+ <span class="badge badge-ghost badge-sm shrink-0" data-source></span>
310
+ </button>
311
+ </li>
312
+ </template>
313
+ </div>
314
+ </div>
315
+
316
+ {{ forItemKinds(reviewKindsWith.uid, 'editor-item-lookup') }}
317
+ <div class="join w-full items-end">
318
+ <div class="w-full">
319
+ {{ field.text('editor-item-uid', reviewFields.uid, '', labelHtml=itemWord('uid'), value=form.review.uid, autocomplete='off', spellcheck='false', error=(error if errorField == 'editor-item-uid' else ''), hintId='editor-item-uid-hint') }}
320
+ </div>
321
+ <button type="submit" class="btn btn-sm btn-soft join-item mb-1" name="{{ itemLookupButton }}" value="1" formnovalidate data-item-lookup="{{ itemLookupUrl }}">Look up</button>
322
+ </div>
323
+ <p class="text-sm{{ LOOKUP_TONES | modifier('failed' if itemLookup.failed else 'found') }}" id="editor-item-lookup-status" role="status">{{ itemLookup.message if itemLookup else '' }}</p>
324
+ <p class="label block whitespace-normal" id="editor-item-uid-hint">Its address on TMDb, IMDb, MusicBrainz, OpenStreetMap or its own site, or isbn:… or doi:… for a book or a paper. Look up fills the empty boxes from it.</p>
325
+ </div>
326
+ {{ field.text('editor-item-name', reviewFields.name, '', labelHtml=itemWord('name'), value=form.review.name, error=(error if errorField == 'editor-item-name' else '')) }}
327
+ {{ forItemKinds(reviewKindsWith.category) }}
328
+ {{ field.text('editor-item-category', reviewFields.category, 'What is it', value=form.review.category, hint='A restaurant, a plumber, an app. Collection pages group by this, so reuse the same word.', hintId='editor-item-category-hint') }}
329
+ </div>
330
+ <div class="grid gap-x-4 sm:grid-cols-2">
331
+ {{ forItemKinds(reviewKindsWith.by) }}
332
+ {{ field.text('editor-item-by', reviewFields.by, '', labelHtml=itemWord('by'), value=form.review.by) }}
333
+ </div>
334
+ {{ forItemKinds(reviewKindsWith.year) }}
335
+ {{ field.text('editor-item-year', reviewFields.year, 'Year', value=form.review.year, inputmode='numeric', error=(error if errorField == 'editor-item-year' else '')) }}
336
+ </div>
337
+ </div>
338
+ <div class="grid gap-x-4 sm:grid-cols-2">
339
+ {{ forItemKinds(reviewKindsWith.season) }}
340
+ {{ field.text('editor-item-season', reviewFields.season, 'Season', value=form.review.season, inputmode='numeric', error=(error if errorField == 'editor-item-season' else ''), hint='Leave Season and Episode empty to review the whole show.', hintId='editor-item-season-hint') }}
341
+ </div>
342
+ {{ forItemKinds(reviewKindsWith.episode) }}
343
+ {{ field.text('editor-item-episode', reviewFields.episode, 'Episode', value=form.review.episode, inputmode='numeric', error=(error if errorField == 'editor-item-episode' else '')) }}
344
+ </div>
345
+ </div>
346
+ {{ forItemKinds(['place']) }}
347
+ {{ field.text('editor-item-street-address', reviewFields.streetAddress, 'Street address', value=form.review.streetAddress) }}
348
+ <div class="grid gap-x-4 sm:grid-cols-3">
349
+ {{ field.text('editor-item-locality', reviewFields.locality, 'Locality', value=form.review.locality) }}
350
+ {{ field.text('editor-item-region', reviewFields.region, 'Region', value=form.review.region) }}
351
+ {{ field.text('editor-item-country-name', reviewFields.countryName, 'Country', value=form.review.countryName) }}
352
+ </div>
353
+ </div>
354
+ <div class="grid gap-x-4 sm:grid-cols-2">
355
+ {{ forItemKinds(reviewKindsWith.url) }}
356
+ {{ field.text('editor-item-url', reviewFields.url, '', labelHtml=itemWord('url'), value=form.review.url, type='url', error=(error if errorField == 'editor-item-url' else '')) }}
357
+ </div>
358
+ {{ forItemKinds(reviewKindsWith.date) }}
359
+ {{ field.text('editor-item-date', reviewFields.date, '', labelHtml=itemWord('date'), value=form.review.date, type='date', error=(error if errorField == 'editor-item-date' else '')) }}
360
+ </div>
361
+ </div>
362
+ {{ forItemKinds(reviewKindsWith.photo) }}
363
+ <div class="flex gap-3">
364
+ <img class="h-24 w-16 shrink-0 rounded-box object-cover" id="editor-item-photo-preview"{% if reviewPhotoThumbnail %} src="{{ reviewPhotoThumbnail }}"{% else %} hidden{% endif %} alt="" loading="lazy" />
365
+ <div class="flex min-w-0 flex-1 flex-col">
366
+ {{ field.text('editor-item-photo', reviewFields.photo, '', labelHtml=itemWord('picture'), value=form.review.photo, list='editor-photo-uploads', autocomplete='off', error=(error if errorField == 'editor-item-photo' else ''), hint='An image in the media library or a web address. It is the item’s picture, not a photo for the post: it shows beside the item’s details.', hintId='editor-item-photo-hint') }}
367
+ {{ field.text('editor-item-photo-alt', reviewFields.photoAlt, '', labelHtml=itemWord('picture', ' alt text'), value=form.review.photoAlt) }}
368
+ </div>
369
+ </div>
370
+ </div>
371
+
372
+ <fieldset class="fieldset rounded-box border border-base-300 bg-base-200/40 p-3">
373
+ <legend class="fieldset-legend">Rating</legend>
374
+ <div class="flex flex-wrap items-center gap-2">
375
+ <div class="rating rating-md">
376
+ <input id="editor-rating-none" class="rating-hidden" type="radio" name="{{ reviewFields.rating }}" value="" aria-label="No rating"{% if form.review.rating == "" %} checked{% endif %} />
377
+ {% for stars in [1, 2, 3, 4, 5] %}
378
+ <input{% if loop.first %} id="editor-rating"{{ invalidIf('editor-rating') }}{% endif %} class="mask mask-star-2 bg-warning" type="radio" name="{{ reviewFields.rating }}" value="{{ stars }}" aria-label="{{ stars }} out of 5"{% if form.review.rating == stars | string %} checked{% endif %} />
379
+ {% endfor %}
380
+ </div>
381
+ <label class="btn btn-ghost btn-xs" for="editor-rating-none">Clear</label>
382
+ {% if form.review.rating and form.review.rating not in ["1", "2", "3", "4", "5"] %}
383
+ <label class="label text-base-content">
384
+ <input class="radio radio-sm" type="radio" name="{{ reviewFields.rating }}" value="{{ form.review.rating }}" checked />
385
+ {{ form.review.rating }} (not recognized)
386
+ </label>
387
+ {% endif %}
388
+ </div>
389
+ {{ groupError('editor-rating') }}
390
+ </fieldset>
391
+ {% if not own %}
392
+ <p class="label block whitespace-normal text-xs">Empty every box and clear the rating to make it a post again.</p>
393
+ {% endif %}
394
+ {% endif %}
395
+ {% endmacro %}
396
+
78
397
  {% block content %}
79
- <h1 class="mb-6 text-2xl font-bold">{{ heading }}</h1>
398
+ <div class="mb-6 flex flex-wrap items-center gap-3">
399
+ <h1 class="flex min-w-0 items-center gap-2 break-words text-2xl font-bold">
400
+ {% if editorKind %}<span class="text-primary">{{ icon(editorKind.icon) }}</span>{% endif %}
401
+ <span class="min-w-0">{{ heading }}</span>
402
+ </h1>
403
+ {% if editorKind %}
404
+ {% call dropdown('editor-change-type', 'Change type', style='ghost') %}
405
+ <ul class="menu w-full">
406
+ {% for choice in postKinds %}
407
+ <li>
408
+ <button type="submit" form="editor-form" name="switch-kind" value="{{ choice.key }}" formnovalidate{% if choice.key == postKind %} class="menu-active" aria-current="true"{% endif %}>{{ icon(choice.icon, 'sm') }} {{ choice.label }}</button>
409
+ </li>
410
+ {% endfor %}
411
+ </ul>
412
+ {% endcall %}
413
+ {% if child == 'new' %}
414
+ <a class="link link-hover text-sm" href="{{ chooserUrl }}">All kinds</a>
415
+ {% endif %}
416
+ {% endif %}
417
+ </div>
80
418
 
81
419
  {% if error and errorField %}
82
420
  {% call field.summary('Nothing was saved. Fix the field marked below and try again.') %}
@@ -101,7 +439,7 @@
101
439
  </div>
102
440
  {% endif %}
103
441
 
104
- <form class="grid items-start gap-6 xl:grid-cols-[minmax(0,1fr)_20rem]" method="post" action="{{ saveUrl }}">
442
+ <form class="group/editor grid items-start gap-6 xl:grid-cols-[minmax(0,1fr)_20rem]" id="editor-form" method="post" action="{{ saveUrl }}">
105
443
  <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
106
444
  {# The hash of the file this form was filled in from. A save whose hash no
107
445
  longer matches what is on disk is refused rather than overwriting
@@ -109,15 +447,26 @@
109
447
  <input type="hidden" name="hash" value="{{ form.hash }}" />
110
448
  {# Which layout the preview renders through. The save handler ignores it. #}
111
449
  <input type="hidden" name="type" value="{{ kind.type }}" />
450
+ {% if editorKind %}
451
+ <input type="hidden" name="kind" value="{{ postKind }}" />
452
+ {% endif %}
112
453
 
113
454
  <div class="flex min-w-0 flex-col gap-4">
114
455
  {% call card() %}
115
- {% 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 %}
116
- {#- A post may go without one, and is then a note; a page is always named. -#}
117
- {{ field.text('editor-title', 'title', 'Title', value=form.title, required=(kind.type == "page"), autofocus=true, error=(error if errorField == 'editor-title' else ''), hintHtml=titleHint) }}
118
- {{ pluginActions('editor-title', editorActions['editor-title']) }}
456
+ {% if not editorKind or editorKind.title != 'none' or form.title %}
457
+ {% set titleHint %}{% if postKind == 'note' %}Add a title and the note becomes an article.{% endif %}{% endset %}
458
+ {#- A post may go without one, and is then a note; a page is always named. -#}
459
+ {{ field.text('editor-title', 'title', ('Title (optional)' if editorKind and editorKind.title != 'named' else 'Title'), value=form.title, required=(kind.type == "page"), autofocus=true, error=(error if errorField == 'editor-title' else ''), hintHtml=titleHint) }}
460
+ {{ pluginActions('editor-title', editorActions['editor-title']) }}
461
+ {% endif %}
119
462
 
120
- {{ field.textarea('editor-body', 'body', 'Body', value=form.body, rows=24) }}
463
+ {% if editorKind %}
464
+ {% for name in sections.own %}
465
+ {{ section(name, true) }}
466
+ {% endfor %}
467
+ {% endif %}
468
+
469
+ {{ field.textarea('editor-body', 'body', (editorKind.body if editorKind else 'Body'), value=form.body, rows=(8 if editorKind and editorKind.sections | length else 20)) }}
121
470
 
122
471
  {# Everything the editor's own script needs, on the element it hangs
123
472
  off. With JavaScript off this is one button that posts the form to
@@ -136,90 +485,27 @@
136
485
  </div>
137
486
  {% endcall %}
138
487
 
139
- {% if kind.type == "post" %}
140
- {% call collapse('Photos', open.photos, fields=true) %}
141
- <p class="label block whitespace-normal text-xs">
142
- Each is an image in the media library or a web address. Leave the alt text
143
- empty to use the media library’s. Empty an address to remove that photo.
144
- </p>
145
- {% for row in photoRows %}
146
- {% set n = loop.index0 %}
147
- {% call collapse(('Photo ' + loop.index) if row.url else 'Add a photo', row.url, fields=true) %}
148
- {{ 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 '')) }}
149
- {{ field.text('editor-photo-alt-' + n, photoFields.alt + n, 'Alt text', value=row.alt, placeholder=row.libraryAlt) }}
150
- {% endcall %}
151
- {% endfor %}
152
- <datalist id="editor-photo-uploads">
153
- {% for url in photoChoices %}
154
- <option value="{{ url }}"></option>
155
- {% endfor %}
156
- </datalist>
157
- {% endcall %}
158
-
159
- {% call collapse('Location', open.location, fields=true) %}
160
- <p class="label block whitespace-normal text-xs">
161
- Kept in <code>data/locations.json</code>, not in the post’s file. What readers see of it
162
- is chosen on Settings &gt; Privacy. Empty every box to remove it.
163
- </p>
164
- <div>
165
- {{ button('Use my location', id='editor-location-here', style='soft', hidden=true) }}
166
- </div>
167
- <p id="editor-location-status" class="label block whitespace-normal text-xs" role="status"></p>
168
- {{ 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 ''), hintHtml='Latitude, longitude, as decimal degrees. A <code>geo:</code> URI works too.', hintId='editor-location-geo-hint') }}
169
- {{ 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') }}
170
- {{ field.text('editor-location-name', locationFields.name, 'Place', value=form.location.name) }}
171
- {{ field.text('editor-location-locality', locationFields.locality, 'Locality', value=form.location.locality) }}
172
- {{ field.text('editor-location-region', locationFields.region, 'Region', value=form.location.region) }}
173
- {{ field.text('editor-location-country', locationFields.country, 'Country', value=form.location.country) }}
174
- {{ 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') }}
175
- {% endcall %}
176
-
177
- {% call collapse('Recording', open.recording, fields=true) %}
178
- {% 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') %}
179
- <option value=""{% if form.enclosure.url == "" %} selected{% endif %}>None</option>
180
- {% for choice in enclosureChoices.media %}
181
- <option value="{{ choice.url }}"{% if choice.url == form.enclosure.url %} selected{% endif %}>{{ choice.label }}</option>
182
- {% endfor %}
183
- {% endcall %}
184
-
185
- {{ 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') }}
186
-
187
- {{ 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') }}
188
- <datalist id="editor-transcript-uploads">
189
- {% for url in enclosureChoices.transcripts %}
190
- <option value="{{ url }}"></option>
191
- {% endfor %}
192
- </datalist>
193
- {% 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 a web address. A linked transcript needs its type.', hintId='editor-enclosure-transcript-hint') %}
194
- <option value=""{% if form.enclosure.transcriptType == "" %} selected{% endif %}>From the file</option>
195
- {% for type in transcriptTypes %}
196
- <option value="{{ type }}"{% if type == form.enclosure.transcriptType %} selected{% endif %}>{{ type }}</option>
197
- {% endfor %}
488
+ {% if editorKind and sections.also | length %}
489
+ <h2 class="px-1 text-sm font-semibold">Also on this post</h2>
490
+ {% for name in sections.also %}
491
+ {% call collapse(SECTION_TITLES[name], true, fields=true) %}
492
+ {{ section(name, false) }}
198
493
  {% endcall %}
494
+ {% endfor %}
495
+ {% endif %}
199
496
 
200
- {% call collapse('Other versions', open.versions, fields=true) %}
201
- <p class="label block whitespace-normal text-xs">
202
- A video of an audio episode, a smaller file, another language. Each is a file in the media
203
- library or a web address, which needs its type. Empty an address to remove
204
- that version.
205
- </p>
206
- {% for row in alternateRows %}
207
- {% set n = loop.index0 %}
208
- {% call collapse(('Version ' + loop.index) if row.url else 'Add a version', row.url, fields=true) %}
209
- {{ 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 '')) }}
210
- {{ 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 '')) }}
211
- {{ 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 '')) }}
212
- {{ 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 '')) }}
213
- {{ 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 '')) }}
214
- {% endcall %}
215
- {% endfor %}
216
- <datalist id="editor-media-uploads">
217
- {% for choice in enclosureChoices.media %}
218
- <option value="{{ choice.url }}"></option>
219
- {% endfor %}
220
- </datalist>
221
- {% endcall %}
222
- {% endcall %}
497
+ {% if kind.type == "post" %}
498
+ <datalist id="editor-photo-uploads">
499
+ {% for url in photos.choices %}
500
+ <option value="{{ url }}"></option>
501
+ {% endfor %}
502
+ </datalist>
503
+ {% if form.previewHidden and not (citedPreviews | length) %}
504
+ <input type="hidden" name="preview" value="hide">
505
+ {% endif %}
506
+ {% if form.citedAlt and not describesCitedImage %}
507
+ <input type="hidden" name="cited-alt" value="{{ form.citedAlt }}">
508
+ {% endif %}
223
509
  {% endif %}
224
510
  </div>
225
511
 
@@ -233,16 +519,14 @@
233
519
  {{ buttonLink('View', viewUrl, style='ghost') }}
234
520
  {% endif %}
235
521
  {% endcall %}
236
- {% endcall %}
237
522
 
238
- {% call collapse('Publishing', open.publishing, fields=true) %}
239
523
  {{ field.checkbox('editor-draft', 'draft', 'Draft', checked=form.draft) }}
240
524
 
241
525
  {% if kind.dated %}
242
526
  {# The file holds a UTC instant; this field is that instant as the
243
527
  site's own clock reads it, so the zone has to be on the screen. #}
244
528
  {% set dateHint %}Wall-clock time in {{ dateZone }}. Stored as UTC.{% endset %}
245
- {{ field.text('editor-date', 'date', 'Date', value=form.date, error=(error if errorField == 'editor-date' else ''), hintHtml=dateHint) }}
529
+ {{ field.text('editor-date', 'date', 'When', value=form.date, error=(error if errorField == 'editor-date' else ''), hintHtml=dateHint) }}
246
530
  {% endif %}
247
531
 
248
532
  {% set visibilityHint %}
@@ -263,12 +547,6 @@
263
547
  <option value="unlisted"{% if form.visibility == "unlisted" %} selected{% endif %}>Unlisted</option>
264
548
  {% endcall %}
265
549
 
266
- {% if kind.type == "post" %}
267
- {# TASK-207: shown at the top of the author's fediverse profile, from
268
- the featured collection their actor links. #}
269
- {{ field.checkbox('editor-pinned', 'pinned', 'Pinned', checked=form.pinned, error=(error if errorField == 'editor-pinned' else ''), hintHtml='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.') }}
270
- {% endif %}
271
-
272
550
  {# Who the front matter says wrote this (TASK-67). A select of the site's
273
551
  users rather than a free box, because after decision-14 the value is a
274
552
  person rather than a string: it decides whose archive the post is on
@@ -294,75 +572,18 @@
294
572
  {% endcall %}
295
573
  {% endif %}
296
574
 
575
+ {% if kind.type == "post" and sidebarLocation %}
576
+ {% call collapse('Location', open.location, fields=true) %}
577
+ {{ placeFields(false) }}
578
+ {% endcall %}
579
+ {% endif %}
580
+
297
581
  {% call collapse('Address', open.address, fields=true) %}
298
582
  {{ field.text('editor-slug', 'slug', 'Slug', value=form.slug, error=(error if errorField == 'editor-slug' else '')) }}
299
583
  {% set permalinkHint %}Leave it empty to let the {{ kind.singular }} take the URL its slug implies.{% endset %}
300
584
  {{ field.text('editor-permalink', 'permalink', 'Permalink', value=form.permalink, hintHtml=permalinkHint) }}
301
585
  {% endcall %}
302
586
 
303
- {% if kind.type == "post" %}
304
- {% call collapse('Responding to', open.responding, fields=true) %}
305
- {#- The mf2 `in-reply-to`: a URL here makes the post a reply, threads
306
- it in the fediverse and sends that page a webmention. -#}
307
- {{ 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 '')) }}
308
- {{ citedCard('in-reply-to') }}
309
- <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>
310
-
311
- {% call field.select('editor-rsvp', 'rsvp', 'RSVP', error=(error if errorField == 'editor-rsvp' else '')) %}
312
- <option value=""{% if form.rsvp == "" %} selected{% endif %}>Not an RSVP</option>
313
- {% for choice in rsvpChoices %}
314
- <option value="{{ choice.value }}"{% if form.rsvp == choice.value %} selected{% endif %}>{{ choice.label }}</option>
315
- {% endfor %}
316
- {% if unrecognizedRsvp %}
317
- <option value="{{ unrecognizedRsvp }}" selected>{{ unrecognizedRsvp }} (not recognized)</option>
318
- {% endif %}
319
- {% endcall %}
320
-
321
- {#- The mf2 `like-of`, `repost-of` and `bookmark-of` (TASK-169): each
322
- cites a page the way a reply does, and sends it a webmention. -#}
323
- {{ field.text('editor-like-of', 'like-of', 'Like of', value=form.likeOf, type='url', error=(error if errorField == 'editor-like-of' else '')) }}
324
- {{ citedCard('like-of') }}
325
- {{ field.text('editor-repost-of', 'repost-of', 'Repost of', value=form.repostOf, type='url', error=(error if errorField == 'editor-repost-of' else '')) }}
326
- {{ citedCard('repost-of') }}
327
- {{ field.text('editor-bookmark-of', 'bookmark-of', 'Bookmark of', value=form.bookmarkOf, type='url', error=(error if errorField == 'editor-bookmark-of' else '')) }}
328
- {{ citedCard('bookmark-of') }}
329
- <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>
330
- {% if form.previewHidden and not (citedPreviews | length) %}
331
- <input type="hidden" name="preview" value="hide">
332
- {% endif %}
333
- {% if form.citedAlt and not describesCitedImage %}
334
- <input type="hidden" name="cited-alt" value="{{ form.citedAlt }}">
335
- {% endif %}
336
- {% endcall %}
337
-
338
- {% call collapse('Event', open.event, fields=true) %}
339
- {% set eventTimeHint %}Wall-clock time in {{ dateZone }}. Stored as UTC.{% endset %}
340
- {{ field.text('editor-event-start', eventFields.start, 'Starts', value=form.event.start, placeholder='2026-10-10 09:00', autocomplete='off', error=(error if errorField == 'editor-event-start' else ''), hintHtml=eventTimeHint, hintId='editor-event-time-hint') }}
341
- {{ field.text('editor-event-end', eventFields.end, 'Ends', value=form.event.end, placeholder='2026-10-10 17:00', autocomplete='off', error=(error if errorField == 'editor-event-end' else ''), hintId='editor-event-time-hint') }}
342
- {{ field.text('editor-event-location', eventFields.location, 'Where', value=form.event.location, error=(error if errorField == 'editor-event-location' else ''), hint='The place, or the address to join an online event at.', hintId='editor-event-location-hint') }}
343
- <p class="label block whitespace-normal text-xs">A post with a start is an event, named by its Title and described by its Body. Empty every box to make it a post again.</p>
344
- {% endcall %}
345
-
346
- {% call collapse('Read', open.read, fields=true) %}
347
- {% call field.select('editor-read-status', readFields.status, 'Read status', error=(error if errorField == 'editor-read-status' else '')) %}
348
- <option value=""{% if form.readStatus == "" %} selected{% endif %}>Not a read post</option>
349
- {% for status in readStatuses %}
350
- <option value="{{ status.value }}"{% if form.readStatus == status.value %} selected{% endif %}>{{ status.label }}</option>
351
- {% endfor %}
352
- {% if unrecognizedReadStatus %}
353
- <option value="{{ unrecognizedReadStatus }}" selected>{{ unrecognizedReadStatus }} (not recognized)</option>
354
- {% endif %}
355
- {% endcall %}
356
- {{ field.text('editor-read-of-name', readFields.name, 'Title', value=form.readOf.name, error=(error if errorField == 'editor-read-of-name' else '')) }}
357
- {{ field.text('editor-read-of-author', readFields.author, 'Author', value=form.readOf.author) }}
358
- {{ field.text('editor-read-of-uid', readFields.uid, 'Identifier', value=form.readOf.uid, placeholder='isbn:9780441478125', autocomplete='off', spellcheck='false') }}
359
- {{ field.text('editor-read-of-url', readFields.url, 'Address', value=form.readOf.url, type='url', error=(error if errorField == 'editor-read-of-url' else '')) }}
360
- {{ field.text('editor-read-of-photo', readFields.photo, 'Cover', value=form.readOf.photo, list='editor-photo-uploads', autocomplete='off', error=(error if errorField == 'editor-read-of-photo' else ''), hint='An image in the media library or a web address. Empty it to remove the cover.', hintId='editor-read-of-photo-hint') }}
361
- {{ field.text('editor-read-of-photo-alt', readFields.photoAlt, 'Cover alt text', value=form.readOf.photoAlt) }}
362
- <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>
363
- {% endcall %}
364
- {% endif %}
365
-
366
587
  {% call collapse('Summary and language', open.summary, fields=true) %}
367
588
  {{ field.text('editor-description', 'description', 'Description', value=form.description, error=(error if errorField == 'editor-description' else '')) }}
368
589
  {{ pluginActions('editor-description', editorActions['editor-description']) }}
@@ -398,6 +619,12 @@
398
619
  <option value="{{ commentSettings.closed }}"{% if form.comments == commentSettings.closed %} selected{% endif %}>Closed</option>
399
620
  {% endcall %}
400
621
 
622
+ {% if kind.type == "post" %}
623
+ {# TASK-207: shown at the top of the author's fediverse profile, from
624
+ the featured collection their actor links. #}
625
+ {{ field.checkbox('editor-pinned', 'pinned', 'Pinned', checked=form.pinned, error=(error if errorField == 'editor-pinned' else ''), hintHtml='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.') }}
626
+ {% endif %}
627
+
401
628
  {% if kind.excludable %}
402
629
  {# doc-2's eleventyExcludeFromCollections: still published and still at
403
630
  its URL, but out of every collection a template lists from. #}
@@ -446,4 +673,7 @@
446
673
  {% if editorActions | length %}
447
674
  <script defer src="{{ assetPrefix }}editor-actions.js"></script>
448
675
  {% endif %}
676
+ {% if sections and sections.shown.review %}
677
+ <script defer src="{{ assetPrefix }}item-lookup.js"></script>
678
+ {% endif %}
449
679
  {% endblock %}