@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
@@ -4,14 +4,18 @@
4
4
  so a screen reader moving between buttons hears which one it is on. #}
5
5
  {% extends "layouts/shell.njk" %}
6
6
  {% import "components/fields.njk" as field %}
7
+ {% from "components/alert.njk" import alert %}
8
+ {% from "components/badge.njk" import badge %}
9
+ {% from "components/button.njk" import button %}
10
+ {% from "components/table.njk" import table %}
7
11
 
8
12
  {% block title %}Connected apps{% endblock %}
9
13
 
10
14
  {% block content %}
11
- <h1>Connected apps</h1>
15
+ <h1 class="mb-6 text-2xl font-bold">Connected apps</h1>
12
16
 
13
17
  {% if connections | length %}
14
- <p class="admin-hint">
18
+ <p class="mb-4 max-w-prose text-sm">
15
19
  These apps signed in as you and can act as you, within the permissions
16
20
  listed. An app that keeps using its connection stays connected; one left
17
21
  unused is disconnected on the date shown. A token you created works
@@ -19,8 +23,7 @@
19
23
  refused, and it has to ask you again.
20
24
  </p>
21
25
 
22
- <table class="admin-list">
23
- <caption class="admin-visually-hidden">Connected apps</caption>
26
+ {% call table('Connected apps') %}
24
27
  <thead>
25
28
  <tr>
26
29
  <th scope="col">App</th>
@@ -36,9 +39,9 @@
36
39
  <tr>
37
40
  <td>
38
41
  {% if connection.url %}
39
- <a href="{{ connection.url }}">{{ connection.name }}</a>
42
+ <a class="link link-hover font-medium" href="{{ connection.url }}">{{ connection.name }}</a>
40
43
  {% else %}
41
- {{ connection.name }} <span class="admin-status">Token you created</span>
44
+ <span class="font-medium">{{ connection.name }}</span> {{ badge('Token you created') }}
42
45
  {% endif %}
43
46
  </td>
44
47
  <td>
@@ -50,29 +53,29 @@
50
53
  {% endfor %}
51
54
  </ul>
52
55
  </td>
53
- <td><time datetime="{{ connection.issued.iso }}">{{ connection.issued.text }}</time></td>
54
- <td>
56
+ <td class="whitespace-nowrap"><time datetime="{{ connection.issued.iso }}">{{ connection.issued.text }}</time></td>
57
+ <td class="whitespace-nowrap">
55
58
  {% if connection.lastUsed %}
56
59
  <time datetime="{{ connection.lastUsed.iso }}">{{ connection.lastUsed.text }}</time>
57
60
  {% else %}
58
- <span class="admin-status">Not yet</span>
61
+ Not yet
59
62
  {% endif %}
60
63
  </td>
61
- <td><time datetime="{{ connection.expires.iso }}">{{ connection.expires.text }}</time></td>
62
- <td class="admin-actions">
64
+ <td class="whitespace-nowrap"><time datetime="{{ connection.expires.iso }}">{{ connection.expires.text }}</time></td>
65
+ <td>
63
66
  <form method="post" action="{{ revokeUrl }}">
64
67
  <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
65
68
  <input type="hidden" name="{{ connectionField }}" value="{{ connection.id }}" />
66
- <button type="submit">Revoke {{ connection.name }}</button>
69
+ {{ button('Revoke ' + connection.name, type='submit', style='ghost', size='xs') }}
67
70
  </form>
68
71
  </td>
69
72
  </tr>
70
73
  {% endfor %}
71
74
  </tbody>
72
- </table>
75
+ {% endcall %}
73
76
  {% else %}
74
77
  <p>No apps are connected to your account.</p>
75
- <p class="admin-hint">
78
+ <p class="mt-2 max-w-prose text-sm">
76
79
  An app connects here when you sign in to it with your site's address and
77
80
  approve what it asks for: a Micropub app such as Quill that posts to
78
81
  your site, or an MCP client such as an AI assistant. Each one you approve
@@ -80,101 +83,110 @@
80
83
  </p>
81
84
  {% endif %}
