@geekity/cms 0.10.0 → 0.12.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 (83) hide show
  1. package/README.md +22 -5
  2. package/admin/components/fields.njk +55 -12
  3. package/admin/layouts/base.njk +5 -1
  4. package/admin/layouts/settings-page.njk +7 -2
  5. package/admin/layouts/shell.njk +1 -1
  6. package/admin/pages/account/forgot.njk +1 -1
  7. package/admin/pages/account/login.njk +1 -1
  8. package/admin/pages/account/reset.njk +4 -2
  9. package/admin/pages/account/setup.njk +1 -1
  10. package/admin/pages/dashboard/home.njk +1 -0
  11. package/admin/pages/documents/list.njk +1 -0
  12. package/admin/pages/documents/taxonomy.njk +1 -0
  13. package/admin/pages/federation/followers.njk +3 -0
  14. package/admin/pages/federation/settings.njk +5 -0
  15. package/admin/pages/media/library.njk +39 -0
  16. package/admin/pages/navigation/menus.njk +6 -1
  17. package/admin/pages/settings/discussion.njk +7 -0
  18. package/admin/pages/settings/email.njk +10 -0
  19. package/admin/pages/settings/general.njk +7 -0
  20. package/admin/pages/settings/permalinks.njk +6 -0
  21. package/admin/pages/settings/reading.njk +7 -0
  22. package/admin/pages/tools/content-index.njk +1 -0
  23. package/admin/pages/tools/personal-data.njk +5 -0
  24. package/admin/pages/users/edit.njk +7 -5
  25. package/admin/pages/users/list.njk +1 -0
  26. package/admin/pages/users/new.njk +5 -1
  27. package/admin/static/admin-bar.css +16 -0
  28. package/admin/static/admin.css +103 -3
  29. package/dist/admin/documents.d.ts.map +1 -1
  30. package/dist/admin/documents.js +18 -0
  31. package/dist/admin/documents.js.map +1 -1
  32. package/dist/admin/media.d.ts +13 -2
  33. package/dist/admin/media.d.ts.map +1 -1
  34. package/dist/admin/media.js +58 -3
  35. package/dist/admin/media.js.map +1 -1
  36. package/dist/admin/store.d.ts +1 -1
  37. package/dist/admin/store.d.ts.map +1 -1
  38. package/dist/admin/store.js +1 -1
  39. package/dist/admin/store.js.map +1 -1
  40. package/dist/admin/uploads.d.ts +5 -2
  41. package/dist/admin/uploads.d.ts.map +1 -1
  42. package/dist/admin/uploads.js +6 -2
  43. package/dist/admin/uploads.js.map +1 -1
  44. package/dist/config.d.ts +10 -0
  45. package/dist/config.d.ts.map +1 -1
  46. package/dist/config.js +1 -0
  47. package/dist/config.js.map +1 -1
  48. package/dist/federation/article.d.ts.map +1 -1
  49. package/dist/federation/article.js +37 -1
  50. package/dist/federation/article.js.map +1 -1
  51. package/dist/images/alt-text.d.ts +51 -0
  52. package/dist/images/alt-text.d.ts.map +1 -0
  53. package/dist/images/alt-text.js +113 -0
  54. package/dist/images/alt-text.js.map +1 -0
  55. package/dist/images/markup.d.ts +11 -0
  56. package/dist/images/markup.d.ts.map +1 -1
  57. package/dist/images/markup.js +12 -0
  58. package/dist/images/markup.js.map +1 -1
  59. package/dist/web/render.d.ts.map +1 -1
  60. package/dist/web/render.js +27 -12
  61. package/dist/web/render.js.map +1 -1
  62. package/package.json +14 -9
  63. package/themes/default/README.md +230 -106
  64. package/themes/default/layouts/404.njk +2 -0
  65. package/themes/default/layouts/500.njk +2 -0
  66. package/themes/default/layouts/503.njk +2 -0
  67. package/themes/default/layouts/author.njk +3 -1
  68. package/themes/default/layouts/base.njk +1 -1
  69. package/themes/default/layouts/category.njk +4 -2
  70. package/themes/default/layouts/front-page.njk +1 -1
  71. package/themes/default/layouts/page.njk +4 -2
  72. package/themes/default/layouts/post.njk +41 -23
  73. package/themes/default/layouts/search.njk +2 -5
  74. package/themes/default/layouts/tag.njk +7 -1
  75. package/themes/default/partials/comment-form.njk +41 -14
  76. package/themes/default/partials/contact-form.njk +35 -14
  77. package/themes/default/partials/conversation.njk +11 -4
  78. package/themes/default/partials/kicker.njk +34 -0
  79. package/themes/default/partials/post-list.njk +40 -46
  80. package/themes/default/partials/reply-context.njk +12 -5
  81. package/themes/default/src/style.css +1095 -0
  82. package/themes/default/static/style.css +2079 -1083
  83. package/themes/default/theme.json +1 -1
