@geekity/cms 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +23 -0
  2. package/admin/components/admin-bar.njk +27 -6
  3. package/admin/components/alert.njk +28 -0
  4. package/admin/components/badge.njk +58 -0
  5. package/admin/components/button.njk +56 -0
  6. package/admin/components/card.njk +40 -0
  7. package/admin/components/collapse.njk +36 -0
  8. package/admin/components/copy.njk +20 -0
  9. package/admin/components/dropdown.njk +32 -0
  10. package/admin/components/fields.njk +81 -45
  11. package/admin/components/flash.njk +12 -5
  12. package/admin/components/menu.njk +51 -0
  13. package/admin/components/navbar.njk +23 -0
  14. package/admin/components/pagination.njk +47 -0
  15. package/admin/components/public-admin-bar.njk +4 -41
  16. package/admin/components/stat.njk +35 -0
  17. package/admin/components/table.njk +74 -0
  18. package/admin/components/tabs.njk +33 -0
  19. package/admin/components/theme-choice.njk +18 -0
  20. package/admin/layouts/base.njk +9 -16
  21. package/admin/layouts/settings-page.njk +15 -10
  22. package/admin/layouts/shell.njk +23 -31
  23. package/admin/pages/account/forgot.njk +39 -33
  24. package/admin/pages/account/login.njk +22 -16
  25. package/admin/pages/account/reset.njk +34 -25
  26. package/admin/pages/account/setup.njk +21 -15
  27. package/admin/pages/appearance/themes.njk +50 -44
  28. package/admin/pages/comments/all.njk +88 -106
  29. package/admin/pages/dashboard/home.njk +50 -62
  30. package/admin/pages/documents/conflict.njk +29 -24
  31. package/admin/pages/documents/editor.njk +155 -275
  32. package/admin/pages/documents/list.njk +60 -69
  33. package/admin/pages/documents/syndication.njk +98 -60
  34. package/admin/pages/documents/taxonomy.njk +36 -32
  35. package/admin/pages/error.njk +8 -5
  36. package/admin/pages/federation/followers.njk +91 -97
  37. package/admin/pages/federation/settings.njk +10 -11
  38. package/admin/pages/indieauth/consent.njk +60 -44
  39. package/admin/pages/indieauth/refused.njk +6 -3
  40. package/admin/pages/media/library.njk +86 -107
  41. package/admin/pages/messages/all.njk +71 -77
  42. package/admin/pages/navigation/menus.njk +90 -85
  43. package/admin/pages/placeholder.njk +13 -8
  44. package/admin/pages/settings/discussion.njk +30 -29
  45. package/admin/pages/settings/email.njk +26 -28
  46. package/admin/pages/settings/general.njk +10 -4
  47. package/admin/pages/settings/permalinks.njk +8 -9
  48. package/admin/pages/settings/privacy.njk +3 -3
  49. package/admin/pages/settings/reading.njk +3 -3
  50. package/admin/pages/tools/content-index.njk +79 -73
  51. package/admin/pages/tools/personal-data.njk +61 -55
  52. package/admin/pages/users/activity-entry.njk +12 -12
  53. package/admin/pages/users/activity.njk +13 -19
  54. package/admin/pages/users/apps.njk +112 -100
  55. package/admin/pages/users/edit.njk +65 -58
  56. package/admin/pages/users/list.njk +26 -24
  57. package/admin/pages/users/new.njk +11 -7
  58. package/admin/src/admin.css +174 -0
  59. package/admin/static/admin-bar.css +33 -31
  60. package/admin/static/admin-bar.js +22 -0
  61. package/admin/static/admin.css +7388 -1195
  62. package/admin/static/copy.js +1 -1
  63. package/admin/static/editor.js +18 -18
  64. package/dist/admin/accounts.d.ts +20 -0
  65. package/dist/admin/accounts.d.ts.map +1 -1
  66. package/dist/admin/accounts.js +25 -0
  67. package/dist/admin/accounts.js.map +1 -1
  68. package/dist/admin/admin-theme.d.ts +157 -0
  69. package/dist/admin/admin-theme.d.ts.map +1 -0
  70. package/dist/admin/admin-theme.js +52 -0
  71. package/dist/admin/admin-theme.js.map +1 -0
  72. package/dist/admin/assets.d.ts +4 -3
  73. package/dist/admin/assets.d.ts.map +1 -1
  74. package/dist/admin/assets.js +4 -3
  75. package/dist/admin/assets.js.map +1 -1
  76. package/dist/admin/documents.js +1 -1
  77. package/dist/admin/documents.js.map +1 -1
  78. package/dist/admin/headers.js +1 -1
  79. package/dist/admin/headers.js.map +1 -1
  80. package/dist/admin/routes.d.ts.map +1 -1
  81. package/dist/admin/routes.js +17 -8
  82. package/dist/admin/routes.js.map +1 -1
  83. package/dist/admin/templates.d.ts +6 -3
  84. package/dist/admin/templates.d.ts.map +1 -1
  85. package/dist/admin/templates.js +25 -3
  86. package/dist/admin/templates.js.map +1 -1
  87. package/dist/admin/users.d.ts +4 -0
  88. package/dist/admin/users.d.ts.map +1 -1
  89. package/dist/admin/users.js +37 -1
  90. package/dist/admin/users.js.map +1 -1
  91. package/dist/content/post-type.d.ts.map +1 -1
  92. package/dist/content/post-type.js +7 -2
  93. package/dist/content/post-type.js.map +1 -1
  94. package/dist/web/admin-bar.d.ts +4 -4
  95. package/dist/web/admin-bar.d.ts.map +1 -1
  96. package/dist/web/admin-bar.js +11 -34
  97. package/dist/web/admin-bar.js.map +1 -1
  98. package/package.json +8 -4
  99. package/themes/default/README.md +3 -1