82
85
 
83
- <h2 id="create-token">Create a token</h2>
86
+ <section class="mt-10 max-w-2xl">
87
+ <h2 id="create-token" class="mb-3 text-lg font-semibold">Create a token</h2>
84
88
 
85
- {% if createToken.created %}
86
- <div class="admin-flash" role="status">
87
- {{ field.text('created-token', 'created-token', 'Token for ' ~ createToken.created.name, createToken.created.token,
88
- readonly=true, spellcheck='false',
89
- hint='Copy it now. It will not be shown again: the site keeps only a fingerprint of it.') }}
90
- </div>
91
- {% endif %}
89
+ {% if createToken.created %}
90
+ <div class="mb-4">
91
+ {% call alert(color='success', style='soft', polite=true) %}
92
+ <div class="w-full">
93
+ {{ field.text('created-token', 'created-token', 'Token for ' ~ createToken.created.name, createToken.created.token,
94
+ readonly=true, spellcheck='false',
95
+ hint='Copy it now. It will not be shown again: the site keeps only a fingerprint of it.') }}
96
+ </div>
97
+ {% endcall %}
98
+ </div>
99
+ {% endif %}
92
100
 
93
- {% if createToken.problems.name or createToken.problems.scopes or createToken.problems.expires %}
94
- {% call field.summary('Nothing was saved. Fix the fields marked below and try again.') %}
95
- {{ field.problem('create-token-name', createToken.problems.name) }}
96
- {{ field.problem('create-token-scope-profile', createToken.problems.scopes) }}
97
- {{ field.problem('create-token-expires', createToken.problems.expires) }}
98
- {% endcall %}
99
- {% endif %}
101
+ {% if createToken.problems.name or createToken.problems.scopes or createToken.problems.expires %}
102
+ {% call field.summary('Nothing was saved. Fix the fields marked below and try again.') %}
103
+ {{ field.problem('create-token-name', createToken.problems.name) }}
104
+ {{ field.problem('create-token-scope-profile', createToken.problems.scopes) }}
105
+ {{ field.problem('create-token-expires', createToken.problems.expires) }}
106
+ {% endcall %}
107
+ {% endif %}
100
108
 
101
- <p class="admin-hint">
102
- Some tools and scripts cannot sign in, but accept a token you paste in,
103
- such as the Manual tab of micropub.rocks. A token acts as you on this
104
- site's Micropub endpoint, with only the permissions you tick, until it
105
- expires or you revoke it above.
106
- </p>
109
+ <p class="mb-2 max-w-prose text-sm">
110
+ Some tools and scripts cannot sign in, but accept a token you paste in,
111
+ such as the Manual tab of micropub.rocks. A token acts as you on this
112
+ site's Micropub endpoint, with only the permissions you tick, until it
113
+ expires or you revoke it above.
114
+ </p>
107
115
 
108
- <form method="post" action="{{ createToken.url }}">
109
- <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
110
- {{ field.text('create-token-name', 'name', 'Name', createToken.name,
111
- error=createToken.problems.name, required=true,
112
- hint='What will use it, such as Publishing script.') }}
113
- <fieldset>
114
- <legend>It may</legend>
115
- {% for scope in createToken.scopes %}
116
- {{ field.checkbox('create-token-scope-' ~ scope.name, 'scope', scope.label, checked=scope.checked, value=scope.name,
117
- error=(createToken.problems.scopes if loop.first else '')) }}
118
- {% endfor %}
119
- </fieldset>
120
- {% call field.select('create-token-expires', 'expires', 'Expires after', error=createToken.problems.expires) %}
121
- {% for expiry in createToken.expiries %}
122
- <option value="{{ expiry.days }}"{% if expiry.selected %} selected{% endif %}>{% if expiry.days == 365 %}1 year{% else %}{{ expiry.days }} days{% endif %}</option>
123
- {% endfor %}
124
- {% endcall %}
125
- <button type="submit">Create token</button>
126
- </form>
116
+ <form method="post" action="{{ createToken.url }}">
117
+ <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
118
+ {{ field.text('create-token-name', 'name', 'Name', createToken.name,
119
+ error=createToken.problems.name, required=true,
120
+ hint='What will use it, such as Publishing script.') }}
121
+ <fieldset class="fieldset">
122
+ <legend class="fieldset-legend">It may</legend>
123
+ {% for scope in createToken.scopes %}
124
+ {{ field.checkbox('create-token-scope-' ~ scope.name, 'scope', scope.label, checked=scope.checked, value=scope.name,
125
+ error=(createToken.problems.scopes if loop.first else '')) }}
126
+ {% endfor %}
127
+ </fieldset>
128
+ {% call field.select('create-token-expires', 'expires', 'Expires after', error=createToken.problems.expires) %}
129
+ {% for expiry in createToken.expiries %}
130
+ <option value="{{ expiry.days }}"{% if expiry.selected %} selected{% endif %}>{% if expiry.days == 365 %}1 year{% else %}{{ expiry.days }} days{% endif %}</option>
131
+ {% endfor %}
132
+ {% endcall %}
133
+ <div class="mt-4">{{ button('Create token', type='submit', color='primary') }}</div>
134
+ </form>
135
+ </section>
127
136
 
