domma-cms 0.54.2 → 0.66.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 (221) hide show
  1. package/CLAUDE.md +151 -77
  2. package/README.md +4 -4
  3. package/admin/css/admin.css +1 -1
  4. package/admin/dist/domma/domma-tools.css +3 -3
  5. package/admin/dist/domma/domma-tools.min.js +3 -3
  6. package/admin/js/api.js +1 -1
  7. package/admin/js/app.js +3 -3
  8. package/admin/js/lib/page-picker.js +1 -0
  9. package/admin/js/lib/plugin-accent.js +1 -0
  10. package/admin/js/lib/plugin-chrome.js +1 -0
  11. package/admin/js/lib/shortcode-context-menu.js +2 -2
  12. package/admin/js/lib/sidebar-grouping.js +1 -1
  13. package/admin/js/lib/sidebar-grouping.test.js +1 -1
  14. package/admin/js/lib/sidebar-renderer.js +4 -4
  15. package/admin/js/lib/slideover-resizable.js +1 -0
  16. package/admin/js/templates/context-menu-editor.html +212 -0
  17. package/admin/js/templates/context-menus.html +16 -0
  18. package/admin/js/templates/menu-editor.html +19 -25
  19. package/admin/js/templates/plugin-code.html +1 -1
  20. package/{plugins/site-search/admin/templates/site-search.html → admin/js/templates/search.html} +50 -4
  21. package/admin/js/templates/settings.html +27 -97
  22. package/admin/js/templates/theme.html +173 -0
  23. package/admin/js/views/context-menu-editor.js +55 -0
  24. package/admin/js/views/context-menus.js +5 -0
  25. package/admin/js/views/form-editor.js +7 -7
  26. package/admin/js/views/index.js +1 -1
  27. package/admin/js/views/menu-editor.js +13 -13
  28. package/admin/js/views/plugin-marketplace.js +1 -1
  29. package/admin/js/views/plugins.js +25 -23
  30. package/admin/js/views/search.js +1 -0
  31. package/admin/js/views/settings.js +3 -3
  32. package/admin/js/views/theme.js +1 -0
  33. package/bin/cli.js +11 -2
  34. package/bin/lib/smtp-defaults.js +53 -0
  35. package/bin/update.js +13 -2
  36. package/config/menus/admin-sidebar.json +129 -23
  37. package/config/plugins.json +5 -5
  38. package/config/search.json +13 -0
  39. package/config/theme.json +18 -0
  40. package/package.json +12 -4
  41. package/plugins/_lib/admin/mail/compose-window.js +914 -0
  42. package/plugins/_lib/admin/mail/contacts.js +301 -0
  43. package/plugins/_lib/admin/mail/diagnostics-section.js +133 -0
  44. package/plugins/_lib/admin/mail/folder-tree.js +254 -0
  45. package/plugins/_lib/admin/mail/identity.js +480 -0
  46. package/plugins/_lib/admin/mail/image-senders-section.js +131 -0
  47. package/plugins/_lib/admin/mail/keyboard.js +136 -0
  48. package/plugins/_lib/admin/mail/mail.css +1 -0
  49. package/plugins/_lib/admin/mail/mail.html +71 -0
  50. package/plugins/_lib/admin/mail/panes.js +253 -0
  51. package/plugins/_lib/admin/mail/reader-view.js +4453 -0
  52. package/plugins/_lib/admin/mail/resizable.js +26 -0
  53. package/plugins/_lib/admin/mail/rules.js +343 -0
  54. package/plugins/_lib/admin/mail/scheduling.js +203 -0
  55. package/plugins/_lib/admin/mail/section-kit.js +277 -0
  56. package/plugins/_lib/admin/mail/templates.js +238 -0
  57. package/plugins/_lib/admin/mail/threads.js +200 -0
  58. package/plugins/_lib/admin/mail/vacation.js +269 -0
  59. package/plugins/_lib/admin/ui/help.css +1 -0
  60. package/plugins/_lib/admin/ui/help.js +174 -0
  61. package/plugins/_lib/admin/ui/resizable.js +151 -0
  62. package/plugins/_lib/dataStore.js +117 -0
  63. package/plugins/_lib/mail/accounts.js +919 -0
  64. package/plugins/_lib/mail/bodyTokens.js +101 -0
  65. package/plugins/_lib/mail/compose.js +256 -0
  66. package/plugins/_lib/mail/defaults.js +57 -0
  67. package/plugins/_lib/mail/diagnostics.js +154 -0
  68. package/plugins/_lib/mail/envelope.js +161 -0
  69. package/plugins/_lib/mail/folders.js +192 -0
  70. package/plugins/_lib/mail/handoff.js +274 -0
  71. package/plugins/_lib/mail/imapPool.js +293 -0
  72. package/plugins/_lib/mail/mbox.js +74 -0
  73. package/plugins/_lib/mail/pollSchedule.js +79 -0
  74. package/plugins/_lib/mail/poller.js +135 -0
  75. package/plugins/_lib/mail/priority.js +138 -0
  76. package/plugins/_lib/mail/readRoutes.js +680 -0
  77. package/plugins/_lib/mail/render.js +291 -0
  78. package/plugins/_lib/mail/ruleRunner.js +152 -0
  79. package/plugins/_lib/mail/scheduler.js +254 -0
  80. package/plugins/_lib/mail/secretbox.js +229 -0
  81. package/plugins/_lib/mail/send.js +396 -0
  82. package/plugins/_lib/mail/store.js +1002 -0
  83. package/plugins/_lib/mail/sync.js +292 -0
  84. package/plugins/_lib/mail/syncPlan.js +126 -0
  85. package/plugins/_lib/mail/syncSelection.js +82 -0
  86. package/plugins/_lib/mail/unsubscribe.js +183 -0
  87. package/plugins/_lib/mail/vacationRunner.js +114 -0
  88. package/plugins/_lib/mail/write.js +473 -0
  89. package/plugins/_lib/schemaSync.js +83 -0
  90. package/plugins/_template/admin/css/index.css +0 -0
  91. package/plugins/_template/admin/templates/index.html +4 -4
  92. package/plugins/_template/admin/views/index.js +7 -0
  93. package/plugins/analytics/admin/css/index.css +1 -0
  94. package/plugins/analytics/admin/templates/analytics.html +22 -13
  95. package/plugins/analytics/plugin.json +3 -0
  96. package/plugins/blog/admin/css/index.css +1 -0
  97. package/plugins/blog/admin/templates/blog.html +30 -18
  98. package/plugins/blog/admin/templates/categories.html +2 -2
  99. package/plugins/blog/admin/templates/comments.html +2 -2
  100. package/plugins/blog/admin/templates/post-editor.html +34 -34
  101. package/plugins/blog/admin/templates/settings.html +6 -3
  102. package/plugins/blog/admin/views/blog.js +8 -5
  103. package/plugins/blog/admin/views/categories.js +5 -10
  104. package/plugins/blog/admin/views/comments.js +5 -5
  105. package/plugins/blog/admin/views/post-editor.js +39 -20
  106. package/plugins/blog/admin/views/settings.js +52 -50
  107. package/plugins/blog/collections/categories/schema.json +7 -6
  108. package/plugins/blog/collections/comments/schema.json +11 -10
  109. package/plugins/blog/collections/posts/schema.json +14 -13
  110. package/plugins/blog/plugin.js +36 -13
  111. package/plugins/blog/plugin.json +13 -5
  112. package/plugins/blog/plugin.public.js +312 -0
  113. package/plugins/contacts/admin/css/index.css +1 -0
  114. package/plugins/contacts/admin/templates/contacts.html +128 -0
  115. package/plugins/contacts/admin/views/contacts.js +237 -4
  116. package/plugins/contacts/collections/user-contacts/schema.json +108 -0
  117. package/plugins/contacts/plugin.js +214 -27
  118. package/plugins/contacts/plugin.json +4 -1
  119. package/plugins/invoice/admin/css/index.css +1 -0
  120. package/plugins/invoice/admin/templates/editor.html +140 -49
  121. package/plugins/invoice/admin/templates/index.html +153 -23
  122. package/plugins/invoice/admin/templates/issuers.html +2 -5
  123. package/plugins/invoice/admin/templates/receivers.html +2 -5
  124. package/plugins/invoice/admin/views/contacts-source.js +266 -0
  125. package/plugins/invoice/admin/views/editor.js +366 -199
  126. package/plugins/invoice/admin/views/export.js +199 -0
  127. package/plugins/invoice/admin/views/help-content.js +61 -0
  128. package/plugins/invoice/admin/views/index.js +582 -94
  129. package/plugins/invoice/admin/views/issuers.js +24 -17
  130. package/plugins/invoice/admin/views/media.js +172 -0
  131. package/plugins/invoice/admin/views/party-view.js +305 -67
  132. package/plugins/invoice/admin/views/payments.js +127 -0
  133. package/plugins/invoice/admin/views/print.js +130 -0
  134. package/plugins/invoice/admin/views/receivers.js +49 -16
  135. package/plugins/invoice/admin/views/send.js +212 -0
  136. package/plugins/invoice/admin/views/settings.js +594 -0
  137. package/plugins/invoice/admin/views/view-lifecycle.js +33 -0
  138. package/plugins/invoice/collections/invoice-issuers/schema.json +77 -11
  139. package/plugins/invoice/collections/invoice-receivers/schema.json +10 -9
  140. package/plugins/invoice/collections/invoices/schema.json +19 -13
  141. package/plugins/invoice/config.js +27 -6
  142. package/plugins/invoice/pdf.js +164 -0
  143. package/plugins/invoice/plugin.js +1217 -44
  144. package/plugins/invoice/plugin.json +10 -9
  145. package/plugins/invoice/templates/_base.css +1 -0
  146. package/plugins/invoice/templates/classic-nologo.html +100 -0
  147. package/plugins/invoice/templates/classic.html +91 -0
  148. package/plugins/invoice/templates/invoice-print.html +24 -0
  149. package/plugins/invoice/templates/minimal.html +99 -0
  150. package/plugins/invoice/templates/modern-nologo.html +114 -0
  151. package/plugins/invoice/templates/modern.html +113 -0
  152. package/plugins/invoice/templates/templates.json +11 -0
  153. package/plugins/mail-reader/admin/views/mail.js +19 -0
  154. package/plugins/mail-reader/config.js +7 -0
  155. package/plugins/mail-reader/plugin.js +48 -0
  156. package/plugins/mail-reader/plugin.json +33 -0
  157. package/plugins/notes/admin/views/notes.js +1 -1
  158. package/plugins/notes/plugin.json +2 -2
  159. package/plugins/surveys/lib/audience.js +37 -0
  160. package/plugins/surveys/lib/campaigns.js +43 -0
  161. package/plugins/surveys/lib/ledger.js +110 -0
  162. package/plugins/surveys/lib/sending.js +106 -0
  163. package/plugins/surveys/lib/stats.js +62 -0
  164. package/plugins/surveys/lib/submit.js +95 -0
  165. package/plugins/surveys/lib/tokens.js +28 -0
  166. package/plugins/surveys/plugin.public.js +149 -0
  167. package/plugins/theme-switcher/admin/templates/theme-switcher.html +1 -1
  168. package/public/css/forms.css +1 -1
  169. package/public/css/menu-highlight.css +1 -1
  170. package/public/css/search.css +1 -0
  171. package/public/css/site.css +1 -1
  172. package/public/js/collection-context.js +2 -2
  173. package/public/js/context-menus.js +1 -0
  174. package/public/js/form-logic-engine.js +1 -1
  175. package/public/js/forms.js +2 -2
  176. package/public/js/menu-decor.mjs +1 -1
  177. package/public/js/search.js +1 -0
  178. package/public/js/site.js +1 -1
  179. package/scripts/build.js +37 -3
  180. package/scripts/copy-domma.js +48 -0
  181. package/scripts/seed.js +1996 -0
  182. package/scripts/setup.js +8 -0
  183. package/server/routes/api/collections.js +34 -0
  184. package/server/routes/api/context-menus.js +104 -0
  185. package/server/routes/api/forms.js +42 -3
  186. package/server/routes/api/notifications.js +69 -19
  187. package/server/routes/api/plugins.js +50 -6
  188. package/server/routes/api/search.js +43 -0
  189. package/server/routes/api/theme.js +69 -0
  190. package/server/routes/public.js +42 -7
  191. package/server/server.js +74 -0
  192. package/server/services/adapters/FileAdapter.js +6 -1
  193. package/server/services/content.js +26 -0
  194. package/server/services/contextMenus.js +477 -0
  195. package/server/services/email.js +29 -3
  196. package/server/services/health.js +23 -2
  197. package/server/services/markdown.js +70 -9
  198. package/server/services/menuRender.js +28 -4
  199. package/server/services/menus.js +25 -1
  200. package/server/services/permissionRegistry.js +24 -0
  201. package/server/services/pluginFiles.js +52 -11
  202. package/server/services/plugins.js +229 -6
  203. package/server/services/renderer.js +148 -22
  204. package/server/services/roles.js +1 -1
  205. package/server/services/search-migration.js +82 -0
  206. package/server/services/search.js +413 -0
  207. package/server/services/sidebar-migration.js +1 -0
  208. package/server/services/themeSettings.js +541 -0
  209. package/server/services/users.js +8 -0
  210. package/server/templates/page.html +4 -2
  211. package/plugins/contacts/data/contacts.json +0 -20
  212. package/plugins/notes/data/notes.json +0 -1
  213. package/plugins/site-search/admin/views/site-search.js +0 -116
  214. package/plugins/site-search/config.js +0 -15
  215. package/plugins/site-search/plugin.js +0 -188
  216. package/plugins/site-search/plugin.json +0 -40
  217. package/plugins/site-search/public/inject-body.html +0 -17
  218. package/plugins/site-search/public/inject-head.html +0 -1
  219. package/plugins/site-search/public/search.css +0 -1
  220. package/plugins/site-search/public/search.js +0 -1
  221. package/plugins/todo/data/todos.json +0 -1