@@ -1,16 +1,22 @@
1
1
  {#
2
- One post, as the source design's `content-essay.php` draws it (decision-16,
3
- TASK-83): `article.blog-post.h-entry`, the title as its `p-name`, the body
4
- and the Published line together in one `section.e-content`, then a rule and a
5
- footer holding the bio. The neighbouring posts, the conversation and the
6
- comment form follow the entry.
2
+ One post, in the Paper design (doc-9): `article.blog-post.h-entry` opening on
3
+ a kicker that names its kind (Article, Note, Reply) and the categories it is
4
+ filed under.
5
+
6
+ An article, and a reply with a title, has the only headline: the title as its
7
+ `p-name` and the author's `description` under it as the deck, a
8
+ `p-summary`. A post with no title opens on its words, set larger. A reply
9
+ cites what it answers above its words, or above its header when it has one.
10
+ Then the Published line, the tags, a rule and the bio. The neighbouring posts,
11
+ the conversation and the comment form follow the entry.
7
12
 
8
13
  The Published line is inside the `e-content` on purpose. It is what the
9
- source theme does, and it is what makes a reader who copies the words out
14
+ source design does, and it is what makes a reader who copies the words out
10
15
  take the date and the permalink with them.
11
16
  #}
12
17
  {% extends "layouts/base.njk" %}
13
18
  {% import "partials/tags.njk" as taxonomy with context %}
19
+ {% import "partials/kicker.njk" as kicker with context %}
14
20
 