128
- <h2 id="without-pkce">Apps allowed without PKCE</h2>
137
+ <section class="mt-10">
138
+ <h2 id="without-pkce" class="mb-3 text-lg font-semibold">Apps allowed without PKCE</h2>
129
139
 
130
- {% if withoutPkce.problem %}
131
- {% call field.summary('Nothing was saved. Fix the field marked below and try again.') %}
132
- {{ field.problem('without-pkce-client', withoutPkce.problem) }}
133
- {% endcall %}
134
- {% endif %}
140
+ {% if withoutPkce.problem %}
141
+ <div class="max-w-2xl">
142
+ {% call field.summary('Nothing was saved. Fix the field marked below and try again.') %}
143
+ {{ field.problem('without-pkce-client', withoutPkce.problem) }}
144
+ {% endcall %}
145
+ </div>
146
+ {% endif %}
135
147
 
136
- <p class="admin-hint">
137
- PKCE protects a sign-in when somebody intercepts the one-time code an app
138
- is handed: without the secret the app kept, the code is useless. Every app
139
- must use it, unless you list it here. List only an app you trust that
140
- cannot sign in otherwise, such as iA Writer. A listed app is let in without
141
- PKCE only when it sends you back to an https address on its own host, and
142
- the consent screen tells you when it is.
143
- </p>
148
+ <p class="mb-4 max-w-prose text-sm">
149
+ PKCE protects a sign-in when somebody intercepts the one-time code an app
150
+ is handed: without the secret the app kept, the code is useless. Every app
151
+ must use it, unless you list it here. List only an app you trust that
152
+ cannot sign in otherwise, such as iA Writer. A listed app is let in without
153
+ PKCE only when it sends you back to an https address on its own host, and
154
+ the consent screen tells you when it is.
155
+ </p>
144
156
 
145
- {% if withoutPkce.clients | length %}
146
- <table class="admin-list">
147
- <caption class="admin-visually-hidden">Apps allowed without PKCE</caption>
148
- <thead>
149
- <tr>
150
- <th scope="col">client_id</th>
151
- <th scope="col">Actions</th>
152
- </tr>
153
- </thead>
154
- <tbody>
155
- {% for client in withoutPkce.clients %}
157
+ {% if withoutPkce.clients | length %}
158
+ {% call table('Apps allowed without PKCE') %}
159
+ <thead>
156
160
  <tr>
157
- <td><code>{{ client }}</code></td>
158
- <td class="admin-actions">
159
- <form method="post" action="{{ withoutPkce.removeUrl }}">
160
- <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
161
- <input type="hidden" name="{{ withoutPkce.field }}" value="{{ client }}" />
162
- <button type="submit">Remove {{ client }}</button>
163
- </form>
164
- </td>
161
+ <th scope="col">client_id</th>
162
+ <th scope="col">Actions</th>
165
163
  </tr>
