@geekity/cms 0.34.0 → 0.36.1

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 (255) hide show
  1. package/admin/components/job-status.njk +15 -10
  2. package/admin/components/upload.njk +26 -0
  3. package/admin/pages/documents/chooser.njk +30 -0
  4. package/admin/pages/documents/editor.njk +410 -165
  5. package/admin/pages/federation/followers.njk +2 -1
  6. package/admin/pages/media/file.njk +143 -0
  7. package/admin/pages/media/library.njk +107 -198
  8. package/admin/pages/settings/general.njk +52 -2
  9. package/admin/pages/settings/privacy.njk +1 -1
  10. package/admin/pages/tools/export.njk +54 -51
  11. package/admin/pages/tools/maintenance.njk +49 -0
  12. package/admin/pages/users/edit.njk +3 -0
  13. package/admin/src/admin.css +2 -0
  14. package/admin/static/admin.css +843 -106
  15. package/admin/static/copy.js +30 -23
  16. package/admin/static/item-lookup.js +179 -0
  17. package/admin/static/job-status.js +158 -0
  18. package/admin/static/media.js +82 -0
  19. package/admin/static/upload-drop.js +225 -0
  20. package/dist/admin/documents.d.ts.map +1 -1
  21. package/dist/admin/documents.js +141 -24
  22. package/dist/admin/documents.js.map +1 -1
  23. package/dist/admin/editor-actions.d.ts.map +1 -1
  24. package/dist/admin/editor-actions.js +2 -3
  25. package/dist/admin/editor-actions.js.map +1 -1
  26. package/dist/admin/editor-fields.d.ts +20 -8
  27. package/dist/admin/editor-fields.d.ts.map +1 -1
  28. package/dist/admin/editor-fields.js +35 -12
  29. package/dist/admin/editor-fields.js.map +1 -1
  30. package/dist/admin/editor-kinds.d.ts +30 -0
  31. package/dist/admin/editor-kinds.d.ts.map +1 -0
  32. package/dist/admin/editor-kinds.js +136 -0
  33. package/dist/admin/editor-kinds.js.map +1 -0
  34. package/dist/admin/editor-layout.d.ts +10 -1
  35. package/dist/admin/editor-layout.d.ts.map +1 -1
  36. package/dist/admin/editor-layout.js +38 -31
  37. package/dist/admin/editor-layout.js.map +1 -1
  38. package/dist/admin/federation.d.ts +1 -1
  39. package/dist/admin/federation.d.ts.map +1 -1
  40. package/dist/admin/federation.js +2 -1
  41. package/dist/admin/federation.js.map +1 -1
  42. package/dist/admin/formatting.d.ts +2 -0
  43. package/dist/admin/formatting.d.ts.map +1 -1
  44. package/dist/admin/formatting.js +8 -0
  45. package/dist/admin/formatting.js.map +1 -1
  46. package/dist/admin/index.d.ts +1 -1
  47. package/dist/admin/index.d.ts.map +1 -1
  48. package/dist/admin/index.js +1 -1
  49. package/dist/admin/index.js.map +1 -1
  50. package/dist/admin/item-lookup.d.ts +16 -0
  51. package/dist/admin/item-lookup.d.ts.map +1 -0
  52. package/dist/admin/item-lookup.js +192 -0
  53. package/dist/admin/item-lookup.js.map +1 -0
  54. package/dist/admin/job-status.d.ts +1 -0
  55. package/dist/admin/job-status.d.ts.map +1 -1
  56. package/dist/admin/job-status.js +1 -0
  57. package/dist/admin/job-status.js.map +1 -1
  58. package/dist/admin/maintenance.d.ts +2 -0
  59. package/dist/admin/maintenance.d.ts.map +1 -1
  60. package/dist/admin/maintenance.js +14 -0
  61. package/dist/admin/maintenance.js.map +1 -1
  62. package/dist/admin/media.d.ts +47 -36
  63. package/dist/admin/media.d.ts.map +1 -1
  64. package/dist/admin/media.js +293 -68
  65. package/dist/admin/media.js.map +1 -1
  66. package/dist/admin/preview.d.ts.map +1 -1
  67. package/dist/admin/preview.js +9 -12
  68. package/dist/admin/preview.js.map +1 -1
  69. package/dist/admin/routes.d.ts.map +1 -1
  70. package/dist/admin/routes.js +4 -0
  71. package/dist/admin/routes.js.map +1 -1
  72. package/dist/admin/settings-general.d.ts +6 -0
  73. package/dist/admin/settings-general.d.ts.map +1 -1
  74. package/dist/admin/settings-general.js +46 -1
  75. package/dist/admin/settings-general.js.map +1 -1
  76. package/dist/admin/templates.d.ts +2 -0
  77. package/dist/admin/templates.d.ts.map +1 -1
  78. package/dist/admin/templates.js +4 -0
  79. package/dist/admin/templates.js.map +1 -1
  80. package/dist/admin/uploads.d.ts +2 -1
  81. package/dist/admin/uploads.d.ts.map +1 -1
  82. package/dist/admin/uploads.js +5 -2
  83. package/dist/admin/uploads.js.map +1 -1
  84. package/dist/cli.d.ts.map +1 -1
  85. package/dist/cli.js +2 -1
  86. package/dist/cli.js.map +1 -1
  87. package/dist/cms/app.d.ts.map +1 -1
  88. package/dist/cms/app.js +1 -0
  89. package/dist/cms/app.js.map +1 -1
  90. package/dist/cms/services.d.ts +2 -0
  91. package/dist/cms/services.d.ts.map +1 -1
  92. package/dist/cms/services.js +13 -0
  93. package/dist/cms/services.js.map +1 -1
  94. package/dist/content/document-form.d.ts +5 -13
  95. package/dist/content/document-form.d.ts.map +1 -1
  96. package/dist/content/document-form.js +2 -3
  97. package/dist/content/document-form.js.map +1 -1
  98. package/dist/content/excerpt.d.ts +1 -3
  99. package/dist/content/excerpt.d.ts.map +1 -1
  100. package/dist/content/excerpt.js +2 -7
  101. package/dist/content/excerpt.js.map +1 -1
  102. package/dist/content/post-fields.d.ts +1 -1
  103. package/dist/content/post-fields.d.ts.map +1 -1
  104. package/dist/content/post-fields.js +30 -35
  105. package/dist/content/post-fields.js.map +1 -1
  106. package/dist/content/post-type.d.ts +2 -3
  107. package/dist/content/post-type.d.ts.map +1 -1
  108. package/dist/content/post-type.js +8 -9
  109. package/dist/content/post-type.js.map +1 -1
  110. package/dist/content/review-form.d.ts +49 -0
  111. package/dist/content/review-form.d.ts.map +1 -0
  112. package/dist/content/review-form.js +282 -0
  113. package/dist/content/review-form.js.map +1 -0
  114. package/dist/content/review.d.ts +57 -0
  115. package/dist/content/review.d.ts.map +1 -0
  116. package/dist/content/review.js +264 -0
  117. package/dist/content/review.js.map +1 -0
  118. package/dist/content/write.d.ts.map +1 -1
  119. package/dist/content/write.js +6 -9
  120. package/dist/content/write.js.map +1 -1
  121. package/dist/exports.d.ts +1 -1
  122. package/dist/exports.d.ts.map +1 -1
  123. package/dist/federation/article.d.ts.map +1 -1
  124. package/dist/federation/article.js +27 -29
  125. package/dist/federation/article.js.map +1 -1
  126. package/dist/federation/federation.d.ts +3 -0
  127. package/dist/federation/federation.d.ts.map +1 -1
  128. package/dist/federation/federation.js +5 -1
  129. package/dist/federation/federation.js.map +1 -1
  130. package/dist/federation/refusals.d.ts +18 -0
  131. package/dist/federation/refusals.d.ts.map +1 -0
  132. package/dist/federation/refusals.js +84 -0
  133. package/dist/federation/refusals.js.map +1 -0
  134. package/dist/images/variants.d.ts +1 -0
  135. package/dist/images/variants.d.ts.map +1 -1
  136. package/dist/images/variants.js +10 -0
  137. package/dist/images/variants.js.map +1 -1
  138. package/dist/item-lookup/item-id.d.ts +33 -0
  139. package/dist/item-lookup/item-id.d.ts.map +1 -0
  140. package/dist/item-lookup/item-id.js +71 -0
  141. package/dist/item-lookup/item-id.js.map +1 -0
  142. package/dist/item-lookup/lookup.d.ts +58 -0
  143. package/dist/item-lookup/lookup.d.ts.map +1 -0
  144. package/dist/item-lookup/lookup.js +532 -0
  145. package/dist/item-lookup/lookup.js.map +1 -0
  146. package/dist/media/library.d.ts +9 -0
  147. package/dist/media/library.d.ts.map +1 -1
  148. package/dist/media/library.js +25 -1
  149. package/dist/media/library.js.map +1 -1
  150. package/dist/micropub/create.d.ts +1 -1
  151. package/dist/micropub/create.d.ts.map +1 -1
  152. package/dist/micropub/create.js +4 -2
  153. package/dist/micropub/create.js.map +1 -1
  154. package/dist/micropub/endpoint.d.ts.map +1 -1
  155. package/dist/micropub/endpoint.js +3 -10
  156. package/dist/micropub/endpoint.js.map +1 -1
  157. package/dist/micropub/update.d.ts.map +1 -1
  158. package/dist/micropub/update.js +12 -6
  159. package/dist/micropub/update.js.map +1 -1
  160. package/dist/plugin.d.ts +26 -2
  161. package/dist/plugin.d.ts.map +1 -1
  162. package/dist/plugin.js.map +1 -1
  163. package/dist/settings/names.d.ts +1 -1
  164. package/dist/settings/names.d.ts.map +1 -1
  165. package/dist/settings/names.js +8 -1
  166. package/dist/settings/names.js.map +1 -1
  167. package/dist/settings/registry.d.ts.map +1 -1
  168. package/dist/settings/registry.js +2 -1
  169. package/dist/settings/registry.js.map +1 -1
  170. package/dist/settings/tmdb.d.ts +5 -0
  171. package/dist/settings/tmdb.d.ts.map +1 -0
  172. package/dist/settings/tmdb.js +22 -0
  173. package/dist/settings/tmdb.js.map +1 -0
  174. package/dist/store/profiles.d.ts +3 -2
  175. package/dist/store/profiles.d.ts.map +1 -1
  176. package/dist/store/profiles.js +2 -4
  177. package/dist/store/profiles.js.map +1 -1
  178. package/dist/web/context.d.ts +9 -21
  179. package/dist/web/context.d.ts.map +1 -1
  180. package/dist/web/context.js +20 -20
  181. package/dist/web/context.js.map +1 -1
  182. package/dist/web/feed-item.d.ts +6 -6
  183. package/dist/web/feed-item.d.ts.map +1 -1
  184. package/dist/web/feed-item.js +7 -3
  185. package/dist/web/feed-item.js.map +1 -1
  186. package/dist/web/plugin-federation.d.ts +2 -0
  187. package/dist/web/plugin-federation.d.ts.map +1 -1
  188. package/dist/web/plugin-federation.js +3 -0
  189. package/dist/web/plugin-federation.js.map +1 -1
  190. package/dist/webmention/cited-picture.d.ts +1 -0
  191. package/dist/webmention/cited-picture.d.ts.map +1 -1
  192. package/dist/webmention/cited-picture.js +7 -4
  193. package/dist/webmention/cited-picture.js.map +1 -1
  194. package/dist/webmention/fetch-public.d.ts +3 -0
  195. package/dist/webmention/fetch-public.d.ts.map +1 -1
  196. package/dist/webmention/fetch-public.js +1 -1
  197. package/dist/webmention/fetch-public.js.map +1 -1
  198. package/dist/webmention/reply-context/json-ld.d.ts +8 -0
  199. package/dist/webmention/reply-context/json-ld.d.ts.map +1 -1
  200. package/dist/webmention/reply-context/json-ld.js +21 -0
  201. package/dist/webmention/reply-context/json-ld.js.map +1 -1
  202. package/dist/webmention/service.d.ts.map +1 -1
  203. package/dist/webmention/service.js +3 -5
  204. package/dist/webmention/service.js.map +1 -1
  205. package/package.json +6 -1
  206. package/themes/default/layouts/post.njk +6 -5
  207. package/themes/default/partials/post-list.njk +15 -20
  208. package/themes/default/partials/review.njk +1 -0
  209. package/themes/default/src/style.css +24 -4
  210. package/themes/default/static/style.css +33 -5
  211. package/dist/comments/routes.d.ts +0 -65
  212. package/dist/comments/routes.d.ts.map +0 -1
  213. package/dist/comments/routes.js +0 -235
  214. package/dist/comments/routes.js.map +0 -1
  215. package/dist/content/media.d.ts +0 -85
  216. package/dist/content/media.d.ts.map +0 -1
  217. package/dist/content/media.js +0 -237
  218. package/dist/content/media.js.map +0 -1
  219. package/dist/content/read-form.d.ts +0 -31
  220. package/dist/content/read-form.d.ts.map +0 -1
  221. package/dist/content/read-form.js +0 -157
  222. package/dist/content/read-form.js.map +0 -1
  223. package/dist/content/read.d.ts +0 -29
  224. package/dist/content/read.d.ts.map +0 -1
  225. package/dist/content/read.js +0 -82
  226. package/dist/content/read.js.map +0 -1
  227. package/dist/export/export-archives.d.ts +0 -29
  228. package/dist/export/export-archives.d.ts.map +0 -1
  229. package/dist/export/export-archives.js +0 -88
  230. package/dist/export/export-archives.js.map +0 -1
  231. package/dist/federation/mount.d.ts +0 -53
  232. package/dist/federation/mount.d.ts.map +0 -1
  233. package/dist/federation/mount.js +0 -344
  234. package/dist/federation/mount.js.map +0 -1
  235. package/dist/mail/credentials.d.ts +0 -62
  236. package/dist/mail/credentials.d.ts.map +0 -1
  237. package/dist/mail/credentials.js +0 -86
  238. package/dist/mail/credentials.js.map +0 -1
  239. package/dist/site/setting-rows.d.ts +0 -45
  240. package/dist/site/setting-rows.d.ts.map +0 -1
  241. package/dist/site/setting-rows.js +0 -361
  242. package/dist/site/setting-rows.js.map +0 -1
  243. package/dist/site/settings-form.d.ts +0 -30
  244. package/dist/site/settings-form.d.ts.map +0 -1
  245. package/dist/site/settings-form.js +0 -54
  246. package/dist/site/settings-form.js.map +0 -1
  247. package/dist/site/site-json.d.ts +0 -38
  248. package/dist/site/site-json.d.ts.map +0 -1
  249. package/dist/site/site-json.js +0 -69
  250. package/dist/site/site-json.js.map +0 -1
  251. package/dist/webmention/routes.d.ts +0 -37
  252. package/dist/webmention/routes.d.ts.map +0 -1
  253. package/dist/webmention/routes.js +0 -75
  254. package/dist/webmention/routes.js.map +0 -1
  255. 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,55 @@
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 %}
16
+ {% from "components/upload.njk" import uploadControl %}
15
17
 