@@ -6,62 +6,64 @@
6
6
  readonly fields, which select and copy like any other text; copy.js adds a
7
7
  button beside each of them and nothing else. #}
8
8
  {% extends "layouts/shell.njk" %}
9
+ {% from "components/badge.njk" import status %}
10
+ {% from "components/button.njk" import button, buttonLink %}
11
+ {% from "components/card.njk" import card, cardActions %}
12
+ {% from "components/copy.njk" import copyField %}
13
+ {% from "components/pagination.njk" import pagination %}
14
+ {% from "components/table.njk" import table %}
9
15
 
10
16
  {% block title %}{{ heading }}{% endblock %}
11
17
 
12
18
  {% block content %}
13
- <h1>{{ heading }}</h1>
19
+ <h1 class="mb-6 text-2xl font-bold">{{ heading }}</h1>
14
20
 
15
21
  {% if confirm %}
16
- <form class="admin-settings" method="post" action="{{ deleteUrl }}">
22
+ <form class="mb-6 max-w-2xl" method="post" action="{{ deleteUrl }}">
17
23
  <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
18
24
  <input type="hidden" name="{{ fields.path }}" value="{{ confirm.path }}" />
19
25
  <input type="hidden" name="{{ fields.confirm }}" value="1" />
20
-
21
- <h2>Delete {{ confirm.url }}?</h2>
22
-
23
- <p>
24
- {{ confirm.references.length }}
25
- {% if confirm.references.length == 1 %}document still points{% else %}documents still point{% endif %}
26
- at it. Deleting the file does not change them, so they will point at nothing.
27
- </p>
28
-
29
- <ul class="admin-references">
30
- {% for reference in confirm.references %}
31
- <li>
32
- <a href="{{ reference.editUrl }}">{{ reference.title }}</a>
33
- {% if reference.trashed %}<span class="admin-status admin-status-trashed">Trash</span>{% endif %}
34
- <code>{{ reference.path }}</code>
35
- </li>
36
- {% endfor %}
37
- </ul>
38
-
39
- <p class="admin-settings-buttons">
40
- <button type="submit">Delete it anyway</button>
41
- <a href="{{ listUrl }}">Cancel</a>
42
- </p>
26
+ {% call card(title='Delete ' + confirm.url + '?', style='border') %}
27
+ <p>
28
+ {{ confirm.references.length }}
29
+ {% if confirm.references.length == 1 %}document still points{% else %}documents still point{% endif %}
30
+ at it. Deleting the file does not change them, so they will point at nothing.
31
+ </p>
32
+ <ul class="list-disc pl-6">
33
+ {% for reference in confirm.references %}
34
+ <li>
35
+ <a class="link" href="{{ reference.editUrl }}">{{ reference.title }}</a>
36
+ {% if reference.trashed %}{{ status('trashed', 'Trash') }}{% endif %}
37
+ <code>{{ reference.path }}</code>
38
+ </li>
39
+ {% endfor %}
40
+ </ul>
41
+ {% call cardActions() %}
42
+ {{ buttonLink('Cancel', listUrl, style='ghost') }}
43
+ {{ button('Delete it anyway', type='submit', color='error') }}
44
+ {% endcall %}
45
+ {% endcall %}
43
46
  </form>
