@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
@@ -2,88 +2,100 @@
2
2
 
3
3
  Two lists, and a menu is in exactly one of them. First the areas the active
4
4
  theme declares, in the theme's own order and under the theme's own labels,
5
- each a box to type links into — an area the site has never filled in is an
6
- empty box rather than a missing one. Then every other menu the site stores,
7
- under a heading that says this theme renders them nowhere, each with a
8
- Delete, which is the only way a menu is removed. #}
5
+ each a card with a box to type links into: an area the site has never
6
+ filled in is an empty box rather than a missing one. Then every other menu
7
+ the site stores, under a heading that says this theme renders them nowhere,
8
+ each with a Delete, which is the only way a menu is removed. Each save, add
9
+ and delete is a form of its own, so a refused one loses nothing typed into
10
+ another. #}
9
11
  {% extends "layouts/shell.njk" %}
10
12
  {% import "components/fields.njk" as field %}
13
+ {% from "components/badge.njk" import badge %}
14
+ {% from "components/button.njk" import button %}
15
+ {% from "components/card.njk" import card, cardActions %}
11
16
 
12
17
  {% block title %}{{ heading }}{% endblock %}
13
18
 
14
- {# One menu: its heading, the box its items are typed into, and the button
15
- that saves it. `extra` is whatever else belongs under the box — the Delete
16
- form, on a menu nothing renders. #}
17
- {% macro box(menu, fields, url, csrfToken, extra='') %}
18
- <li class="admin-menu-box">
19
- <h3>{{ menu.label }}</h3>
20
-
21
- <p class="admin-menu-name">
22
- <code>menus.{{ menu.name }}</code>
23
- {% if menu.stored %}
24
- <span class="admin-status">{{ menu.count }} item{% if menu.count != 1 %}s{% endif %}</span>
25
- {% else %}
26
- <span class="admin-status">empty — nothing has been typed here yet</span>
27
- {% endif %}
28
- </p>
29
-
30
- <form method="post" action="{{ url }}">
31
- <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
32
- <input type="hidden" name="{{ fields.menu }}" value="{{ menu.name }}" />
33
- {% set itemsHint %}
34
- One item per line, written <code>Label | URL</code> &mdash; for example
35
- <code>About | /about/</code> or
36
- <code>Mastodon | https://example.social/@me</code>. They are the whole of
37
- this menu, in this order: nothing else puts a link in it. A page serving
38
- as the front page goes in as <code>Home | /</code>, where a reader lands.
39
- End a line with the
40
- <code>rel</code> values the link carries, a word each &mdash;
41
- <code>Mastodon | https://example.social/@me | me</code>,
42
- <code>A source | https://example.com/thing | nofollow noopener</code>.
43
- <code>me</code> marks <strong>a profile that is you</strong>, which is
44
- how Mastodon and the rest of the IndieWeb verify that the site and the
45
- profile it links are the same person; the rest are whatever HTML link
46
- types you want on it.
47
- {% endset %}
48
- {{ field.textarea('menu-' + menu.name, fields.items, 'Items', menu.items,
49
- error=menu.problem, hint=itemsHint, rows='5', spellcheck='false') }}
50
- <p class="admin-menu-buttons">
51
- <button type="submit">Save menu</button>
19
+ {# One menu: its card, the box its items are typed into, and the button that
20
+ saves it. `kept` adds the Delete form, on a menu nothing renders. #}
21
+ {% macro box(menu, fields, url, deleteUrl, csrfToken, kept=false) %}
22
+ <li>
23
+ {% call card(title=menu.label) %}
24
+ <p class="flex flex-wrap items-center gap-2">
25
+ <code>menus.{{ menu.name }}</code>
26
+ {% if menu.stored %}
27
+ {{ badge(menu.count + (' item' if menu.count == 1 else ' items'), style='outline') }}
28
+ {% else %}
29
+ {{ badge('empty — nothing has been typed here yet', style='ghost') }}
30
+ {% endif %}
52
31
  </p>
53
- </form>
54
32
 
55
- {{ extra | safe }}
33
+ <form method="post" action="{{ url }}">
34
+ <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
35
+ <input type="hidden" name="{{ fields.menu }}" value="{{ menu.name }}" />
36
+ {% set itemsHint %}
37
+ One item per line, written <code>Label | URL</code> &mdash; for example
38
+ <code>About | /about/</code> or
39
+ <code>Mastodon | https://example.social/@me</code>. They are the whole of
40
+ this menu, in this order: nothing else puts a link in it. A page serving
41
+ as the front page goes in as <code>Home | /</code>, where a reader lands.
42
+ End a line with the
43
+ <code>rel</code> values the link carries, a word each &mdash;
44
+ <code>Mastodon | https://example.social/@me | me</code>,
45
+ <code>A source | https://example.com/thing | nofollow noopener</code>.
46
+ <code>me</code> marks <strong>a profile that is you</strong>, which is
47
+ how Mastodon and the rest of the IndieWeb verify that the site and the
48
+ profile it links are the same person; the rest are whatever HTML link
49
+ types you want on it.
50
+ {% endset %}
51
+ {{ field.textarea('menu-' + menu.name, fields.items, 'Items', menu.items,
52
+ error=menu.problem, hint=itemsHint, rows='5', spellcheck='false') }}
53
+ <div class="mt-2">{{ button('Save menu', type='submit', color='primary') }}</div>
54
+ </form>
55
+
56
+ {% if kept %}
57
+ <form method="post" action="{{ deleteUrl }}">
58
+ <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
59
+ <input type="hidden" name="{{ fields.menu }}" value="{{ menu.name }}" />
60
+ {% call cardActions() %}
61
+ {{ button('Delete this menu', type='submit', color='error', style='ghost') }}
62
+ {% endcall %}
63
+ </form>
64
+ {% endif %}
65
+ {% endcall %}
56
66
  </li>
57
67
  {% endmacro %}
58
68
 
59
69
  {% block content %}
60
- <h1>{{ heading }}</h1>
70
+ <h1 class="mb-6 text-2xl font-bold">{{ heading }}</h1>
61
71
 
62
72
  {% if hasProblems %}
63
- {% call field.summary('Nothing was saved. Fix the box marked below and try again.') %}
64
- {% for menu in areas.concat(others) %}
65
- {{ field.problem('menu-' + menu.name, menu.problem) }}
66
- {% endfor %}
67
- {{ field.problem('menu-name', nameProblem) }}
68
- {% endcall %}
73
+ <div class="mb-6 max-w-2xl">
74
+ {% call field.summary('Nothing was saved. Fix the box marked below and try again.') %}
75
+ {% for menu in areas.concat(others) %}
76
+ {{ field.problem('menu-' + menu.name, menu.problem) }}
77
+ {% endfor %}
78
+ {{ field.problem('menu-name', nameProblem) }}
79
+ {% endcall %}
80
+ </div>
69
81
  {% endif %}
70
82
 
71
- <h2>Where {{ themeName }} puts a menu</h2>
83
+ <h2 class="mb-3 text-lg font-semibold">Where {{ themeName }} puts a menu</h2>
72
84
 
73
85
  {% if areas.length %}
74
- <p class="admin-hint">
86
+ <p class="mb-4 max-w-prose text-sm">
75
87
  These are the areas the active theme declares in its <code>theme.json</code>, in the order it
76
88
  declares them. A theme renders a menu by asking for it by name, and an area with nothing in it
77
89
  renders nothing at all.
78
90
  </p>
79
91
 
80
- <ul class="admin-menu-boxes">
92
+ <ul class="mb-10 grid max-w-2xl grid-cols-1 gap-6">
81
93
  {% for menu in areas %}
82
- {{ box(menu, fields, navigationUrl, csrfToken) }}
94
+ {{ box(menu, fields, navigationUrl, deleteUrl, csrfToken) }}
83
95
  {% endfor %}
84
96
  </ul>
85
97
  {% else %}
86
- <p class="admin-hint">
98
+ <p class="mb-10 max-w-prose text-sm">
87
99
  {{ themeName }} declares no areas, so it asks for no menu by name. A theme declares them as an
88
100
  <code>areas</code> list in its <code>theme.json</code>, each entry a <code>name</code> &mdash;
89
101
  the name the template loops over as <code>menus.&lt;name&gt;</code> &mdash; and a
@@ -93,45 +105,38 @@
93
105
  {% endif %}
94
106
 
95
107
  {% if others.length %}
96
- <h2>Kept, rendered nowhere</h2>
108
+ <h2 class="mb-3 text-lg font-semibold">Kept, rendered nowhere</h2>
97
109
 
98
- <p class="admin-hint">
110
+ <p class="mb-4 max-w-prose text-sm">
99
111
  This site stores these menus and {{ themeName }} renders none of them: no template loops over
100
112
  those names, so nothing on the public site shows them. They are kept, because that is how the
101
113
  menu a theme will use gets written before the site switches to it. Delete is the only way one
102
114
  is removed.
103
115
  </p>
104
116
 
105
- <ul class="admin-menu-boxes">
117
+ <ul class="mb-10 grid max-w-2xl grid-cols-1 gap-6">
106
118
  {% for menu in others %}
107
- {% set remove %}
108
- <form class="admin-menu-delete" method="post" action="{{ deleteUrl }}">
109
- <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
110
- <input type="hidden" name="{{ fields.menu }}" value="{{ menu.name }}" />
111
- <button type="submit">Delete this menu</button>
112
- </form>
113
- {% endset %}
114
- {{ box(menu, fields, navigationUrl, csrfToken, remove) }}
119
+ {{ box(menu, fields, navigationUrl, deleteUrl, csrfToken, kept=true) }}
115
120
  {% endfor %}
116
121
  </ul>
117
122
  {% endif %}
118
123
 
119
- <h2>Add a menu</h2>
120
-
121
- <form class="admin-menu-add" method="post" action="{{ addUrl }}">
122
- <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
123
- {% set nameHint %}
124
- The name a theme asks for the menu by: lower-case letters, digits and
125
- underscores, starting with a letter. It is written after the dot &mdash;
126
- <code>{% raw %}{% for item in menus.footer %}{% endraw %}</code> &mdash; which is why a dash
127
- is not allowed in one. A name this theme declares puts the menu in its place above; any other
128
- name is a menu kept and rendered nowhere, which is how you fill one in before switching to the
129
- theme that will use it.
130
- {% endset %}
131
- {{ field.text('menu-name', fields.name, 'Name', newName,
132
- error=nameProblem, hint=nameHint, spellcheck='false', required=true) }}
133
- <p class="admin-menu-buttons">
134
- <button type="submit">Add menu</button>
135
- </p>
136
- </form>
124
+ <div class="max-w-2xl">
125
+ {% call card(title='Add a menu') %}
126
+ <form method="post" action="{{ addUrl }}">
127
+ <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
128
+ {% set nameHint %}
129
+ The name a theme asks for the menu by: lower-case letters, digits and
130
+ underscores, starting with a letter. It is written after the dot &mdash;
131
+ <code>{% raw %}{% for item in menus.footer %}{% endraw %}</code> &mdash; which is why a dash
132
+ is not allowed in one. A name this theme declares puts the menu in its place above; any other
133
+ name is a menu kept and rendered nowhere, which is how you fill one in before switching to the
134
+ theme that will use it.
135
+ {% endset %}
136
+ {{ field.text('menu-name', fields.name, 'Name', newName,
137
+ error=nameProblem, hint=nameHint, spellcheck='false', required=true) }}
138
+ <div class="mt-2">{{ button('Add menu', type='submit', color='primary') }}</div>
139
+ </form>
140
+ {% endcall %}
141
+ </div>
137
142
  {% endblock %}
@@ -1,14 +1,19 @@
1
+ {# A screen a later task builds. It exists so the navigation goes somewhere
2
+ and the guard covers it from the start.
3
+
4
+ It sits at the top of `pages/` rather than inside one of the section
5
+ folders because it is not any one section's screen: it stands in for
6
+ whichever section has nothing built yet, and the route heads it. #}
1
7
  {% extends "layouts/shell.njk" %}
8
+ {% from "components/card.njk" import card %}
2
9
 
3
10
  {% block title %}{{ heading }}{% endblock %}
4
11
 
5
12
  {% block content %}
6
- {# A screen a later task builds. It exists so the navigation goes somewhere
7
- and the guard covers it from the start.
8
-
9
- It sits at the top of `pages/` rather than inside one of the section
10
- folders because it is not any one section's screen: it stands in for
11
- whichever section has nothing built yet, and the route heads it. #}
12
- <h1>{{ heading }}{% if slug %}: {{ slug }}{% endif %}</h1>
13
- <p class="admin-note">This screen is not built yet.</p>
13
+ <h1 class="mb-6 text-2xl font-bold">{{ heading }}{% if slug %}: {{ slug }}{% endif %}</h1>
14
+ <div class="max-w-2xl">
15
+ {% call card() %}
16
+ <p>This screen is not built yet.</p>
17
+ {% endcall %}
18
+ </div>
14
19
  {% endblock %}
@@ -1,6 +1,9 @@
1
1
  {# /admin/settings/discussion: comments, webmentions, and what checks them. #}
2
2
  {% extends "layouts/settings-page.njk" %}
3
3
  {% import "components/fields.njk" as field %}
4
+ {% from "components/badge.njk" import status %}
5
+ {% from "components/button.njk" import button %}
6
+ {% from "components/card.njk" import card, cardActions %}
4
7
 
5
8
  {# What a retention field says about the period as it stands: kept forever at
6
9
  0, removed after so many days otherwise. Nothing for a value the form is
@@ -21,7 +24,7 @@
21
24
  {% endblock %}
22
25
 
23
26
  {% block settingsFields %}
24
- <h2>Comments</h2>
27
+ <h2 class="mt-2 text-lg font-semibold">Comments</h2>
25
28
 
26
29
  {% set commentsHint %}
27
30
  Clear this and no post shows a comment form and every submission is
@@ -41,7 +44,7 @@
41
44
  form.commentsCloseAfterDays, type='number', min='0', step='1',
42
45
  error=problems.commentsCloseAfterDays, hint=closeHint) }}
43
46
 
44
- <h2>Webmentions</h2>
47
+ <h2 class="mt-6 text-lg font-semibold">Webmentions</h2>
45
48
 
46
49
  {% set sendHint %}
47
50
  When a post is published or edited, every external page it links to is
@@ -60,9 +63,9 @@
60
63
  {{ field.checkbox('settings-webmentions-receive', fields.webmentionsReceive,
61
64
  'Accept webmentions sent here', checked=form.webmentionsReceive, hint=receiveHint) }}
62
65
 
63
- <h2>Keeping personal data</h2>
66
+ <h2 class="mt-6 text-lg font-semibold">Keeping personal data</h2>
64
67
 
65
- <p class="admin-hint">
68
+ <p class="max-w-prose text-sm">
66
69
  How long the site keeps what commenters and people writing through the
67
70
  contact form hand it, in days from when it arrived; <code>0</code> keeps
68
71
  it forever. A site that has never set these keeps everything, so upgrading
@@ -107,21 +110,19 @@
107
110
  {# Akismet is its own pair of forms for the reason the avatar is: the key is
108
111
  a credential in data/akismet.json rather than a setting in site.json, and
109
112
  a key Akismet will not take must not lose an edit to the fields above. #}
110
- <section class="admin-settings admin-akismet-panel">
111
- <h2>Spam checking</h2>
112
-
113
- <p class="admin-akismet-state admin-akismet-{{ akismetState }}">
113
+ {% call card(title='Spam checking') %}
114
+ <p>
114
115
  {% if akismetState == 'valid' %}
115
- <strong>Connected</strong> &mdash; key {{ akismetKeyHint }}, checked {{ akismetCheckedAt }}.
116
+ {{ status('connected', 'Connected') }} Key {{ akismetKeyHint }}, checked {{ akismetCheckedAt }}.
116
117
  {% elif akismetState == 'invalid' %}
117
- <strong>Akismet does not recognise this key</strong> &mdash; key {{ akismetKeyHint }}, checked {{ akismetCheckedAt }}.
118
+ {{ status('rejected', 'Not recognised') }} Akismet does not recognise this key: key {{ akismetKeyHint }}, checked {{ akismetCheckedAt }}.
118
119
  {% elif akismetState == 'unchecked' %}
119
- <strong>Akismet could not be reached</strong> to check key {{ akismetKeyHint }}.
120
+ {{ status('unreachable', 'Not checked') }} Akismet could not be reached to check key {{ akismetKeyHint }}.
120
121
  {% else %}
121
- <strong>Not connected</strong> &mdash; this site has no key.
122
+ {{ status('disconnected', 'Not connected') }} This site has no key.
122
123
  {% endif %}
123
124
  </p>
124
- <p class="admin-hint">{{ akismetHint }}</p>
125
+ <p class="text-sm">{{ akismetHint }}</p>
125
126
 
126
127
  <form method="post" action="{{ akismetUrl }}">
127
128
  <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
@@ -132,28 +133,28 @@
132
133
  {% endif %}
133
134
  {{ field.password('settings-akismet-key', akismetFields.key, 'Akismet API key',
134
135
  autocomplete='off', spellcheck='false', placeholder=akismetPlaceholder) }}
135
- <p class="admin-settings-buttons">
136
- <button type="submit">{% if akismetPresent %}Replace key{% else %}Save key{% endif %}</button>
137
- </p>
136
+ <div class="mt-2">
137
+ {{ button('Replace key' if akismetPresent else 'Save key', type='submit', color='primary') }}
138
+ </div>
138
139
  </form>
139
140
 
140
- {% if akismetPresent %}
141
- <form method="post" action="{{ akismetUrl }}">
142
- <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
143
- <input type="hidden" name="{{ akismetFields.action }}" value="{{ akismetRemove }}" />
144
- <p class="admin-settings-buttons">
145
- <button type="submit">Remove key</button>
146
- </p>
147
- </form>
148
- {% endif %}
149
-
150
- <p class="admin-hint">
151
- A key from <a href="https://akismet.com/">akismet.com</a> turns on spam
141
+ <p class="text-sm">
142
+ A key from <a class="link" href="https://akismet.com/">akismet.com</a> turns on spam
152
143
  checking for native comments and incoming webmentions. It is checked with
153
144
  Akismet when it is saved and kept in <code>data/akismet.json</code>, never
154
145
  in <code>site.json</code> and never on the public site. Marking something
155
146
  spam or not spam on the Comments screen tells Akismet, which is the only
156
147
  way it learns. Fediverse replies are never sent to it.
157
148
  </p>
158
- </section>
149
+
150
+ {% if akismetPresent %}
151
+ <form method="post" action="{{ akismetUrl }}">
152
+ <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
153
+ <input type="hidden" name="{{ akismetFields.action }}" value="{{ akismetRemove }}" />
154
+ {% call cardActions() %}
155
+ {{ button('Remove key', type='submit', color='error', style='ghost') }}
156
+ {% endcall %}
157
+ </form>
158
+ {% endif %}
159
+ {% endcall %}
159
160
  {% endblock %}
@@ -2,6 +2,9 @@
2
2
  to it goes. #}
3
3
  {% extends "layouts/settings-page.njk" %}
4
4
  {% import "components/fields.njk" as field %}
5
+ {% from "components/badge.njk" import status %}
6
+ {% from "components/button.njk" import button %}
7
+ {% from "components/card.njk" import card, cardActions %}
5
8
 
6
9
  {% block settingsProblems %}
7
10
  {{ field.problem('settings-mail-provider', problems.mailProvider) }}
@@ -50,7 +53,7 @@
50
53
  {{ field.text('settings-contact-email', fields.contactEmail, 'Contact address',
51
54
  form.contactEmail, error=problems.contactEmail, hint=contactHint, inputmode='email') }}
52
55
 
53
- <h2>Security contact</h2>
56
+ <h2 class="mt-6 text-lg font-semibold">Security contact</h2>
54
57
 
55
58
  {% set securityContactsHint %}
56
59
  Where somebody who finds a vulnerability in this site should report it, one per line:
@@ -76,19 +79,18 @@
76
79
  key is: an API key and an SMTP password belong in data/mail.json, not in
77
80
  the public site.json, and a mistyped one must not lose an edit to the
78
81
  From line. #}
79
- <section class="admin-settings admin-mail-panel">
80
- <h2>Mail credentials</h2>
82
+ {% call card(title='Mail credentials') %}
81
83
 
82
- <p class="admin-mail-state admin-mail-{{ mailProvider }}">
84
+ <p>
83
85
  {% if mailConfigured %}
84
- <strong>Configured</strong> &mdash; sending through {{ mailProvider }}.
86
+ {{ status('connected', 'Configured') }} Sending through {{ mailProvider }}.
85
87
  {% elif mailProvider == 'none' %}
86
- <strong>Not sending</strong> &mdash; no provider is chosen.
88
+ {{ status('disconnected', 'Not sending') }} No provider is chosen.
87
89
  {% else %}
88
- <strong>Not configured</strong> &mdash; {{ mailProvider }} is chosen but has no credential.
90
+ {{ status('missing', 'Not configured') }} {{ mailProvider }} is chosen but has no credential.
89
91
  {% endif %}
90
92
  </p>
91
- <p class="admin-hint">{{ mailHint }}</p>
93
+ <p class="text-sm">{{ mailHint }}</p>
92
94
 
93
95
  {# One provider's boxes, never both. A site sending through Brevo has no
94
96
  use for an SMTP port and a site sending through SMTP has none for an API
@@ -103,14 +105,14 @@
103
105
  would reasonably call this broken. Doing it the other way round would
104
106
  take JavaScript, and no other admin screen needs any. #}
105
107
  {% if mailProvider == 'none' %}
106
- <p class="admin-hint">
108
+ <p class="text-sm">
107
109
  No provider is <em>saved</em>, so there is no credential to give and no
108
110
  box here to give it in. Choose one under <strong>Provider</strong> above
109
111
  and press <strong>Save settings</strong> &mdash; picking it in the select
110
112
  alone changes nothing &mdash; and the boxes it needs appear here.
111
113
  </p>
112
114
  {% else %}
113
- <p class="admin-hint">
115
+ <p class="text-sm">
114
116
  These are the boxes
115
117
  <strong>{% if mailProvider == 'brevo' %}Brevo{% else %}SMTP{% endif %}</strong>
116
118
  needs, and they follow the provider this site has <em>saved</em> &mdash; not
@@ -122,9 +124,9 @@
122
124
  <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
123
125
 
124
126
  {% if mailProvider == 'brevo' %}
125
- <h3>Brevo</h3>
127
+ <h3 class="font-semibold">Brevo</h3>
126
128
  {% if mailBrevoPresent %}
127
- <p class="admin-hint">A key is stored, ending {{ mailBrevoKeyHint }}.</p>
129
+ <p class="text-sm">A key is stored, ending {{ mailBrevoKeyHint }}.</p>
128
130
  {% set brevoPlaceholder = 'Leave blank to keep the stored key' %}
129
131
  {% else %}
130
132
  {% set brevoPlaceholder = 'Paste your key' %}
@@ -132,7 +134,7 @@
132
134
  {{ field.password('settings-mail-brevo-key', mailFields.brevoApiKey, 'API key',
133
135
  autocomplete='off', spellcheck='false', placeholder=brevoPlaceholder) }}
134
136
  {% else %}
135
- <h3>SMTP</h3>
137
+ <h3 class="font-semibold">SMTP</h3>
136
138
  {{ field.text('settings-mail-smtp-host', mailFields.smtpHost, 'Host', mailSmtp.host,
137
139
  autocomplete='off') }}
138
140
 
@@ -159,9 +161,7 @@
159
161
  autocomplete='off', spellcheck='false', placeholder=smtpPlaceholder) }}
160
162
  {% endif %}
161
163
 
162
- <p class="admin-settings-buttons">
163
- <button type="submit">Save credentials</button>
164
- </p>
164
+ <div class="mt-2">{{ button('Save credentials', type='submit', color='primary') }}</div>
165
165
  </form>
166
166
  {% endif %}
167
167
 
@@ -173,22 +173,22 @@
173
173
  {% set brevoStoredElsewhere = mailBrevoPresent and mailProvider != 'brevo' %}
174
174
  {% set smtpStoredElsewhere = mailSmtpPresent and mailProvider != 'smtp' %}
175
175
  {% if brevoStoredElsewhere or smtpStoredElsewhere %}
176
- <h3>Also stored</h3>
176
+ <h3 class="font-semibold">Also stored</h3>
177
177
 
178
178
  {% if brevoStoredElsewhere %}
179
- <p class="admin-field-static">
179
+ <p>
180
180
  A <strong>Brevo</strong> API key, ending {{ mailBrevoKeyHint }}.
181
181
  </p>
182
182
  {% endif %}
183
183
  {% if smtpStoredElsewhere %}
184
- <p class="admin-field-static">
184
+ <p>
185
185
  An <strong>SMTP</strong> server:
186
186
  {% if mailSmtp.user %}{{ mailSmtp.user }} at {% endif %}{{ mailSmtp.host }}
187
187
  port {{ mailSmtp.port }}{% if mailSmtp.hasPassword %}, with a password{% endif %}.
188
188
  </p>
189
189
  {% endif %}
190
190
 
191
- <p class="admin-hint">
191
+ <p class="text-sm">
192
192
  Kept in <code>data/mail.json</code> and used by nothing while
193
193
  {% if mailProvider == 'none' %}no provider is chosen{% else %}{% if mailProvider == 'brevo' %}Brevo{% else %}SMTP{% endif %} is the chosen provider{% endif %}.
194
194
  Choose that provider above to edit it, or press
@@ -201,9 +201,9 @@
201
201
  <form method="post" action="{{ mailUrl }}">
202
202
  <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
203
203
  <input type="hidden" name="{{ mailFields.action }}" value="{{ mailRemove }}" />
204
- <p class="admin-settings-buttons">
205
- <button type="submit">Remove credentials</button>
206
- </p>
204
+ {% call cardActions() %}
205
+ {{ button('Remove credentials', type='submit', color='error', style='ghost') }}
206
+ {% endcall %}
207
207
  </form>
208
208
  {% endif %}
209
209
 
@@ -216,18 +216,16 @@
216
216
  <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
217
217
  {{ field.text('settings-mail-test-to', mailTestFields.to, 'Send a test message to', '',
218
218
  inputmode='email', placeholder='you@example.com') }}
219
- <p class="admin-settings-buttons">
220
- <button type="submit">Send test email</button>
221
- </p>
219
+ <div class="mt-2">{{ button('Send test email', type='submit') }}</div>
222
220
  </form>
223
221
  {% endif %}
224
222
 
225
- <p class="admin-hint">
223
+ <p class="text-sm">
226
224
  Neither the API key nor the SMTP password is ever printed back into this
227
225
  page. Both are kept in <code>data/mail.json</code> at mode
228
226
  <code>0600</code>, beside the password hashes and the actor's private
229
227
  keys, and never in <code>site.json</code>, which is public and in git.
230
228
  Leaving a secret blank keeps the one already stored.
231
229
  </p>
232
- </section>
230
+ {% endcall %}
233
231
  {% endblock %}
@@ -50,13 +50,19 @@
50
50
  {{ field.text('settings-icon', fields.icon, 'Site icon', form.icon,
51
51
  error=problems.icon, hint=iconHint) }}
52
52
 
53
- <div class="admin-avatar-current">
53
+ <div class="flex items-center gap-4">
54
54
  {% if iconPreview %}
55
- <img class="admin-icon-preview" src="{{ iconPreview }}" alt="The current site icon" width="96" height="96" />
55
+ <div class="avatar shrink-0">
56
+ <div class="w-24 rounded-box">
57
+ <img class="object-cover" src="{{ iconPreview }}" alt="The current site icon" width="96" height="96" />
58
+ </div>
59
+ </div>
56
60
  {% else %}
57
- <span class="admin-icon-preview admin-avatar-blank"></span>
61
+ <div class="avatar avatar-placeholder shrink-0" aria-hidden="true">
62
+ <div class="w-24 rounded-box bg-base-300"></div>
63
+ </div>
58
64
  {% endif %}
59
- <p class="admin-hint">
65
+ <p class="text-sm">
60
66
  {% if iconUnderived and not imageOptimization %}
61
67
  Image optimization is off, so no icons are derived and the site links none.
62
68
  {% elif iconUnderived %}
@@ -2,6 +2,8 @@
2
2
  used to. #}
3
3
  {% extends "layouts/settings-page.njk" %}
4
4
  {% import "components/fields.njk" as field %}
5
+ {% from "components/card.njk" import card %}
6
+ {% from "components/table.njk" import table %}
5
7
 
6
8
  {% block settingsProblems %}
7
9
  {{ field.problem('settings-tag-base', problems.tagBase) }}
@@ -22,12 +24,9 @@
22
24
  {# The recorded renames are a record of what happened rather than a
23
25
  preference: the taxonomy screens write them as a term is renamed or merged,
24
26
  and this page is where the URLs they are about are decided. #}
25
- <section class="admin-settings admin-redirects-panel">
26
- <h2>Archive redirects</h2>
27
-
27
+ {% call card(title='Archive redirects') %}
28
28
  {% if taxonomyRedirects.length %}
29
- <table class="admin-list">
30
- <caption class="admin-visually-hidden">Archive redirects</caption>
29
+ {% call table('Archive redirects', inCard=true) %}
31
30
  <thead>
32
31
  <tr><th scope="col">Taxonomy</th><th scope="col">Was</th><th scope="col">Is now</th></tr>
33
32
  </thead>
@@ -40,17 +39,17 @@
40
39
  </tr>
41
40
  {% endfor %}
42
41
  </tbody>
43
- </table>
44
- <p class="admin-hint">
42
+ {% endcall %}
43
+ <p class="text-sm">
45
44
  Every one of these answers with a permanent redirect to the archive the
46
45
  term lives at now, so a link somebody published still arrives.
47
46
  </p>
48
47
  {% else %}
49
- <p class="admin-hint">
48
+ <p class="text-sm">
50
49
  Nothing has moved yet. Renaming or merging a term on the Tags or
51
50
  Categories screen records the archive URL it used to have here, and the
52
51
  site answers that URL with a permanent redirect from then on.
53
52
  </p>
54
53
  {% endif %}
55
- </section>
54
+ {% endcall %}
56
55
  {% endblock %}
@@ -6,7 +6,7 @@
6
6
  {% endblock %}
7
7
 
8
8
  {% block settingsFields %}
9
- <h2>Location</h2>
9
+ <h2 class="mt-2 text-lg font-semibold">Location</h2>
10
10
 
11
11
  {% set locationHint %}
12
12
  A Micropub app such as Quill can send where a post was written, and the
@@ -26,9 +26,9 @@
26
26
  {% endfor %}
27
27
  {% endcall %}
28
28
 
29
- <h2>Uploads</h2>
29
+ <h2 class="mt-6 text-lg font-semibold">Uploads</h2>
30
30
 
31
- <p class="admin-hint">
31
+ <p class="max-w-prose text-sm">
32
32
  Location and camera metadata is always removed from pictures and videos as
33
33
  they are uploaded, whatever is chosen above. Files uploaded before this
34
34
  version keep theirs until <code>geekity strip-metadata</code> is run over
@@ -24,7 +24,7 @@
24
24
  radio and a select that can disagree with each other. #}
25
25
  {% set homepageGone %}
26
26
  {% if homepageMissing %}
27
- <p class="admin-field-error">
27
+ <p>
28
28
  The homepage was <code>{{ form.homepage }}</code>, which is no longer published,
29
29
  so the site is showing its latest posts. Publish that page again to have it back,
30
30
  or pick another.
@@ -45,7 +45,7 @@
45
45
 
46
46
  {% set postsPageGone %}
47
47
  {% if postsPageMissing %}
48
- <p class="admin-field-error">
48
+ <p>
49
49
  The posts page was <code>{{ form.postsPage }}</code>, which is no longer published,
50
50
  so the listing has no page of its own.
51
51
  </p>
@@ -100,7 +100,7 @@
100
100
 
101
101
  {# TASK-148. What /robots.txt tells crawlers beyond keeping them out of the
102
102
  admin, which it does whatever is set here. #}
103
- <h2>Crawlers</h2>
103
+ <h2 class="mt-6 text-lg font-semibold">Crawlers</h2>
104
104
 
105
105
  {% set aiHint %}
106
106
  Training crawlers gather pages to train AI models. Retrieval crawlers fetch a