@geekity/cms 0.20.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 (183) 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/comments.js +3 -1
  77. package/dist/admin/comments.js.map +1 -1
  78. package/dist/admin/documents.d.ts.map +1 -1
  79. package/dist/admin/documents.js +16 -16
  80. package/dist/admin/documents.js.map +1 -1
  81. package/dist/admin/federation.d.ts +3 -1
  82. package/dist/admin/federation.d.ts.map +1 -1
  83. package/dist/admin/federation.js +6 -4
  84. package/dist/admin/federation.js.map +1 -1
  85. package/dist/admin/headers.js +1 -1
  86. package/dist/admin/headers.js.map +1 -1
  87. package/dist/admin/preview.d.ts.map +1 -1
  88. package/dist/admin/preview.js +1 -1
  89. package/dist/admin/preview.js.map +1 -1
  90. package/dist/admin/routes.d.ts.map +1 -1
  91. package/dist/admin/routes.js +18 -9
  92. package/dist/admin/routes.js.map +1 -1
  93. package/dist/admin/templates.d.ts +6 -3
  94. package/dist/admin/templates.d.ts.map +1 -1
  95. package/dist/admin/templates.js +25 -3
  96. package/dist/admin/templates.js.map +1 -1
  97. package/dist/admin/users.d.ts +4 -0
  98. package/dist/admin/users.d.ts.map +1 -1
  99. package/dist/admin/users.js +37 -1
  100. package/dist/admin/users.js.map +1 -1
  101. package/dist/comments/routes.d.ts.map +1 -1
  102. package/dist/comments/routes.js +1 -0
  103. package/dist/comments/routes.js.map +1 -1
  104. package/dist/comments/submission.d.ts +2 -0
  105. package/dist/comments/submission.d.ts.map +1 -1
  106. package/dist/comments/submission.js +1 -1
  107. package/dist/comments/submission.js.map +1 -1
  108. package/dist/content/citation.d.ts +13 -0
  109. package/dist/content/citation.d.ts.map +1 -1
  110. package/dist/content/citation.js +16 -0
  111. package/dist/content/citation.js.map +1 -1
  112. package/dist/content/post-type.d.ts +2 -1
  113. package/dist/content/post-type.d.ts.map +1 -1
  114. package/dist/content/post-type.js +31 -3
  115. package/dist/content/post-type.js.map +1 -1
  116. package/dist/federation/article.d.ts.map +1 -1
  117. package/dist/federation/article.js +14 -16
  118. package/dist/federation/article.js.map +1 -1
  119. package/dist/federation/delivery.d.ts +10 -0
  120. package/dist/federation/delivery.d.ts.map +1 -1
  121. package/dist/federation/delivery.js +48 -4
  122. package/dist/federation/delivery.js.map +1 -1
  123. package/dist/federation/federation.d.ts +3 -0
  124. package/dist/federation/federation.d.ts.map +1 -1
  125. package/dist/federation/federation.js.map +1 -1
  126. package/dist/federation/mount.js +1 -0
  127. package/dist/federation/mount.js.map +1 -1
  128. package/dist/federation/relays.d.ts +2 -0
  129. package/dist/federation/relays.d.ts.map +1 -1
  130. package/dist/federation/relays.js +1 -0
  131. package/dist/federation/relays.js.map +1 -1
  132. package/dist/index.d.ts.map +1 -1
  133. package/dist/index.js +20 -2
  134. package/dist/index.js.map +1 -1
  135. package/dist/notifications/comments.d.ts +2 -0
  136. package/dist/notifications/comments.d.ts.map +1 -1
  137. package/dist/notifications/comments.js +1 -1
  138. package/dist/notifications/comments.js.map +1 -1
  139. package/dist/notifications/digest.d.ts +2 -0
  140. package/dist/notifications/digest.d.ts.map +1 -1
  141. package/dist/notifications/digest.js +1 -1
  142. package/dist/notifications/digest.js.map +1 -1
  143. package/dist/web/admin-bar.d.ts +4 -4
  144. package/dist/web/admin-bar.d.ts.map +1 -1
  145. package/dist/web/admin-bar.js +11 -34
  146. package/dist/web/admin-bar.js.map +1 -1
  147. package/dist/web/archive.d.ts +2 -1
  148. package/dist/web/archive.d.ts.map +1 -1
  149. package/dist/web/archive.js +2 -2
  150. package/dist/web/archive.js.map +1 -1
  151. package/dist/web/context.d.ts +3 -3
  152. package/dist/web/context.d.ts.map +1 -1
  153. package/dist/web/context.js +2 -2
  154. package/dist/web/context.js.map +1 -1
  155. package/dist/web/conversation.d.ts +2 -0
  156. package/dist/web/conversation.d.ts.map +1 -1
  157. package/dist/web/conversation.js +1 -1
  158. package/dist/web/conversation.js.map +1 -1
  159. package/dist/web/feed-item.d.ts.map +1 -1
  160. package/dist/web/feed-item.js +6 -12
  161. package/dist/web/feed-item.js.map +1 -1
  162. package/dist/web/feeds.js +8 -2
  163. package/dist/web/feeds.js.map +1 -1
  164. package/dist/web/oembed.d.ts +1 -0
  165. package/dist/web/oembed.d.ts.map +1 -1
  166. package/dist/web/oembed.js +3 -5
  167. package/dist/web/oembed.js.map +1 -1
  168. package/dist/web/render.d.ts.map +1 -1
  169. package/dist/web/render.js +13 -11
  170. package/dist/web/render.js.map +1 -1
  171. package/dist/web/routes.d.ts.map +1 -1
  172. package/dist/web/routes.js +6 -4
  173. package/dist/web/routes.js.map +1 -1
  174. package/dist/webmention/cited-picture.d.ts +0 -1
  175. package/dist/webmention/cited-picture.d.ts.map +1 -1
  176. package/dist/webmention/cited-picture.js +1 -4
  177. package/dist/webmention/cited-picture.js.map +1 -1
  178. package/dist/webmention/reply-contexts.d.ts +2 -0
  179. package/dist/webmention/reply-contexts.d.ts.map +1 -1
  180. package/dist/webmention/reply-contexts.js +4 -0
  181. package/dist/webmention/reply-contexts.js.map +1 -1
  182. package/package.json +8 -4
  183. package/themes/default/README.md +6 -0
