@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.
- package/README.md +23 -0
- package/admin/components/admin-bar.njk +27 -6
- package/admin/components/alert.njk +28 -0
- package/admin/components/badge.njk +58 -0
- package/admin/components/button.njk +56 -0
- package/admin/components/card.njk +40 -0
- package/admin/components/collapse.njk +36 -0
- package/admin/components/copy.njk +20 -0
- package/admin/components/dropdown.njk +32 -0
- package/admin/components/fields.njk +81 -45
- package/admin/components/flash.njk +12 -5
- package/admin/components/menu.njk +51 -0
- package/admin/components/navbar.njk +23 -0
- package/admin/components/pagination.njk +47 -0
- package/admin/components/public-admin-bar.njk +4 -41
- package/admin/components/stat.njk +35 -0
- package/admin/components/table.njk +74 -0
- package/admin/components/tabs.njk +33 -0
- package/admin/components/theme-choice.njk +18 -0
- package/admin/layouts/base.njk +9 -16
- package/admin/layouts/settings-page.njk +15 -10
- package/admin/layouts/shell.njk +23 -31
- package/admin/pages/account/forgot.njk +39 -33
- package/admin/pages/account/login.njk +22 -16
- package/admin/pages/account/reset.njk +34 -25
- package/admin/pages/account/setup.njk +21 -15
- package/admin/pages/appearance/themes.njk +50 -44
- package/admin/pages/comments/all.njk +88 -106
- package/admin/pages/dashboard/home.njk +50 -62
- package/admin/pages/documents/conflict.njk +29 -24
- package/admin/pages/documents/editor.njk +155 -275
- package/admin/pages/documents/list.njk +60 -69
- package/admin/pages/documents/syndication.njk +98 -60
- package/admin/pages/documents/taxonomy.njk +36 -32
- package/admin/pages/error.njk +8 -5
- package/admin/pages/federation/followers.njk +91 -97
- package/admin/pages/federation/settings.njk +10 -11
- package/admin/pages/indieauth/consent.njk +60 -44
- package/admin/pages/indieauth/refused.njk +6 -3
- package/admin/pages/media/library.njk +86 -107
- package/admin/pages/messages/all.njk +71 -77
- package/admin/pages/navigation/menus.njk +90 -85
- package/admin/pages/placeholder.njk +13 -8
- package/admin/pages/settings/discussion.njk +30 -29
- package/admin/pages/settings/email.njk +26 -28
- package/admin/pages/settings/general.njk +10 -4
- package/admin/pages/settings/permalinks.njk +8 -9
- package/admin/pages/settings/privacy.njk +3 -3
- package/admin/pages/settings/reading.njk +3 -3
- package/admin/pages/tools/content-index.njk +79 -73
- package/admin/pages/tools/personal-data.njk +61 -55
- package/admin/pages/users/activity-entry.njk +12 -12
- package/admin/pages/users/activity.njk +13 -19
- package/admin/pages/users/apps.njk +112 -100
- package/admin/pages/users/edit.njk +65 -58
- package/admin/pages/users/list.njk +26 -24
- package/admin/pages/users/new.njk +11 -7
- package/admin/src/admin.css +174 -0
- package/admin/static/admin-bar.css +33 -31
- package/admin/static/admin-bar.js +22 -0
- package/admin/static/admin.css +7388 -1195
- package/admin/static/copy.js +1 -1
- package/admin/static/editor.js +18 -18
- package/dist/admin/accounts.d.ts +20 -0
- package/dist/admin/accounts.d.ts.map +1 -1
- package/dist/admin/accounts.js +25 -0
- package/dist/admin/accounts.js.map +1 -1
- package/dist/admin/admin-theme.d.ts +157 -0
- package/dist/admin/admin-theme.d.ts.map +1 -0
- package/dist/admin/admin-theme.js +52 -0
- package/dist/admin/admin-theme.js.map +1 -0
- package/dist/admin/assets.d.ts +4 -3
- package/dist/admin/assets.d.ts.map +1 -1
- package/dist/admin/assets.js +4 -3
- package/dist/admin/assets.js.map +1 -1
- package/dist/admin/documents.js +1 -1
- package/dist/admin/documents.js.map +1 -1
- package/dist/admin/headers.js +1 -1
- package/dist/admin/headers.js.map +1 -1
- package/dist/admin/routes.d.ts.map +1 -1
- package/dist/admin/routes.js +17 -8
- package/dist/admin/routes.js.map +1 -1
- package/dist/admin/templates.d.ts +6 -3
- package/dist/admin/templates.d.ts.map +1 -1
- package/dist/admin/templates.js +25 -3
- package/dist/admin/templates.js.map +1 -1
- package/dist/admin/users.d.ts +4 -0
- package/dist/admin/users.d.ts.map +1 -1
- package/dist/admin/users.js +37 -1
- package/dist/admin/users.js.map +1 -1
- package/dist/content/post-type.d.ts.map +1 -1
- package/dist/content/post-type.js +7 -2
- package/dist/content/post-type.js.map +1 -1
- package/dist/web/admin-bar.d.ts +4 -4
- package/dist/web/admin-bar.d.ts.map +1 -1
- package/dist/web/admin-bar.js +11 -34
- package/dist/web/admin-bar.js.map +1 -1
- package/package.json +8 -4
- package/themes/default/README.md +3 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{# A menu: a list of links, down the side or across the top.
|
|
2
|
+
|
|
3
|
+
`menu(items, …)` emits
|
|
4
|
+
|
|
5
|
+
<ul class="menu w-full">
|
|
6
|
+
<li>
|
|
7
|
+
<a class="{RING}" href="…">Posts</a>
|
|
8
|
+
<ul>
|
|
9
|
+
<li><a class="{RING} menu-active" href="…" aria-current="page">All posts</a></li>
|
|
10
|
+
</ul>
|
|
11
|
+
</li>
|
|
12
|
+
</ul>
|
|
13
|
+
|
|
14
|
+
Each item is `{ label, url, current, open, children }`, as
|
|
15
|
+
`src/admin/menu.ts` builds it: `current` marks the screen being looked at
|
|
16
|
+
with `menu-active` and `aria-current`, and an item's `children` are listed
|
|
17
|
+
only while it is `open`. Labels are escaped. No `<nav>` is emitted; the
|
|
18
|
+
screen wraps the menu in one where it is a landmark.
|
|
19
|
+
|
|
20
|
+
`{RING}` is the focus-ring classes on every link: DaisyUI's menu sets
|
|
21
|
+
`outline-style: none` on `:focus-visible` and skips `.menu-active`.
|
|
22
|
+
|
|
23
|
+
Modifiers:
|
|
24
|
+
|
|
25
|
+
direction vertical (the default, filling its column), horizontal, or
|
|
26
|
+
responsive (down on a phone, across from `lg` up)
|
|
27
|
+
size xs, sm, md, lg, xl
|
|
28
|
+
|
|
29
|
+
A value outside its set is a render error. #}
|
|
30
|
+
|
|
31
|
+
{% set DIRECTIONS = {
|
|
32
|
+
vertical: 'w-full',
|
|
33
|
+
horizontal: 'menu-horizontal',
|
|
34
|
+
responsive: 'menu-vertical lg:menu-horizontal'
|
|
35
|
+
} %}
|
|
36
|
+
{% set SIZES = { xs: 'menu-xs', sm: 'menu-sm', md: 'menu-md', lg: 'menu-lg', xl: 'menu-xl' } %}
|
|
37
|
+
|
|
38
|
+
{% macro _items(items) -%}
|
|
39
|
+
{% for item in items %}
|
|
40
|
+
<li>
|
|
41
|
+
<a class="focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-base-content{% if item.current %} menu-active{% endif %}" href="{{ item.url }}"{% if item.current %} aria-current="page"{% endif %}>{{ item.label }}</a>
|
|
42
|
+
{% if item.open and item.children.length %}
|
|
43
|
+
<ul>{{ _items(item.children) }}</ul>
|
|
44
|
+
{% endif %}
|
|
45
|
+
</li>
|
|
46
|
+
{% endfor %}
|
|
47
|
+
{%- endmacro %}
|
|
48
|
+
|
|
49
|
+
{% macro menu(items, direction='vertical', size=none) -%}
|
|
50
|
+
<ul class="menu{{ DIRECTIONS | modifier(direction) }}{{ SIZES | modifier(size) }}">{{ _items(items) }}</ul>
|
|
51
|
+
{%- endmacro %}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{# A navbar, the bar across the top of a page.
|
|
2
|
+
|
|
3
|
+
`{% call navbar() %}…{% endcall %}` emits
|
|
4
|
+
`<div class="navbar bg-base-100 shadow-sm">` around its body. Inside it,
|
|
5
|
+
`navbarStart()`, `navbarCenter()` and `navbarEnd()`, each optional, take a
|
|
6
|
+
body through `{% call %}` and emit `<div class="navbar-start">`,
|
|
7
|
+
`navbar-center` and `navbar-end`. Every body is markup. #}
|
|
8
|
+
|
|
9
|
+
{% macro navbar() -%}
|
|
10
|
+
<div class="navbar bg-base-100 shadow-sm">{{ caller() }}</div>
|
|
11
|
+
{%- endmacro %}
|
|
12
|
+
|
|
13
|
+
{% macro navbarStart() -%}
|
|
14
|
+
<div class="navbar-start">{{ caller() }}</div>
|
|
15
|
+
{%- endmacro %}
|
|
16
|
+
|
|
17
|
+
{% macro navbarCenter() -%}
|
|
18
|
+
<div class="navbar-center">{{ caller() }}</div>
|
|
19
|
+
{%- endmacro %}
|
|
20
|
+
|
|
21
|
+
{% macro navbarEnd() -%}
|
|
22
|
+
<div class="navbar-end">{{ caller() }}</div>
|
|
23
|
+
{%- endmacro %}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{# Pagination: newer, where you are, and older, joined into one group of
|
|
2
|
+
buttons.
|
|
3
|
+
|
|
4
|
+
`pagination(page, pages=…, previousUrl=…, nextUrl=…)` emits
|
|
5
|
+
|
|
6
|
+
<nav aria-label="Pages">
|
|
7
|
+
<div class="join">
|
|
8
|
+
<a class="join-item btn btn-sm" rel="prev" href="…">Newer</a>
|
|
9
|
+
<span class="join-item btn btn-sm btn-active" aria-current="page">Page 2 of 5</span>
|
|
10
|
+
<a class="join-item btn btn-sm" rel="next" href="…">Older</a>
|
|
11
|
+
</div>
|
|
12
|
+
</nav>
|
|
13
|
+
|
|
14
|
+
An end with nowhere to go is a `btn-disabled` span. `pages` is left out of
|
|
15
|
+
the middle when it is not given. With neither a newer nor an older page it
|
|
16
|
+
draws nothing.
|
|
17
|
+
|
|
18
|
+
`previousLabel`, `nextLabel` and `label` change the words; all three are
|
|
19
|
+
escaped. #}
|
|
20
|
+
|
|
21
|
+
{% macro _end(url, rel, text) -%}
|
|
22
|
+
{% if url %}
|
|
23
|
+
<a class="join-item btn btn-sm" rel="{{ rel }}" href="{{ url }}">{{ text }}</a>
|
|
24
|
+
{% else %}
|
|
25
|
+
<span class="join-item btn btn-sm btn-disabled" aria-disabled="true">{{ text }}</span>
|
|
26
|
+
{% endif %}
|
|
27
|
+
{%- endmacro %}
|
|
28
|
+
|
|
29
|
+
{% macro pagination(
|
|
30
|
+
page,
|
|
31
|
+
pages=none,
|
|
32
|
+
previousUrl='',
|
|
33
|
+
nextUrl='',
|
|
34
|
+
previousLabel='Newer',
|
|
35
|
+
nextLabel='Older',
|
|
36
|
+
label='Pages'
|
|
37
|
+
) -%}
|
|
38
|
+
{% if previousUrl or nextUrl %}
|
|
39
|
+
<nav aria-label="{{ label }}">
|
|
40
|
+
<div class="join">
|
|
41
|
+
{{ _end(previousUrl, 'prev', previousLabel) }}
|
|
42
|
+
<span class="join-item btn btn-sm btn-active" aria-current="page">Page {{ page }}{% if pages %} of {{ pages }}{% endif %}</span>
|
|
43
|
+
{{ _end(nextUrl, 'next', nextLabel) }}
|
|
44
|
+
</div>
|
|
45
|
+
</nav>
|
|
46
|
+
{% endif %}
|
|
47
|
+
{%- endmacro %}
|
|
@@ -1,43 +1,6 @@
|
|
|
1
|
-
{# The admin bar as the public site
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
root: no selector of the theme's reaches the markup inside, and the
|
|
6
|
-
stylesheet inside reaches nothing outside. The stylesheet is inlined rather
|
|
7
|
-
than linked so the bar is never drawn unstyled; the public site has no
|
|
8
|
-
content CSP to refuse it.
|
|
9
|
-
|
|
10
|
-
The host is fixed to the top of the window, as WordPress's is. In normal
|
|
11
|
-
flow it sat inside whatever padding or margin the theme gave <body>, so it
|
|
12
|
-
stopped short of the window's edges. Out of flow, nothing the theme does to
|
|
13
|
-
<body> moves it, and the inline reset keeps the theme's rules for the host
|
|
14
|
-
element itself, a `body > *` or a grid on <body>, from restyling it; only an
|
|
15
|
-
`!important` rule can, which is also how a theme that insists hides it. The
|
|
16
|
-
page is pushed down from outside, by a stylesheet the CMS puts in <head>
|
|
17
|
-
(OFFSET_STYLE in src/web/admin-bar.ts) reading --geekity-admin-bar-height.
|
|
18
|
-
|
|
19
|
-
The script does two things. It measures the bar and sets that height on
|
|
20
|
-
<html>, so a bar that wraps to two lines on a phone, or one a theme has
|
|
21
|
-
hidden, pushes the page down by what it really is. And it gives focus back
|
|
22
|
-
to the greeting when the menu closes with focus inside it: Chrome does that
|
|
23
|
-
for a popover in the light DOM, not from inside a shadow root, where the
|
|
24
|
-
focus is simply lost. A click elsewhere has already moved focus by then,
|
|
25
|
-
and keeps it. #}
|
|
26
|
-
<geekity-admin-bar style="all: initial; display: block; position: fixed; top: 0; left: 0; right: 0; z-index: 99999"><template shadowrootmode="open"><style>{% include "static/admin-bar.css" %}</style>{% include "components/admin-bar.njk" %}<script>
|
|
27
|
-
(() => {
|
|
28
|
-
const host = document.querySelector('geekity-admin-bar');
|
|
29
|
-
const root = host.shadowRoot;
|
|
30
|
-
new ResizeObserver(() => {
|
|
31
|
-
const height = host.getBoundingClientRect().height;
|
|
32
|
-
document.documentElement.style.setProperty('--geekity-admin-bar-height', `${height}px`);
|
|
33
|
-
}).observe(host);
|
|
34
|
-
const menu = root.getElementById('admin-account-menu');
|
|
35
|
-
menu?.addEventListener('beforetoggle', (event) => {
|
|
36
|
-
if (event.newState === 'closed' && menu.contains(root.activeElement)) {
|
|
37
|
-
setTimeout(() => root.querySelector('.admin-bar-account').focus());
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
})();
|
|
41
|
-
</script></template></geekity-admin-bar>{% if notice %}
|
|
1
|
+
{# The admin bar as the public site includes it (TASK-183, decision-30),
|
|
2
|
+
straight after the theme's <body> tag, with the unpublished notice under
|
|
3
|
+
it. #}
|
|
4
|
+
{% include "components/admin-bar.njk" %}{% if notice %}
|
|
42
5
|
<p class="geekity-unpublished-notice" style="all: initial; display: block; box-sizing: border-box; margin: 0; padding: 8px 16px; background: #fcf9e8; color: #1d2327; border-bottom: 1px solid #dba617; font: 14px/1.5 -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif">{{ notice }}</p>
|
|
43
6
|
{%- endif %}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{# Stats, numbers set out in a row.
|
|
2
|
+
|
|
3
|
+
`{% call stats(…) %}…{% endcall %}` emits
|
|
4
|
+
`<div class="stats bg-base-100 shadow">` around its body, and
|
|
5
|
+
`stat(title, value, desc='', href='')` inside it emits one
|
|
6
|
+
`<div class="stat">` holding a `stat-title`, a `stat-value` and, when it is
|
|
7
|
+
given one, a `stat-desc`. Title, value and description are escaped.
|
|
8
|
+
|
|
9
|
+
Given an `href`, the stat is an `<a class="stat">` instead, so the whole
|
|
10
|
+
stat is the link and its name is the title and the value together; the
|
|
11
|
+
value is drawn in the primary colour to say it leads somewhere.
|
|
12
|
+
|
|
13
|
+
`direction` is horizontal, vertical, or responsive (a column on a phone, a
|
|
14
|
+
row from `lg` up); left out, the stats lie in a row. Any other value is a
|
|
15
|
+
render error. #}
|
|
16
|
+
|
|
17
|
+
{% set DIRECTIONS = {
|
|
18
|
+
horizontal: 'stats-horizontal',
|
|
19
|
+
vertical: 'stats-vertical',
|
|
20
|
+
responsive: 'stats-vertical lg:stats-horizontal'
|
|
21
|
+
} %}
|
|
22
|
+
|
|
23
|
+
{% macro stats(direction=none) -%}
|
|
24
|
+
<div class="stats bg-base-100 shadow{{ DIRECTIONS | modifier(direction) }}">{{ caller() }}</div>
|
|
25
|
+
{%- endmacro %}
|
|
26
|
+
|
|
27
|
+
{% macro stat(title, value, desc='', href='') -%}
|
|
28
|
+
{% if href %}<a class="stat hover:bg-base-200" href="{{ href }}">{% else %}<div class="stat">{% endif %}
|
|
29
|
+
<div class="stat-title">{{ title }}</div>
|
|
30
|
+
<div class="stat-value{% if href %} text-primary{% endif %}">{{ value }}</div>
|
|
31
|
+
{% if desc %}
|
|
32
|
+
<div class="stat-desc">{{ desc }}</div>
|
|
33
|
+
{% endif %}
|
|
34
|
+
{% if href %}</a>{% else %}</div>{% endif %}
|
|
35
|
+
{%- endmacro %}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{# A table on a surface of its own, scrolling sideways inside it instead of
|
|
2
|
+
widening the page.
|
|
3
|
+
|
|
4
|
+
`{% call table(caption, …) %}<thead>…</thead><tbody>…</tbody>{% endcall %}`
|
|
5
|
+
emits
|
|
6
|
+
|
|
7
|
+
<div class="max-w-full overflow-x-auto contain-inline-size rounded-box bg-base-100 p-2 shadow-sm">
|
|
8
|
+
<table class="table table-sm">
|
|
9
|
+
<caption class="sr-only">caption</caption>
|
|
10
|
+
body
|
|
11
|
+
</table>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
The caption is required and escaped. The body is the rows.
|
|
15
|
+
|
|
16
|
+
`contain-inline-size` keeps the table's intrinsic width from sizing the
|
|
17
|
+
card or grid cell around it.
|
|
18
|
+
|
|
19
|
+
Modifiers:
|
|
20
|
+
|
|
21
|
+
size xs, sm (the default), md, lg, xl
|
|
22
|
+
zebra true stripes the rows
|
|
23
|
+
pinRows true keeps the head and foot in view as the rows scroll
|
|
24
|
+
pinCols true keeps the `th` cells of each row in view
|
|
25
|
+
inCard true leaves off the surface, for a table already in a card's body
|
|
26
|
+
|
|
27
|
+
A size outside its set is a render error.
|
|
28
|
+
|
|
29
|
+
Rows that stack below lg instead of scrolling the table sideways. Each
|
|
30
|
+
row is a `{% call row() %}`. Its first cell is written out and ends with
|
|
31
|
+
`rowToggle(name)`; every other cell is
|
|
32
|
+
`{% call cell(label, nowrap) %}value{% endcall %}`, and every heading
|
|
33
|
+
after the first is `heading(label)`, with the same label:
|
|
34
|
+
|
|
35
|
+
<tr class="group …">
|
|
36
|
+
<td>title, row actions <details><summary>Details of name</summary></details></td>
|
|
37
|
+
<td data-label="Author" class="…">ada</td>
|
|
38
|
+
</tr>
|
|
39
|
+
|
|
40
|
+
From lg up the row is a table row and the toggle is not drawn. Below lg
|
|
41
|
+
the row shows its first cell with the toggle at its end, and opening the
|
|
42
|
+
toggle shows each other cell under it as a line of label then value. #}
|
|
43
|
+
|
|
44
|
+
{% set SIZES = { xs: 'table-xs', sm: 'table-sm', md: 'table-md', lg: 'table-lg', xl: 'table-xl' } %}
|
|
45
|
+
|
|
46
|
+
{% macro table(caption, size='sm', zebra=false, pinRows=false, pinCols=false, inCard=false) -%}
|
|
47
|
+
<div class="max-w-full overflow-x-auto contain-inline-size{% if not inCard %} rounded-box bg-base-100 p-2 shadow-sm {%- endif %}">
|
|
48
|
+
<table class="table{{ SIZES | modifier(size) }}{% if zebra %} table-zebra {%- endif %}{% if pinRows %} table-pin-rows {%- endif %}{% if pinCols %} table-pin-cols {%- endif %}">
|
|
49
|
+
<caption class="sr-only">{{ caption }}</caption>
|
|
50
|
+
{{ caller() }}
|
|
51
|
+
</table>
|
|
52
|
+
</div>
|
|
53
|
+
{%- endmacro %}
|
|
54
|
+
|
|
55
|
+
{% macro row() -%}
|
|
56
|
+
<tr class="group *:align-top max-lg:relative max-lg:block max-lg:pe-10 max-lg:not-last:border-b max-lg:border-base-content/5 max-lg:*:border-b-0">{{ caller() }}</tr>
|
|
57
|
+
{%- endmacro %}
|
|
58
|
+
|
|
59
|
+
{% macro heading(label) -%}
|
|
60
|
+
<th scope="col" class="max-lg:hidden">{{ label }}</th>
|
|
61
|
+
{%- endmacro %}
|
|
62
|
+
|
|
63
|
+
{% macro cell(label, nowrap=false) -%}
|
|
64
|
+
<td data-label="{{ label }}" class="{% if nowrap %}whitespace-nowrap {% endif %}max-lg:hidden max-lg:gap-2 max-lg:py-1 max-lg:group-has-open:flex max-lg:before:w-24 max-lg:before:shrink-0 max-lg:before:font-semibold max-lg:before:text-base-content/60 max-lg:before:content-[attr(data-label)]">{{ caller() }}</td>
|
|
65
|
+
{%- endmacro %}
|
|
66
|
+
|
|
67
|
+
{% macro rowToggle(name) -%}
|
|
68
|
+
<details class="absolute end-1 top-1 lg:hidden">
|
|
69
|
+
<summary class="btn btn-ghost btn-sm btn-square">
|
|
70
|
+
<span class="sr-only">Details of {{ name }}</span>
|
|
71
|
+
<span aria-hidden="true" class="-mt-1 size-2 rotate-45 border-e-2 border-b-2 transition-transform group-has-open:mt-1 group-has-open:rotate-225"></span>
|
|
72
|
+
</summary>
|
|
73
|
+
</details>
|
|
74
|
+
{%- endmacro %}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{# Tabs: a row of links between views of one screen.
|
|
2
|
+
|
|
3
|
+
`tabs(items, label, …)` emits
|
|
4
|
+
|
|
5
|
+
<nav class="tabs tabs-box" aria-label="label">
|
|
6
|
+
<a class="tab tab-active" href="…" aria-current="page">All (3)</a>
|
|
7
|
+
<a class="tab" href="…">Spam (0)</a>
|
|
8
|
+
</nav>
|
|
9
|
+
|
|
10
|
+
Each item is `{ label, url, current, count }`: `current` marks the view
|
|
11
|
+
being looked at, and `count`, when there is one, is printed after the
|
|
12
|
+
label. Labels are escaped.
|
|
13
|
+
|
|
14
|
+
No `role="tablist"` or `role="tab"`: those ARIA roles promise panels and
|
|
15
|
+
arrow keys, and these are plain links.
|
|
16
|
+
|
|
17
|
+
Modifiers:
|
|
18
|
+
|
|
19
|
+
style box (the default, a segmented control), border, lift
|
|
20
|
+
size xs, sm, md, lg, xl
|
|
21
|
+
|
|
22
|
+
A value outside its set is a render error. #}
|
|
23
|
+
|
|
24
|
+
{% set STYLES = { box: 'tabs-box', border: 'tabs-border', lift: 'tabs-lift' } %}
|
|
25
|
+
{% set SIZES = { xs: 'tabs-xs', sm: 'tabs-sm', md: 'tabs-md', lg: 'tabs-lg', xl: 'tabs-xl' } %}
|
|
26
|
+
|
|
27
|
+
{% macro tabs(items, label, style='box', size=none) -%}
|
|
28
|
+
<nav class="tabs{{ STYLES | modifier(style) }}{{ SIZES | modifier(size) }}" aria-label="{{ label }}">
|
|
29
|
+
{% for item in items %}
|
|
30
|
+
<a class="tab{% if item.current %} tab-active{% endif %}" href="{{ item.url }}"{% if item.current %} aria-current="page"{% endif %}>{{ item.label }}{% if item.count is defined %} ({{ item.count }}){% endif %}</a>
|
|
31
|
+
{% endfor %}
|
|
32
|
+
</nav>
|
|
33
|
+
{%- endmacro %}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{% import "components/fields.njk" as field %}
|
|
2
|
+
{% from "components/card.njk" import card, cardActions %}
|
|
3
|
+
{% from "components/button.njk" import button %}
|
|
4
|
+
|
|
5
|
+
{% call card(title='Theme') %}
|
|
6
|
+
<form method="post" action="{{ userThemeUrl }}">
|
|
7
|
+
<input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
|
|
8
|
+
{% call field.select('admin-theme', fields.adminTheme, 'Draw the admin in',
|
|
9
|
+
hint='Only you see it. Follow the system draws light or dark as your device is set.') %}
|
|
10
|
+
{% for choice in themeChoices %}
|
|
11
|
+
<option value="{{ choice.value }}"{% if choice.chosen %} selected{% endif %}>{{ choice.label }}</option>
|
|
12
|
+
{% endfor %}
|
|
13
|
+
{% endcall %}
|
|
14
|
+
{% call cardActions() %}
|
|
15
|
+
{{ button('Save theme', type='submit', color='primary') }}
|
|
16
|
+
{% endcall %}
|
|
17
|
+
</form>
|
|
18
|
+
{% endcall %}
|
package/admin/layouts/base.njk
CHANGED
|
@@ -1,29 +1,22 @@
|
|
|
1
|
-
{# The admin's base layout
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
This is the bare page: head, stylesheet and one centred column. Screens that
|
|
6
|
-
need the navigation extend layouts/shell.njk, which extends this and wraps
|
|
7
|
-
the same content block in the admin chrome. Login and setup extend this one
|
|
8
|
-
directly, because a visitor who cannot log in yet has nowhere to navigate. #}
|
|
1
|
+
{# The admin's base layout, the bare page: head, the compiled stylesheet and
|
|
2
|
+
one column. `dataTheme` is the signed-in user's theme; with none, the light
|
|
3
|
+
and dark defaults follow the system. #}
|
|
9
4
|
<!doctype html>
|
|
10
|
-
<html lang="en">
|
|
5
|
+
<html lang="en"{% if dataTheme %} data-theme="{{ dataTheme }}"{% endif %}>
|
|
11
6
|
<head>
|
|
12
7
|
<meta charset="utf-8" />
|
|
13
8
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
14
9
|
<title>{% block title %}Admin{% endblock %}{% if site.title %} · {{ site.title }}{% endif %}</title>
|
|
15
10
|
<meta name="robots" content="noindex, nofollow" />
|
|
16
|
-
<link rel="stylesheet" href="{{ assetPrefix }}admin-bar.css" />
|
|
17
11
|
<link rel="stylesheet" href="{{ assetPrefix }}admin.css" />
|
|
18
12
|
</head>
|
|
19
|
-
<body>
|
|
20
|
-
{# First on every screen, so a keyboard gets past the bar and the menu
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
<a class="admin-skip-link" href="#main">Skip to main content</a>
|
|
13
|
+
<body class="min-h-screen bg-base-200 text-base-content">
|
|
14
|
+
{# First on every screen, so a keyboard gets past the bar and the menu in
|
|
15
|
+
one key. #}
|
|
16
|
+
<a class="fixed top-(--geekity-admin-bar-height) left-0 z-50 -translate-y-full bg-base-100 p-3 font-semibold focus:translate-y-0" href="#main">Skip to main content</a>
|
|
24
17
|
{% block chrome %}{% endblock %}
|
|
25
18
|
{% block main %}
|
|
26
|
-
<main id="main" class="
|
|
19
|
+
<main id="main" class="mx-auto flex min-h-screen max-w-md flex-col justify-center p-6">
|
|
27
20
|
{% include "components/flash.njk" %}
|
|
28
21
|
{% block content %}{% endblock %}
|
|
29
22
|
</main>
|
|
@@ -1,31 +1,36 @@
|
|
|
1
1
|
{# What every settings page is: a heading, one form that saves the fields that
|
|
2
|
-
page carries, and whatever panels sit under it. A page fills in the
|
|
2
|
+
page carries, and whatever panels sit under it. A page fills in the three
|
|
3
3
|
blocks, and names in `settingsProblems` each field a save can refuse, so
|
|
4
4
|
the summary at the top links to it; nothing about the chrome is written six
|
|
5
5
|
times. #}
|
|
6
6
|
{% extends "layouts/shell.njk" %}
|
|
7
7
|
{% import "components/fields.njk" as field %}
|
|
8
|
+
{% from "components/button.njk" import button %}
|
|
8
9
|
|
|
9
10
|
{% block title %}{{ settingsLabel }} settings{% endblock %}
|
|
10
11
|
|
|
11
12
|
{% block content %}
|
|
12
|
-
<h1>{{ settingsLabel }}</h1>
|
|
13
|
+
<h1 class="mb-6 text-2xl font-bold">{{ settingsLabel }}</h1>
|
|
13
14
|
|
|
14
15
|
{% if hasProblems %}
|
|
15
|
-
|
|
16
|
-
{%
|
|
17
|
-
|
|
16
|
+
<div class="mb-6 max-w-2xl">
|
|
17
|
+
{% call field.summary('Nothing was saved. Fix the fields marked below and try again.') %}
|
|
18
|
+
{% block settingsProblems %}{% endblock %}
|
|
19
|
+
{% endcall %}
|
|
20
|
+
</div>
|
|
18
21
|
{% endif %}
|
|
19
22
|
|
|
20
|
-
<form class="
|
|
23
|
+
<form class="flex max-w-2xl flex-col gap-2" method="post" action="{{ settingsUrl }}">
|
|
21
24
|
<input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
|
|
22
25
|
|
|
23
26
|
{% block settingsFields %}{% endblock %}
|
|
24
27
|
|
|
25
|
-
<
|
|
26
|
-
|
|
27
|
-
</
|
|
28
|
+
<div class="mt-4">
|
|
29
|
+
{{ button('Save settings', type='submit', color='primary') }}
|
|
30
|
+
</div>
|
|
28
31
|
</form>
|
|
29
32
|
|
|
30
|
-
|
|
33
|
+
<div class="mt-10 flex max-w-2xl flex-col gap-6">
|
|
34
|
+
{% block settingsPanels %}{% endblock %}
|
|
35
|
+
</div>
|
|
31
36
|
{% endblock %}
|
package/admin/layouts/shell.njk
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
{# The signed-in admin:
|
|
2
|
-
screen itself
|
|
1
|
+
{# The signed-in admin: the bar across the top, the sections down the left,
|
|
2
|
+
the screen itself beside them. Every screen behind the login extends this
|
|
3
|
+
and fills in `content`, starting with its own <h1>. #}
|
|
3
4
|
{% extends "layouts/base.njk" %}
|
|
5
|
+
{% from "components/menu.njk" import menu %}
|
|
4
6
|
|
|
5
7
|
{% block chrome %}
|
|
6
8
|
{# View site, because in here you are not looking at it; + New, as on the
|
|
@@ -15,34 +17,24 @@
|
|
|
15
17
|
{% endblock %}
|
|
16
18
|
|
|
17
19
|
{% block main %}
|
|
18
|
-
<div class="
|
|
19
|
-
{#
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
<
|
|
25
|
-
<
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
{% endif %}
|
|
38
|
-
</li>
|
|
39
|
-
{% endfor %}
|
|
40
|
-
</ul>
|
|
41
|
-
</nav>
|
|
42
|
-
|
|
43
|
-
<main id="main" class="admin-main">
|
|
44
|
-
{% include "components/flash.njk" %}
|
|
45
|
-
{% block content %}{% endblock %}
|
|
46
|
-
</main>
|
|
20
|
+
<div class="drawer lg:drawer-open">
|
|
21
|
+
{# `autocomplete="off"`: a browser going back restores a checkbox's state
|
|
22
|
+
otherwise. #}
|
|
23
|
+
<input id="admin-menu" type="checkbox" class="drawer-toggle" autocomplete="off" />
|
|
24
|
+
{# Before the screen, so a keyboard meets the menu first; DaisyUI's
|
|
25
|
+
drawer places both by grid column. #}
|
|
26
|
+
<div class="drawer-side lg:top-(--geekity-admin-bar-height) lg:h-[calc(100dvh-var(--geekity-admin-bar-height))]">
|
|
27
|
+
<label for="admin-menu" class="drawer-overlay" aria-hidden="true"></label>
|
|
28
|
+
<nav aria-label="Sections" class="min-h-full w-64 bg-base-100 pt-(--geekity-admin-bar-height) lg:pt-0">
|
|
29
|
+
{{ menu(navigation) }}
|
|
30
|
+
</nav>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="drawer-content">
|
|
33
|
+
<label for="admin-menu" class="btn btn-sm drawer-button mx-4 mt-4 lg:hidden">Menu</label>
|
|
34
|
+
<main id="main" class="p-4 sm:p-6 lg:p-8">
|
|
35
|
+
{% include "components/flash.njk" %}
|
|
36
|
+
{% block content %}{% endblock %}
|
|
37
|
+
</main>
|
|
38
|
+
</div>
|
|
47
39
|
</div>
|
|
48
40
|
{% endblock %}
|
|
@@ -6,42 +6,48 @@
|
|
|
6
6
|
out which accounts a site has. #}
|
|
7
7
|
{% extends "layouts/base.njk" %}
|
|
8
8
|
{% import "components/fields.njk" as field %}
|
|
9
|
+
{% from "components/button.njk" import button %}
|
|
10
|
+
{% from "components/card.njk" import card %}
|
|
9
11
|
|
|
10
12
|
{% block title %}Forgotten password{% endblock %}
|
|
11
13
|
|
|
12
14
|
{% block content %}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
{
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
{
|
|
15
|
+
{% call card() %}
|
|
16
|
+
<h1 class="card-title text-2xl">Forgotten password</h1>
|
|
17
|
+
|
|
18
|
+
{% if not configured %}
|
|
19
|
+
{# No provider and no credential, so there is nothing to send a link
|
|
20
|
+
with. Saying so beats a form that quietly does nothing. #}
|
|
21
|
+
<p>This site cannot send email, so a password cannot be reset from here.</p>
|
|
22
|
+
<p>
|
|
23
|
+
Whoever runs the server can add an account from a shell with
|
|
24
|
+
<code>geekity user add <username></code>, and sign in with that to
|
|
25
|
+
change anything else. Turning on a mail provider under
|
|
26
|
+
<strong>Settings → Email</strong> makes this page work.
|
|
27
|
+
</p>
|
|
28
|
+
<p><a class="link" href="{{ loginUrl }}">Back to the login form</a></p>
|
|
29
|
+
{% elif sent %}
|
|
30
|
+
<p>{{ answer }}</p>
|
|
31
|
+
<p><a class="link" href="{{ loginUrl }}">Back to the login form</a></p>
|
|
32
|
+
{% else %}
|
|
33
|
+
<p>Type your username or your email address, and a link to set a new password is sent to the address on your account.</p>
|
|
34
|
+
|
|
35
|
+
{% if error %}
|
|
36
|
+
{{ field.summary(error) }}
|
|
37
|
+
{% endif %}
|
|
38
|
+
|
|
39
|
+
<form class="flex flex-col gap-2" method="post" action="{{ forgotUrl }}">
|
|
40
|
+
<input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
|
|
41
|
+
|
|
42
|
+
{{ field.text('identifier', fields.identifier, 'Username or email address',
|
|
43
|
+
autocomplete='username', required=true, autofocus=true) }}
|
|
44
|
+
|
|
45
|
+
<div class="mt-4">
|
|
46
|
+
{{ button('Send a reset link', type='submit', color='primary', shape='block') }}
|
|
47
|
+
</div>
|
|
48
|
+
</form>
|
|
49
|
+
|
|
50
|
+
<p class="text-sm"><a class="link" href="{{ loginUrl }}">Back to the login form</a></p>
|
|
34
51
|
{% endif %}
|
|
35
|
-
|
|
36
|
-
<form method="post" action="{{ forgotUrl }}">
|
|
37
|
-
<input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
|
|
38
|
-
|
|
39
|
-
{{ field.text('identifier', fields.identifier, 'Username or email address',
|
|
40
|
-
autocomplete='username', required=true, autofocus=true) }}
|
|
41
|
-
|
|
42
|
-
<button type="submit">Send a reset link</button>
|
|
43
|
-
</form>
|
|
44
|
-
|
|
45
|
-
<p class="admin-hint"><a href="{{ loginUrl }}">Back to the login form</a></p>
|
|
46
|
-
{% endif %}
|
|
52
|
+
{% endcall %}
|
|
47
53
|
{% endblock %}
|
|
@@ -1,29 +1,35 @@
|
|
|
1
1
|
{% extends "layouts/base.njk" %}
|
|
2
2
|
{% import "components/fields.njk" as field %}
|
|
3
|
+
{% from "components/button.njk" import button %}
|
|
4
|
+
{% from "components/card.njk" import card %}
|
|
3
5
|
|
|
4
6
|
{% block title %}Log in{% endblock %}
|
|
5
7
|
|
|
6
8
|
{% block content %}
|
|
7
|
-
|
|
9
|
+
{% call card() %}
|
|
10
|
+
<h1 class="card-title text-2xl">Log in</h1>
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
{% endif %}
|
|
12
|
-
|
|
13
|
-
<form method="post" action="{{ loginUrl }}">
|
|
14
|
-
<input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
|
|
15
|
-
{% if returnTo %}
|
|
16
|
-
<input type="hidden" name="return_to" value="{{ returnTo }}" />
|
|
12
|
+
{% if error %}
|
|
13
|
+
{{ field.summary(error) }}
|
|
17
14
|
{% endif %}
|
|
18
15
|
|
|
19
|
-
{{
|
|
20
|
-
|
|
16
|
+
<form class="flex flex-col gap-2" method="post" action="{{ loginUrl }}">
|
|
17
|
+
<input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
|
|
18
|
+
{% if returnTo %}
|
|
19
|
+
<input type="hidden" name="return_to" value="{{ returnTo }}" />
|
|
20
|
+
{% endif %}
|
|
21
|
+
|
|
22
|
+
{{ field.text('username', 'username', 'Username', username,
|
|
23
|
+
autocomplete='username', required=true, autofocus=true) }}
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
{{ field.password('password', 'password', 'Password',
|
|
26
|
+
autocomplete='current-password', required=true) }}
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
<div class="mt-4">
|
|
29
|
+
{{ button('Log in', type='submit', color='primary', shape='block') }}
|
|
30
|
+
</div>
|
|
31
|
+
</form>
|
|
27
32
|
|
|
28
|
-
|
|
33
|
+
<p class="text-sm"><a class="link" href="{{ forgotUrl }}">Forgotten your password?</a></p>
|
|
34
|
+
{% endcall %}
|
|
29
35
|
{% endblock %}
|