44
47
  {% endif %}
45
48
 
46
- <form class="admin-settings admin-media-upload" method="post" action="{{ uploadUrl }}" enctype="multipart/form-data">
49
+ <form class="mb-8 max-w-2xl" method="post" action="{{ uploadUrl }}" enctype="multipart/form-data">
47
50
  <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
48
- <label for="media-file">Add a file</label>
49
- <input id="media-file" name="{{ fields.file }}" type="file" accept="{{ accepts }}" required />
50
- <p class="admin-hint">
51
- This site accepts {{ accepts | replace(',', ', ') }}, up to {{ maxBytes }} bytes, or
52
- {{ mediaMaxBytes }} bytes for audio and video. The same
53
- rules the editor’s upload control enforces.
54
- </p>
55
- <p class="admin-settings-buttons">
56
- <button type="submit">Upload</button>
57
- </p>
51
+ <div class="fieldset">
52
+ <label class="fieldset-legend" for="media-file">Add a file</label>
53
+ <input id="media-file" name="{{ fields.file }}" type="file" accept="{{ accepts }}" required class="file-input file-input-sm w-full" />
54
+ <p class="label block whitespace-normal">
55
+ This site accepts {{ accepts | replace(',', ', ') }}, up to {{ maxBytes }} bytes, or
56
+ {{ mediaMaxBytes }} bytes for audio and video. The same
57
+ rules the editor’s upload control enforces.
58
+ </p>
59
+ </div>
60
+ <div class="mt-2">{{ button('Upload', type='submit', color='primary') }}</div>
58
61
  </form>
59
62
 
60
63
  {% if total == 0 %}
61
- <p class="admin-empty">Nothing has been uploaded yet.</p>
64
+ <p>Nothing has been uploaded yet.</p>
62
65
  {% else %}
63
- <table class="admin-list admin-media">
64
- <caption class="admin-visually-hidden">{{ heading }}</caption>
66
+ {% call table(heading) %}
65
67
  <thead>
66
68
  <tr>
67
69
  <th scope="col">File</th>
@@ -76,117 +78,94 @@
76
78
  <tbody>
77
79
  {% for file in files %}
78
80
  <tr>
79
- <td class="admin-media-file">
80
- {% if file.kind == 'image' %}
81
- <a href="{{ file.url }}">
82
- <img class="admin-media-thumbnail" src="{{ file.url }}" alt="" width="60" height="60" />
83
- </a>
84
- {% else %}
85
- <span class="admin-media-icon" aria-hidden="true">{{ file.extension | replace('.', '') | upper }}</span>
86
- {% endif %}
87
- <a href="{{ file.url }}">{{ file.path }}</a>
81
+ <td>
82
+ <div class="flex min-w-56 items-center gap-3">
83
+ {% if file.kind == 'image' %}
84
+ <a class="shrink-0" href="{{ file.url }}">
85
+ <img class="size-15 rounded-box bg-base-200 object-cover" src="{{ file.url }}" alt="" width="60" height="60" />
86
+ </a>
87
+ {% else %}
88
+ <span class="flex size-15 shrink-0 items-center justify-center rounded-box bg-base-200 text-xs font-bold" aria-hidden="true">{{ file.extension | replace('.', '') | upper }}</span>
89
+ {% endif %}
90
+ <a class="link link-hover break-all" href="{{ file.url }}">{{ file.path }}</a>
91
+ </div>
88
92
  </td>