@@ -14,6 +14,13 @@ export const indexView = {
14
14
  // Confirm: if (!await E.confirm('Are you sure?')) return;
15
15
  // Create table: T.create(el, { data, columns: [...] });
16
16
  // Create form: F.create({ fields }, defaults, { showSubmitButton: false }).renderTo(el);
17
+ //
18
+ // Route params: onMount is handed ONE argument and never the route
19
+ // params, and R.current is a METHOD - R.current.params
20
+ // is always undefined. An editor view reads its id off
21
+ // the hash:
22
+ // const m = location.hash.match(/^#\/plugins\/PLUGIN_SLUG\/edit\/([^?#]+)/);
23
+ // const id = m ? decodeURIComponent(m[1]) : null;
17
24
  I.scan();
18
25
  }
19
26
  };
@@ -0,0 +1 @@
1
+ .analytics-filter{max-width:240px}
@@ -1,6 +1,6 @@
1
1
  <div class="view-header">
2
2
  <h1><span data-icon="chart-bar"></span> Analytics</h1>
3
- <div class="d-flex gap-2">
3
+ <div class="view-header-actions">
4
4
  <button id="export-btn" class="btn btn-ghost btn-sm" type="button">
5
5
  <span data-icon="download"></span> Export CSV
6
6
  </button>