166
- {% endfor %}
167
- </tbody>
168
- </table>
169
- {% else %}
170
- <p>No apps are listed. Every app must use PKCE to sign in.</p>
171
- {% endif %}
164
+ </thead>
165
+ <tbody>
166
+ {% for client in withoutPkce.clients %}
167
+ <tr>
168
+ <td><code>{{ client }}</code></td>
169
+ <td>
170
+ <form method="post" action="{{ withoutPkce.removeUrl }}">
171
+ <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
172
+ <input type="hidden" name="{{ withoutPkce.field }}" value="{{ client }}" />
173
+ {{ button('Remove ' + client, type='submit', style='ghost', size='xs') }}
174
+ </form>
175
+ </td>
176
+ </tr>
177
+ {% endfor %}
178
+ </tbody>
179
+ {% endcall %}
180
+ {% else %}
181
+ <p>No apps are listed. Every app must use PKCE to sign in.</p>
182
+ {% endif %}
172
183
 
173
- <form method="post" action="{{ withoutPkce.addUrl }}">
174
- <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
175
- {{ field.text('without-pkce-client', withoutPkce.field, 'App&rsquo;s client_id', withoutPkce.value,
176
- error=withoutPkce.problem, inputmode='url', spellcheck='false', required=true,
177
- hint='The URL the app names itself by when it signs in, as App activity shows it, such as <code>https://ia.net/writer</code>.') }}
178
- <button type="submit">Allow without PKCE</button>
179
- </form>
184
+ <form class="mt-4 max-w-2xl" method="post" action="{{ withoutPkce.addUrl }}">
185
+ <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
186
+ {{ field.text('without-pkce-client', withoutPkce.field, 'App&rsquo;s client_id', withoutPkce.value,
187
+ error=withoutPkce.problem, inputmode='url', spellcheck='false', required=true,
188
+ hint='The URL the app names itself by when it signs in, as App activity shows it, such as <code>https://ia.net/writer</code>.') }}
189
+ <div class="mt-4">{{ button('Allow without PKCE', type='submit', color='primary') }}</div>
190
+ </form>
191
+ </section>
180
192
  {% endblock %}
@@ -12,25 +12,29 @@
12
12
  lands back here, so a Save always shows its own result. #}
13
13
  {% extends "layouts/shell.njk" %}
14
14
  {% import "components/fields.njk" as field %}
15
+ {% from "components/badge.njk" import badge, status %}
16
+ {% from "components/button.njk" import button %}
17
+ {% from "components/card.njk" import card, cardActions %}
15
18
 
16
19
  {% block title %}Edit {{ account.username }}{% endblock %}
17
20
 
18
21
  {% block content %}
19
- <h1>Edit {{ account.username }}{% if account.you %} <span class="admin-status">(you)</span>{% endif %}</h1>
22
+ <h1 class="mb-2 flex flex-wrap items-center gap-2 text-2xl font-bold">Edit {{ account.username }}{% if account.you %} {{ badge('(you)', style='outline', size='md') }}{% endif %}</h1>
20
23
 
21
- <p class="admin-hint"><a href="{{ usersUrl }}">Back to all users</a></p>
24
+ <p class="mb-6 text-sm"><a class="link" href="{{ usersUrl }}">Back to all users</a></p>
22
25
 
23
- <form class="admin-settings" method="post" action="{{ userEmailUrl }}">
26
+ <div class="flex max-w-2xl flex-col gap-6">
27
+
28
+ {% call card(title='Account') %}
29
+ <form method="post" action="{{ userEmailUrl }}">
24
30
  <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
25
31
  <input type="hidden" name="{{ fields.userId }}" value="{{ account.id }}" />
26
32
 
27
- <h2>Account</h2>
28
-
29
- <p class="admin-field-static">
33
+ <p>
30
34
  Signs in as <strong>{{ account.username }}</strong>, and writes at
31
- <a href="{{ account.archiveUrl }}">{{ account.archiveUrl }}</a>.
35
+ <a class="link break-all" href="{{ account.archiveUrl }}">{{ account.archiveUrl }}</a>.
32
36
  </p>