89
- <td class="admin-media-alt">
93
+ <td>
90
94
  {% if file.kind == 'image' %}
91
- <form method="post" action="{{ altUrl }}">
95
+ <form class="flex min-w-72 flex-wrap items-center gap-2" method="post" action="{{ altUrl }}">
92
96
  <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
93
97
  <input type="hidden" name="{{ fields.path }}" value="{{ file.path }}" />
94
98
  <input type="hidden" name="{{ fields.page }}" value="{{ page }}" />
95
- <label class="admin-visually-hidden" for="media-alt-{{ loop.index }}">
96
- Alt text for {{ file.name }}
97
- </label>
99
+ <label class="sr-only" for="media-alt-{{ loop.index }}">Alt text for {{ file.name }}</label>
98
100
  <input
99
101
  id="media-alt-{{ loop.index }}"
100
102
  name="{{ fields.alt }}"
101
103
  type="text"
102
104
  value="{% if file.alt and file.alt.kind == 'described' %}{{ file.alt.text }}{% endif %}"
103
105
  aria-describedby="media-alt-state-{{ loop.index }}"
106
+ class="input input-sm min-w-40 flex-1"
104
107
  />
105
- <span class="admin-check">
108
+ <label class="label text-sm text-base-content" for="media-decorative-{{ loop.index }}">
106
109
  <input
107
110
  id="media-decorative-{{ loop.index }}"
108
111
  name="{{ fields.decorative }}"
109
112
  type="checkbox"
110
113
  value="1"
111
114
  {% if file.alt and file.alt.kind == 'decorative' %}checked{% endif %}
115
+ class="checkbox checkbox-sm"
112
116
  />
113
- <label for="media-decorative-{{ loop.index }}">Decorative</label>
114
- </span>
115
- <button type="submit">Save</button>
116
- <span id="media-alt-state-{{ loop.index }}" class="admin-status{% if not file.alt %} admin-status-missing{% endif %}">
117
- {%- if not file.alt %}Missing: describe it, or mark it decorative
118
- {%- elif file.alt.kind == 'decorative' %}Decorative: inserted with an empty alt
119
- {%- else %}Offered when it is inserted{% endif -%}
120
- </span>
117
+ Decorative
118
+ </label>
119
+ {{ button('Save', type='submit', size='sm') }}
120
+ <p id="media-alt-state-{{ loop.index }}" class="w-full text-sm">
121
+ {%- if not file.alt %}{{ status('missing', 'Missing') }} Describe it, or mark it decorative.
122
+ {%- elif file.alt.kind == 'decorative' %}{{ status('decorative', 'Decorative') }} Inserted with an empty alt.
123
+ {%- else %}{{ status('described', 'Described') }} Offered when it is inserted.{% endif -%}
124
+ </p>
121
125
  </form>
122
126
  {% else %}
123
- <span class="admin-status">Not an image</span>
127
+ Not an image
124
128
  {% endif %}
125
129
  </td>