15
21
  {#- Who the bio credits: the person the file names, and otherwise whoever the
16
22
  site is by, which is what the source theme's bio reads. A post naming
@@ -24,16 +30,30 @@
24
30
  {% set entryUrl = page.url or url %}
25
31
  <article class="blog-post h-entry">
26
32
  {#- A post with no name of its own (a note, or a reply without a title) is
27
- its words, so it opens on them rather than on an empty heading. -#}
33
+ its words, so it opens on them in sight. Its page still needs one h1 to
34
+ navigate by, so it gets one that says what it is, who wrote it and when,
35
+ hidden from sight. It is no `p-name`: a parser would take that as a
36
+ title and read the note as an article. -#}
28
37
  {% if named %}
29
- <header>
30
- <h1 class="p-name">{{ title }}</h1>
31
- </header>
32
- {% endif %}
33
-
34
38
  {#- A reply cites the post it answers, whose URL is also the link its
35
39
  webmention points a receiver at. -#}
36
40
  {% include "partials/reply-context.njk" %}
41
+ <header class="post-header">
42
+ {{ kicker.line(postType | capitalize, false, false, categories) }}
43
+ <h1 class="p-name">{{ title }}</h1>
44
+ {% if description %}
45
+ <p class="post-deck p-summary">{{ description }}</p>
46
+ {% endif %}
47
+ </header>
48
+ {% else %}
49
+ <h1 class="screen-reader-text">
50
+ {{- "Reply" if postType == "reply" else "Note" }}
51
+ {%- if bioAuthor.name %} by {{ bioAuthor.name }}{% endif %}
52
+ {%- if date %}, {{ date | date }}{% endif -%}
53
+ </h1>
54
+ {{ kicker.line(postType | capitalize, false, false, categories) }}
55
+ {% include "partials/reply-context.njk" %}
56
+ {% endif %}
37
57
 
38
58
  <section class="e-content">
39
59
  {{ content | safe }}
@@ -49,16 +69,14 @@
49
69
  {#- Updated only when it is a different day in the site's own timezone:
50
70
  a typo fixed an hour after publishing is not news. -#}
51
71
  {%- if updated and (not date or (updated | date("html")) != (date | date("html"))) %}
52
- {% if date %}<br>{% endif %}<time class="small dt-updated" datetime="{{ updated | date("iso") }}">Updated {{ updated | date }}</time>
72
+ {% if date %}<span aria-hidden="true">·</span> {% endif %}<time class="small dt-updated" datetime="{{ updated | date("iso") }}">Updated {{ updated | date }}</time>
53
73
  {%- endif -%}
54
74
  </p>
55
75
  </section>
56
76
 
57
- {# What it is filed under, inside the entry so that each link is a
58
- `p-category` of it: the categories first, then the tags. #}
59
- {{ taxonomy.categories(categories) }}
77
+ {# The tags, inside the entry so that each link is a `p-category` of it. The
78
+ categories are in the kicker. #}
60
79
  {{ taxonomy.list(tags) }}
61
-
62
80
  {#- The rule and the footer are the bio's own: a post nobody is credited with
63
81
  ends at its words rather than at an empty box. -#}
64
82
  {% if bioAuthor %}
@@ -69,17 +87,17 @@
69
87
  {% endif %}
70
88
  </article>
71
89
 
72
- {# The post either side of this one. Each is on the context only when there is
73
- one, so the ends of the archive draw one arrow and a site of a single post
74
- draws none. #}
90
+ {# The post either side of this one, as two cards. Each is on the context only
91
+ when there is one, so the ends of the archive draw one card and a site of a
92
+ single post draws none. #}
75
93
  {% if previous or next %}
76
- <nav class="blog-post-nav">
94
+ <nav class="blog-post-nav" aria-label="Neighbouring posts">
77
95
  <ul>
78
96
  {% if previous %}
79
- <li><a rel="prev" href="{{ previous.url | url }}">&larr; {{ previous.title }}</a></li>
97
+ <li><a rel="prev" href="{{ previous.url | url }}"><span class="blog-post-nav-label">Previous</span> {{ previous.title }}</a></li>
80
98
  {% endif %}
81
99
  {% if next %}
82
- <li><a rel="next" href="{{ next.url | url }}">{{ next.title }} &rarr;</a></li>
100
+ <li><a rel="next" href="{{ next.url | url }}"><span class="blog-post-nav-label">Next</span> {{ next.title }}</a></li>
83
101
  {% endif %}
84
102
  </ul>
85
103
  </nav>
@@ -20,6 +20,7 @@
20
20
  <meta name="robots" content="noindex">
21
21
  {% endblock %}
22
22
 
23
+ {% import "partials/kicker.njk" as kicker with context %}
23
24
  {% block content %}
24
25
  <h1 class="page-title">Search</h1>
25
26
  {% include "partials/search-form.njk" %}
@@ -37,6 +38,7 @@
37
38
  <div class="search-results">
38
39
  {% for post in posts %}
39
40
  <article class="search-result h-entry">
41
+ {{ kicker.line(("Page" if post.type == "page" else post.postType) | capitalize, post.date) }}
40
42
  {#- A post with no name of its own has none to claim, so its first words
41
43
  link it without being marked up as one. -#}
42
44
  <h2 class="feed-title{% if post.named %} p-name{% endif %}">
@@ -45,11 +47,6 @@
45
47
  {% if post.snippet %}
46
48
  <p class="search-snippet">{{ post.snippet | safe }}</p>
47
49
  {% endif %}
48
- {% if post.date %}
49
- <p class="feed-meta">
50
- <time class="feed-date dt-published" datetime="{{ post.date | date("iso") }}">{{ post.date | date }}</time>
51
- </p>
52
- {% endif %}
53
50
  </article>
54
51
  {% endfor %}
55
52
  </div>
@@ -9,8 +9,14 @@
9
9
  {{ feeds.feedLinks(tagRoot, site.title + ": " + tag) }}
10
10
  {% endblock %}
11
11
 
12
+ {% import "partials/kicker.njk" as kicker with context %}
12
13
  {% block content %}
13
- <h1 class="page-title">Tagged &ldquo;{{ tag }}&rdquo;</h1>
14
+ {# The archive's header: the kicker says what kind of archive, the heading
15
+ whose. #}
16
+ <header class="archive-header">
17
+ {{ kicker.line("Tag") }}
18
+ <h1 class="page-title">{{ tag }}</h1>
19
+ </header>
14
20
  {% include "partials/post-list.njk" %}
15
21
  {% include "partials/pagination.njk" %}
16
22
  {% endblock %}
@@ -24,13 +24,37 @@
24
24
  #}
25
25
  <section class="comment-respond" id="respond">
26
26
  <h2 class="comment-reply-title">Leave a comment</h2>
27
+ {#- A site that takes webmentions says so here, where somebody deciding how
28
+ to answer is looking. -#}
29
+ {% if webmention %}
30
+ <p class="comment-help">Or reply from your own site and send a <a href="https://indieweb.org/Webmention">webmention</a>.</p>
31
+ {% endif %}
27
32
 
28
33
  {% if commentNotice %}
29
34
  <p class="comment-notice" role="status">{{ commentNotice }}</p>
30
35
  {% endif %}
31
36
 
32
- {% if commentForm.error %}
33
- <p class="comment-error" role="alert">{{ commentForm.error }}</p>
37
+ {#
38
+ A refused submission leads with this: what went wrong, and a link to each
39
+ field to put right. It is an alert and takes focus on load, so a screen
40
+ reader hears it at once and a keyboard starts from it, with no script. A
41
+ field it links to is marked `aria-invalid` and names its message with
42
+ `aria-describedby`, so the message is read with the field.
43
+ #}
44
+ {% set problems = commentForm.problems %}
45
+ {% if commentForm.error or problems.name or problems.email or problems.url or problems.body %}
46
+ <div class="comment-error" role="alert" tabindex="-1" autofocus aria-labelledby="comment-error-heading">
47
+ <h3 id="comment-error-heading">{{ commentForm.error or 'Your comment was not posted. Fix the fields marked below.' }}</h3>
48
+ {% if problems.name or problems.email or problems.url or problems.body %}
49
+ <ul>
50
+ {% for key in ['name', 'email', 'url', 'body'] %}
51
+ {% if problems[key] %}
52
+ <li><a href="#comment-{{ key }}">{{ problems[key] }}</a></li>
53
+ {% endif %}
54
+ {% endfor %}
55
+ </ul>
56
+ {% endif %}
57
+ </div>
34
58
  {% endif %}
35
59
 
36
60
  <form method="post" action="{{ commentForm.action }}">
@@ -81,36 +105,39 @@
81
105
  <input id="comment-website" name="{{ commentForm.fields.trap }}" type="text" tabindex="-1" autocomplete="off" value="" />
82
106
  </div>
83
107
 
108
+ {#- Name and email side by side on a wide screen. -#}
109
+ <div class="field-row">
84
110
  <p class="comment-field">
85
111
  <label for="comment-name">Name</label>
86
- <input id="comment-name" name="{{ commentForm.fields.name }}" type="text" value="{{ commentForm.values.name }}" maxlength="{{ commentForm.nameLength }}" autocomplete="name" required />
87
- {% if commentForm.problems.name %}
88
- <span class="comment-field-error">{{ commentForm.problems.name }}</span>
112
+ <input id="comment-name" name="{{ commentForm.fields.name }}" type="text" value="{{ commentForm.values.name }}" maxlength="{{ commentForm.nameLength }}" autocomplete="name" required{% if problems.name %} aria-invalid="true" aria-describedby="comment-name-error"{% endif %} />
113
+ {% if problems.name %}
114
+ <span class="comment-field-error" id="comment-name-error">{{ problems.name }}</span>
89
115
  {% endif %}
90
116
  </p>
91
117
 
92
118
  <p class="comment-field">
93
119
  <label for="comment-email">Email <span class="comment-optional">(optional, never shown)</span></label>
94
- <input id="comment-email" name="{{ commentForm.fields.email }}" type="email" value="{{ commentForm.values.email }}" autocomplete="email" />
95
- {% if commentForm.problems.email %}
96
- <span class="comment-field-error">{{ commentForm.problems.email }}</span>
120
+ <input id="comment-email" name="{{ commentForm.fields.email }}" type="email" value="{{ commentForm.values.email }}" autocomplete="email"{% if problems.email %} aria-invalid="true" aria-describedby="comment-email-error"{% endif %} />
121
+ {% if problems.email %}
122
+ <span class="comment-field-error" id="comment-email-error">{{ problems.email }}</span>
97
123
  {% endif %}
98
124
  </p>
125
+ </div>
99
126
 
100
127
  <p class="comment-field">
101
128
  <label for="comment-url">Your site <span class="comment-optional">(optional)</span></label>
102
- <input id="comment-url" name="{{ commentForm.fields.url }}" type="text" inputmode="url" value="{{ commentForm.values.url }}" autocomplete="url" />
103
- {% if commentForm.problems.url %}
104
- <span class="comment-field-error">{{ commentForm.problems.url }}</span>
129
+ <input id="comment-url" name="{{ commentForm.fields.url }}" type="text" inputmode="url" value="{{ commentForm.values.url }}" autocomplete="url"{% if problems.url %} aria-invalid="true" aria-describedby="comment-url-error"{% endif %} />
130
+ {% if problems.url %}
131
+ <span class="comment-field-error" id="comment-url-error">{{ problems.url }}</span>
105
132
  {% endif %}
106
133
  </p>
107
134
  {% endif %}
108
135
 
109
136
  <p class="comment-field">
110
137
  <label for="comment-body">Comment</label>
111
- <textarea id="comment-body" name="{{ commentForm.fields.body }}" rows="6" maxlength="{{ commentForm.bodyLength }}" required>{{ commentForm.values.body }}</textarea>
112
- {% if commentForm.problems.body %}
113
- <span class="comment-field-error">{{ commentForm.problems.body }}</span>
138
+ <textarea id="comment-body" name="{{ commentForm.fields.body }}" rows="6" maxlength="{{ commentForm.bodyLength }}" required{% if problems.body %} aria-invalid="true" aria-describedby="comment-body-error"{% endif %}>{{ commentForm.values.body }}</textarea>
139
+ {% if problems.body %}
140
+ <span class="comment-field-error" id="comment-body-error">{{ problems.body }}</span>
114
141
  {% endif %}
115
142
  <span class="comment-hint">Markdown works. Links are marked <code>nofollow ugc</code>; HTML is not.</span>
116
143
  </p>
@@ -29,8 +29,26 @@
29
29
  {% else %}
30
30
  <h2 class="contact-form-title">Send a message</h2>
31
31
 
32
- {% if contactForm.error %}
33
- <p class="contact-error" role="alert">{{ contactForm.error }}</p>
32
+ {#
33
+ A refused submission leads with this, as the comment form does: what went
34
+ wrong, and a link to each field to put right. It is an alert and takes
35
+ focus on load; a field it links to is marked `aria-invalid` and names its
36
+ message with `aria-describedby`.
37
+ #}
38
+ {% set problems = contactForm.problems %}
39
+ {% if contactForm.error or problems.name or problems.email or problems.subject or problems.message %}
40
+ <div class="contact-error" role="alert" tabindex="-1" autofocus aria-labelledby="contact-error-heading">
41
+ <h3 id="contact-error-heading">{{ contactForm.error or 'Your message was not sent. Fix the fields marked below.' }}</h3>
42
+ {% if problems.name or problems.email or problems.subject or problems.message %}
43
+ <ul>
44
+ {% for key in ['name', 'email', 'subject', 'message'] %}
45
+ {% if problems[key] %}
46
+ <li><a href="#contact-{{ key }}">{{ problems[key] }}</a></li>
47
+ {% endif %}
48
+ {% endfor %}
49
+ </ul>
50
+ {% endif %}
51
+ </div>
34
52
  {% endif %}
35
53
 
36
54
  <form method="post" action="{{ contactForm.action }}">
@@ -47,36 +65,39 @@
47
65
  <input id="contact-website" name="{{ contactForm.fields.trap }}" type="text" tabindex="-1" autocomplete="off" value="" />
48
66
  </div>
49
67
 
68
+ {#- Name and email side by side on a wide screen. -#}
69
+ <div class="field-row">
50
70
  <p class="contact-field">
51
71
  <label for="contact-name">Name</label>
52
- <input id="contact-name" name="{{ contactForm.fields.name }}" type="text" value="{{ contactForm.values.name }}" maxlength="{{ contactForm.nameLength }}" autocomplete="name" required />
53
- {% if contactForm.problems.name %}
54
- <span class="contact-field-error">{{ contactForm.problems.name }}</span>
72
+ <input id="contact-name" name="{{ contactForm.fields.name }}" type="text" value="{{ contactForm.values.name }}" maxlength="{{ contactForm.nameLength }}" autocomplete="name" required{% if problems.name %} aria-invalid="true" aria-describedby="contact-name-error"{% endif %} />
73
+ {% if problems.name %}
74
+ <span class="contact-field-error" id="contact-name-error">{{ problems.name }}</span>
55
75
  {% endif %}
56
76
  </p>
57
77
 
58
78
  <p class="contact-field">
59
79
  <label for="contact-email">Email</label>
60
- <input id="contact-email" name="{{ contactForm.fields.email }}" type="email" value="{{ contactForm.values.email }}" autocomplete="email" required />
61
- {% if contactForm.problems.email %}
62
- <span class="contact-field-error">{{ contactForm.problems.email }}</span>
80
+ <input id="contact-email" name="{{ contactForm.fields.email }}" type="email" value="{{ contactForm.values.email }}" autocomplete="email" required{% if problems.email %} aria-invalid="true" aria-describedby="contact-email-error"{% endif %} />
81
+ {% if problems.email %}
82
+ <span class="contact-field-error" id="contact-email-error">{{ problems.email }}</span>
63
83
  {% endif %}
64
84
  <span class="contact-hint">So there is somewhere to reply to. It is never shown on the site.</span>
65
85
  </p>
86
+ </div>
66
87
 
67
88
  <p class="contact-field">
68
89
  <label for="contact-subject">Subject <span class="contact-optional">(optional)</span></label>
69
- <input id="contact-subject" name="{{ contactForm.fields.subject }}" type="text" value="{{ contactForm.values.subject }}" maxlength="{{ contactForm.subjectLength }}" />
70
- {% if contactForm.problems.subject %}
71
- <span class="contact-field-error">{{ contactForm.problems.subject }}</span>
90
+ <input id="contact-subject" name="{{ contactForm.fields.subject }}" type="text" value="{{ contactForm.values.subject }}" maxlength="{{ contactForm.subjectLength }}"{% if problems.subject %} aria-invalid="true" aria-describedby="contact-subject-error"{% endif %} />
91
+ {% if problems.subject %}
92
+ <span class="contact-field-error" id="contact-subject-error">{{ problems.subject }}</span>
72
93
  {% endif %}
73
94
  </p>
74
95
 
75
96
  <p class="contact-field">
76
97
  <label for="contact-message">Message</label>
77
- <textarea id="contact-message" name="{{ contactForm.fields.message }}" rows="8" maxlength="{{ contactForm.messageLength }}" required>{{ contactForm.values.message }}</textarea>
78
- {% if contactForm.problems.message %}
79
- <span class="contact-field-error">{{ contactForm.problems.message }}</span>
98
+ <textarea id="contact-message" name="{{ contactForm.fields.message }}" rows="8" maxlength="{{ contactForm.messageLength }}" required{% if problems.message %} aria-invalid="true" aria-describedby="contact-message-error"{% endif %}>{{ contactForm.values.message }}</textarea>
99
+ {% if problems.message %}
100
+ <span class="contact-field-error" id="contact-message-error">{{ problems.message }}</span>
80
101
  {% endif %}
81
102
  </p>
82
103
 
@@ -42,7 +42,7 @@
42
42
  {% macro group(items, label, kind, icon, source=false) %}
43
43
  {% if items.length %}
44
44
  <div class="reaction-group {{ kind }}">
45
- <h2 class="reaction-title">{{ label }} ({{ items.length }})</h2>
45
+ <h2 class="reaction-title">{{ label }} <span class="reaction-count">{{ items.length }}</span></h2>
46
46
  <div class="facepile">
47
47
  {%- for item in items %}{{ face(item, icon, (item.url or item.author.url) if source else (item.author.url or item.url)) }}{% endfor %}
48
48
  </div>
@@ -61,6 +61,8 @@
61
61
  <div class="comment-author vcard p-author h-card">
62
62
  {%- if reply.author.avatar %}
63
63
  <img class="avatar u-photo" src="{{ reply.author.avatar }}" alt="" width="40" height="40" loading="lazy">
64
+ {%- else %}
65
+ <span class="comment-avatar-empty" aria-hidden="true">{{ reply.author.name | first | upper }}</span>
64
66
  {%- endif %}
65
67
  <b class="fn p-name">{% if reply.author.url %}<a class="url u-url" href="{{ reply.author.url }}" rel="nofollow ugc noopener noreferrer">{{ reply.author.name }}</a>{% else %}{{ reply.author.name }}{% endif %}</b>
66
68
  </div>
@@ -70,6 +72,11 @@
70
72
  {%- else %}
71
73
  <time class="dt-published" datetime="{{ reply.published | date("iso") }}">{{ reply.published | date }}</time>
72
74
  {%- endif %}
75
+ {%- if reply.source == "webmention" %}
76
+ <span class="comment-source">via webmention</span>
77
+ {%- elif reply.source == "activitypub" %}
78
+ <span class="comment-source">via the fediverse</span>
79
+ {%- endif %}
73
80
  </div>
74
81
  </footer>
75
82
  <div class="comment-content e-content">
@@ -111,8 +118,8 @@
111
118
 
112
119
  {#
113
120
  And the thread. The title counts the whole of it rather than the top level,
114
- which is what `counts.replies` is, and names the post the way the source
115
- theme does.
121
+ which is what `counts.replies` is. It does not quote the post's title: the
122
+ post is the page it sits under, and a note has no title to quote (TASK-186).
116
123
 
117
124
  "Comments are closed." is printed under a post that has been answered and is
118
125
  no longer taking comments. That a post is closed is what the absence of
@@ -123,7 +130,7 @@
123
130
  {% if conversation.replies.length %}
124
131
  <div id="comments" class="comments-area">
125
132
  <h2 class="comments-title">
126
- {%- if conversation.counts.replies == 1 %}One comment{% else %}{{ conversation.counts.replies }} comments{% endif %} on &ldquo;<span>{{ title }}</span>&rdquo;
133
+ {%- if conversation.counts.replies == 1 %}One reply{% else %}{{ conversation.counts.replies }} replies{% endif -%}
127
134
  </h2>
128
135
 
129
136
  <ol class="comment-list">
@@ -0,0 +1,34 @@
1
+ {#
2
+ The kicker: what an entry is, said once, small, above it (doc-9). The kind
3
+ word first, then the date, then the categories, each after a middle dot the
4
+ screen reader skips.
5
+
6
+ Import it with the context, because the category links read the archive
7
+ base off `site`:
8
+
9
+ {% import "partials/kicker.njk" as kicker with context %}
10
+ {{ kicker.line("Note", post.date, post.url) }}
11
+
12
+ `kind` is the word printed: Article, Note or Reply for a post (its
13
+ `postType`, capitalised), Page for a page, and whatever names an archive or
14
+ an error page. `date` is the entry's `dt-published`, printed long. With
15
+ `href` the date is the entry's `u-url`, which is how a feed links an entry
16
+ that has no title to link; an entry with no date links `label` instead.
17
+ `categories` are printed as `p-category` links.
18
+ #}
19
+ {% macro line(kind, date=false, href=false, categories=[], label="") %}
20
+ <p class="kicker">
21
+ <span class="kicker-kind">{{ kind }}</span>
22
+ {%- if href %}
23
+ <span class="kicker-dot" aria-hidden="true">·</span>
24
+ <a href="{{ href | url }}" class="u-url">{% if date %}<time class="feed-date dt-published" datetime="{{ date | date("iso") }}">{{ date | date }}</time>{% else %}{{ label }}{% endif %}</a>
25
+ {%- elif date %}
26
+ <span class="kicker-dot" aria-hidden="true">·</span>
27
+ <time class="feed-date dt-published" datetime="{{ date | date("iso") }}">{{ date | date }}</time>
28
+ {%- endif %}
29
+ {%- for category in categories %}
30
+ <span class="kicker-dot" aria-hidden="true">·</span>
31
+ <a href="{{ ("/" + (site.categoryBase or "category") + "/" + (category | urlencode) + "/") | url }}" class="p-category" rel="category">{{ category }}</a>
32
+ {%- endfor %}
33
+ </p>
34
+ {% endmacro %}
@@ -1,21 +1,23 @@
1
1
  {#
2
- The feed a listing is made of: `h-feed` of `h-entry` items, as the source
3
- design lists them (decision-16).
2
+ The feed a listing is made of: `h-feed` of `h-entry` items (doc-9).
4
3
 
5
- Expects `posts`. A note, which has no title, is printed whole as its
6
- `e-content`, with its date as the link to it. Every other entry prints its
7
- title as a linked `p-name`, its `summary` — the very line the feeds publish —
8
- as a `p-summary` cut to about 280 characters, a Continue reading link, its
9
- publish date and the categories it is filed under. Tags are deliberately not here: an entry's tags belong
10
- under the entry, and a feed of twenty posts listing everyone's would be a
11
- wall of links rather than a list of writing.
4
+ Expects `posts`. Every entry opens on a kicker naming its kind and its date.
5
+ An article is the only entry with a headline: its title as a linked
6
+ `p-name`, its `summary` (the very line the feeds publish) as a `p-summary`
7
+ cut to about 280 characters, and a Continue reading link, with the
8
+ categories it is filed under in its kicker. A note, which has no title, is
9
+ printed whole as its `e-content`, and its date in the kicker is the link to
10
+ it. A reply is a note that first cites what it answers, and a reply with a
11
+ title is an article that does. Tags are deliberately not here: an entry's
12
+ tags belong under the entry, and a feed of twenty posts listing everyone's
13
+ would be a wall of links rather than a list of writing.
12
14
 
13
15
  `feedHeading` is the heading level, which belongs to the layout rather than
14
16
  to the list: a listing is a page of entries and heads each one at 2, and a
15
17
  front page whose feed sits under a heading of its own sets 3 so the outline
16
18
  never skips a level.
17
19
  #}
18
- {% import "partials/tags.njk" as taxonomy with context %}
20
+ {% import "partials/kicker.njk" as kicker with context %}
19
21
  {% set feedTitleTag = "h" + (feedHeading | default(2)) %}
20
22
  {% if posts.length %}
21
23
  <div class="feed h-feed">
@@ -24,43 +26,35 @@
24
26
  <hr class="feed-separator">
25
27
  {% endif %}
26
28
  <article class="feed-item h-entry">
27
- <div class="feed-content">
28
- {% if post.inReplyTo %}
29
- <p class="feed-reply small">In reply to <a class="u-in-reply-to" href="{{ post.inReplyTo }}">{{ post.inReplyTo }}</a></p>
30
- {% endif %}
31
- {% if not post.named %}
32
- <div class="feed-excerpt e-content">{{ post.content | safe }}</div>
33
- {% else %}
34
- <{{ feedTitleTag }} class="feed-title p-name">
35
- <a href="{{ post.url | url }}" class="u-url">{{ post.title }}</a>
36
- </{{ feedTitleTag }}>
37
- {% if post.summary %}
38
- <div class="feed-excerpt p-summary">
39
- {#- `truncate` cuts at the last space before the length and adds an
40
- ellipsis, so a description short enough is printed whole and a long
41
- one never breaks a word. -#}
42
- <p>{{ post.summary | truncate(280) }}</p>
43
- </div>
44
- {% endif %}
45
- <p class="feed-more">
46
- <a href="{{ post.url | url }}" aria-label="Continue reading: {{ post.title }}">Continue reading<span aria-hidden="true"> &rarr;</span></a>
47
- </p>
48
- {% endif %}
49
- <div class="feed-meta">
50
- {% if not post.named %}
51
- <p>
52
- <a href="{{ post.url | url }}" class="u-url">{% if post.date %}<time class="feed-date dt-published" datetime="{{ post.date | date("iso") }}">{{ post.date | date }}</time>{% else %}{{ post.label }}{% endif %}</a>
53
- </p>
54
- {% elif post.date %}
55
- <p>
56
- <time class="feed-date dt-published" datetime="{{ post.date | date("iso") }}">
57
- {{ post.date | date }}
58
- </time>
59
- </p>
60
- {% endif %}
61
- {{ taxonomy.categories(post.categories) }}
62
- </div>
29
+ {% if post.named %}
30
+ {{ kicker.line(post.postType | capitalize, post.date, false, post.categories) }}
31
+ {% else %}
32
+ {{ kicker.line(post.postType | capitalize, post.date, post.url, [], post.label) }}
33
+ {% endif %}
34
+ {#- What a reply answers, from the same partial an entry cites it with. -#}
35
+ {% if post.inReplyTo %}
36
+ {% set inReplyTo = post.inReplyTo %}
37
+ {% set replyContext = post.replyContext %}
38
+ {% include "partials/reply-context.njk" %}
39
+ {% endif %}
40
+ {% if not post.named %}
41
+ <div class="feed-excerpt e-content">{{ post.content | safe }}</div>
42
+ {% else %}
43
+ <{{ feedTitleTag }} class="feed-title p-name">
44
+ <a href="{{ post.url | url }}" class="u-url">{{ post.title }}</a>
45
+ </{{ feedTitleTag }}>
46
+ {% if post.summary %}
47
+ <div class="feed-excerpt p-summary">
48
+ {#- `truncate` cuts at the last space before the length and adds an
49
+ ellipsis, so a description short enough is printed whole and a long
50
+ one never breaks a word. -#}
51
+ <p>{{ post.summary | truncate(280) }}</p>
63
52
  </div>
53
+ {% endif %}
54
+ <p class="feed-more">
55
+ <a href="{{ post.url | url }}" aria-label="Continue reading: {{ post.title }}">Continue reading<span aria-hidden="true"> &rarr;</span></a>
56
+ </p>
57
+ {% endif %}
64
58
  </article>
65
59
  {% endfor %}
66
60
  </div>
@@ -3,15 +3,22 @@
3
3
  `u-in-reply-to h-cite` that always carries the target's URL, so a parser
4
4
  reads what this post answers even when nothing more is known about it.
5
5
 
6
+ Drawn as the citation block (doc-9): a rule on the start side, a sans line
7
+ naming the target, and its words as an italic quote. An entry includes it
8
+ with `inReplyTo` and `replyContext` on the context; a feed sets both from
9
+ the entry it is printing.
10
+
6
11
  `replyContext` is what the target said about itself when the reply was saved
7
12
  or synced: its name, a short excerpt, its author and its date, each only when
8
13
  it said so. It is somebody else's words, so every piece is printed through
9
- autoescaping and none of it with `safe`.
14
+ autoescaping and none of it with `safe`. With a name, the name is the link;
15
+ with only an author, "a post" is; with neither, the bare URL.
10
16
  #}
11
17
  {% if inReplyTo %}
12
- <div class="reply-context u-in-reply-to h-cite">
13
- <p class="small">In reply to
18
+ <div class="reply-context cite u-in-reply-to h-cite">
19
+ <p class="cite-line small">In reply to
14
20
  {%- if replyContext.name %} <a class="u-url p-name" href="{{ inReplyTo }}">{{ replyContext.name }}</a>
21
+ {%- elif replyContext.author %} <a class="u-url" href="{{ inReplyTo }}">a post</a>
15
22
  {%- else %} <a class="u-url" href="{{ inReplyTo }}">{{ inReplyTo }}</a>
16
23
  {%- endif %}
17
24
  {%- if replyContext.author %} by <span class="p-author h-card">
@@ -19,11 +26,11 @@
19
26
  {%- else %}<span class="p-name">{{ replyContext.author.name }}</span>{% endif -%}
20
27
  </span>
21
28
  {%- endif %}
22
- {%- if replyContext.published %} on <time class="dt-published" datetime="{{ replyContext.published | date("iso") }}">{{ replyContext.published | date }}</time>
29
+ {%- if replyContext.published %}, <time class="dt-published" datetime="{{ replyContext.published | date("iso") }}">{{ replyContext.published | date }}</time>
23
30
  {%- endif -%}
24
31
  </p>
25
32
  {% if replyContext.text %}
26
- <blockquote class="p-content">{{ replyContext.text }}</blockquote>
33
+ <blockquote class="cite-quote p-content">{{ replyContext.text }}</blockquote>
27
34
  {% endif %}
28
35
  </div>
29
36
  {% endif %}