@rmdes/indiekit-endpoint-blogroll 1.0.14 → 1.0.15

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.
@@ -0,0 +1,342 @@
1
+ /* Blogroll endpoint styles */
2
+
3
+ /* Stats grid */
4
+ .blogroll-stats {
5
+ display: grid;
6
+ gap: var(--space-s);
7
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
8
+ }
9
+
10
+ .blogroll-stat {
11
+ background: var(--color-background);
12
+ border-radius: var(--radius-s);
13
+ padding: var(--space-s);
14
+ text-align: center;
15
+ }
16
+
17
+ .blogroll-stat dt {
18
+ color: var(--color-text-secondary);
19
+ font-size: var(--step--1);
20
+ margin-block-end: var(--space-2xs);
21
+ }
22
+
23
+ .blogroll-stat dd {
24
+ font-size: var(--step-0);
25
+ font-weight: var(--font-weight-semibold);
26
+ margin: 0;
27
+ }
28
+
29
+ .blogroll-stat dd.blogroll-stat--error {
30
+ color: var(--color-error);
31
+ }
32
+
33
+ /* Quick links (button row) */
34
+ .blogroll-actions {
35
+ display: flex;
36
+ flex-wrap: wrap;
37
+ gap: var(--space-s);
38
+ margin-block-start: var(--space-m);
39
+ }
40
+
41
+ /* List (shared between blogs, sources, errors, dashboard lists) */
42
+ .blogroll-list {
43
+ display: flex;
44
+ flex-direction: column;
45
+ gap: var(--space-s);
46
+ list-style: none;
47
+ margin: 0;
48
+ padding: 0;
49
+ }
50
+
51
+ .blogroll-list__item {
52
+ align-items: flex-start;
53
+ background: var(--color-offset);
54
+ border-radius: var(--radius-m);
55
+ display: flex;
56
+ flex-wrap: wrap;
57
+ gap: var(--space-s);
58
+ justify-content: space-between;
59
+ padding: var(--space-m);
60
+ }
61
+
62
+ .blogroll-list__item--compact {
63
+ padding: var(--space-xs) var(--space-s);
64
+ }
65
+
66
+ .blogroll-list__item--pinned {
67
+ border-inline-start: 3px solid var(--color-accent);
68
+ }
69
+
70
+ .blogroll-list__item--hidden {
71
+ opacity: 0.6;
72
+ }
73
+
74
+ /* Item content (left side) */
75
+ .blogroll-item__info {
76
+ flex: 1;
77
+ min-inline-size: 200px;
78
+ }
79
+
80
+ .blogroll-item__title {
81
+ font-size: var(--step-0);
82
+ font-weight: var(--font-weight-semibold);
83
+ margin: 0 0 var(--space-2xs);
84
+ }
85
+
86
+ .blogroll-item__title a {
87
+ color: inherit;
88
+ text-decoration: none;
89
+ }
90
+
91
+ .blogroll-item__title a:hover {
92
+ text-decoration: underline;
93
+ }
94
+
95
+ .blogroll-item__meta {
96
+ align-items: center;
97
+ color: var(--color-text-secondary);
98
+ display: flex;
99
+ flex-wrap: wrap;
100
+ font-size: var(--step--1);
101
+ gap: var(--space-xs);
102
+ }
103
+
104
+ .blogroll-item__url {
105
+ color: var(--color-accent);
106
+ font-family: monospace;
107
+ font-size: var(--step--2);
108
+ margin-block-start: var(--space-2xs);
109
+ word-break: break-all;
110
+ }
111
+
112
+ .blogroll-item__error {
113
+ color: var(--color-error);
114
+ font-size: var(--step--1);
115
+ margin-block-start: var(--space-2xs);
116
+ }
117
+
118
+ /* Item actions (right side) */
119
+ .blogroll-item__actions {
120
+ display: flex;
121
+ flex-wrap: wrap;
122
+ gap: var(--space-xs);
123
+ }
124
+
125
+ /* Filters */
126
+ .blogroll-filters {
127
+ align-items: center;
128
+ display: flex;
129
+ flex-wrap: wrap;
130
+ gap: var(--space-s);
131
+ }
132
+
133
+ .blogroll-filter-select {
134
+ appearance: none;
135
+ background-color: var(--color-background);
136
+ border: 1px solid var(--color-border);
137
+ border-radius: var(--radius-s);
138
+ font-size: var(--step--1);
139
+ min-inline-size: 150px;
140
+ padding: var(--space-2xs) var(--space-s);
141
+ }
142
+
143
+ /* Form fields */
144
+ .blogroll-form {
145
+ max-inline-size: 600px;
146
+ }
147
+
148
+ .blogroll-field {
149
+ display: flex;
150
+ flex-direction: column;
151
+ gap: var(--space-2xs);
152
+ margin-block-end: var(--space-m);
153
+ }
154
+
155
+ .blogroll-field label {
156
+ font-weight: var(--font-weight-semibold);
157
+ }
158
+
159
+ .blogroll-field-hint {
160
+ color: var(--color-text-secondary);
161
+ font-size: var(--step--1);
162
+ }
163
+
164
+ .blogroll-field input,
165
+ .blogroll-field select,
166
+ .blogroll-field textarea {
167
+ appearance: none;
168
+ background-color: var(--color-background);
169
+ border: 1px solid var(--color-border);
170
+ border-radius: var(--radius-s);
171
+ font-size: var(--step--1);
172
+ padding: var(--space-2xs) var(--space-s);
173
+ width: 100%;
174
+ }
175
+
176
+ .blogroll-field textarea {
177
+ min-block-size: 100px;
178
+ }
179
+
180
+ .blogroll-field input:focus,
181
+ .blogroll-field select:focus,
182
+ .blogroll-field textarea:focus {
183
+ border-color: var(--color-accent);
184
+ outline: 2px solid var(--color-accent);
185
+ outline-offset: 1px;
186
+ }
187
+
188
+ .blogroll-field--inline {
189
+ align-items: center;
190
+ flex-direction: row;
191
+ gap: var(--space-s);
192
+ }
193
+
194
+ .blogroll-field--inline input[type="checkbox"] {
195
+ appearance: auto;
196
+ cursor: pointer;
197
+ width: auto;
198
+ }
199
+
200
+ /* API list */
201
+ .blogroll-api-list {
202
+ display: flex;
203
+ flex-direction: column;
204
+ gap: var(--space-xs);
205
+ list-style: none;
206
+ margin: 0;
207
+ padding: 0;
208
+ }
209
+
210
+ .blogroll-api-list li {
211
+ background: var(--color-background);
212
+ border-radius: var(--radius-s);
213
+ font-size: var(--step--1);
214
+ padding: var(--space-xs) var(--space-s);
215
+ }
216
+
217
+ .blogroll-api-list code {
218
+ color: var(--color-accent);
219
+ font-weight: var(--font-weight-semibold);
220
+ }
221
+
222
+ /* Feed items (inside blog-edit) */
223
+ .blogroll-items-list {
224
+ display: flex;
225
+ flex-direction: column;
226
+ gap: var(--space-xs);
227
+ list-style: none;
228
+ margin: 0;
229
+ padding: 0;
230
+ }
231
+
232
+ .blogroll-items-list li {
233
+ background: var(--color-offset);
234
+ border-radius: var(--radius-s);
235
+ padding: var(--space-xs) var(--space-s);
236
+ }
237
+
238
+ .blogroll-items-list .blogroll-item__title {
239
+ font-size: var(--step--1);
240
+ margin: 0;
241
+ }
242
+
243
+ .blogroll-items-list .blogroll-item__meta {
244
+ font-size: var(--step--2);
245
+ }
246
+
247
+ /* Feed discovery (blog-edit new) */
248
+ .blogroll-discover {
249
+ background: var(--color-offset);
250
+ border-radius: var(--radius-m);
251
+ margin-block-end: var(--space-m);
252
+ padding: var(--space-m);
253
+ }
254
+
255
+ .blogroll-discover .blogroll-field {
256
+ margin-block-end: var(--space-s);
257
+ }
258
+
259
+ .blogroll-discover__input {
260
+ display: flex;
261
+ gap: var(--space-s);
262
+ }
263
+
264
+ .blogroll-discover__input input {
265
+ appearance: none;
266
+ background-color: var(--color-background);
267
+ border: 1px solid var(--color-border);
268
+ border-radius: var(--radius-s);
269
+ flex: 1;
270
+ font-size: var(--step--1);
271
+ padding: var(--space-2xs) var(--space-s);
272
+ }
273
+
274
+ .blogroll-discover__result {
275
+ background: var(--color-background);
276
+ border-radius: var(--radius-s);
277
+ font-size: var(--step--1);
278
+ margin-block-start: var(--space-s);
279
+ padding: var(--space-s);
280
+ }
281
+
282
+ .blogroll-discover__result--error {
283
+ color: var(--color-error);
284
+ }
285
+
286
+ .blogroll-discover__result--success {
287
+ color: var(--color-success);
288
+ }
289
+
290
+ .blogroll-discover__feeds {
291
+ display: flex;
292
+ flex-direction: column;
293
+ gap: var(--space-xs);
294
+ list-style: none;
295
+ margin: var(--space-xs) 0 0;
296
+ padding: 0;
297
+ }
298
+
299
+ .blogroll-discover__feed {
300
+ align-items: center;
301
+ background: var(--color-offset);
302
+ border-radius: var(--radius-s);
303
+ cursor: pointer;
304
+ display: flex;
305
+ gap: var(--space-s);
306
+ padding: var(--space-xs);
307
+ }
308
+
309
+ .blogroll-discover__feed:hover {
310
+ opacity: 0.8;
311
+ }
312
+
313
+ .blogroll-discover__feed-url {
314
+ flex: 1;
315
+ font-family: monospace;
316
+ font-size: var(--step--2);
317
+ word-break: break-all;
318
+ }
319
+
320
+ .blogroll-discover__feed-type {
321
+ background: var(--color-accent);
322
+ border-radius: var(--radius-s);
323
+ color: var(--color-on-accent);
324
+ font-size: var(--step--2);
325
+ padding: var(--space-3xs) var(--space-2xs);
326
+ text-transform: uppercase;
327
+ }
328
+
329
+ /* Empty state */
330
+ .blogroll-empty {
331
+ color: var(--color-text-secondary);
332
+ font-size: var(--step--1);
333
+ padding: var(--space-m);
334
+ text-align: center;
335
+ }
336
+
337
+ /* Divider */
338
+ .blogroll-divider {
339
+ border: none;
340
+ border-block-start: 1px solid var(--color-border);
341
+ margin: var(--space-m) 0;
342
+ }
@@ -43,6 +43,7 @@ async function list(request, response) {
43
43
 
44
44
  response.render("blogroll-blogs", {
45
45
  title: request.__("blogroll.blogs.title"),
46
+ parent: { text: request.__("blogroll.title"), href: request.baseUrl },
46
47
  blogs: filteredBlogs,
47
48
  categories,
48
49
  filterCategory: category,
@@ -66,6 +67,7 @@ async function list(request, response) {
66
67
  function newForm(request, response) {
67
68
  response.render("blogroll-blog-edit", {
68
69
  title: request.__("blogroll.blogs.new"),
70
+ parent: { text: request.__("blogroll.blogs.title"), href: `${request.baseUrl}/blogs` },
69
71
  blog: null,
70
72
  isNew: true,
71
73
  baseUrl: request.baseUrl,
@@ -175,6 +177,7 @@ async function edit(request, response) {
175
177
 
176
178
  response.render("blogroll-blog-edit", {
177
179
  title: request.__("blogroll.blogs.edit"),
180
+ parent: { text: request.__("blogroll.blogs.title"), href: `${request.baseUrl}/blogs` },
178
181
  blog,
179
182
  items,
180
183
  isNew: false,
@@ -38,6 +38,9 @@ async function get(request, response) {
38
38
  const errorBlogs = await getBlogs(application, { includeHidden: true, limit: 100 });
39
39
  const blogsWithErrors = errorBlogs.filter((b) => b.status === "error");
40
40
 
41
+ // Extract flash messages for native Indiekit notification banner
42
+ const flash = consumeFlashMessage(request);
43
+
41
44
  response.render("blogroll-dashboard", {
42
45
  title: request.__("blogroll.title"),
43
46
  sources,
@@ -51,6 +54,7 @@ async function get(request, response) {
51
54
  syncStatus,
52
55
  blogsWithErrors: blogsWithErrors.slice(0, 5),
53
56
  baseUrl: request.baseUrl,
57
+ ...flash,
54
58
  });
55
59
  } catch (error) {
56
60
  console.error("[Blogroll] Dashboard error:", error);
@@ -151,6 +155,21 @@ async function status(request, response) {
151
155
  }
152
156
  }
153
157
 
158
+ /**
159
+ * Extract and clear flash messages from session
160
+ * Returns { success, error } for Indiekit's native notificationBanner
161
+ */
162
+ function consumeFlashMessage(request) {
163
+ const result = {};
164
+ if (request.session?.messages?.length) {
165
+ const msg = request.session.messages[0];
166
+ if (msg.type === "success") result.success = msg.content;
167
+ else if (msg.type === "error" || msg.type === "warning") result.error = msg.content;
168
+ request.session.messages = null;
169
+ }
170
+ return result;
171
+ }
172
+
154
173
  export const dashboardController = {
155
174
  get,
156
175
  sync,
@@ -37,10 +37,15 @@ async function list(request, response) {
37
37
  : null,
38
38
  }));
39
39
 
40
+ // Extract flash messages for native Indiekit notification banner
41
+ const flash = consumeFlashMessage(request);
42
+
40
43
  response.render("blogroll-sources", {
41
44
  title: request.__("blogroll.sources.title"),
45
+ parent: { text: request.__("blogroll.title"), href: request.baseUrl },
42
46
  sources,
43
47
  baseUrl: request.baseUrl,
48
+ ...flash,
44
49
  });
45
50
  } catch (error) {
46
51
  console.error("[Blogroll] Sources list error:", error);
@@ -66,6 +71,7 @@ async function newForm(request, response) {
66
71
 
67
72
  response.render("blogroll-source-edit", {
68
73
  title: request.__("blogroll.sources.new"),
74
+ parent: { text: request.__("blogroll.sources.title"), href: `${request.baseUrl}/sources` },
69
75
  source: null,
70
76
  isNew: true,
71
77
  baseUrl: request.baseUrl,
@@ -185,6 +191,7 @@ async function edit(request, response) {
185
191
 
186
192
  response.render("blogroll-source-edit", {
187
193
  title: request.__("blogroll.sources.edit"),
194
+ parent: { text: request.__("blogroll.sources.title"), href: `${request.baseUrl}/sources` },
188
195
  source,
189
196
  isNew: false,
190
197
  baseUrl: request.baseUrl,
@@ -336,6 +343,21 @@ async function sync(request, response) {
336
343
  }
337
344
  }
338
345
 
346
+ /**
347
+ * Extract and clear flash messages from session
348
+ * Returns { success, error } for Indiekit's native notificationBanner
349
+ */
350
+ function consumeFlashMessage(request) {
351
+ const result = {};
352
+ if (request.session?.messages?.length) {
353
+ const msg = request.session.messages[0];
354
+ if (msg.type === "success") result.success = msg.content;
355
+ else if (msg.type === "error" || msg.type === "warning") result.error = msg.content;
356
+ request.session.messages = null;
357
+ }
358
+ return result;
359
+ }
360
+
339
361
  export const sourcesController = {
340
362
  list,
341
363
  newForm,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-blogroll",
3
- "version": "1.0.14",
3
+ "version": "1.0.15",
4
4
  "description": "Blogroll endpoint for Indiekit. Aggregates blog feeds from OPML, JSON feeds, or manual entry.",
5
5
  "keywords": [
6
6
  "indiekit",