126
- <td>{{ file.modified | date('html') }}</td>
127
- <td>{{ file.bytes }}</td>
128
- <td class="admin-media-links">
129
- <label class="admin-visually-hidden" for="media-url-{{ loop.index }}">
130
- URL of {{ file.name }}
131
- </label>
132
- <span class="admin-copy">
133
- <input id="media-url-{{ loop.index }}" type="text" value="{{ file.url }}" readonly />
134
- <button type="button" class="admin-copy-button" data-copy="media-url-{{ loop.index }}" hidden>
135
- Copy
136
- </button>
137
- </span>
138
-
139
- <label class="admin-visually-hidden" for="media-markdown-{{ loop.index }}">
140
- Markdown for {{ file.name }}
141
- </label>
142
- <span class="admin-copy">
143
- <input id="media-markdown-{{ loop.index }}" type="text" value="{{ file.markdown }}" readonly />
144
- <button type="button" class="admin-copy-button" data-copy="media-markdown-{{ loop.index }}" hidden>
145
- Copy
146
- </button>
147
- </span>
130
+ <td class="whitespace-nowrap">{{ file.modified | date('html') }}</td>
131
+ <td class="whitespace-nowrap">{{ file.bytes }}</td>
132
+ <td>
133
+ <div class="flex min-w-60 flex-col gap-2">
134
+ {{ copyField('media-url-' + loop.index, 'URL of ' + file.name, file.url) }}
135
+ {{ copyField('media-markdown-' + loop.index, 'Markdown for ' + file.name, file.markdown) }}
136
+ </div>
148
137
  </td>
149
138
  <td>
150
139
  {% if file.references.length %}
151
- <ul class="admin-references">
140
+ <ul>
152
141
  {% for reference in file.references %}
153
- <li>
154
- <a href="{{ reference.editUrl }}">{{ reference.title }}</a>
155
- {% if reference.trashed %}
156
- <span class="admin-status admin-status-trashed">Trash</span>
157
- {% endif %}
142
+ <li class="whitespace-nowrap">
143
+ <a class="link link-hover" href="{{ reference.editUrl }}">{{ reference.title }}</a>
144
+ {% if reference.trashed %}{{ status('trashed', 'Trash') }}{% endif %}
158
145
  </li>
159
146
  {% endfor %}
160
147
  </ul>
161
148
  {% else %}
162
- <span class="admin-status">Nothing</span>
149
+ Nothing
163
150
  {% endif %}
164
151
  </td>
165
- <td class="admin-actions">
152
+ <td>
166
153
  <form method="post" action="{{ deleteUrl }}">
167
154
  <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
168
155
  <input type="hidden" name="{{ fields.path }}" value="{{ file.path }}" />
169
- <button type="submit">Delete</button>
156
+ {{ button('Delete', type='submit', style='ghost', size='xs') }}
170
157
  </form>
171
158
  </td>
172
159
  </tr>
173
160
  {% endfor %}
174
161
  </tbody>
175
- </table>
162
+ {% endcall %}
176
163
 
177
- {% if previousUrl or nextUrl %}
178
- <nav class="admin-pagination" aria-label="Pages">
179
- {% if previousUrl %}
180
- <a rel="prev" href="{{ previousUrl }}">Newer</a>
181
- {% endif %}
182
- <span>Page {{ page }} of {{ pages }}</span>
183
- {% if nextUrl %}
184
- <a rel="next" href="{{ nextUrl }}">Older</a>
185
- {% endif %}
186
- </nav>
187
- {% endif %}
164
+ <div class="mt-4">
165
+ {{ pagination(page, pages=pages, previousUrl=previousUrl, nextUrl=nextUrl) }}
166
+ </div>
188
167
 
189
- <p class="admin-hint">
168
+ <p class="mt-4 max-w-prose text-sm">
190
169
  The list is the <code>content/uploads</code> directory itself, read afresh on every request,
191
170
  so a file copied in by hand is here without a restart. Deleting one removes the file and
192
171
  whatever was derived from it; it does not touch the documents that link to it, which is why