package/README.md CHANGED
@@ -1880,6 +1880,29 @@ flash messages. A message queued with `flash(c, 'notice', '…')` is kept on the
1880
1880
  session row, shown on the next page the browser asks for, and cleared as it is
1881
1881
  read, so it survives exactly one redirect.
1882
1882
 
1883
+ ### The admin's look
1884
+
1885
+ The admin is drawn in DaisyUI 5 on Tailwind v4 (decision-30). Its stylesheet,
1886
+ `admin/static/admin.css`, is compiled at build time from `admin/src/admin.css`
1887
+ and ships in the package like the default theme's. It carries every built-in
1888
+ DaisyUI theme, with `light` as the default and `dark` when the system prefers
1889
+ dark. A site cannot override it or add a theme of its own.
1890
+
1891
+ The admin bar is the same component on the admin and across the top of every
1892
+ public page a signed-in user reads: a `<geekity-admin-bar>` element holding a
1893
+ declarative shadow root, so neither the site theme nor the admin's theme
1894
+ reaches inside it. It draws in a light or a dark palette of its own, following
1895
+ the user's admin theme. On a public page it pushes the page down by
1896
+ `--geekity-admin-bar-height` and adds the `geekity-admin-bar` class to
1897
+ `<html>` for a theme that needs to make room of its own.
1898
+
1899
+ Each user picks the theme the admin is drawn in for themselves, on their own
1900
+ screen under Users: any built-in DaisyUI theme, or Follow the system. The
1901
+ choice is stored as `adminTheme` on the user in `data/users.json`, and every
1902
+ admin page drawn for that user carries it as `data-theme` on `<html>`. Following
1903
+ the system stores nothing and puts no `data-theme` on the page. The login,
1904
+ setup, forgot-password and reset screens never carry one.
1905
+
1883
1906
  ### The menu
1884
1907
 
1885
1908
  The menu is WordPress classic: twelve sections — Dashboard, Posts, Pages,