33
- <p class="admin-hint">
37
+ <p class="text-sm">
34
38
  A username is not editable: it is the login, the author URL above, and
35
39
  the actor a follower holds. Somebody who needs a different one is a new
36
40
  account.
@@ -39,7 +43,7 @@
39
43
  purpose: it is what the fediverse already holds, and changing it would
40
44
  orphan every follow. #}
41
45
  {% if account.storedActorId %}
42
- <p class="admin-hint">
46
+ <p class="text-sm">
43
47
  Also <code>{{ account.storedActorId }}</code> — what the fediverse knows
44
48
  this user by. Set by the import or by hand in
45
49
  <code>data/users.json</code>, never here.
@@ -50,16 +54,15 @@
50
54
  {% if mailConfigured %}
51
55
  Optional, and never shown on the public site. It is what a forgotten password is recovered through and where the notices below go. Empty it to stop the site writing to this person at all.
52
56
  {% else %}
53
- Optional, and this site sends no mail yet, so nothing will be sent to one. Choose a provider on <a href="{{ adminUrl }}/settings/email">Settings &rsaquo; Email</a> to turn password recovery on.
57
+ Optional, and this site sends no mail yet, so nothing will be sent to one. Choose a provider on <a class="link" href="{{ adminUrl }}/settings/email">Settings &rsaquo; Email</a> to turn password recovery on.
54
58
  {% endif %}
55
59
  {% endset %}
56
60
  {{ field.text('user-email', fields.email, 'Email address', account.email,
57
61
  type='email', autocomplete='email', hint=emailHint) }}
58
62
 
59
- <p class="admin-settings-buttons">
60
- <button type="submit">Save email address</button>
61
- </p>
63
+ <div class="mt-2">{{ button('Save email address', type='submit', color='primary') }}</div>
62
64
  </form>
65
+ {% endcall %}
63
66
 