@@ -1,104 +1,98 @@
1
1
  {# /admin/messages: what the contact form left behind (TASK-56).
2
2
 
3
- A message is a file under data/contact/ and nothing else, so the two buttons
4
- here are a rewrite and an unlink. Every button is a form of its own, so the
5
- whole screen works with JavaScript switched off. #}
3
+ One card per message, as on Comments. An unread one says so in a badge and
4
+ in a heavier name, so it is not told by colour alone. A message is a file
5
+ under data/contact/ and nothing else, so the two buttons are a rewrite and
6
+ an unlink, each a form of its own. #}
6
7
  {% extends "layouts/shell.njk" %}
8
+ {% from "components/badge.njk" import status as statusBadge %}
9
+ {% from "components/button.njk" import button, buttonLink %}
10
+ {% from "components/card.njk" import card %}
11
+ {% from "components/pagination.njk" import pagination %}
12
+ {% from "components/tabs.njk" import tabs as tabsNav %}
7
13
 
8
14
  {% block title %}{{ heading }}{% endblock %}
9
15
 
10
16
  {% block content %}
11
- <h1>{{ heading }}</h1>
17
+ <h1 class="mb-6 text-2xl font-bold">{{ heading }}</h1>
12
18
 
13
- <nav class="admin-filters" aria-label="Message status">
14
- <ul>
15
- {% for tab in tabs %}
16
- <li>
17
- <a href="{{ tab.url }}"{% if tab.current %} aria-current="page"{% endif %}>
18
- {{ tab.label }} ({{ tab.count }})
19
- </a>
20
- </li>
21
- {% endfor %}
22
- </ul>
23
- </nav>
19
+ <div class="mb-6">{{ tabsNav(tabs, 'Message status') }}</div>
24
20
 
25
21
  {% if rows.length == 0 %}
26
- <p class="admin-empty">Nothing here.</p>
22
+ <p class="mb-6">Nothing here.</p>
27
23
  {% else %}
28
- <ul class="admin-comments">
24
+ <ul class="mb-6 flex max-w-3xl flex-col gap-4">
29
25
  {% for row in rows %}
30
- <li class="admin-comment{% if not row.read %} admin-comment-unread{% endif %}">
31
- <div class="admin-comment-meta">
32
- <span class="admin-comment-author">{{ row.author }}</span>
33
- <a class="admin-comment-email" href="mailto:{{ row.email }}">{{ row.email }}</a>
34
- {% if not row.read %}
35
- <span class="admin-status admin-status-pending">unread</span>
36
- {% endif %}
37
- {% if row.status == 'spam' %}
38
- <span class="admin-status admin-status-spam">spam</span>
39
- {% endif %}
40
- </div>
26
+ <li>
27
+ {% call card() %}
28
+ <p class="flex flex-wrap items-center gap-x-3 gap-y-1">
29
+ {% if row.read %}
30
+ <span class="font-medium">{{ row.author }}</span>
31
+ {% else %}
32
+ <span class="font-bold">{{ row.author }}</span>
33
+ {% endif %}
34
+ <a class="link break-all" href="mailto:{{ row.email }}">{{ row.email }}</a>
35
+ {% if not row.read %}
36
+ {{ statusBadge('unread', 'Unread') }}
37
+ {% endif %}
38
+ {% if row.status == 'spam' %}
39
+ {{ statusBadge('spam', 'Spam') }}
40
+ {% endif %}
41
+ </p>
41
42
 
42
- <p class="admin-comment-where">
43
- <time>{{ row.received }}</time>
44
- from
45
- {% if row.pageUrl %}
46
- <a href="{{ row.pageUrl }}">{{ row.page }}</a>
47
- {% else %}
48
- {{ row.page }}
49
- {% endif %}
50
- {% if row.address %}
51
- &middot; <code title="A salted hash of the address it came from">{{ row.address }}</code>
52
- {% endif %}
53
- </p>
43
+ <p class="text-sm">
44
+ <time>{{ row.received }}</time>
45
+ from
46
+ {% if row.pageUrl %}
47
+ <a class="link" href="{{ row.pageUrl }}">{{ row.page }}</a>
48
+ {% else %}
49
+ {{ row.page }}
50
+ {% endif %}
51
+ {% if row.address %}
52
+ &middot; <code title="A salted hash of the address it came from">{{ row.address }}</code>
53
+ {% endif %}
54
+ </p>
54
55
 
55
- {% if row.subject %}
56
- <p class="admin-message-subject"><strong>{{ row.subject }}</strong></p>
57
- {% endif %}
56
+ {% if row.subject %}
57
+ <p class="font-semibold">{{ row.subject }}</p>
58
+ {% endif %}
58
59
 
59
- {# Printed, never rendered: a contact message is plain text, and
60
- nothing a stranger typed is turned into markup here. #}
61
- <div class="admin-comment-body admin-message-body">{{ row.message }}</div>
60
+ {# Printed, never rendered: a contact message is plain text, and
61
+ nothing a stranger typed is turned into markup here. #}
62
+ <div class="whitespace-pre-line break-words">{{ row.message }}</div>
62
63
 
63
- <div class="admin-actions">
64
- <a class="admin-button admin-button-quiet" href="{{ row.replyUrl }}">Reply</a>
64
+ <div class="flex flex-wrap gap-1">
65
+ {{ buttonLink('Reply', row.replyUrl, style='ghost', size='xs') }}
65
66
 
66
- <form method="post" action="{{ readUrl }}">
67
- <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
68
- <input type="hidden" name="{{ fields.id }}" value="{{ row.id }}" />
69
- <input type="hidden" name="{{ fields.status }}" value="{{ status }}" />
70
- {% if row.read %}
71
- <button type="submit" name="{{ fields.read }}" value="">Mark unread</button>
72
- {% else %}
73
- <button type="submit" name="{{ fields.read }}" value="1">Mark read</button>
74
- {% endif %}
75
- </form>
67
+ <form method="post" action="{{ readUrl }}">
68
+ <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
69
+ <input type="hidden" name="{{ fields.id }}" value="{{ row.id }}" />
70
+ <input type="hidden" name="{{ fields.status }}" value="{{ status }}" />
71
+ {% if row.read %}
72
+ {{ button('Mark unread', type='submit', style='ghost', size='xs', name=fields.read) }}
73
+ {% else %}
74
+ {{ button('Mark read', type='submit', style='ghost', size='xs', name=fields.read, value='1') }}
75
+ {% endif %}
76
+ </form>
76
77
 
77
- <form method="post" action="{{ deleteUrl }}">
78
- <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
79
- <input type="hidden" name="{{ fields.id }}" value="{{ row.id }}" />
80
- <input type="hidden" name="{{ fields.status }}" value="{{ status }}" />
81
- <button type="submit">Delete</button>
82
- </form>
83
- </div>
78
+ <form method="post" action="{{ deleteUrl }}">
79
+ <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
80
+ <input type="hidden" name="{{ fields.id }}" value="{{ row.id }}" />
81
+ <input type="hidden" name="{{ fields.status }}" value="{{ status }}" />
82
+ {{ button('Delete', type='submit', color='error', style='ghost', size='xs') }}
83
+ </form>
84
+ </div>
85
+ {% endcall %}
84
86
  </li>
85
87
  {% endfor %}
86
88
  </ul>
87
89
 
88
- {% if previousUrl or nextUrl %}
89
- <nav class="admin-pagination" aria-label="Pages">
90
- {% if previousUrl %}
91
- <a rel="prev" href="{{ previousUrl }}">Newer</a>
92
- {% endif %}
93
- <span>Page {{ page }} of {{ pages }}</span>
94
- {% if nextUrl %}
95
- <a rel="next" href="{{ nextUrl }}">Older</a>
96
- {% endif %}
97
- </nav>
98
- {% endif %}
90
+ <div class="mb-6">
91
+ {{ pagination(page, pages=pages, previousUrl=previousUrl, nextUrl=nextUrl) }}
92
+ </div>
99
93
  {% endif %}
100
94
 
101
- <p class="admin-hint">
95
+ <p class="max-w-prose text-sm">
102
96
  A page carrying <code>contact: true</code> in its front matter offers a contact form, and every
103
97
  message it takes is one JSON file under <code>data/contact/</code> &mdash; written before
104
98
  anything is emailed, so a message survives a mail outage and a site with no mail configured