16
18
  {% set ACTION_COLORS = { 'publish': 'primary', 'update': 'primary', 'trash': 'error' } %}
17
19
  {% set ACTION_STYLES = { 'trash': 'ghost' } %}
20
+ {% set SECTION_TITLES = { photos: 'Photos', link: 'Link', rsvp: 'RSVP', event: 'Event', episode: 'Recording', review: 'Review', checkin: 'Check-in' } %}
21
+
22
+ {% set ICON_SIZES = { md: 'size-6', sm: 'size-4' } %}
23
+
24
+ {% macro photoTile(name, url, thumbnail, alt) -%}
25
+ <label class="relative cursor-pointer">
26
+ <input class="peer sr-only" type="checkbox" name="{{ name }}" value="{{ url }}" />
27
+ <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="{{ thumbnail }}" alt="{{ alt }}" loading="lazy" />
28
+ <span class="badge badge-primary badge-xs absolute end-1 top-1 hidden peer-checked:inline-flex" aria-hidden="true">✓</span>
29
+ </label>
30
+ {%- endmacro %}
31
+
32
+ {% macro icon(path, size='md') -%}
33
+ <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>
34
+ {%- endmacro %}
35
+
36
+ {% set ITEM_KIND_SHOWS = {
37
+ book: 'group-has-[[name=item-kind][value=book]:checked]/editor:block',
38
+ movie: 'group-has-[[name=item-kind][value=movie]:checked]/editor:block',
39
+ tv: 'group-has-[[name=item-kind][value=tv]:checked]/editor:block',
40
+ album: 'group-has-[[name=item-kind][value=album]:checked]/editor:block',
41
+ place: 'group-has-[[name=item-kind][value=place]:checked]/editor:block',
42
+ other: 'group-has-[[data-as-other]:checked]/editor:block'
43
+ } %}
44
+
45
+ {% set LOOKUP_TONES = { found: 'text-base-content', failed: 'text-error' } %}
46
+
47
+ {% macro forItemKinds(kinds, id='') -%}
48
+ <div class="hidden{% for kind in kinds %}{{ ITEM_KIND_SHOWS | modifier(kind) }}{% endfor %}"{% if id %} id="{{ id }}"{% endif %}>
49
+ {%- endmacro %}
50
+
51
+ {% macro itemWord(key, after='', before='') -%}
52
+ {% for kind in reviewKinds %}{% if kind[key] %}<span class="hidden{{ ITEM_KIND_SHOWS | modifier(kind.value) }}">{{ before }}{{ kind[key] }}{{ after }}</span>{% endif %}{% endfor %}
53
+ {%- endmacro %}
54
+
55
+ {% macro invalidIf(id) -%}
56
+ {% if errorField == id %} aria-invalid="true" aria-describedby="{{ id }}-error"{% endif %}
57
+ {%- endmacro %}
58
+
59
+ {% macro groupError(id) %}
60
+ {% if errorField == id %}
61
+ <p class="text-sm text-error" id="{{ id }}-error">{{ error }}</p>
62
+ {% endif %}
63
+ {% endmacro %}
18
64
 