64
67
  {# The public face of this person (TASK-67). Anybody's, not only your own:
65
68
  there is one role, so an admin who has just added a colleague can put a
@@ -67,13 +70,12 @@
67
70
  sign in. After decision-14 these same fields are what their ActivityPub
68
71
  actor carries, which is why they live beside the account rather than in
69
72
  the site settings — and why saving them tells their followers. #}
70
- <form class="admin-settings" method="post" action="{{ userProfileUrl }}">
73
+ {% call card(title='Profile') %}
74
+ <form method="post" action="{{ userProfileUrl }}">
71
75
  <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
72
76
  <input type="hidden" name="{{ fields.userId }}" value="{{ account.id }}" />
73
77
 
74
- <h2>Profile</h2>
75
-
76
- <p class="admin-hint">
78
+ <p class="text-sm">
77
79
  A profile is public. Nothing in it is required — a user with no profile
78
80
  still has an archive under their username — and a box left empty is a
79
81
  field they do not have.
@@ -116,7 +118,7 @@
116
118
  <code>Mastodon | https://example.social/@me | me</code>,
117
119
  <code>Their post | https://example.com/post | me nofollow author</code>.
118
120
  It is the same line the menu boxes on
119
- <a href="{{ adminUrl }}/navigation">Navigation</a> take.
121
+ <a class="link" href="{{ adminUrl }}/navigation">Navigation</a> take.
120
122
  Every link here is published with <code>rel="me"</code> whether or not
121
123
  you type it: it is how Mastodon verifies a profile field that points back
122
124
  at this site, and how IndieAuth knows these are yours. Nothing else in
@@ -125,10 +127,9 @@
125
127
  {{ field.textarea('profile-links', fields.links, 'Links', account.profile.links, rows='4',
126
128
  hint=linksHint, error=profileProblems.links) }}
127
129
 
128
- <p class="admin-settings-buttons">
129
- <button type="submit">Save profile</button>
130
- </p>
130
+ <div class="mt-2">{{ button('Save profile', type='submit', color='primary') }}</div>
131
131
  </form>
132
+ {% endcall %}
132
133
 
133
134
  {# Only once there is both somewhere to write to and something to write
134
135
  with. These switches decide what is sent to the address above, so without
@@ -137,10 +138,9 @@
137
138
  lines below say which is missing where the panel would have been, rather
138
139
  than letting it vanish without explanation. #}
139
140
  {% if account.email and mailConfigured %}
140
- <section class="admin-settings">
141
- <h2>Email {{ account.username }} about</h2>
141
+ {% call card(title='Email ' + account.username + ' about') %}
142
142
 
143
- <p class="admin-hint">
143
+ <p class="text-sm">
144
144
  What this site writes to {{ account.username }} at <code>{{ account.email }}</code>.
145
145
  </p>
146
146
 
@@ -149,17 +149,22 @@
149
149
  this off" from "left it alone". Adding a notice is one entry in the
150
150
  registry and no change here. #}
151
151
  {% for notice in account.notifications %}
152
- <div class="admin-notice">
153
- <h3>{{ notice.label }}</h3>
154
- <p class="admin-hint">{{ notice.description }}</p>
152
+ <div class="flex flex-col gap-2 border-t border-base-300 pt-4">
153
+ <h3 class="font-semibold">{{ notice.label }}</h3>
154
+ <p class="text-sm">{{ notice.description }}</p>
155
155
 
156
- <form class="admin-inline" method="post" action="{{ userNotificationsUrl }}">
156
+ <form class="flex flex-wrap items-center gap-2" method="post" action="{{ userNotificationsUrl }}">
157
157
  <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
158
158
  <input type="hidden" name="{{ fields.userId }}" value="{{ account.id }}" />
159
159
  <input type="hidden" name="{{ fields.event }}" value="{{ notice.name }}" />
160
160
  <input type="hidden" name="{{ fields.on }}" value="{% if notice.on %}{% else %}1{% endif %}" />
161
- <span class="admin-status">{% if notice.on %}On{% else %}Off{% endif %}</span>
162
- <button type="submit">{% if notice.on %}Turn off{% else %}Turn on{% endif %}</button>
161
+ {% if notice.on %}
162
+ {{ status('active', 'On') }}
163
+ {{ button('Turn off', type='submit', style='outline', size='xs') }}
164
+ {% else %}
165
+ {{ status('hidden', 'Off') }}
166
+ {{ button('Turn on', type='submit', style='outline', size='xs') }}
167
+ {% endif %}
163
168
  </form>
164
169
 
165
170
  {# And, for a notice the registry says can be batched, how often it
@@ -167,7 +172,7 @@
167
172
  it and the switch above is a button; the registry decides which
168
173
  notices get one. #}
169
174
  {% if notice.batched %}
170
- <form class="admin-inline" method="post" action="{{ userNotificationModeUrl }}">
175
+ <form class="flex flex-wrap items-end gap-2" method="post" action="{{ userNotificationModeUrl }}">
171
176
  <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
172
177
  <input type="hidden" name="{{ fields.userId }}" value="{{ account.id }}" />
173
178
  <input type="hidden" name="{{ fields.event }}" value="{{ notice.name }}" />
@@ -176,9 +181,9 @@
176
181
  <option value="{{ choice.value }}"{% if choice.chosen %} selected{% endif %}>{{ choice.label }}</option>
177
182
  {% endfor %}
178
183
  {% endcall %}
179
- <button type="submit">Save</button>
184
+ <div class="pb-1">{{ button('Save', type='submit') }}</div>
180
185
  </form>
181
- <p class="admin-hint">
186
+ <p class="text-sm">
182
187
  On an hourly or a daily digest nothing is sent as they arrive;
183
188
  instead one message a window lists everything still waiting, and
184
189
  no message goes at all when nothing is.
@@ -186,25 +191,24 @@
186
191
  {% endif %}
187
192
  </div>
188
193
  {% endfor %}
189
- </section>
194
+ {% endcall %}
190
195
  {% else %}
191
- <section class="admin-settings">
192
- <h2>Email {{ account.username }} about</h2>
196
+ {% call card(title='Email ' + account.username + ' about') %}
193
197
 
194
198
  {% if not mailConfigured %}
195
- <p class="admin-hint">
199
+ <p class="text-sm">
196
200
  Nothing, while this site has no mail provider: there is nothing to send
197
- with. Choose one on <a href="{{ adminUrl }}/settings/email">Settings
201
+ with. Choose one on <a class="link" href="{{ adminUrl }}/settings/email">Settings
198
202
  &rsaquo; Email</a> and what to send comes back here.
199
203
  </p>
200
204
  {% else %}
201
- <p class="admin-hint">
205
+ <p class="text-sm">
202
206
  Nothing, while the box above is empty: there is nowhere to write to.
203
207
  Give {% if account.you %}yourself{% else %}this account{% endif %} an
204
208
  address and what to send comes back here.
205
209
  </p>
206
210
  {% endif %}
207
- </section>
211
+ {% endcall %}
208
212
  {% endif %}
209
213
 
210
214
  {# Only on your own page: it asks for the current password, so it is about
@@ -214,20 +218,22 @@
214
218
  {# Only on your own page, as the screen it links to only ever lists your
215
219
  own connections (TASK-162). #}
216
220
  {% if account.you %}
217
- <section class="admin-settings">
218
- <h2>Connected apps</h2>
219
- <p class="admin-hint">
221
+ {% call card(title='Connected apps') %}
222
+ <p class="text-sm">
220
223
  Apps you signed in to with your site's address can act as you.
221
- <a href="{{ connectedAppsUrl }}">Review or revoke them</a>.
224
+ <a class="link" href="{{ connectedAppsUrl }}">Review or revoke them</a>.
222
225
  </p>
223
- </section>
226
+ {% endcall %}
224
227
  {% endif %}
225
228
 
226
229
  {% if account.you %}
227
- <form class="admin-settings" id="change-password" method="post" action="{{ changePasswordUrl }}">
228
- <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
230
+ {% include "components/theme-choice.njk" %}
231
+ {% endif %}
229
232
 
230
- <h2>Change your password</h2>
233
+ {% if account.you %}
234
+ {% call card(title='Change your password') %}
235
+ <form id="change-password" method="post" action="{{ changePasswordUrl }}">
236
+ <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
231
237
 
232
238
  {% if passwordProblems.currentPassword or passwordProblems.newPassword %}
233
239
  {% call field.summary('Your password was not changed. Fix the fields marked below and try again.') %}
@@ -246,31 +252,32 @@
246
252
  {{ field.password('new-password-confirmation', fields.newPasswordConfirmation,
247
253
  'New password again', autocomplete='new-password', required=true) }}
248
254
 
249
- <p class="admin-hint">Every other browser signed in as you is signed out. This one stays.</p>
255
+ <p class="text-sm">Every other browser signed in as you is signed out. This one stays.</p>
250
256
 
251
- <p class="admin-settings-buttons">
252
- <button type="submit">Change password</button>
253
- </p>
257
+ <div class="mt-2">{{ button('Change password', type='submit', color='primary') }}</div>
254
258
  </form>
259
+ {% endcall %}
255
260
  {% endif %}
256
261
 
257
- <section class="admin-settings">
258
- <h2>Delete</h2>
262
+ {% call card(title='Delete') %}
259
263
 
260
264
  {# Either the button or the reason there is none, never both: the screen
261
265
  does not offer an action it is about to refuse. #}
262
266
  {% if deleteRefusal %}
263
- <p class="admin-hint">{{ deleteRefusal }}</p>
267
+ <p class="text-sm">{{ deleteRefusal }}</p>
264
268
  {% else %}
265
- <p class="admin-hint">
269
+ <p class="text-sm">
266
270
  Deleting {{ account.username }} signs out every browser they are in and
267
271
  cannot be undone. What they have written stays where it is.
268
272
  </p>
269
273
  <form method="post" action="{{ deleteUserUrl }}">
270
274
  <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
271
275
  <input type="hidden" name="{{ fields.userId }}" value="{{ account.id }}" />
272
- <button type="submit">Delete {{ account.username }}</button>
276
+ {% call cardActions() %}
277
+ {{ button('Delete ' + account.username, type='submit', color='error') }}
278
+ {% endcall %}
273
279
  </form>
274
280
  {% endif %}
275
- </section>
281
+ {% endcall %}
282
+ </div>
276
283
  {% endblock %}
@@ -3,17 +3,21 @@
3
3
  A listing and nothing else (TASK-97). Every field a user has is edited on
4
4
  that user's own screen, which the Edit link in each row goes to, so no box
5
5
  in this table can be mistaken for another one. Adding somebody is its own
6
- screen next door, `new.njk`, because Users > Add new is its own menu entry.
7
- #}
6
+ screen next door, `new.njk`, because Users > Add new is its own menu entry. #}
8
7
  {% extends "layouts/shell.njk" %}
8
+ {% from "components/badge.njk" import badge %}
9
+ {% from "components/button.njk" import button, buttonLink %}
10
+ {% from "components/table.njk" import table %}
9
11
 
10
12
  {% block title %}Users{% endblock %}
11
13
 
12
14
  {% block content %}
13
- <h1>Users <a class="admin-button admin-button-small" href="{{ addUserUrl }}">Add new</a></h1>
15
+ <div class="mb-6 flex flex-wrap items-center gap-4">
16
+ <h1 class="text-2xl font-bold">Users</h1>
17
+ {{ buttonLink('Add new', addUserUrl, color='primary') }}
18
+ </div>
14
19
 
15
- <table class="admin-list">
16
- <caption class="admin-visually-hidden">Users</caption>
20
+ {% call table('Users') %}
17
21
  <thead>
18
22
  <tr>
19
23
  <th scope="col">Username</th>
@@ -25,28 +29,26 @@
25
29
  <tbody>
26
30
  {% for entry in users %}
27
31
  <tr>
28
- <td>
29
- <a href="{{ entry.editUrl }}">{{ entry.username }}</a>{% if entry.you %} <span class="admin-status">(you)</span>{% endif %}
30
- </td>
31
- <td>{% if entry.email %}{{ entry.email }}{% else %}<span class="admin-status">None</span>{% endif %}</td>
32
- <td>{{ entry.createdAt | date('html') }}</td>
32
+ <td><a class="link link-hover font-medium" href="{{ entry.editUrl }}">{{ entry.username }}</a>{% if entry.you %} {{ badge('You') }}{% endif %}</td>
33
+ <td>{{ entry.email or 'None' }}</td>
34
+ <td class="whitespace-nowrap">{{ entry.createdAt | date('html') }}</td>
33
35
  {# Edit, then what can be done, then View: the order and the shape
34
- `pages/documents/list.njk` uses, down to Edit being a plain link
35
- rather than a button. A row here is a row there. #}
36
- <td class="admin-actions">
37
- <a href="{{ entry.editUrl }}">Edit</a>
38
- {% if entry.deletable %}
39
- <form method="post" action="{{ deleteUserUrl }}">
40
- <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
41
- <input type="hidden" name="{{ fields.userId }}" value="{{ entry.id }}" />
42
- <button type="submit">Delete</button>
43
- </form>
44
- {% endif %}
45
- <a href="{{ entry.archiveUrl }}">View</a>
36
+ `pages/documents/list.njk` uses. A row here is a row there. #}
37
+ <td>
38
+ <div class="flex items-center gap-1 whitespace-nowrap">
39
+ {{ buttonLink('Edit', entry.editUrl, style='ghost', size='xs') }}
40
+ {% if entry.deletable %}
41
+ <form method="post" action="{{ deleteUserUrl }}">
42
+ <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
43
+ <input type="hidden" name="{{ fields.userId }}" value="{{ entry.id }}" />
44
+ {{ button('Delete', type='submit', style='ghost', size='xs') }}
45
+ </form>
46
+ {% endif %}
47
+ {{ buttonLink('View', entry.archiveUrl, style='ghost', size='xs') }}
48
+ </div>
46
49
  </td>
47
50
  </tr>
48
51
  {% endfor %}
49
52
  </tbody>
50
- </table>
51
-
53
+ {% endcall %}
52
54
  {% endblock %}