@@ -12,7 +12,7 @@
12
12
 
13
13
  <div class="card mb-4">
14
14
  <div class="card-body">
15
- <div id="range-tabs" class="d-flex gap-2 flex-wrap">
15
+ <div id="range-tabs" class="flex gap-2 flex-wrap">
16
16
  <button class="btn btn-ghost btn-sm range-btn" data-range="today">Today</button>
17
17
  <button class="btn btn-ghost btn-sm range-btn active" data-range="7d">Last 7 days</button>
18
18
  <button class="btn btn-ghost btn-sm range-btn" data-range="30d">Last 30 days</button>
@@ -23,22 +23,31 @@
23
23
  </div>
24
24
 
25
25
  <div class="grid grid-cols-3 gap-4 mb-4">
26
- <div class="card">
26
+ <div class="card stat-card">
27
27
  <div class="card-body">
28
- <div class="text-muted text-sm">Hits in range</div>
29
- <div class="h2 mb-0" id="kpi-range">-</div>
28
+ <div class="stat-icon"><span data-icon="activity"></span></div>
29
+ <div>
30
+ <div class="stat-value" id="kpi-range">-</div>
31
+ <div class="stat-label">Hits in range</div>
32
+ </div>
30
33
  </div>
31
34
  </div>
32
- <div class="card">
35
+ <div class="card stat-card">
33
36
  <div class="card-body">