19
65
  {% macro citedCard(property) %}
20
66
  {% set cited = citedPreviews[property] %}
@@ -75,8 +121,314 @@
75
121
  {% endif %}
76
122
  {% endmacro %}
77
123
 
124
+ {% macro placeFields(checkin) %}
125
+ <div>
126
+ {{ button('Use my location', id='editor-location-here', style='soft', hidden=true) }}
127
+ </div>
128
+ <p id="editor-location-status" class="label block whitespace-normal text-xs" role="status"></p>
129
+ {{ field.text('editor-location-name', locationFields.name, 'Place', value=form.location.name, placeholder='A café, a park, a venue') }}
130
+ <div class="grid gap-x-4 sm:grid-cols-3">
131
+ {{ field.text('editor-location-locality', locationFields.locality, 'Locality', value=form.location.locality) }}
132
+ {{ field.text('editor-location-region', locationFields.region, 'Region', value=form.location.region) }}
133
+ {{ field.text('editor-location-country', locationFields.country, 'Country', value=form.location.country) }}
134
+ </div>
135
+ {{ 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') }}
136
+ {{ 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') }}
137
+ {% if checkin %}
138
+ <input type="hidden" name="{{ locationFields.checkin }}" value="1" />
139
+ {% elif form.location.checkin %}
140
+ {{ 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') }}
141
+ {% endif %}
142
+ <p class="label block whitespace-normal text-xs">
143
+ Kept in <code>data/locations.json</code>, not in the post’s file. What readers see of it
144
+ is chosen on Settings &gt; Privacy. Empty every box to remove it.
145
+ </p>
146
+ {% endmacro %}
147
+
148
+ {% macro section(name, own) %}
149
+ {% if name == 'photos' %}
150
+ <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 %}">
151
+ {% if own %}<p class="fieldset-legend">{% if postKind == 'review' %}Your photos{% else %}Photos{% endif %}</p>{% endif %}
152
+ {% for row in photos.rows %}
153
+ {% set n = loop.index0 %}
154
+ <div class="flex gap-3 rounded-box border border-base-300 p-3">
155
+ {% if row.thumbnail %}
156
+ <img class="size-20 shrink-0 rounded-box object-cover" src="{{ row.thumbnail }}" alt="" loading="lazy" />
157
+ {% endif %}
158
+ <div class="flex min-w-0 flex-1 flex-col">
159
+ {{ 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 '')) }}
160
+ {{ 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.') }}
161
+ </div>
162
+ </div>
163
+ {% endfor %}
164
+ <fieldset class="fieldset rounded-box">
165
+ <legend class="fieldset-legend">Add from the media library</legend>
166
+ <div class="grid grid-cols-3 gap-2 sm:grid-cols-6" id="editor-photo-picks">
167
+ {% for pick in photos.picks %}
168
+ {{ photoTile(photoFields.url + pick.index, pick.url, pick.thumbnail, pick.alt or pick.url) }}
169
+ {% endfor %}
170
+ </div>
171
+ <template data-upload-tile="editor-photo-picks">{{ photoTile('', '', '', '') }}</template>
172
+ {% if not photos.picks | length %}
173
+ <p class="text-sm" data-upload-empty>The media library has no images yet.</p>
174
+ {% endif %}
175
+ {{ uploadControl('photos', 'editor-photo-picks', uploadsUrl, images=true, name=photoFields.url) }}
176
+ <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>
177
+ <div{% if not photos.picks | length %} hidden data-upload-reveal{% endif %}>{{ button('Add the ticked photos', type='submit', name='switch-kind', value=postKind, style='soft', formnovalidate=true) }}</div>
178
+ </fieldset>
179
+ <div class="grid gap-x-4 sm:grid-cols-2">
180
+ {{ 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 '')) }}
181
+ {{ field.text('editor-photo-alt-' + photos.blank, photoFields.alt + photos.blank, 'Its alt text', value='') }}
182
+ </div>
183
+ </div>
184
+
185
+ {% elif name == 'link' %}
186
+ <fieldset class="fieldset">
187
+ <legend class="fieldset-legend">This post</legend>
188
+ <div class="join flex-wrap">
189
+ {% for verb in link.verbs %}
190
+ <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 %} />
191
+ {% endfor %}
192
+ </div>
193
+ </fieldset>
194
+ {{ 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.') }}
195
+ {{ citedCard(link.chosen) }}
196
+ {% for other in link.others %}
197
+ {{ 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.') }}
198
+ {{ citedCard(other.property) }}
199
+ {% endfor %}
200
+
201
+ {% elif name == 'rsvp' %}
202
+ {{ 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.') }}
203
+ {{ citedCard('in-reply-to') }}
204
+ <fieldset class="fieldset">
205
+ <legend class="fieldset-legend">Going?</legend>
206
+ <div class="join flex-wrap">
207
+ {% for choice in rsvpChoices %}
208
+ <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 %} />
209
+ {% endfor %}
210
+ {% if unrecognizedRsvp %}
211
+ <input class="join-item btn btn-sm" type="radio" name="rsvp" value="{{ unrecognizedRsvp }}" aria-label="{{ unrecognizedRsvp }} (not recognized)" checked />
212
+ {% endif %}
213
+ {% if not own %}
214
+ <input class="join-item btn btn-sm" type="radio" name="rsvp" value="" aria-label="Not an RSVP" />
215
+ {% endif %}
216
+ </div>
217
+ {{ groupError('editor-rsvp') }}
218
+ </fieldset>
219
+
220
+ {% elif name == 'event' %}
221
+ {% set eventTimeHint %}Wall-clock time in {{ dateZone }}. Stored as UTC.{% endset %}
222
+ <div class="grid gap-x-4 sm:grid-cols-2">
223
+ {{ 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') }}
224
+ {{ 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') }}
225
+ </div>
226
+ {{ 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') }}
227
+ {% if not own %}
228
+ <p class="label block whitespace-normal text-xs">Empty every box to make it a post again.</p>
229
+ {% endif %}
230
+
231
+ {% elif name == 'checkin' %}
232
+ {{ placeFields(true) }}
233
+
234
+ {% elif name == 'episode' %}
235
+ {% call field.select('editor-enclosure-url', enclosureFields.url, 'Audio or video file', error=(error if errorField == 'editor-enclosure-url' else ''), hint='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') %}
236
+ <option value=""{% if form.enclosure.url == "" %} selected{% endif %}>None</option>
237
+ {% for choice in enclosureChoices.media %}
238
+ <option value="{{ choice.url }}"{% if choice.url == form.enclosure.url %} selected{% endif %}>{{ choice.label }}</option>
239
+ {% endfor %}
240
+ {% endcall %}
241
+ {{ uploadControl('select', 'editor-enclosure-url', uploadsUrl, accept='audio/*,video/*') }}
242
+ <div class="grid gap-x-4 sm:grid-cols-2">
243
+ {{ 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') }}
244
+ <div>
245
+ {{ 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') }}
246
+ {{ uploadControl('field', 'editor-enclosure-transcript', uploadsUrl, accept='.vtt,.srt,.txt') }}
247
+ </div>
248
+ </div>
249
+ <datalist id="editor-transcript-uploads">
250
+ {% for url in enclosureChoices.transcripts %}
251
+ <option value="{{ url }}"></option>
252
+ {% endfor %}
253
+ </datalist>
254
+ {% 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') %}
255
+ <option value=""{% if form.enclosure.transcriptType == "" %} selected{% endif %}>From the file</option>
256
+ {% for type in transcriptTypes %}
257
+ <option value="{{ type }}"{% if type == form.enclosure.transcriptType %} selected{% endif %}>{{ type }}</option>
258
+ {% endfor %}
259
+ {% endcall %}
260
+
261
+ {% call collapse('Other versions', open.versions, fields=true) %}
262
+ <p class="label block whitespace-normal text-xs">
263
+ A video of an audio episode, a smaller file, another language. Each is a file in the media
264
+ library or a web address, which needs its type. Empty an address to remove
265
+ that version.
266
+ </p>
267
+ {% for row in alternateRows %}
268
+ {% set n = loop.index0 %}
269
+ {% call collapse(('Version ' + loop.index) if row.url else 'Add a version', row.url, fields=true) %}
270
+ {{ 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 '')) }}
271
+ {{ uploadControl('field', 'editor-alternate-url-' + n, uploadsUrl, accept='audio/*,video/*') }}
272
+ {{ 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 '')) }}
273
+ {{ 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 '')) }}
274
+ {{ 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 '')) }}
275
+ {{ 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 '')) }}
276
+ {% endcall %}
277
+ {% endfor %}
278
+ <datalist id="editor-media-uploads">
279
+ {% for choice in enclosureChoices.media %}
280
+ <option value="{{ choice.url }}"></option>
281
+ {% endfor %}
282
+ </datalist>
283
+ {% endcall %}
284
+
285
+ {% elif name == 'review' %}
286
+ <fieldset class="fieldset">
287
+ <legend class="fieldset-legend">Reviewing a</legend>
288
+ <div class="join flex-wrap">
289
+ {% for kind in reviewKinds %}
290
+ <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 %} />
291
+ {% endfor %}
292
+ {% if unrecognizedReviewKind %}
293
+ <input class="join-item btn btn-sm" type="radio" name="{{ reviewFields.kind }}" value="{{ unrecognizedReviewKind }}" aria-label="{{ unrecognizedReviewKind }} (not recognized)" checked data-as-other />
294
+ {% endif %}
295
+ </div>
296
+ {{ groupError('editor-item-kind') }}
297
+ </fieldset>
298
+
299
+ {#- Nominatim's usage policy forbids autocomplete, so Find searches only on
300
+ Enter or Search. -#}
301
+ {{ forItemKinds(itemSearchKinds) }}
302
+ <div id="editor-item-find" class="fieldset" data-item-search="{{ itemSearchUrl }}" hidden>
303
+ <label class="fieldset-legend" for="editor-item-find-query">{{ itemWord('noun', before='Find the ') }}</label>
304
+ <div class="join w-full">
305
+ <label class="input input-sm join-item w-full">
306
+ {{ 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') }}
307
+ <input id="editor-item-find-query" type="search" class="grow" placeholder="Search by title" autocomplete="off" aria-describedby="editor-item-find-status" />
308
+ </label>
309
+ <button type="button" class="btn btn-sm btn-soft join-item" id="editor-item-find-button">Search</button>
310
+ </div>
311
+ <p class="text-sm" id="editor-item-find-status" role="status"></p>
312
+ <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>
313
+ <li class="menu-title text-xs font-normal" id="editor-item-find-credit" hidden></li>
314
+ <li class="menu-title text-xs font-normal">Not here? Paste a link in the box below and press Look up.</li>
315
+ </ul>
316
+ <template id="editor-item-find-match">
317
+ <li>
318
+ <button type="button" class="flex items-center gap-3 text-left">
319
+ <img class="h-12 w-9 shrink-0 rounded object-cover" alt="" hidden />
320
+ <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>
321
+ <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>
322
+ <span class="badge badge-ghost badge-sm shrink-0" data-source></span>
323
+ </button>
324
+ </li>
325
+ </template>
326
+ </div>
327
+ </div>
328
+
329
+ {{ forItemKinds(reviewKindsWith.uid, 'editor-item-lookup') }}
330
+ <div class="join w-full items-end">
331
+ <div class="w-full">
332
+ {{ 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') }}
333
+ </div>
334
+ <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>
335
+ </div>
336
+ <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>
337
+ <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>
338
+ </div>
339
+ {{ field.text('editor-item-name', reviewFields.name, '', labelHtml=itemWord('name'), value=form.review.name, error=(error if errorField == 'editor-item-name' else '')) }}
340
+ {{ forItemKinds(reviewKindsWith.category) }}
341
+ {{ 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') }}
342
+ </div>
343
+ <div class="grid gap-x-4 sm:grid-cols-2">
344
+ {{ forItemKinds(reviewKindsWith.by) }}
345
+ {{ field.text('editor-item-by', reviewFields.by, '', labelHtml=itemWord('by'), value=form.review.by) }}
346
+ </div>
347
+ {{ forItemKinds(reviewKindsWith.year) }}
348
+ {{ field.text('editor-item-year', reviewFields.year, 'Year', value=form.review.year, inputmode='numeric', error=(error if errorField == 'editor-item-year' else '')) }}
349
+ </div>
350
+ </div>
351
+ <div class="grid gap-x-4 sm:grid-cols-2">
352
+ {{ forItemKinds(reviewKindsWith.season) }}
353
+ {{ 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') }}
354
+ </div>
355
+ {{ forItemKinds(reviewKindsWith.episode) }}
356
+ {{ field.text('editor-item-episode', reviewFields.episode, 'Episode', value=form.review.episode, inputmode='numeric', error=(error if errorField == 'editor-item-episode' else '')) }}
357
+ </div>
358
+ </div>
359
+ {{ forItemKinds(['place']) }}
360
+ {{ field.text('editor-item-street-address', reviewFields.streetAddress, 'Street address', value=form.review.streetAddress) }}
361
+ <div class="grid gap-x-4 sm:grid-cols-3">
362
+ {{ field.text('editor-item-locality', reviewFields.locality, 'Locality', value=form.review.locality) }}
363
+ {{ field.text('editor-item-region', reviewFields.region, 'Region', value=form.review.region) }}
364
+ {{ field.text('editor-item-country-name', reviewFields.countryName, 'Country', value=form.review.countryName) }}
365
+ </div>
366
+ </div>
367
+ <div class="grid gap-x-4 sm:grid-cols-2">
368
+ {{ forItemKinds(reviewKindsWith.url) }}
369
+ {{ field.text('editor-item-url', reviewFields.url, '', labelHtml=itemWord('url'), value=form.review.url, type='url', error=(error if errorField == 'editor-item-url' else '')) }}
370
+ </div>
371
+ {{ forItemKinds(reviewKindsWith.date) }}
372
+ {{ field.text('editor-item-date', reviewFields.date, '', labelHtml=itemWord('date'), value=form.review.date, type='date', error=(error if errorField == 'editor-item-date' else '')) }}
373
+ </div>
374
+ </div>
375
+ {{ forItemKinds(reviewKindsWith.photo) }}
376
+ <div class="flex gap-3">
377
+ <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" />
378
+ <div class="flex min-w-0 flex-1 flex-col">
379
+ {{ 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') }}
380
+ {{ uploadControl('field', 'editor-item-photo', uploadsUrl, images=true, preview='editor-item-photo-preview') }}
381
+ {{ field.text('editor-item-photo-alt', reviewFields.photoAlt, '', labelHtml=itemWord('picture', ' alt text'), value=form.review.photoAlt) }}
382
+ </div>
383
+ </div>
384
+ </div>
385
+
386
+ <fieldset class="fieldset rounded-box border border-base-300 bg-base-200/40 p-3">
387
+ <legend class="fieldset-legend">Rating</legend>
388
+ <div class="flex flex-wrap items-center gap-2">
389
+ <div class="rating rating-md">
390
+ <input id="editor-rating-none" class="rating-hidden" type="radio" name="{{ reviewFields.rating }}" value="" aria-label="No rating"{% if form.review.rating == "" %} checked{% endif %} />
391
+ {% for stars in [1, 2, 3, 4, 5] %}
392
+ <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 %} />
393
+ {% endfor %}
394
+ </div>
395
+ <label class="btn btn-ghost btn-xs" for="editor-rating-none">Clear</label>
396
+ {% if form.review.rating and form.review.rating not in ["1", "2", "3", "4", "5"] %}
397
+ <label class="label text-base-content">
398
+ <input class="radio radio-sm" type="radio" name="{{ reviewFields.rating }}" value="{{ form.review.rating }}" checked />
399
+ {{ form.review.rating }} (not recognized)
400
+ </label>
401
+ {% endif %}
402
+ </div>
403
+ {{ groupError('editor-rating') }}
404
+ </fieldset>
405
+ {% if not own %}
406
+ <p class="label block whitespace-normal text-xs">Empty every box and clear the rating to make it a post again.</p>
407
+ {% endif %}
408
+ {% endif %}
409
+ {% endmacro %}
410
+
78
411
  {% block content %}
79
- <h1 class="mb-6 text-2xl font-bold">{{ heading }}</h1>
412
+ <div class="mb-6 flex flex-wrap items-center gap-3">
413
+ <h1 class="flex min-w-0 items-center gap-2 break-words text-2xl font-bold">
414
+ {% if editorKind %}<span class="text-primary">{{ icon(editorKind.icon) }}</span>{% endif %}
415
+ <span class="min-w-0">{{ heading }}</span>
416
+ </h1>
417
+ {% if editorKind %}
418
+ {% call dropdown('editor-change-type', 'Change type', style='ghost') %}
419
+ <ul class="menu w-full">
420
+ {% for choice in postKinds %}
421
+ <li>
422
+ <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>
423
+ </li>
424
+ {% endfor %}
425
+ </ul>
426
+ {% endcall %}
427
+ {% if child == 'new' %}
428
+ <a class="link link-hover text-sm" href="{{ chooserUrl }}">All kinds</a>
429
+ {% endif %}
430
+ {% endif %}
431
+ </div>
80
432
 
81
433
  {% if error and errorField %}
82
434
  {% call field.summary('Nothing was saved. Fix the field marked below and try again.') %}
@@ -101,7 +453,7 @@
101
453
  </div>
102
454
  {% endif %}
103
455
 
104
- <form class="grid items-start gap-6 xl:grid-cols-[minmax(0,1fr)_20rem]" method="post" action="{{ saveUrl }}">
456
+ <form class="group/editor grid items-start gap-6 xl:grid-cols-[minmax(0,1fr)_20rem]" id="editor-form" method="post" action="{{ saveUrl }}">
105
457
  <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
106
458
  {# The hash of the file this form was filled in from. A save whose hash no
107
459
  longer matches what is on disk is refused rather than overwriting
@@ -109,15 +461,26 @@
109
461
  <input type="hidden" name="hash" value="{{ form.hash }}" />
110
462
  {# Which layout the preview renders through. The save handler ignores it. #}
111
463
  <input type="hidden" name="type" value="{{ kind.type }}" />
464
+ {% if editorKind %}
465
+ <input type="hidden" name="kind" value="{{ postKind }}" />
466
+ {% endif %}
112
467
 
113
468
  <div class="flex min-w-0 flex-col gap-4">
114
469
  {% 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']) }}
470
+ {% if not editorKind or editorKind.title != 'none' or form.title %}
471
+ {% set titleHint %}{% if postKind == 'note' %}Add a title and the note becomes an article.{% endif %}{% endset %}
472
+ {#- A post may go without one, and is then a note; a page is always named. -#}
473
+ {{ 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) }}
474
+ {{ pluginActions('editor-title', editorActions['editor-title']) }}
475
+ {% endif %}
476
+
477
+ {% if editorKind %}
478
+ {% for name in sections.own %}
479
+ {{ section(name, true) }}
480
+ {% endfor %}
481
+ {% endif %}
119
482
 
120
- {{ field.textarea('editor-body', 'body', 'Body', value=form.body, rows=24) }}
483
+ {{ 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
484
 
122
485
  {# Everything the editor's own script needs, on the element it hangs
123
486
  off. With JavaScript off this is one button that posts the form to
@@ -129,97 +492,34 @@
129
492
  class="flex flex-wrap items-center gap-2"
130
493
  id="editor-enhance"
131
494
  data-preview-url="{{ previewUrl }}"
132
- data-upload-url="{{ uploadUrl }}"
495
+ data-upload-url="{{ uploadsUrl }}"
133
496
  data-kind="{{ kind.type }}"
134
497
  >
135
498
  {{ button('Preview', type='submit', id='editor-preview-fallback', style='soft', formaction=previewUrl, formtarget='_blank', formnovalidate=true) }}
136
499
  </div>
137
500
  {% endcall %}
138
501
 
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 %}
502
+ {% if editorKind and sections.also | length %}
503
+ <h2 class="px-1 text-sm font-semibold">Also on this post</h2>
504
+ {% for name in sections.also %}
505
+ {% call collapse(SECTION_TITLES[name], true, fields=true) %}
506
+ {{ section(name, false) }}
198
507
  {% endcall %}
508
+ {% endfor %}
509
+ {% endif %}
199
510
 
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 %}
511
+ {% if kind.type == "post" %}
512
+ <datalist id="editor-photo-uploads">
513
+ {% for url in photos.choices %}
514
+ <option value="{{ url }}"></option>
515
+ {% endfor %}
516
+ </datalist>
517
+ {% if form.previewHidden and not (citedPreviews | length) %}
518
+ <input type="hidden" name="preview" value="hide">
519
+ {% endif %}
520
+ {% if form.citedAlt and not describesCitedImage %}
521
+ <input type="hidden" name="cited-alt" value="{{ form.citedAlt }}">
522
+ {% endif %}
223
523
  {% endif %}
224
524
  </div>
225
525
 
@@ -233,16 +533,14 @@
233
533
  {{ buttonLink('View', viewUrl, style='ghost') }}
234
534
  {% endif %}
235
535
  {% endcall %}
236
- {% endcall %}
237
536
 
238
- {% call collapse('Publishing', open.publishing, fields=true) %}
239
537
  {{ field.checkbox('editor-draft', 'draft', 'Draft', checked=form.draft) }}
240
538
 
241
539
  {% if kind.dated %}
242
540
  {# The file holds a UTC instant; this field is that instant as the
243
541
  site's own clock reads it, so the zone has to be on the screen. #}
244
542
  {% 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) }}
543
+ {{ field.text('editor-date', 'date', 'When', value=form.date, error=(error if errorField == 'editor-date' else ''), hintHtml=dateHint) }}
246
544
  {% endif %}
247
545
 
248
546
  {% set visibilityHint %}
@@ -263,12 +561,6 @@
263
561
  <option value="unlisted"{% if form.visibility == "unlisted" %} selected{% endif %}>Unlisted</option>
264
562
  {% endcall %}
265
563
 
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
564
  {# Who the front matter says wrote this (TASK-67). A select of the site's
273
565
  users rather than a free box, because after decision-14 the value is a
274
566
  person rather than a string: it decides whose archive the post is on
@@ -294,75 +586,18 @@
294
586
  {% endcall %}
295
587
  {% endif %}
296
588
 
589
+ {% if kind.type == "post" and sidebarLocation %}
590
+ {% call collapse('Location', open.location, fields=true) %}
591
+ {{ placeFields(false) }}
592
+ {% endcall %}
593
+ {% endif %}
594
+
297
595
  {% call collapse('Address', open.address, fields=true) %}
298
596
  {{ field.text('editor-slug', 'slug', 'Slug', value=form.slug, error=(error if errorField == 'editor-slug' else '')) }}
299
597
  {% set permalinkHint %}Leave it empty to let the {{ kind.singular }} take the URL its slug implies.{% endset %}
300
598
  {{ field.text('editor-permalink', 'permalink', 'Permalink', value=form.permalink, hintHtml=permalinkHint) }}
301
599
  {% endcall %}
302
600
 
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
601
  {% call collapse('Summary and language', open.summary, fields=true) %}
367
602
  {{ field.text('editor-description', 'description', 'Description', value=form.description, error=(error if errorField == 'editor-description' else '')) }}
368
603
  {{ pluginActions('editor-description', editorActions['editor-description']) }}
@@ -398,6 +633,12 @@
398
633
  <option value="{{ commentSettings.closed }}"{% if form.comments == commentSettings.closed %} selected{% endif %}>Closed</option>
399
634
  {% endcall %}
400
635
 
636
+ {% if kind.type == "post" %}
637
+ {# TASK-207: shown at the top of the author's fediverse profile, from
638
+ the featured collection their actor links. #}
639
+ {{ 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.') }}
640
+ {% endif %}
641
+
401
642
  {% if kind.excludable %}
402
643
  {# doc-2's eleventyExcludeFromCollections: still published and still at
403
644
  its URL, but out of every collection a template lists from. #}
@@ -440,10 +681,14 @@
440
681
  {# The slug and permalink auto-fill, in a file of its own so the policy never
441
682
  has to allow an inline script. #}
442
683
  <script defer src="{{ assetPrefix }}slug.js"></script>
684
+ <script defer src="{{ assetPrefix }}upload-drop.js"></script>
443
685
  {% if kind.type == 'post' %}
444
686
  <script defer src="{{ assetPrefix }}location.js"></script>
445
687
  {% endif %}
446
688
  {% if editorActions | length %}
447
689
  <script defer src="{{ assetPrefix }}editor-actions.js"></script>
448
690
  {% endif %}
691
+ {% if sections and sections.shown.review %}
692
+ <script defer src="{{ assetPrefix }}item-lookup.js"></script>
693
+ {% endif %}
449
694
  {% endblock %}