@@ -1,13 +1,33 @@
1
- {# The admin bar, in the admin and across the top of the public site alike
2
- (TASK-126, TASK-183). The site title leads to `siteHref` and `barLinks` are
3
- the shortcuts beside it, both chosen by each place for itself: the admin's
4
- title leads to the dashboard, the public site's home (TASK-257). The account
5
- menu ends it.
1
+ {# The admin bar (decision-30), on every signed-in admin screen and every
2
+ public page a signed-in user reads. The context it takes:
3
+ - `siteHref` and `barLinks`: the admin's title leads to the dashboard, the
4
+ public site's home (TASK-257).
5
+ - `me`, `logoutUrl` and `csrfToken`, for the account menu that ends it.
6
+ - `barScheme`, `adminColorScheme` of the user's admin theme; `auto` puts no
7
+ data-scheme on the host.
8
+ - `cspNonce`, on the admin only.
9
+ - `assetPrefix`, where static/admin-bar.js is served from.
6
10
 
7
11
  WordPress's "Howdy," menu, greeting the way this site's owner does. A
8
12
  popover rather than a script: the browser opens and closes it, shuts it on
9
13
  Escape or a click elsewhere, and tells a screen reader whether it is
10
- expanded, all under the admin CSP's script-src 'self'. #}
14
+ expanded.
15
+
16
+ The offset stylesheet makes room for the fixed bar, and the bar's script
17
+ replaces the height with the one it measures. The margin is on <html>, as
18
+ under WordPress's bar, so a theme's own padding is left alone; `!important`
19
+ outlasts a reset such as `html, body { margin: 0 }`, and `:where()` lets a
20
+ theme with a sticky header of its own keep its scroll padding. #}
21
+ {% set HOST_STYLE = "all: initial; display: block; position: fixed; top: 0; left: 0; right: 0; z-index: 99999" -%}
22
+ <style id="geekity-admin-bar-offset"{% if cspNonce %} nonce="{{ cspNonce }}"{% endif %}>
23
+ :root { --geekity-admin-bar-height: 40px; }
24
+ @media (max-width: 600px) { :root { --geekity-admin-bar-height: 80px; } }
25
+ html { margin-top: var(--geekity-admin-bar-height) !important; }
26
+ :where(html) { scroll-padding-top: var(--geekity-admin-bar-height); }
27
+ </style><geekity-admin-bar
28
+ {%- if barScheme == 'light' or barScheme == 'dark' %} data-scheme="{{ barScheme }}"{% endif %}
29
+ {%- if not cspNonce %} style="{{ HOST_STYLE }}"{% endif %}><template shadowrootmode="open"><style{% if cspNonce %} nonce="{{ cspNonce }}"{% endif %}>:host { {{ HOST_STYLE }}; }
30
+ {% include "static/admin-bar.css" %}</style>
11
31
  <nav class="admin-bar" aria-label="Admin bar">
12
32
  <a class="admin-bar-site" href="{{ siteHref }}">{{ site.title }}</a>
13
33
  {% for link in barLinks %}
@@ -25,3 +45,4 @@
25
45
  </div>
26
46
  {% endif %}
27
47
  </nav>
48
+ <script src="{{ assetPrefix }}admin-bar.js" defer></script></template></geekity-admin-bar>
@@ -0,0 +1,28 @@
1
+ {# An alert: `{% call alert(…) %}body{% endcall %}` emits
2
+ `<div role="alert" class="alert">body</div>`. The body is markup.
3
+
4
+ Modifiers, each a name from a closed set that maps to DaisyUI classes:
5
+
6
+ color info, success, warning, error
7
+ style outline, dash, soft
8
+ direction vertical, horizontal, or responsive (stacked on a phone,
9
+ in a row from `sm` up)
10
+
11
+ A value outside its set is a render error.
12
+
13
+ `labelledby` names the id of the heading inside the body. `focus` adds
14
+ `tabindex="-1"` and `autofocus`; only one element on a page may ask for it.
15
+ `polite` makes it `role="status"`, read out when the reader is next idle
16
+ rather than at once. #}
17
+
18
+ {% set COLORS = { info: 'alert-info', success: 'alert-success', warning: 'alert-warning', error: 'alert-error' } %}
19
+ {% set STYLES = { outline: 'alert-outline', dash: 'alert-dash', soft: 'alert-soft' } %}
20
+ {% set DIRECTIONS = {
21
+ vertical: 'alert-vertical',
22
+ horizontal: 'alert-horizontal',
23
+ responsive: 'alert-vertical sm:alert-horizontal'
24
+ } %}
25
+
26
+ {% macro alert(color=none, style=none, direction=none, labelledby='', focus=false, polite=false) -%}
27
+ <div role="{{ 'status' if polite else 'alert' }}"{% if labelledby %} aria-labelledby="{{ labelledby }}"{% endif %}{% if focus %} tabindex="-1" autofocus{% endif %} class="alert{{ COLORS | modifier(color) }}{{ STYLES | modifier(style) }}{{ DIRECTIONS | modifier(direction) }}">{{ caller() }}</div>
28
+ {%- endmacro %}
@@ -0,0 +1,58 @@
1
+ {# A badge: `badge(label, …)` emits
2
+ `<span class="badge badge-outline badge-sm">label</span>`, outlined when it
3
+ is given no colour and no style. The label is escaped.
4
+
5
+ Modifiers, each a name from a closed set that maps to one DaisyUI class:
6
+
7
+ color neutral, primary, secondary, accent, info, success, warning, error
8
+ style outline, dash, soft, ghost
9
+ size xs, sm (the default), md, lg, xl
10
+
11
+ A value outside its set is a render error.
12
+
13
+ `status(name, label)` is the badge for the state a row is in, coloured by
14
+ `STATUSES` below; the label is escaped. A name outside that set is a
15
+ render error. #}
16
+
17
+ {% set COLORS = {
18
+ neutral: 'badge-neutral',
19
+ primary: 'badge-primary',
20
+ secondary: 'badge-secondary',
21
+ accent: 'badge-accent',
22
+ info: 'badge-info',
23
+ success: 'badge-success',
24
+ warning: 'badge-warning',
25
+ error: 'badge-error'
26
+ } %}
27
+ {% set STYLES = { outline: 'badge-outline', dash: 'badge-dash', soft: 'badge-soft', ghost: 'badge-ghost' } %}
28
+ {% set SIZES = { xs: 'badge-xs', sm: 'badge-sm', md: 'badge-md', lg: 'badge-lg', xl: 'badge-xl' } %}
29
+
30
+ {% macro badge(label, color=none, style=none, size='sm') -%}
31
+ <span class="badge{% if not color and not style %} badge-outline{% endif %}{{ COLORS | modifier(color) }}{{ STYLES | modifier(style) }}{{ SIZES | modifier(size) }}">{{ label }}</span>
32
+ {%- endmacro %}
33
+
34
+ {% set STATUSES = {
35
+ published: 'badge-outline',
36
+ approved: 'badge-outline',
37
+ accepted: 'badge-outline',
38
+ described: 'badge-outline',
39
+ decorative: 'badge-outline',
40
+ active: 'badge-primary',
41
+ unread: 'badge-primary',
42
+ connected: 'badge-success',
43
+ scheduled: 'badge-info',
44
+ draft: 'badge-warning',
45
+ pending: 'badge-warning',
46
+ missing: 'badge-warning',
47
+ unreachable: 'badge-warning',
48
+ failed: 'badge-error',
49
+ rejected: 'badge-error',
50
+ spam: 'badge-error',
51
+ hidden: 'badge-ghost',
52
+ trashed: 'badge-ghost',
53
+ disconnected: 'badge-ghost'
54
+ } %}
55
+
56
+ {% macro status(name, label) -%}
57
+ <span class="badge badge-sm{{ STATUSES | modifier(name) }}">{{ label }}</span>
58
+ {%- endmacro %}
@@ -0,0 +1,56 @@
1
+ {# A button, and a link drawn as one.
2
+
3
+ `button(label, …)` emits `<button type="button" class="btn btn-sm">`, and
4
+ `buttonLink(label, href, …)` emits `<a class="btn btn-sm" href>`.
5
+
6
+ Each takes the same modifiers:
7
+
8
+ color neutral, primary, secondary, accent, info, success, warning, error
9
+ style outline, dash, soft, ghost, link
10
+ size xs, sm (the default), md, lg, xl
11
+ shape wide, block, square, circle
12
+ active true draws it pressed
13
+
14
+ A value outside its set is a render error.
15
+ `button` also takes `type` (button, unless it submits), `id`, `name`,
16
+ `value`, `formaction`, `formtarget`, `formnovalidate`, `popovertarget`,
17
+ `disabled` and `hidden`. The label is escaped. #}
18
+
19
+ {% set COLORS = {
20
+ neutral: 'btn-neutral',
21
+ primary: 'btn-primary',
22
+ secondary: 'btn-secondary',
23
+ accent: 'btn-accent',
24
+ info: 'btn-info',
25
+ success: 'btn-success',
26
+ warning: 'btn-warning',
27
+ error: 'btn-error'
28
+ } %}
29
+ {% set STYLES = { outline: 'btn-outline', dash: 'btn-dash', soft: 'btn-soft', ghost: 'btn-ghost', link: 'btn-link' } %}
30
+ {% set SIZES = { xs: 'btn-xs', sm: 'btn-sm', md: 'btn-md', lg: 'btn-lg', xl: 'btn-xl' } %}
31
+ {% set SHAPES = { wide: 'btn-wide', block: 'btn-block', square: 'btn-square', circle: 'btn-circle' } %}
32
+
33
+ {% macro button(
34
+ label,
35
+ type='button',
36
+ color=none,
37
+ style=none,
38
+ size='sm',
39
+ shape=none,
40
+ active=false,
41
+ id='',
42
+ name='',
43
+ value='',
44
+ formaction='',
45
+ formtarget='',
46
+ formnovalidate=false,
47
+ popovertarget='',
48
+ disabled=false,
49
+ hidden=false
50
+ ) -%}
51
+ <button type="{{ type }}"{% if id %} id="{{ id }}"{% endif %} class="btn{{ COLORS | modifier(color) }}{{ STYLES | modifier(style) }}{{ SIZES | modifier(size) }}{{ SHAPES | modifier(shape) }}{% if active %} btn-active {%- endif %}"{% if name %} name="{{ name }}"{% endif %}{% if value %} value="{{ value }}"{% endif %}{% if formaction %} formaction="{{ formaction }}"{% endif %}{% if formtarget %} formtarget="{{ formtarget }}"{% endif %}{% if formnovalidate %} formnovalidate{% endif %}{% if popovertarget %} popovertarget="{{ popovertarget }}"{% endif %}{% if disabled %} disabled{% endif %}{% if hidden %} hidden{% endif %}>{{ label }}</button>
52
+ {%- endmacro %}
53
+
54
+ {% macro buttonLink(label, href, color=none, style=none, size='sm', shape=none, active=false) -%}
55
+ <a class="btn{{ COLORS | modifier(color) }}{{ STYLES | modifier(style) }}{{ SIZES | modifier(size) }}{{ SHAPES | modifier(shape) }}{% if active %} btn-active {%- endif %}" href="{{ href }}">{{ label }}</a>
56
+ {%- endmacro %}
@@ -0,0 +1,40 @@
1
+ {# A card.
2
+
3
+ `{% call card(title=…) %}body{% endcall %}` emits
4
+
5
+ <div class="card bg-base-100 shadow-sm">
6
+ <div class="card-body">
7
+ <h2 class="card-title">title</h2>
8
+ body
9
+ </div>
10
+ </div>
11
+
12
+ The title is escaped and left out when there is none; the body is markup.
13
+
14
+ Modifiers, each a name from a closed set that maps to one DaisyUI class:
15
+
16
+ size xs, sm, md, lg, xl
17
+ style border, dash
18
+
19
+ A value outside its set is a render error.
20
+
21
+ `{% call cardActions() %}…{% endcall %}`, inside a card's body, is the row
22
+ of buttons at its foot: `<div class="card-actions justify-end">`. #}
23
+
24
+ {% set SIZES = { xs: 'card-xs', sm: 'card-sm', md: 'card-md', lg: 'card-lg', xl: 'card-xl' } %}
25
+ {% set STYLES = { border: 'card-border', dash: 'card-dash' } %}
26
+
27
+ {% macro card(title='', size=none, style=none) -%}
28
+ <div class="card bg-base-100 shadow-sm{{ SIZES | modifier(size) }}{{ STYLES | modifier(style) }}">
29
+ <div class="card-body">
30
+ {% if title %}
31
+ <h2 class="card-title">{{ title }}</h2>
32
+ {% endif %}
33
+ {{ caller() }}
34
+ </div>
35
+ </div>
36
+ {%- endmacro %}
37
+
38
+ {% macro cardActions() -%}
39
+ <div class="card-actions justify-end">{{ caller() }}</div>
40
+ {%- endmacro %}
@@ -0,0 +1,36 @@
1
+ {# A collapse: a heading that shows and hides what is under it.
2
+
3
+ `{% call collapse(title, …) %}body{% endcall %}` emits
4
+
5
+ <details class="collapse collapse-arrow border border-base-300 bg-base-100">
6
+ <summary class="collapse-title font-semibold">title</summary>
7
+ <div class="collapse-content">body</div>
8
+ </details>
9
+
10
+ `open` starts it open. The title is escaped; the body is markup.
11
+ `fields` wraps the body in a `<fieldset>` whose `sr-only` legend repeats
12
+ the title.
13
+
14
+ Modifiers:
15
+
16
+ icon arrow (the default), plus
17
+
18
+ A value outside its set is a render error. #}
19
+
20
+ {% set ICONS = { arrow: 'collapse-arrow', plus: 'collapse-plus' } %}
21
+
22
+ {% macro collapse(title, open=false, fields=false, icon='arrow') -%}
23
+ <details class="collapse{{ ICONS | modifier(icon) }} border border-base-300 bg-base-100"{% if open %} open{% endif %}>
24
+ <summary class="collapse-title font-semibold">{{ title }}</summary>
25
+ <div class="collapse-content">
26
+ {% if fields %}
27
+ <fieldset class="flex min-w-0 flex-col gap-1">
28
+ <legend class="sr-only">{{ title }}</legend>
29
+ {{ caller() }}
30
+ </fieldset>
31
+ {% else %}
32
+ {{ caller() }}
33
+ {% endif %}
34
+ </div>
35
+ </details>
36
+ {%- endmacro %}
@@ -0,0 +1,20 @@
1
+ {# A value to copy: a readonly box, and a button that copies what it holds.
2
+
3
+ `copyField(id, label, value)` emits
4
+
5
+ <label class="sr-only" for="id">label</label>
6
+ <div class="join w-full">
7
+ <input id="id" type="text" value="value" readonly class="input input-sm join-item w-full" />
8
+ <button type="button" class="btn btn-sm join-item" data-copy="id" hidden>Copy</button>
9
+ </div>
10
+
11
+ The button stays `hidden` until `static/copy.js` reveals it. Label and
12
+ value are escaped. #}
13
+
14
+ {% macro copyField(id, label, value) -%}
15
+ <label class="sr-only" for="{{ id }}">{{ label }}</label>
16
+ <div class="join w-full">
17
+ <input id="{{ id }}" type="text" value="{{ value }}" readonly class="input input-sm join-item w-full min-w-48" />
18
+ <button type="button" class="btn btn-sm join-item" data-copy="{{ id }}" hidden>Copy</button>
19
+ </div>
20
+ {%- endmacro %}
@@ -0,0 +1,32 @@
1
+ {# A dropdown: a button that opens its body as a popover.
2
+
3
+ `{% call dropdown(id, label, …) %}body{% endcall %}` emits
4
+
5
+ <button type="button" class="btn btn-sm" popovertarget="id">label</button>
6
+ <div class="dropdown rounded-box bg-base-100 z-1 w-52 shadow-sm" popover id="id">
7
+ body
8
+ </div>
9
+
10
+ `id` must be unique on the page. The body is markup. The label is escaped.
11
+
12
+ The trigger is `button` from `components/button.njk` and takes its
13
+ `color`, `style` and `size` (sm by default). `placement` is start, center,
14
+ end, top, bottom, left or right, each mapping to its `dropdown-*` class. A
15
+ value outside its set is a render error. #}
16
+
17
+ {% from "components/button.njk" import button %}
18
+
19
+ {% set PLACEMENTS = {
20
+ start: 'dropdown-start',
21
+ center: 'dropdown-center',
22
+ end: 'dropdown-end',
23
+ top: 'dropdown-top',
24
+ bottom: 'dropdown-bottom',
25
+ left: 'dropdown-left',
26
+ right: 'dropdown-right'
27
+ } %}
28
+
29
+ {% macro dropdown(id, label, color=none, style=none, size='sm', placement=none) -%}
30
+ {{ button(label, color=color, style=style, size=size, popovertarget=id) }}
31
+ <div class="dropdown rounded-box bg-base-100 z-1 w-52 shadow-sm{{ PLACEMENTS | modifier(placement) }}" popover id="{{ id }}">{{ caller() }}</div>
32
+ {%- endmacro %}
@@ -1,4 +1,4 @@
1
- {# One labelled box, written once.
1
+ {# One labelled box, written once, in DaisyUI's form components.
2
2
 
3
3
  Every admin form used to spell out its own `<label for>`, control, hint and
4
4
  error, roughly seventy-five times across sixteen templates, against a class
@@ -7,21 +7,34 @@
7
7
  forget the error line, leave a label unbound, or invent a class the
8
8
  stylesheet has never heard of, and nothing says it is wrong.
9
9
 
10
- Each macro here emits the same four things in the same order:
10
+ Each macro here emits one `fieldset` holding the same four things in the
11
+ same order:
11
12
 
12
- 1. the `<label for>`, bound to the control by its id,
13
- 2. the control,
13
+ 1. the `<label for>`, bound to the control by its id, drawn as the
14
+ field's `fieldset-legend`,
15
+ 2. the control: `input`, `textarea`, `select` or `checkbox`, small, the
16
+ width of its column, and a `validator` when the form refused it,
14
17
  3. the field error, where the form has something to say about this box,
15
- 4. the hint, where the field needs explaining.
18
+ as the `validator-hint` that a refused control turns red,
19
+ 4. the hint, where the field needs explaining, as a `label`.
20
+
21
+ DaisyUI's `fieldset` and `fieldset-legend` are classes on a `<div>` and a
22
+ `<label>` here, not a `<fieldset>` and `<legend>`, which name a group
23
+ rather than one control.
16
24
 
17
25
  Error before hint, because that is the order every screen already used: the
18
26
  sentence about what went wrong belongs next to the box it is about, and the
19
27
  standing explanation sits under both.
20
28
 
21
- This file is under the packaged admin directory, which is the admin
22
- environment's only search root (`src/admin/templates.ts`). A site's theme
23
- can override any public template and cannot reach in here, so no theme can
24
- shadow or break a field on the login form.
29
+ This file is under the admin directory, which is the admin environment's
30
+ only search root (`src/admin/templates.ts`). A site's theme can override
31
+ any public template and cannot reach in here, so no theme can shadow or
32
+ break a field on the login form.
33
+
34
+ `hintId` gives the hint an id and names it in the control's
35
+ `aria-describedby`, after the error where there is one, so a screen reader
36
+ reads the explanation with the box rather than leaving it to be found. A
37
+ textarea given no `name` only shows something and posts nothing.
25
38
 
26
39
  `label` and `hint` are written into the page as markup, because the admin's
27
40
  labels and hints carry `<code>`, `<em>` and links. They are template
@@ -30,19 +43,25 @@
30
43
  interpolates before this ever sees it. `error` is a message from the server
31
44
  and is escaped. #}
32
45
 
46
+ {% from "components/alert.njk" import alert %}
47
+
33
48
  {# The field error, then the hint: the two paragraphs every macro below ends
34
- with. `errorHtml` is the rare second thing a field has to say — the Reading
35
- page's "that page is no longer published" — already written as its own
36
- paragraph. #}
37
- {% macro _below(id, hint, error, errorHtml) %}
49
+ with. The error is a `validator-hint`, which DaisyUI shows through a
50
+ sibling combinator after a `.validator[aria-invalid]`, so it has to stay
51
+ that control's later sibling. `errorHtml` is the rare second thing a field
52
+ has to say — the Reading page's "that page is no longer published" —
53
+ already written as its own paragraph, and drawn in the error colour whether
54
+ or not the form was refused. The hint is a `label` made `block` and
55
+ `whitespace-normal`: DaisyUI's is `inline-flex` and `nowrap`. #}
56
+ {% macro _below(id, hint, error, errorHtml, hintId='') %}
38
57
  {% if error %}
39
- <p class="admin-field-error" id="{{ id }}-error">{{ error }}</p>
58
+ <p class="validator-hint" id="{{ id }}-error">{{ error }}</p>
40
59
  {% endif %}
41
60
  {% if errorHtml %}
42
- {{ errorHtml | safe }}
61
+ <div class="text-error">{{ errorHtml | safe }}</div>
43
62
  {% endif %}
44
63
  {% if hint %}
45
- <p class="admin-hint">{{ hint | safe }}</p>
64
+ <p class="label block whitespace-normal"{% if hintId %} id="{{ hintId }}"{% endif %}>{{ hint | safe }}</p>
46
65
  {% endif %}
47
66
  {% endmacro %}
48
67
 
@@ -50,8 +69,8 @@
50
69
  invalid, and which paragraph says why, so a screen reader reads the message
51
70
  with the field rather than leaving it to be found. `errorHtml` is not named
52
71
  here: it is a standing note about the stored value, not a refusal. #}
53
- {% macro _invalid(id, error) -%}
54
- {% if error %} aria-invalid="true" aria-describedby="{{ id }}-error"{% endif %}
72
+ {% macro _invalid(id, error, hintId='') -%}
73
+ {% if error %} aria-invalid="true" aria-describedby="{{ id }}-error{% if hintId %} {{ hintId }}{% endif %}"{% elif hintId %} aria-describedby="{{ hintId }}"{% endif %}
55
74
  {%- endmacro %}
56
75
 
57
76
  {# A box somebody types into: text, email, number, or whatever `type` says.
@@ -67,20 +86,25 @@
67
86
  hint='',
68
87
  error='',
69
88
  errorHtml='',
89
+ hintId='',
70
90
  min='',
71
91
  step='',
72
92
  inputmode='',
73
93
  autocomplete='',
74
94
  placeholder='',
95
+ list='',
96
+ maxlength='',
75
97
  spellcheck='',
76
98
  required=false,
77
99
  autofocus=false,
78
100
  readonly=false,
79
101
  disabled=false
80
102
  ) %}
81
- <label for="{{ id }}">{{ label | safe }}</label>
82
- <input id="{{ id }}" name="{{ name }}" type="{{ type }}"{% if min %} min="{{ min }}"{% endif %}{% if step %} step="{{ step }}"{% endif %}{% if inputmode %} inputmode="{{ inputmode }}"{% endif %}{% if autocomplete %} autocomplete="{{ autocomplete }}"{% endif %} value="{{ value }}"{% if placeholder %} placeholder="{{ placeholder }}"{% endif %}{% if spellcheck %} spellcheck="{{ spellcheck }}"{% endif %}{% if required %} required{% endif %}{% if autofocus %} autofocus{% endif %}{% if readonly %} readonly{% endif %}{% if disabled %} disabled{% endif %}{{ _invalid(id, error) }} />
83
- {{ _below(id, hint, error, errorHtml) }}
103
+ <div class="fieldset">
104
+ <label class="fieldset-legend" for="{{ id }}">{{ label | safe }}</label>
105
+ <input id="{{ id }}" name="{{ name }}" type="{{ type }}"{% if min %} min="{{ min }}"{% endif %}{% if step %} step="{{ step }}"{% endif %}{% if inputmode %} inputmode="{{ inputmode }}"{% endif %}{% if autocomplete %} autocomplete="{{ autocomplete }}"{% endif %} value="{{ value }}"{% if list %} list="{{ list }}"{% endif %}{% if maxlength %} maxlength="{{ maxlength }}"{% endif %}{% if placeholder %} placeholder="{{ placeholder }}"{% endif %}{% if spellcheck %} spellcheck="{{ spellcheck }}"{% endif %}{% if required %} required{% endif %}{% if autofocus %} autofocus{% endif %}{% if readonly %} readonly{% endif %}{% if disabled %} disabled{% endif %}{{ _invalid(id, error, hintId) }} class="input input-sm w-full read-only:bg-base-200 read-only:border-dashed{% if error %} validator{% endif %}" />
106
+ {{ _below(id, hint, error, errorHtml, hintId) }}
107
+ </div>
84
108
  {%- endmacro %}
85
109
 
86
110
  {# A password box. It carries `value=""` and no way to set one: nothing typed
@@ -100,9 +124,11 @@
100
124
  required=false,
101
125
  autofocus=false
102
126
  ) %}
103
- <label for="{{ id }}">{{ label | safe }}</label>
104
- <input id="{{ id }}" name="{{ name }}" type="password" value=""{% if autocomplete %} autocomplete="{{ autocomplete }}"{% endif %}{% if spellcheck %} spellcheck="{{ spellcheck }}"{% endif %}{% if placeholder %} placeholder="{{ placeholder }}"{% endif %}{% if required %} required{% endif %}{% if autofocus %} autofocus{% endif %}{{ _invalid(id, error) }} />
127
+ <div class="fieldset">
128
+ <label class="fieldset-legend" for="{{ id }}">{{ label | safe }}</label>
129
+ <input id="{{ id }}" name="{{ name }}" type="password" value=""{% if autocomplete %} autocomplete="{{ autocomplete }}"{% endif %}{% if spellcheck %} spellcheck="{{ spellcheck }}"{% endif %}{% if placeholder %} placeholder="{{ placeholder }}"{% endif %}{% if required %} required{% endif %}{% if autofocus %} autofocus{% endif %}{{ _invalid(id, error) }} class="input input-sm w-full{% if error %} validator{% endif %}" />
105
130
  {{ _below(id, hint, error, errorHtml) }}
131
+ </div>
106
132
  {%- endmacro %}
107
133
 
108
134
  {# A box of several lines. #}
@@ -114,6 +140,7 @@
114
140
  hint='',
115
141
  error='',
116
142
  errorHtml='',
143
+ hintId='',
117
144
  rows='',
118
145
  spellcheck='',
119
146
  required=false,
@@ -121,9 +148,11 @@
121
148
  readonly=false,
122
149
  disabled=false
123
150
  ) %}
124
- <label for="{{ id }}">{{ label | safe }}</label>
125
- <textarea id="{{ id }}" name="{{ name }}"{% if rows %} rows="{{ rows }}"{% endif %}{% if spellcheck %} spellcheck="{{ spellcheck }}"{% endif %}{% if required %} required{% endif %}{% if autofocus %} autofocus{% endif %}{% if readonly %} readonly{% endif %}{% if disabled %} disabled{% endif %}{{ _invalid(id, error) }}>{{ value }}</textarea>
126
- {{ _below(id, hint, error, errorHtml) }}
151
+ <div class="fieldset">
152
+ <label class="fieldset-legend" for="{{ id }}">{{ label | safe }}</label>
153
+ <textarea id="{{ id }}"{% if name %} name="{{ name }}"{% endif %}{% if rows %} rows="{{ rows }}"{% endif %}{% if spellcheck %} spellcheck="{{ spellcheck }}"{% endif %}{% if required %} required{% endif %}{% if autofocus %} autofocus{% endif %}{% if readonly %} readonly{% endif %}{% if disabled %} disabled{% endif %}{{ _invalid(id, error, hintId) }} class="textarea textarea-sm w-full read-only:bg-base-200 read-only:border-dashed{% if error %} validator{% endif %}">{{ value }}</textarea>
154
+ {{ _below(id, hint, error, errorHtml, hintId) }}
155
+ </div>
127
156
  {%- endmacro %}
128
157
 
129
158
  {# A list to pick from. The options are the call's body, because which of them
@@ -140,20 +169,22 @@
140
169
  hint='',
141
170
  error='',
142
171
  errorHtml='',
172
+ hintId='',
143
173
  required=false,
144
174
  autofocus=false,
145
175
  disabled=false
146
176
  ) %}
147
- <label for="{{ id }}">{{ label | safe }}</label>
148
- <select id="{{ id }}" name="{{ name }}"{% if required %} required{% endif %}{% if autofocus %} autofocus{% endif %}{% if disabled %} disabled{% endif %}{{ _invalid(id, error) }}>
177
+ <div class="fieldset">
178
+ <label class="fieldset-legend" for="{{ id }}">{{ label | safe }}</label>
179
+ <select id="{{ id }}" name="{{ name }}"{% if required %} required{% endif %}{% if autofocus %} autofocus{% endif %}{% if disabled %} disabled{% endif %}{{ _invalid(id, error, hintId) }} class="select select-sm w-full{% if error %} validator{% endif %}">
149
180
  {{ caller() }}
150
181
  </select>
151
- {{ _below(id, hint, error, errorHtml) }}
182
+ {{ _below(id, hint, error, errorHtml, hintId) }}
183
+ </div>
152
184
  {%- endmacro %}
153
185
 
154
- {# A switch. The label follows the box rather than heading it, which is what a
155
- tick beside a sentence reads as, and `admin-check` is the class the
156
- stylesheet lays that pair out with. #}
186
+ {# A switch: the tick inside its label. A refused box puts `validator` on
187
+ the label, since the `validator-hint` must be its later sibling. #}
157
188
  {% macro checkbox(
158
189
  id,
159
190
  name,
@@ -163,20 +194,23 @@
163
194
  hint='',
164
195
  error='',
165
196
  errorHtml='',
197
+ hintId='',
166
198
  disabled=false
167
199
  ) %}
168
- <p class="admin-check">
169
- <input id="{{ id }}" name="{{ name }}" type="checkbox" value="{{ value }}"{% if checked %} checked{% endif %}{% if disabled %} disabled{% endif %}{{ _invalid(id, error) }} />
170
- <label for="{{ id }}">{{ label | safe }}</label>
171
- </p>
172
- {{ _below(id, hint, error, errorHtml) }}
200
+ <div class="fieldset">
201
+ <label class="label whitespace-normal text-base-content{% if error %} validator{% endif %}" for="{{ id }}">
202
+ <input id="{{ id }}" name="{{ name }}" type="checkbox" value="{{ value }}"{% if checked %} checked{% endif %}{% if disabled %} disabled{% endif %}{{ _invalid(id, error, hintId) }} class="checkbox checkbox-sm" />
203
+ {{ label | safe }}
204
+ </label>
205
+ {{ _below(id, hint, error, errorHtml, hintId) }}
206
+ </div>
173
207
  {%- endmacro %}
174
208
 
175
209
  {# The top of a refused form: what went wrong, and a link to every field that
176
- needs putting right. It is an alert and takes focus when the page loads, so
177
- a screen reader hears it at once and a keyboard starts from it, with no
178
- script. It comes before the form, so its `autofocus` is the first on the
179
- page and wins over any a field carries.
210
+ needs putting right. It is an error alert and takes focus when the page
211
+ loads, so a screen reader hears it at once and a keyboard starts from it,
212
+ with no script. It comes before the form, so its `autofocus` is the first
213
+ on the page and wins over any a field carries.
180
214
 
181
215
  A form with one thing to say calls it bare, with the message as the heading.
182
216
  A form with fields to fix calls it with a `problem` per field; one given no
@@ -190,19 +224,21 @@
190
224
  fixed. #}
191
225
  {% macro summary(heading) %}
192
226
  {% set problems = (caller() if caller else '') | trim %}
193
- <div class="admin-error admin-error-summary" role="alert" tabindex="-1" autofocus aria-labelledby="admin-error-summary-heading">
194
- <h2 id="admin-error-summary-heading">{{ heading }}</h2>
227
+ {% call alert(color='error', labelledby='admin-error-summary-heading', focus=true) %}
228
+ <div>
229
+ <h2 id="admin-error-summary-heading" class="font-semibold">{{ heading }}</h2>
195
230
  {% if problems %}
196
- <ul>
231
+ <ul class="list-disc ps-5">
197
232
  {{ problems | safe }}
198
233
  </ul>
199
234
  {% endif %}
200
235
  </div>
236
+ {% endcall %}
201
237
  {%- endmacro %}
202
238
 
203
239
  {# One field's line in the summary: its message, linking to the field. #}
204
240
  {% macro problem(id, message) %}
205
241
  {% if message %}
206
- <li><a href="#{{ id }}">{{ message }}</a></li>
242
+ <li><a class="link" href="#{{ id }}">{{ message }}</a></li>
207
243
  {% endif %}
208
244
  {%- endmacro %}
@@ -1,5 +1,12 @@
1
- {# Messages queued by the request that redirected here. They were cleared as
2
- they were read, so this is the only page that shows them. #}
3
- {% for entry in flash %}
4
- <p class="admin-flash admin-flash-{{ entry.kind }}" role="status">{{ entry.message }}</p>
5
- {% endfor %}
1
+ {# Messages queued by the request that redirected here, one alert each in the
2
+ colour of its kind. They were cleared as they were read, so this is the only
3
+ page that shows them. #}
4
+ {% from "components/alert.njk" import alert %}
5
+ {% set KINDS = { notice: 'success', warning: 'warning', error: 'error' } %}
6
+ {% if flash and flash.length %}
7
+ <div class="mb-6 flex flex-col gap-2">
8
+ {% for entry in flash %}
9
+ {% call alert(color=KINDS[entry.kind], polite=true) %}<span>{{ entry.message }}</span>{% endcall %}
10
+ {% endfor %}
11
+ </div>
12
+ {% endif %}