34
- <div class="text-muted text-sm">Unique pages</div>
35
- <div class="h2 mb-0" id="kpi-pages">-</div>
37
+ <div class="stat-icon"><span data-icon="file-text"></span></div>
38
+ <div>
39
+ <div class="stat-value" id="kpi-pages">-</div>
40
+ <div class="stat-label">Unique pages</div>
41
+ </div>
36
42
  </div>
37
43
  </div>
38
- <div class="card">
44
+ <div class="card stat-card">
39
45
  <div class="card-body">
40
- <div class="text-muted text-sm">Lifetime hits</div>
41
- <div class="h2 mb-0" id="kpi-lifetime">-</div>
46
+ <div class="stat-icon stat-icon--success"><span data-icon="trending-up"></span></div>
47
+ <div>
48
+ <div class="stat-value" id="kpi-lifetime">-</div>
49
+ <div class="stat-label">Lifetime hits</div>
50
+ </div>
42
51
  </div>
43
52
  </div>
44
53
  </div>
@@ -51,9 +60,9 @@
51
60
  </div>
52
61
 
53
62
  <div class="card">
54
- <div class="card-header d-flex justify-content-between align-items-center">
63
+ <div class="card-header flex justify-between items-center">
55
64
  <strong>Top pages</strong>
56
- <input id="filter-input" type="search" class="form-input form-input-sm" placeholder="Filter URL…" style="max-width: 240px;">
65
+ <input id="filter-input" type="search" class="form-input form-input-sm analytics-filter" placeholder="Filter URL…">
57
66
  </div>
58
67
  <div class="card-body">
59
68
  <div id="analytics-table"></div>
@@ -7,6 +7,9 @@
7
7
  "date": "2026-05-04",
8
8
  "icon": "chart-bar",
9
9
  "admin": {
10
+ "css": [
11
+ "admin/css/index.css"
12
+ ],
10
13
  "sidebar": [
11
14
  {
12
15
  "id": "analytics",
@@ -0,0 +1 @@
1
+ .blog-hidden{display:none!important}.blog-clickable{cursor:pointer}.blog-nowrap{white-space:nowrap}.blog-editor{align-items:flex-start}.blog-editor-main{flex:1;min-width:0}.blog-editor-side{width:280px;flex-shrink:0}@media(max-width:900px){.blog-editor{flex-wrap:wrap}.blog-editor-side{width:100%}}.blog-filter-select{width:auto}.blog-filter-search{width:240px}.blog-settings-card{max-width:600px}.blog-media-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:.75rem;padding:.5rem}.blog-media-tile{border:1px solid var(--dm-border, #333);background:transparent;border-radius:.25rem;padding:.25rem;cursor:pointer;display:flex;flex-direction:column;align-items:center;gap:.25rem}.blog-media-tile img{width:100%;height:90px;object-fit:cover;border-radius:.15rem}.blog-media-tile span{font-size:.7rem;text-overflow:ellipsis;overflow:hidden;white-space:nowrap;width:100%}.blog-modal-form{display:flex;flex-direction:column;gap:1rem}.blog-modal-actions{display:flex;justify-content:flex-end;gap:.5rem;margin-top:.5rem}.blog-image-preview{max-width:100%;border-radius:.25rem;display:block;margin-bottom:.5rem}.blog-title-link{font-weight:600}.blog-empty-cell{opacity:.4}.blog-row-actions{display:flex;gap:.4rem}.blog-comment-body{font-size:.87rem}.blog-moderation-actions{display:flex;gap:.3rem;flex-wrap:wrap}
@@ -1,42 +1,54 @@
1
- <div class="view-header d-flex align-items-center justify-content-between mb-4">
2
- <h1 class="h3 mb-0"><span data-icon="file-text"></span> Blog Posts</h1>
1
+ <div class="view-header">
2
+ <h1><span data-icon="file-text"></span> Blog Posts</h1>
3
3
  <button id="btn-new-post" class="btn btn-primary">
4
4
  <span data-icon="plus"></span> New Post
5
5
  </button>
6
6
  </div>
7
7
 
8
8
  <div class="grid grid-cols-4 gap-3 mb-4">
9
- <div class="card"><div class="card-body">
10
- <div class="text-muted small">Total</div>
11
- <div class="h3 mb-0" id="kpi-total">-</div>
9
+ <div class="card stat-card"><div class="card-body">
10
+ <div class="stat-icon"><span data-icon="file-text"></span></div>
11
+ <div>
12
+ <div class="stat-value" id="kpi-total">-</div>
13
+ <div class="stat-label">Total</div>
14
+ </div>
12
15
  </div></div>
13
- <div class="card"><div class="card-body">
14
- <div class="text-muted small">Published</div>
15
- <div class="h3 mb-0" id="kpi-published">-</div>
16
+ <div class="card stat-card"><div class="card-body">
17
+ <div class="stat-icon stat-icon--success"><span data-icon="check-circle"></span></div>
18
+ <div>
19
+ <div class="stat-value" id="kpi-published">-</div>
20
+ <div class="stat-label">Published</div>
21
+ </div>
16
22
  </div></div>
17
- <div class="card"><div class="card-body">
18
- <div class="text-muted small">Drafts</div>
19
- <div class="h3 mb-0" id="kpi-drafts">-</div>
23
+ <div class="card stat-card"><div class="card-body">
24
+ <div class="stat-icon"><span data-icon="edit"></span></div>
25
+ <div>
26
+ <div class="stat-value" id="kpi-drafts">-</div>
27
+ <div class="stat-label">Drafts</div>
28
+ </div>
20
29
  </div></div>
21
- <div class="card"><div class="card-body">
22
- <div class="text-muted small">Scheduled</div>
23
- <div class="h3 mb-0" id="kpi-scheduled">-</div>
30
+ <div class="card stat-card"><div class="card-body">
31
+ <div class="stat-icon stat-icon--warning"><span data-icon="clock"></span></div>
32
+ <div>
33
+ <div class="stat-value" id="kpi-scheduled">-</div>
34
+ <div class="stat-label">Scheduled</div>
35
+ </div>
24
36
  </div></div>
25
37
  </div>
26
38
 
27
39
  <div class="card mb-3">
28
40
  <div class="card-body">
29
- <div class="d-flex gap-2 flex-wrap">
30
- <select id="filter-status" class="form-select" style="width:auto;">
41
+ <div class="flex gap-2 flex-wrap items-center">
42
+ <select id="filter-status" class="form-select blog-filter-select">
31
43
  <option value="">All statuses</option>
32
44
  <option value="draft">Draft</option>
33
45
  <option value="scheduled">Scheduled</option>
34
46
  <option value="published">Published</option>
35
47
  </select>
36
- <select id="filter-category" class="form-select" style="width:auto;">
48
+ <select id="filter-category" class="form-select blog-filter-select">
37
49
  <option value="">All categories</option>
38
50
  </select>
39
- <input id="filter-search" type="text" class="form-control" style="width:240px;" placeholder="Search title or excerpt…">
51
+ <input id="filter-search" type="search" class="form-input blog-filter-search" placeholder="Search title or excerpt…">
40
52
  <button id="filter-reset" class="btn btn-ghost btn-sm">Reset</button>
41
53
  </div>
42
54
  </div>
@@ -1,5 +1,5 @@
1
- <div class="view-header d-flex align-items-center justify-content-between mb-4">
2
- <h1 class="h3 mb-0">Blog Categories</h1>
1
+ <div class="view-header">
2
+ <h1><span data-icon="folder"></span> Blog Categories</h1>
3
3
  <button id="btn-new-category" class="btn btn-primary">
4
4
  <span data-icon="plus"></span> New Category
5
5
  </button>
@@ -1,5 +1,5 @@
1
- <div class="view-header mb-4">
2
- <h1 class="h3 mb-0">Blog Comments</h1>
1
+ <div class="view-header">
2
+ <h1><span data-icon="message-circle"></span> Blog Comments</h1>
3
3
  </div>
4
4
 
5
5
  <div class="tab-list mb-3">
@@ -1,87 +1,87 @@
1
- <div class="view-header d-flex align-items-center justify-content-between mb-4">
2
- <h1 class="h3 mb-0" id="editor-title">New Post</h1>
3
- <div class="d-flex gap-2">
1
+ <div class="view-header">
2
+ <h1 id="editor-title">New Post</h1>
3
+ <div class="view-header-actions">
4
4
  <button id="btn-save-draft" class="btn btn-secondary">Save Draft</button>
5
- <button id="btn-schedule" class="btn btn-warning" style="display:none;">Schedule</button>
5
+ <button id="btn-schedule" class="btn btn-warning blog-hidden">Schedule</button>
6
6
  <button id="btn-publish" class="btn btn-primary">Publish Now</button>
7
7
  </div>
8
8
  </div>
9
9
 
10
- <div class="d-flex gap-4" style="align-items:flex-start;">
10
+ <div class="flex gap-4 blog-editor">
11
11
  <!-- Main content area -->
12
- <div style="flex:1; min-width:0;">
12
+ <div class="blog-editor-main">
13
13
  <div class="card mb-3">
14
14
  <div class="card-body">
15
15
  <div class="mb-3">
16
- <label class="form-label fw-bold">Title</label>
17
- <input type="text" id="post-title" class="form-control form-control-lg" placeholder="Post title…">
16
+ <label class="form-label font-bold" for="post-title">Title</label>
17
+ <input type="text" id="post-title" class="form-input form-input-lg" placeholder="Post title…">
18
18
  </div>
19
19
  <div class="mb-3">
20
- <label class="form-label">Slug</label>
21
- <div class="d-flex gap-2 align-items-center">
22
- <input type="text" id="post-slug" class="form-control font-monospace">
23
- <label class="d-flex align-items-center gap-1 text-muted small" style="white-space:nowrap;">
24
- <input type="checkbox" id="slug-auto"> Auto
20
+ <label class="form-label" for="post-slug">Slug</label>
21
+ <div class="flex gap-2 items-center">
22
+ <input type="text" id="post-slug" class="form-input font-mono">
23
+ <label class="flex items-center gap-1 text-muted text-sm blog-nowrap" for="slug-auto">
24
+ <input type="checkbox" id="slug-auto" class="form-checkbox"> Auto
25
25
  </label>
26
26
  </div>
27
- <div id="slug-warning" class="text-warning small mt-1" style="display:none;">
27
+ <div id="slug-warning" class="text-warning text-sm mt-1 blog-hidden">
28
28
  ⚠ Changing slug breaks existing links
29
29
  </div>
30
30
  </div>
31
31
  <div class="mb-3">
32
- <label class="form-label">Excerpt</label>
33
- <textarea id="post-excerpt" class="form-control" rows="2" placeholder="Short summary…"></textarea>
32
+ <label class="form-label" for="post-excerpt">Excerpt</label>
33
+ <textarea id="post-excerpt" class="form-textarea" rows="2" placeholder="Short summary…"></textarea>
34
34
  </div>
35
35
  <div class="mb-3">
36
- <label class="form-label">Content</label>
37
- <textarea id="post-content" class="form-control font-monospace" rows="20" placeholder="Write in Markdown…"></textarea>
36
+ <label class="form-label" for="post-content">Content</label>
37
+ <textarea id="post-content" class="form-textarea font-mono" rows="20" placeholder="Write in Markdown…"></textarea>
38
38
  </div>
39
39
  </div>
40
40
  </div>
41
41
 
42
42
  <!-- SEO accordion -->
43
43
  <div class="card mb-3">
44
- <div class="card-header" id="seo-toggle" style="cursor:pointer;">
45
- SEO <span class="text-muted small">(click to expand)</span>
44
+ <div class="card-header blog-clickable" id="seo-toggle">
45
+ SEO <span class="text-muted text-sm">(click to expand)</span>
46
46
  </div>
47
- <div class="card-body" id="seo-panel" style="display:none;">
47
+ <div class="card-body blog-hidden" id="seo-panel">
48
48
  <div class="mb-3">
49
- <label class="form-label">SEO Title</label>
50
- <input type="text" id="seo-title" class="form-control" placeholder="Overrides page title">
49
+ <label class="form-label" for="seo-title">SEO Title</label>
50
+ <input type="text" id="seo-title" class="form-input" placeholder="Overrides page title">
51
51
  </div>
52
52
  <div class="mb-3">
53
- <label class="form-label">SEO Description</label>
54
- <textarea id="seo-description" class="form-control" rows="2" placeholder="Overrides excerpt for meta description"></textarea>
53
+ <label class="form-label" for="seo-description">SEO Description</label>
54
+ <textarea id="seo-description" class="form-textarea" rows="2" placeholder="Overrides excerpt for meta description"></textarea>
55
55
  </div>
56
56
  </div>
57
57
  </div>
58
58
  </div>
59
59
 
60
60
  <!-- Sidebar -->
61
- <div style="width:280px; flex-shrink:0;">
61
+ <div class="blog-editor-side">
62
62
  <div class="card mb-3">
63
63
  <div class="card-header">Status</div>
64
64
  <div class="card-body">
65
- <div id="schedule-picker" style="display:none;" class="mb-3">
66
- <label class="form-label">Scheduled for</label>
67
- <input type="datetime-local" id="post-scheduled-at" class="form-control">
65
+ <div id="schedule-picker" class="mb-3 blog-hidden">
66
+ <label class="form-label" for="post-scheduled-at">Scheduled for</label>
67
+ <input type="datetime-local" id="post-scheduled-at" class="form-input">
68
68
  </div>
69
- <span id="post-status-badge" class="badge bg-secondary">draft</span>
69
+ <span id="post-status-badge" class="badge badge-secondary">draft</span>
70
70
  </div>
71
71
  </div>
72
72
 
73
73
  <div class="card mb-3">
74
74
  <div class="card-header">Categories</div>
75
75
  <div class="card-body" id="categories-checklist">
76
- <p class="text-muted small">Loading…</p>
76
+ <p class="text-muted text-sm">Loading…</p>
77
77
  </div>
78
78
  </div>
79
79
 
80
80
  <div class="card mb-3">
81
81
  <div class="card-header">Tags</div>
82
82
  <div class="card-body">
83
- <input type="text" id="post-tags" class="form-control" placeholder="tag1, tag2, …">
84
- <div class="text-muted small mt-1">Comma-separated</div>
83
+ <input type="text" id="post-tags" class="form-input" placeholder="tag1, tag2, …">
84
+ <div class="text-muted text-sm mt-1">Comma-separated</div>
85
85
  </div>
86
86
  </div>
87
87
 
@@ -90,7 +90,7 @@
90
90
  <div class="card-body">
91
91
  <div id="featured-image-preview" class="mb-2"></div>
92
92
  <button id="btn-pick-image" class="btn btn-sm btn-secondary">Pick Image</button>
93
- <button id="btn-clear-image" class="btn btn-sm btn-outline-secondary" style="display:none;">Clear</button>
93
+ <button id="btn-clear-image" class="btn btn-sm btn-outline blog-hidden">Clear</button>
94
94
  </div>
95
95
  </div>
96
96
  </div>
@@ -1,11 +1,14 @@
1
- <div class="view-header mb-4">
2
- <h1 class="h3 mb-0">Blog Settings</h1>
1
+ <div class="view-header">
2
+ <h1><span data-icon="settings"></span> Blog Settings</h1>
3
+ <button id="btn-save-settings" class="btn btn-primary">
4
+ <span data-icon="save"></span> Save
5
+ </button>
3
6
  </div>
4
7
 
5
8
  <div class="alert alert-warning mb-4">
6
9
  Base path and posts-per-page changes require a server restart to take effect.
7
10
  </div>
8
11
 
9
- <div class="card" style="max-width:600px;">
12
+ <div class="card blog-settings-card">
10
13
  <div class="card-body" id="settings-form"></div>
11
14
  </div>
@@ -44,7 +44,7 @@ export const blogView = {
44
44
  title: 'Title',
45
45
  sortable: true,
46
46
  render: (v, row) =>
47
- `<a href="#/plugins/blog/posts/${escapeHtml(row.id)}" style="font-weight:600;">${escapeHtml(v || 'Untitled')}</a>`
47
+ `<a href="#/plugins/blog/posts/${escapeHtml(row.id)}" class="blog-title-link">${escapeHtml(v || 'Untitled')}</a>`
48
48
  },
49
49
  {
50
50
  key: 'status',
@@ -61,7 +61,7 @@ export const blogView = {
61
61
  sortable: false,
62
62
  render: (val) => Array.isArray(val) && val.length
63
63
  ? val.map((id) => `<span class="badge badge-secondary mr-1">${escapeHtml(catNameById(id) || id)}</span>`).join('')
64
- : '<span style="opacity:0.4;">-</span>'
64
+ : '<span class="blog-empty-cell">-</span>'
65
65
  },
66
66
  {
67
67
  key: 'publishedAt',
@@ -69,7 +69,7 @@ export const blogView = {
69
69
  sortable: true,
70
70
  render: (v) => v
71
71
  ? `<span title="${escapeHtml(D(v).format('DD MMM YYYY HH:mm'))}">${escapeHtml(D(v).format('DD MMM YYYY'))}</span>`
72
- : '<span style="opacity:0.4;">-</span>'
72
+ : '<span class="blog-empty-cell">-</span>'
73
73
  },
74
74
  {
75
75
  key: 'id',
@@ -80,7 +80,7 @@ export const blogView = {
80
80
  const viewLink = row.status === 'published' && slug
81
81
  ? `<a href="/blog/${slug}" target="_blank" class="btn btn-sm btn-outline" title="View on site"><span data-icon="external-link" data-icon-size="14"></span></a>`
82
82
  : '';
83
- return `<div style="display:flex;gap:.4rem;">` +
83
+ return `<div class="blog-row-actions">` +
84
84
  `<button class="btn btn-sm btn-outline post-edit-btn" data-id="${escapeHtml(id)}"><span data-icon="edit" data-icon-size="14"></span></button>` +
85
85
  viewLink +
86
86
  `<button class="btn btn-sm btn-danger post-delete-btn" data-id="${escapeHtml(id)}"><span data-icon="trash" data-icon-size="14"></span></button>` +
@@ -165,7 +165,10 @@ export const blogView = {
165
165
  H.get(`${BASE}/categories`).catch(() => ({ data: [] }))
166
166
  ]);
167
167
  if (postsRes.error) throw new Error(postsRes.error);
168
- allPosts = postsRes.data ?? [];
168
+ // GET /posts sends a BARE ARRAY, so `.data` is undefined and this
169
+ // read emptied the whole screen - no rows, all four KPIs zero.
170
+ // Same guard as categories.js and comments.js.
171
+ allPosts = postsRes.data ?? (Array.isArray(postsRes) ? postsRes : []);
169
172
  categories = catsRes.data ?? catsRes ?? [];
170
173
  } catch {
171
174
  allPosts = [];
@@ -33,9 +33,7 @@ export const categoriesView = {
33
33
 
34
34
  function buildModal() {
35
35
  const form = document.createElement('div');
36
- form.style.display = 'flex';
37
- form.style.flexDirection = 'column';
38
- form.style.gap = '1rem';
36
+ form.className = 'blog-modal-form';
39
37
 
40
38
  const mkField = (labelText, id, tag = 'input', rows) => {
41
39
  const group = document.createElement('div');
@@ -48,7 +46,7 @@ export const categoriesView = {
48
46
 
49
47
  const el = document.createElement(tag);
50
48
  el.id = id;
51
- el.className = 'form-control';
49
+ el.className = tag === 'textarea' ? 'form-textarea' : 'form-input';
52
50
  if (tag === 'textarea' && rows) el.rows = rows;
53
51
  group.appendChild(el);
54
52
 
@@ -65,10 +63,7 @@ export const categoriesView = {
65
63
 
66
64
  // Footer buttons
67
65
  const footer = document.createElement('div');
68
- footer.style.display = 'flex';
69
- footer.style.justifyContent = 'flex-end';
70
- footer.style.gap = '0.5rem';
71
- footer.style.marginTop = '0.5rem';
66
+ footer.className = 'blog-modal-actions';
72
67
 
73
68
  const cancelBtn = document.createElement('button');
74
69
  cancelBtn.className = 'btn btn-secondary';
@@ -159,14 +154,14 @@ export const categoriesView = {
159
154
  key: 'description',
160
155
  title: 'Description',
161
156
  sortable: false,
162
- render: (v) => escapeHtml(v) || '<span style="opacity:0.4;">-</span>'
157
+ render: (v) => escapeHtml(v) || '<span class="blog-empty-cell">-</span>'
163
158
  },
164
159
  {
165
160
  key: 'id',
166
161
  title: 'Actions',
167
162
  sortable: false,
168
163
  render: (id) =>
169
- `<div style="display:flex;gap:.4rem;">` +
164
+ `<div class="blog-row-actions">` +
170
165
  `<button class="btn btn-sm btn-outline cat-edit-btn" data-id="${escapeHtml(id)}">` +
171
166
  `<span data-icon="edit" data-icon-size="14"></span> Edit</button>` +
172
167
  `<button class="btn btn-sm btn-danger cat-delete-btn" data-id="${escapeHtml(id)}">` +
@@ -65,7 +65,7 @@ export const commentsView = {
65
65
  sortable: false,
66
66
  render: (v) => v
67
67
  ? `<a href="mailto:${escapeHtml(v)}">${escapeHtml(v)}</a>`
68
- : '<span style="opacity:0.4;">-</span>'
68
+ : '<span class="blog-empty-cell">-</span>'
69
69
  },
70
70
  {
71
71
  key: 'body',
@@ -74,7 +74,7 @@ export const commentsView = {
74
74
  render: (v) => {
75
75
  const preview = String(v ?? '').slice(0, 80);
76
76
  const ellipsis = String(v ?? '').length > 80 ? '\u2026' : '';
77
- return `<span style="font-size:0.87rem;">${escapeHtml(preview)}${ellipsis}</span>`;
77
+ return `<span class="blog-comment-body">${escapeHtml(preview)}${ellipsis}</span>`;
78
78
  }
79
79
  },
80
80
  {
@@ -83,7 +83,7 @@ export const commentsView = {
83
83
  sortable: false,
84
84
  render: (v) => v
85
85
  ? `<a href="#/plugins/blog/posts/${escapeHtml(v)}">${escapeHtml(v)}</a>`
86
- : '<span style="opacity:0.4;">-</span>'
86
+ : '<span class="blog-empty-cell">-</span>'
87
87
  },
88
88
  {
89
89
  key: 'createdAt',
@@ -91,7 +91,7 @@ export const commentsView = {
91
91
  sortable: true,
92
92
  render: (v) => v
93
93
  ? `<span title="${escapeHtml(D(v).format('DD MMM YYYY HH:mm'))}">${escapeHtml(D(v).fromNow())}</span>`
94
- : '<span style="opacity:0.4;">-</span>'
94
+ : '<span class="blog-empty-cell">-</span>'
95
95
  },
96
96
  {
97
97
  key: 'id',
@@ -110,7 +110,7 @@ export const commentsView = {
110
110
  }
111
111
  btns.push(`<button class="btn btn-sm btn-danger comment-delete-btn" data-id="${escapeHtml(id)}">` +
112
112
  `<span data-icon="trash" data-icon-size="14"></span></button>`);
113
- return `<div style="display:flex;gap:.3rem;flex-wrap:wrap;">${btns.join('')}</div>`;
113
+ return `<div class="blog-moderation-actions">${btns.join('')}</div>`;
114
114
  }
115
115
  }
116
116
  ],