domma-cms 1.6.56 → 1.6.58

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 (111) hide show
  1. package/admin/css/calculator.css +1 -1
  2. package/admin/js/lib/analytics-shape.js +1 -1
  3. package/admin/js/lib/calculator-engine.js +1 -1
  4. package/admin/js/lib/calculator.js +22 -10
  5. package/admin/js/lib/form-editor-arrange.js +1 -1
  6. package/admin/js/lib/forms-arrange.js +1 -1
  7. package/admin/js/lib/notes-arrange.js +1 -1
  8. package/admin/js/lib/search-settings-arrange.js +1 -1
  9. package/admin/js/lib/seo-arrange.js +1 -0
  10. package/admin/js/lib/todo-arrange.js +1 -1
  11. package/admin/js/lib/tool-kit.js +1 -1
  12. package/admin/js/lib/tool-recovery.js +4 -4
  13. package/admin/js/templates/analytics.html +26 -14
  14. package/admin/js/templates/contacts.html +26 -5
  15. package/admin/js/templates/form-editor.html +2 -0
  16. package/admin/js/templates/notes.html +11 -8
  17. package/admin/js/templates/search.html +9 -3
  18. package/admin/js/templates/seo.html +23 -17
  19. package/admin/js/templates/todo.html +18 -14
  20. package/admin/js/views/analytics.js +10 -10
  21. package/admin/js/views/contacts.js +47 -47
  22. package/admin/js/views/form-editor.js +7 -7
  23. package/admin/js/views/form-submissions.js +2 -2
  24. package/admin/js/views/forms.js +5 -5
  25. package/admin/js/views/notes.js +6 -6
  26. package/admin/js/views/search.js +2 -2
  27. package/admin/js/views/seo.js +43 -43
  28. package/admin/js/views/todo.js +8 -8
  29. package/package.json +1 -1
  30. package/plugins/_lib/admin/mail/compose-window.js +4 -1
  31. package/plugins/_lib/admin/mail/mail.css +1 -1
  32. package/plugins/_lib/admin/mail/reader-kit.js +453 -0
  33. package/plugins/_lib/admin/mail/reader-view.js +1207 -165
  34. package/plugins/_lib/admin/mail/when-closed.js +33 -3
  35. package/plugins/_lib/mail/readRoutes.js +48 -10
  36. package/plugins/_lib/mail/unreadBadge.js +4 -1
  37. package/plugins/blog/CLAUDE.md +14 -0
  38. package/plugins/blog/admin/css/index.css +5 -0
  39. package/plugins/blog/admin/lib/drafts.js +95 -0
  40. package/plugins/blog/admin/templates/blog.html +1 -1
  41. package/plugins/blog/admin/templates/categories.html +1 -1
  42. package/plugins/blog/admin/templates/comments.html +1 -1
  43. package/plugins/blog/admin/templates/post-editor.html +5 -1
  44. package/plugins/blog/admin/views/blog.js +44 -19
  45. package/plugins/blog/admin/views/categories.js +138 -20
  46. package/plugins/blog/admin/views/comments.js +32 -23
  47. package/plugins/blog/admin/views/kit.js +232 -22
  48. package/plugins/blog/admin/views/post-editor.js +348 -178
  49. package/plugins/blog/admin/views/settings.js +17 -3
  50. package/plugins/blog/plugin.js +1 -1
  51. package/plugins/blog/plugin.json +7 -7
  52. package/plugins/blog/plugin.public.js +1 -1
  53. package/plugins/blog/tests/drafts.test.js +50 -0
  54. package/plugins/booking/CLAUDE.md +13 -0
  55. package/plugins/booking/admin/lib/drafts.js +139 -0
  56. package/plugins/booking/admin/lib/kit.js +246 -25
  57. package/plugins/booking/admin/views/booking.js +11 -2
  58. package/plugins/booking/admin/views/bookings.js +7 -2
  59. package/plugins/booking/admin/views/diary.js +13 -6
  60. package/plugins/booking/admin/views/panels.js +85 -12
  61. package/plugins/booking/admin/views/people.js +41 -2
  62. package/plugins/booking/admin/views/services.js +45 -3
  63. package/plugins/booking/admin/views/settings.js +22 -2
  64. package/plugins/booking/plugin.json +2 -2
  65. package/plugins/booking/tests/admin-drafts.test.js +67 -0
  66. package/plugins/feedback/CLAUDE.md +12 -0
  67. package/plugins/feedback/admin/css/index.css +5 -0
  68. package/plugins/feedback/admin/lib/drafts.js +145 -0
  69. package/plugins/feedback/admin/lib/kit.js +161 -5
  70. package/plugins/feedback/admin/lib/settings.js +46 -28
  71. package/plugins/feedback/admin/views/feedback.js +1 -1
  72. package/plugins/feedback/admin/views/inbox.js +110 -46
  73. package/plugins/feedback/admin/views/send.js +174 -83
  74. package/plugins/feedback/plugin.json +3 -3
  75. package/plugins/feedback/tests/drafts.test.js +93 -0
  76. package/plugins/free-tier.lock.json +75 -63
  77. package/plugins/security/CLAUDE.md +9 -0
  78. package/plugins/security/admin/lib/screen-state.js +120 -0
  79. package/plugins/security/admin/templates/security.html +15 -10
  80. package/plugins/security/admin/views/security.js +270 -132
  81. package/plugins/security/plugin.json +2 -2
  82. package/plugins/security/tests/screen-state.test.js +69 -0
  83. package/plugins/shopping-cart/CLAUDE.md +13 -0
  84. package/plugins/shopping-cart/admin/css/index.css +21 -5
  85. package/plugins/shopping-cart/admin/lib/drafts.js +174 -0
  86. package/plugins/shopping-cart/admin/lib/kit.js +136 -28
  87. package/plugins/shopping-cart/admin/views/orders.js +117 -36
  88. package/plugins/shopping-cart/admin/views/overview.js +38 -8
  89. package/plugins/shopping-cart/admin/views/products.js +317 -68
  90. package/plugins/shopping-cart/admin/views/sales.js +21 -3
  91. package/plugins/shopping-cart/admin/views/settings.js +62 -3
  92. package/plugins/shopping-cart/admin/views/shop.js +18 -3
  93. package/plugins/shopping-cart/plugin.json +3 -3
  94. package/plugins/shopping-cart/tests/accounts.test.js +11 -0
  95. package/plugins/shopping-cart/tests/drafts.test.js +118 -0
  96. package/plugins/support/CLAUDE.md +14 -0
  97. package/plugins/support/admin/css/index.css +10 -0
  98. package/plugins/support/admin/lib/drafts.js +162 -0
  99. package/plugins/support/admin/lib/kit.js +156 -16
  100. package/plugins/support/admin/lib/settings.js +52 -29
  101. package/plugins/support/admin/views/desk.js +114 -33
  102. package/plugins/support/admin/views/inbox.js +85 -26
  103. package/plugins/support/admin/views/support.js +1 -1
  104. package/plugins/support/plugin.json +3 -3
  105. package/plugins/support/tests/drafts.test.js +92 -0
  106. package/server/routes/api/analytics.js +5 -1
  107. package/server/routes/api/contacts.js +42 -0
  108. package/server/routes/api/forms.js +8 -0
  109. package/server/routes/api/seo.js +4 -1
  110. package/server/routes/api/todo.js +50 -10
  111. package/server/server.js +1 -1
@@ -391,6 +391,8 @@
391
391
  .fex-chip { font-size: var(--dm-text-xs); font-weight: 600; padding: .1rem .55rem; border-radius: 999px;
392
392
  background: color-mix(in srgb, var(--dm-warning) 22%, transparent); color: var(--dm-warning); white-space: nowrap; }
393
393
  .fex-note { color: var(--dm-text-muted); }
394
+ .fex-load-error { margin: 0 0 .75rem; padding: .5rem .75rem; border: 1px solid color-mix(in srgb, var(--dm-danger) 45%, var(--dm-border));
395
+ border-radius: var(--dm-radius-md); background: color-mix(in srgb, var(--dm-danger) 10%, transparent); color: var(--dm-danger); }
394
396
  .fex-spacer { flex: 1; }
395
397
  .fex-bar .btn { display: inline-flex; align-items: center; gap: .3rem; }
396
398
  .fex-kebab { border: 0; background: none; cursor: pointer; font-size: var(--dm-text-lg); line-height: 1; color: var(--dm-text-muted);
@@ -3,7 +3,7 @@
3
3
  <div class="notes-tool">
4
4
  <div class="tool-load-error" role="alert" data-if="loadError.value">
5
5
  <span data-bind-text="loadError.value"></span>
6
- <button type="button" class="btn btn-sm btn-ghost" data-on-click="retry" data-bind-disabled="loading.value">Retry</button>
6
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="retry" data-bind-disabled="busy.value">Retry</button>
7
7
  </div>
8
8
 
9
9
  <!-- One compact bar: find, filter, count, layout, new. -->
@@ -14,7 +14,8 @@
14
14
  data-model="query.value" data-focus="searchFocus.value">
15
15
  </label>
16
16
  <select class="form-input notes-cat-select" aria-label="Filter by category"
17
- data-options="categories.value" data-options-caption="'All categories'"
17
+ data-options="categoryChoices.value" data-options-text="label" data-options-value="value"
18
+ data-options-caption="'All categories'"
18
19
  data-model="category.value"></select>
19
20
  <span data-help-title="Finding notes"
20
21
  data-help="Search matches titles, content and categories as you type. Pick a category to narrow the list, or click a category chip on any note. Press / to jump to search."></span>
@@ -38,7 +39,8 @@
38
39
  </div>
39
40
  </div>
40
41
 
41
- <p class="notes-state" data-if="loading.value">Loading notes…</p>
42
+ <!-- The first load's skeleton (admin/js/lib/loading.js); the text is the fallback for a core without E.skeleton. -->
43
+ <div class="notes-loading dm-loading-slot" data-bind-hidden="!loading.value">Loading notes…</div>
42
44
  <p class="notes-state" data-if="isEmpty.value" data-bind-text="emptyText.value"></p>
43
45
 
44
46
  <!-- Right-click a note (or its ⋮) for everything you can do to it. -->
@@ -64,9 +66,10 @@
64
66
  .notes-bar-end { display: flex; align-items: center; gap: .5rem; margin-left: auto; }
65
67
  .notes-layout { display: inline-flex; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); overflow: hidden; }
66
68
  .notes-layout .btn { border-radius: 0; padding: .25rem .45rem; line-height: 1; color: var(--dm-text-muted); }
67
- .notes-layout .btn.is-active { background: var(--dm-surface-hover, rgba(127,127,127,.15)); color: var(--dm-primary); }
69
+ .notes-layout .btn.is-active { background: var(--dm-hover-bg); color: var(--dm-primary); }
68
70
 
69
71
  .notes-state { margin: 1.5rem 0; text-align: center; color: var(--dm-text-muted); }
72
+ .notes-loading { color: var(--dm-text-muted); }
70
73
 
71
74
  /* Cards: a responsive grid. */
72
75
  .notes-list.is-cards .notes-items {
@@ -86,16 +89,16 @@
86
89
  border: 0; background: none; cursor: pointer; padding: 0 .3rem;
87
90
  font-size: var(--dm-text-lg); line-height: 1.2; color: var(--dm-text-muted); border-radius: var(--dm-radius-sm);
88
91
  }
89
- .note-kebab:hover { color: var(--dm-primary); background: var(--dm-surface-hover, rgba(127,127,127,.15)); }
92
+ .note-kebab:hover { color: var(--dm-primary); background: var(--dm-hover-bg); }
90
93
  .note-preview { margin: 0; font-size: var(--dm-text-sm); color: var(--dm-text-muted); white-space: pre-line; overflow-wrap: anywhere; }
91
94
  .note-preview:empty { display: none; }
92
95
  .note-foot { display: flex; align-items: center; gap: .5rem; margin-top: auto; }
93
96
  .note-cats { display: flex; flex-wrap: wrap; gap: .25rem; flex: 1; min-width: 0; }
94
97
  .note-cat {
95
98
  font-size: var(--dm-text-xs); padding: .05rem .45rem; border-radius: 999px; cursor: pointer;
96
- background: var(--dm-surface-hover, rgba(127,127,127,.15)); color: var(--dm-text);
99
+ background: var(--dm-hover-bg); color: var(--dm-text);
97
100
  }
98
- .note-cat:hover { background: var(--dm-primary); color: var(--dm-text-inverse, #fff); }
101
+ .note-cat:hover { background: var(--dm-primary); color: var(--dm-text-inverse); }
99
102
  .note-time { font-size: var(--dm-text-xs); color: var(--dm-text-muted); white-space: nowrap; }
100
103
  .notes-list.hide-cats .note-cats { visibility: hidden; }
101
104
 
@@ -109,7 +112,7 @@
109
112
  padding: .45rem .5rem .45rem .75rem; border: 0; border-radius: 0; border-bottom: 1px solid var(--dm-border);
110
113
  }
111
114
  .notes-list.is-list .note-card:last-child { border-bottom: 0; }
112
- .notes-list.is-list .note-card:hover { background: var(--dm-surface-hover, rgba(127,127,127,.08)); }
115
+ .notes-list.is-list .note-card:hover { background: var(--dm-hover-bg); }
113
116
  .notes-list.is-list .note-title {
114
117
  flex: 0 0 28%; padding: 0; font-size: var(--dm-text-sm); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
115
118
  }
@@ -2,13 +2,17 @@
2
2
  preview row template (the router renders a templateUrl through _.render, eating any {{…}}).
3
3
  No page heading: the Tool banner above is the heading; Save and View site are banner actions. -->
4
4
  <div class="ssx">
5
- <p class="ssx-alert is-dirty" data-bind-hidden="!dirty.value">
5
+ <div class="ssx-note dm-loading-slot ssx-loading">Loading…</div>
6
+ <p class="ssx-alert is-bad ssx-failed" role="alert" hidden></p>
7
+ <!-- Shown once the settings are in: nothing here is bound before then. -->
8
+ <div class="ssx-body" hidden>
9
+ <p class="ssx-alert is-dirty" hidden data-bind-hidden="!dirty.value">
6
10
  <span data-icon="edit-3" data-icon-size="16"></span>
7
11
  <span>Unsaved changes - Save (Ctrl+S) to apply them. Leave and they are kept as a draft for next time.</span>
8
12
  <span class="ssx-spacer"></span>
9
13
  <button type="button" class="btn btn-sm btn-ghost" data-on-click="discard">Discard</button>
10
14
  <button type="button" class="btn btn-sm btn-primary" data-on-click="save" data-bind-disabled="saving.value">Save</button></p>
11
- <p class="ssx-alert" data-bind-hidden="!clampText.value">
15
+ <p class="ssx-alert" hidden data-bind-hidden="!clampText.value">
12
16
  <span data-icon="alert-triangle" data-icon-size="16"></span>
13
17
  <span data-bind-text="clampText.value"></span></p>
14
18
 
@@ -156,10 +160,12 @@
156
160
  </div>
157
161
  </div>
158
162
  </div>
163
+ </div>
159
164
  </div>
160
165
 
161
166
  <style>
162
- .ssx { display: flex; flex-direction: column; gap: .75rem; }
167
+ .ssx, .ssx-body { display: flex; flex-direction: column; gap: .75rem; }
168
+ .ssx-alert.is-bad { background: color-mix(in srgb, var(--dm-danger) 14%, transparent); color: var(--dm-danger); }
163
169
  .ssx [hidden] { display: none !important; }
164
170
  .ssx-alert { margin: 0; display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; padding: .6rem .8rem; border-radius: var(--dm-radius-md);
165
171
  background: color-mix(in srgb, var(--dm-warning) 16%, transparent); color: var(--dm-text); }
@@ -1,7 +1,7 @@
1
1
  <!-- SEO. Bound with M.applyBindings - admin/js/views/seo.js inserts the item templates (the router renders a
2
2
  templateUrl through _.render, eating any {{…}}). Refresh and Settings are banner actions. -->
3
3
  <div class="sox">
4
- <p class="sox-note is-bad" data-bind-hidden="!error.value" data-bind-text="error.value"></p>
4
+ <p class="sox-note is-bad" hidden data-bind-hidden="!error.value" data-bind-text="error.value"></p>
5
5
 
6
6
  <nav class="sox-tabs" role="tablist" aria-label="SEO">
7
7
  <button type="button" role="tab" data-on-click="tab_overview" data-bind-class="tab.value === 'overview' && 'is-on'"><span data-icon="search" data-icon-size="14"></span> Pages <small class="sox-count" data-bind-hidden="!attention.value" data-bind-text="attention.value"></small></button>
@@ -11,7 +11,7 @@
11
11
  </nav>
12
12
 
13
13
  <!-- Pages: the audit -->
14
- <section class="sox-panel" data-bind-hidden="tab.value !== 'overview'" data-bind-class="busy.value && 'is-busy'">
14
+ <section class="sox-panel" data-bind-hidden="tab.value !== 'overview'" data-bind-class="busy.value && !pagesLoading.value && 'is-busy'">
15
15
  <div class="sox-tiles">
16
16
  <div class="sox-card sox-tile sox-score" data-bind-data-tone="scoreTone.value">
17
17
  <small>Score <span data-help="The average of every page's score. A page's score is the share of its checks that pass - a check that could be better counts half; one for information only is left out. Pages hidden from search engines are not counted."></span></small>
@@ -31,9 +31,9 @@
31
31
 
32
32
  <div class="sox-toolbar">
33
33
  <div class="sox-seg" role="group" aria-label="Which pages">
34
- <button type="button" data-on-click="show_all" data-bind-class="show.value === 'all' && 'is-on'">All</button>
35
- <button type="button" data-on-click="show_problems" data-bind-class="show.value === 'problems' && 'is-on'">Need attention</button>
36
- <button type="button" data-on-click="show_hidden" data-bind-class="show.value === 'hidden' && 'is-on'">Hidden</button>
34
+ <button type="button" data-on-click="show_all" data-bind-class="show.value === 'all' && 'is-on'">All <small class="sox-count" hidden data-bind-hidden="!counts.value.all" data-bind-text="counts.value.all"></small></button>
35
+ <button type="button" data-on-click="show_problems" data-bind-class="show.value === 'problems' && 'is-on'">Need attention <small class="sox-count" hidden data-bind-hidden="!counts.value.problems" data-bind-text="counts.value.problems"></small></button>
36
+ <button type="button" data-on-click="show_hidden" data-bind-class="show.value === 'hidden' && 'is-on'">Hidden <small class="sox-count" hidden data-bind-hidden="!counts.value.hidden" data-bind-text="counts.value.hidden"></small></button>
37
37
  </div>
38
38
  <select class="form-input sox-select" aria-label="Where from" data-model="source.value" data-bind-hidden="sourceOptions.value.length < 2"></select>
39
39
  <input type="search" class="form-input sox-search" placeholder="Address, title…" aria-label="Search pages" data-model="query.value">
@@ -42,13 +42,14 @@
42
42
  <span data-help-title="Pages" data-help="Click a page to see its search result and share card as they will look, what could be better, and to change its title, description, focus keyphrase and share image. Right-click (or ⋮) for more."></span>
43
43
  </div>
44
44
  <div class="sox-card sox-flush">
45
- <p class="sox-note sox-pad" data-bind-hidden="pageRows.value.length" data-bind-text="pagesEmpty.value"></p>
45
+ <div class="sox-note sox-pad dm-loading-slot sox-pages-loading" data-bind-hidden="!pagesLoading.value">Loading…</div>
46
+ <p class="sox-note sox-pad" hidden data-bind-hidden="pagesLoading.value || pageRows.value.length" data-bind-text="pagesEmpty.value"></p>
46
47
  <div class="sox-list sox-pages" data-each="pageRows.value key=id"></div>
47
48
  </div>
48
49
  </section>
49
50
 
50
51
  <!-- Redirects -->
51
- <section class="sox-panel" data-bind-hidden="tab.value !== 'redirects'">
52
+ <section class="sox-panel" hidden data-bind-hidden="tab.value !== 'redirects'">
52
53
  <div class="sox-card">
53
54
  <div class="sox-card-title">Add a redirect
54
55
  <span data-help-title="Redirects" data-help="Sends an old address to a new one, so links and search results keep working. 301 is permanent (search engines move the old page's standing to the new one); 302 is for a while; 410 says the page is gone for good. End the old address with /* to move everything under it: /news/* to /blog/* sends /news/hello to /blog/hello."></span>
@@ -74,13 +75,14 @@
74
75
  <small class="sox-muted" data-bind-text="ruleText.value"></small>
75
76
  </div>
76
77
  <div class="sox-card sox-flush">
77
- <p class="sox-note sox-pad" data-bind-hidden="ruleRows.value.length" data-bind-text="rulesEmpty.value"></p>
78
+ <div class="sox-note sox-pad dm-loading-slot sox-rules-loading" data-bind-hidden="!rulesLoading.value">Loading…</div>
79
+ <p class="sox-note sox-pad" hidden data-bind-hidden="rulesLoading.value || ruleRows.value.length" data-bind-text="rulesEmpty.value"></p>
78
80
  <div class="sox-list sox-rules" data-each="ruleRows.value key=id"></div>
79
81
  </div>
80
82
  </section>
81
83
 
82
84
  <!-- Sitemap -->
83
- <section class="sox-panel" data-bind-hidden="tab.value !== 'sitemap'">
85
+ <section class="sox-panel" hidden data-bind-hidden="tab.value !== 'sitemap'">
84
86
  <div class="sox-grid2">
85
87
  <div class="sox-card">
86
88
  <div class="sox-card-title">What it lists
@@ -101,20 +103,22 @@
101
103
  </div>
102
104
  <div class="sox-toolbar">
103
105
  <div class="sox-seg" role="group" aria-label="Which addresses">
104
- <button type="button" data-on-click="sm_in" data-bind-class="smShow.value === 'in' && 'is-on'">Listed</button>
105
- <button type="button" data-on-click="sm_out" data-bind-class="smShow.value === 'out' && 'is-on'">Left out</button>
106
+ <button type="button" data-on-click="sm_in" data-bind-class="smShow.value === 'in' && 'is-on'">Listed <small class="sox-count" hidden data-bind-hidden="!smCounts.value.in" data-bind-text="smCounts.value.in"></small></button>
107
+ <button type="button" data-on-click="sm_out" data-bind-class="smShow.value === 'out' && 'is-on'">Left out <small class="sox-count" hidden data-bind-hidden="!smCounts.value.out" data-bind-text="smCounts.value.out"></small></button>
106
108
  </div>
107
109
  <input type="search" class="form-input sox-search" placeholder="Address…" aria-label="Search the sitemap" data-model="smQuery.value">
108
110
  <span class="sox-spacer"></span>
109
111
  <small class="sox-muted" data-bind-text="smText.value"></small>
110
112
  </div>
111
113
  <div class="sox-card sox-flush">
112
- <p class="sox-note sox-pad" data-bind-hidden="smRows.value.length" data-bind-text="smEmpty.value"></p>
114
+ <div class="sox-note sox-pad dm-loading-slot sox-entries-loading" data-bind-hidden="!smLoading.value">Loading…</div>
115
+ <p class="sox-note sox-pad" hidden data-bind-hidden="smLoading.value || smRows.value.length" data-bind-text="smEmpty.value"></p>
113
116
  <div class="sox-list sox-entries" data-each="smRows.value key=id"></div>
114
117
  </div>
115
118
  </section>
116
119
 
117
- <section class="sox-ext" data-bind-hidden="!extOn.value"><div class="sox-panel sox-ext-body"></div></section>
120
+ <!-- An extension's tab (SEO Pro): each mount gets its own <div class="sox-panel sox-ext-body"> here. -->
121
+ <section class="sox-ext" hidden data-bind-hidden="!extOn.value"></section>
118
122
  </div>
119
123
 
120
124
  <style>
@@ -134,6 +138,8 @@
134
138
  .sox-tabs button:hover { color: var(--dm-text); }
135
139
  .sox-tabs button.is-on { color: var(--dm-text); border-bottom-color: var(--dm-primary); font-weight: 600; }
136
140
  .sox-pro { font-size: .65rem; font-weight: 700; letter-spacing: .04em; padding: 0 .3rem; border-radius: var(--dm-radius-sm); background: color-mix(in srgb, var(--dm-primary) 16%, transparent); color: var(--dm-primary); }
141
+ .sox-seg .sox-count { margin-left: .2rem; }
142
+ .sox-seg button.is-on .sox-count { background: color-mix(in srgb, var(--dm-primary-text) 22%, transparent); }
137
143
  .sox-count { font-size: var(--dm-text-xs); padding: 0 .4rem; border-radius: 999px; background: color-mix(in srgb, var(--dm-text) 10%, transparent); }
138
144
  .sox-card { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .75rem .9rem; display: flex; flex-direction: column; gap: .6rem; min-width: 0; }
139
145
  .sox-flush { padding: 0; }
@@ -195,10 +201,10 @@
195
201
  .sox-meter[data-tone="good"] .sox-bar span { background: var(--dm-success); }
196
202
  .sox-meter[data-tone="poor"] .sox-bar span { background: var(--dm-warning); }
197
203
  .sox-meter[data-tone="bad"] .sox-bar span { background: var(--dm-danger); }
198
- .sox-serp { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .8rem 1rem; background: #fff; color: #202124; font-family: Arial, sans-serif; }
199
- .sox-serp-crumbs { font-size: 12px; color: #4d5156; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
200
- .sox-serp-title { font-size: 20px; line-height: 1.3; color: #1a0dab; margin: .15rem 0 .2rem; overflow-wrap: anywhere; }
201
- .sox-serp-desc { font-size: 14px; line-height: 1.58; color: #4d5156; overflow-wrap: anywhere; }
204
+ .sox-serp { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .8rem 1rem; background: var(--dm-surface, var(--dm-bg)); color: var(--dm-text); font-family: Arial, sans-serif; }
205
+ .sox-serp-crumbs { font-size: 12px; color: var(--dm-text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
206
+ .sox-serp-title { font-size: 20px; line-height: 1.3; color: var(--dm-link, var(--dm-primary)); margin: .15rem 0 .2rem; overflow-wrap: anywhere; }
207
+ .sox-serp-desc { font-size: 14px; line-height: 1.58; color: var(--dm-text-muted); overflow-wrap: anywhere; }
202
208
  .sox-share { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); overflow: hidden; max-width: 30rem; background: var(--dm-surface, var(--dm-bg)); }
203
209
  .sox-share-img { aspect-ratio: 1.91 / 1; background: color-mix(in srgb, var(--dm-text) 8%, transparent) center / cover no-repeat; display: flex; align-items: center; justify-content: center; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
204
210
  .sox-share-body { padding: .55rem .75rem; display: flex; flex-direction: column; gap: .15rem; }
@@ -4,7 +4,7 @@
4
4
  <div class="todo-tool">
5
5
  <div class="tool-load-error" role="alert" data-if="loadError.value">
6
6
  <span data-bind-text="loadError.value"></span>
7
- <button type="button" class="btn btn-sm btn-ghost" data-on-click="retry" data-bind-disabled="loading.value">Retry</button>
7
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="retry" data-bind-disabled="busy.value">Retry</button>
8
8
  </div>
9
9
 
10
10
  <!-- Quick add: type and press Enter. -->
@@ -36,7 +36,7 @@
36
36
  <select class="form-input todo-filter" aria-label="Show" data-options="filterOptions.value"
37
37
  data-options-text="label" data-options-value="value" data-model="filter.value"></select>
38
38
  <span data-help-title="Working with tasks"
39
- data-help="Tick a task to complete it. Right-click a task (or its ⋮, or click its status or priority) to change status, priority or due date, edit, or delete it. Completed tasks always sit at the bottom. Press / to search."></span>
39
+ data-help="Tick a task to complete it. Click its text (or press Enter on it) to edit. Right-click a task, press Shift+F10 on it, or click its ⋮, status or priority for everything else: status, priority, due date, delete. Completed tasks always sit at the bottom. Press / to search."></span>
40
40
  <span class="todo-count" data-bind-text="countText.value"></span>
41
41
  <button type="button" class="btn btn-sm btn-ghost todo-clear" data-if="counts.value.completed"
42
42
  data-on-click="clearCompleted" title="Delete every completed task">
@@ -44,7 +44,8 @@
44
44
  </button>
45
45
  </div>
46
46
 
47
- <p class="todo-state" data-if="loading.value">Loading tasks…</p>
47
+ <!-- The first load's skeleton (admin/js/lib/loading.js); the text is the fallback for a core without E.skeleton. -->
48
+ <div class="todo-loading dm-loading-slot" data-bind-hidden="!loading.value">Loading tasks…</div>
48
49
  <p class="todo-state" data-if="isEmpty.value" data-bind-text="emptyText.value"></p>
49
50
 
50
51
  <div class="todo-list">
@@ -68,6 +69,7 @@
68
69
  .todo-count { font-size: var(--dm-text-xs); color: var(--dm-text-muted); white-space: nowrap; }
69
70
  .todo-clear { margin-left: auto; color: var(--dm-text-muted); }
70
71
  .todo-state { margin: 1.5rem 0; text-align: center; color: var(--dm-text-muted); }
72
+ .todo-loading { color: var(--dm-text-muted); }
71
73
 
72
74
  .todo-rows { display: flex; flex-direction: column; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); overflow: hidden; }
73
75
  .todo-rows:empty { display: none; }
@@ -76,33 +78,35 @@
76
78
  border-bottom: 1px solid var(--dm-border);
77
79
  }
78
80
  .todo-row:last-child { border-bottom: 0; }
79
- .todo-row:hover, .todo-row:focus-within { background: var(--dm-surface-hover, rgba(127,127,127,.08)); }
81
+ .todo-row:hover, .todo-row:focus-within { background: var(--dm-hover-bg); }
82
+ .todo-row:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--dm-primary); }
80
83
  .todo-row .todo-check { flex: 0 0 auto; width: 1rem; height: 1rem; cursor: pointer; margin: 0; }
81
84
  .todo-text { flex: 1; min-width: 0; font-size: var(--dm-text-sm); overflow-wrap: anywhere; cursor: pointer; }
82
85
  .todo-row.is-done .todo-text { text-decoration: line-through; opacity: .5; }
83
86
  .todo-due { flex: 0 0 auto; font-size: var(--dm-text-xs); padding: .05rem .45rem; border-radius: 999px; white-space: nowrap;
84
- background: var(--dm-surface-hover, rgba(127,127,127,.15)); color: var(--dm-text-muted); }
87
+ background: var(--dm-hover-bg); color: var(--dm-text-muted); }
85
88
  .todo-due:empty { display: none; }
86
- .todo-due.is-today { color: var(--dm-warning, #d97706); }
87
- .todo-due.is-overdue { background: color-mix(in srgb, var(--dm-danger, #dc2626) 16%, transparent); color: var(--dm-danger, #dc2626); }
89
+ .todo-due.is-today { color: var(--dm-warning); }
90
+ .todo-due.is-overdue { background: color-mix(in srgb, var(--dm-danger) 16%, transparent); color: var(--dm-danger); }
88
91
  .todo-pill {
89
92
  flex: 0 0 auto; border: 0; cursor: pointer; font: inherit; font-size: var(--dm-text-xs); font-weight: 500;
90
93
  padding: .1rem .55rem; border-radius: 999px; white-space: nowrap;
91
94
  }
92
95
  .todo-kebab { flex: 0 0 auto; border: 0; background: none; cursor: pointer; font-size: var(--dm-text-lg); line-height: 1.2;
93
96
  color: var(--dm-text-muted); border-radius: var(--dm-radius-sm); padding: 0 .3rem; }
94
- .todo-kebab:hover { color: var(--dm-primary); background: var(--dm-surface-hover, rgba(127,127,127,.15)); }
97
+ .todo-kebab:hover { color: var(--dm-primary); background: var(--dm-hover-bg); }
95
98
  @media (max-width: 640px) { .todo-pill[data-kind="status"] { display: none; } }
96
99
 
97
100
  /* Pills take the theme's own tokens, so they read in dark mode too. */
98
- .todo-pill[data-value="not-started"] { background: var(--dm-surface-hover, rgba(127,127,127,.15)); color: var(--dm-text-muted); }
101
+ .todo-pill[data-value="not-started"] { background: var(--dm-hover-bg); color: var(--dm-text-muted); }
99
102
  .todo-pill[data-value="in-progress"] { background: color-mix(in srgb, var(--dm-primary) 18%, transparent); color: var(--dm-primary); }
100
- .todo-pill[data-value="completed"] { background: color-mix(in srgb, var(--dm-success, #16a34a) 18%, transparent); color: var(--dm-success, #16a34a); }
101
- .todo-pill[data-value="highest"] { background: color-mix(in srgb, var(--dm-danger, #dc2626) 20%, transparent); color: var(--dm-danger, #dc2626); }
102
- .todo-pill[data-value="high"] { background: color-mix(in srgb, #ea580c 18%, transparent); color: #ea580c; }
103
- .todo-pill[data-value="medium"] { background: color-mix(in srgb, var(--dm-warning, #d97706) 18%, transparent); color: var(--dm-warning, #d97706); }
103
+ .todo-pill[data-value="completed"] { background: color-mix(in srgb, var(--dm-success) 18%, transparent); color: var(--dm-success); }
104
+ .todo-pill[data-value="highest"] { background: color-mix(in srgb, var(--dm-danger) 20%, transparent); color: var(--dm-danger); }
105
+ /* No orange token in the themes: high sits between danger and warning. */
106
+ .todo-pill[data-value="high"] { background: color-mix(in srgb, color-mix(in srgb, var(--dm-danger) 55%, var(--dm-warning)) 18%, transparent); color: color-mix(in srgb, var(--dm-danger) 55%, var(--dm-warning)); }
107
+ .todo-pill[data-value="medium"] { background: color-mix(in srgb, var(--dm-warning) 18%, transparent); color: var(--dm-warning); }
104
108
  .todo-pill[data-value="low"] { background: color-mix(in srgb, var(--dm-primary) 14%, transparent); color: var(--dm-primary); }
105
- .todo-pill[data-value="lowest"] { background: var(--dm-surface-hover, rgba(127,127,127,.15)); color: var(--dm-text-muted); }
109
+ .todo-pill[data-value="lowest"] { background: var(--dm-hover-bg); color: var(--dm-text-muted); }
106
110
 
107
111
  /* Editor and settings slideovers */
108
112
  .todo-panel { display: flex; flex-direction: column; gap: 1rem; padding: .25rem 0 .5rem; }
@@ -1,29 +1,29 @@
1
- import{apiRequest as h,getToken as se,refreshAccessToken as ne}from"../api.js";import{addBannerAction as oe}from"../lib/plugin-chrome.js";import{helpPopovers as J}from"../lib/help-popover.js";import{onceClosed as le,openMenuAt as ie,settingsStore as re,sidebarCount as ce,viewLifetime as de}from"../lib/tool-kit.js";import{DEFAULT_SETTINGS as ue,journeyRows as pe,kpis as ve,LIMITS as me,normaliseSettings as z,RANGE_LABEL as U,RANGES as be,seriesRange as he,topRows as fe}from"../lib/analytics-shape.js";import{fmtDate as ye}from"../lib/dates.js";const ge="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js",we=3e4,Y=re("analytics.settings",z),Me=`
2
- <div class="an-row" data-url="{{url}}">
1
+ import{apiRequest as b,getToken as be,refreshAccessToken as he}from"../api.js";import{addBannerAction as fe}from"../lib/plugin-chrome.js";import{helpPopovers as te}from"../lib/help-popover.js";import{onceClosed as ye,openMenuAt as ae,settingsStore as ge,sidebarCount as se,viewLifetime as we}from"../lib/tool-kit.js";import{DEFAULT_SETTINGS as Me,journeyRows as ke,kpis as Ee,LIMITS as Re,normaliseSettings as ne,pageEditHash as Se,RANGE_LABEL as V,RANGES as Ce,seriesRange as Le,siteAddress as Ae,tabCounts as Te,topRows as xe}from"../lib/analytics-shape.js";import{fmtDate as Pe}from"../lib/dates.js";import{SHAPES as $e,showLoading as je}from"../lib/loading.js";const He="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js",Ie=3e4,oe=ge("analytics.settings",ne),Ue=`
2
+ <div class="an-row" tabindex="0" data-url="{{url}}">
3
3
  <span class="an-bar-fill" style="width: {{bar}}"></span>
4
4
  <span class="an-rank">{{rank}}</span>
5
5
  <span class="an-url" title="{{url}}">{{url}}</span>
6
6
  <span class="an-num">{{hitsText}}</span>
7
7
  <span class="an-share">{{share}}</span>
8
8
  <button type="button" class="an-kebab" aria-label="Page actions" title="Actions">\u22EE</button>
9
- </div>`,H=`
10
- <div class="an-row" data-url="{{url}}">
9
+ </div>`,B=`
10
+ <div class="an-row" tabindex="0" data-url="{{url}}">
11
11
  <span class="an-bar-fill" style="width: {{bar}}"></span>
12
12
  <span class="an-url" title="{{label}}">{{label}}</span>
13
13
  <span class="an-num">{{count}}</span>
14
14
  <button type="button" class="an-kebab" aria-label="Page actions" title="Actions">\u22EE</button>
15
- </div>`,Re=`
16
- <div class="an-row" data-url="{{url}}">
15
+ </div>`,De=`
16
+ <div class="an-row" tabindex="0" data-url="{{url}}">
17
17
  <span class="an-url" title="{{url}}">{{url}}</span>
18
18
  <span class="an-num" title="Views per hour today, against the usual {{baseline}}">{{hits}}/h</span>
19
19
  <span class="an-share">\xD7{{ratio}}</span>
20
20
  <button type="button" class="an-kebab" aria-label="Page actions" title="Actions">\u22EE</button>
21
- </div>`;function ke(){return window.Chart?Promise.resolve():new Promise((r,s)=>{const a=document.createElement("script");a.src=ge,a.async=!0,a.onload=()=>r(),a.onerror=()=>s(new Error("chart.js load failed")),document.head.appendChild(a)})}function Ee(r){const s=document.createElement("template");return s.innerHTML=r.trim(),s.content.firstElementChild}async function Ce(r){const s=f=>fetch("/api/analytics/export.csv?range="+encodeURIComponent(r),{headers:f?{Authorization:"Bearer "+f}:{}});let a=await s(se());if(a.status===401&&(a=await s(await ne())),!a.ok)throw new Error("export failed");const c=await a.blob(),d=(a.headers.get("Content-Disposition")||"").match(/filename="?([^"]+)"?/),p=URL.createObjectURL(c),i=document.createElement("a");i.href=p,i.download=d?d[1]:"analytics.csv",document.body.appendChild(i),i.click(),i.remove(),URL.revokeObjectURL(p)}const l=de(/^#\/analytics\b/);export const analyticsView={templateUrl:"/admin/js/templates/analytics.html",async onMount(r){l.reset();const s=r.get(0),a=M.observable(Y.read()),c=M.observable(a.peek().range),d=M.observable("pages"),p=M.observable(""),i=M.observable(null),f=M.observable([]),R=M.observable(null),O=M.observable([]),v=M.observable(null),y=M.observable(""),m=M.observable(""),L=M.observable(null),S=M.observable(null),T=M.observable(null),_=M.observable([]),N=M.observable("");l.add(M.effect(()=>{const e=i.value,{rows:t,matched:n}=fe(e?.top,{filter:p.value,limit:a.value.limit,total:e?.total});_.value=t,N.value=e?e.top.length?n?n>t.length?`Showing ${t.length} of ${n} pages - the cog changes how many.`:"":"No pages match that filter.":"No page views recorded in this range yet.":"Loading\u2026"}));const V=M.observable([]),q=M.observable([]),B=M.observable([]);l.add(M.effect(()=>{const e=pe(R.value);V.value=e.entry,q.value=e.exit,B.value=e.paths}));const K={range:c,section:d,filter:p,spikes:O,pageRows:_,pagesNote:N,entryRows:V,exitRows:q,pathRows:B,loadError:y,liveError:m,freshness:M.computed(()=>L.value?`Last updated ${D(L.value).format("HH:mm:ss")} \xB7 ${U[T.value]||T.value}${y.value?" \xB7 Figures are stale":""}`:y.value?"Figures unavailable":"Loading figures\u2026"),kpi:M.computed(()=>ve(i.value,R.value)),isAllTime:M.computed(()=>c.value==="all"),sessionsCount:M.computed(()=>R.value?.totalSessions??0),liveText:M.computed(()=>m.value?v.value===null?"Active now unavailable":`${v.value} active \xB7 stale`:v.value===null?"Active now: -":`${v.value} active now`),liveClass:M.computed(()=>!m.value&&v.value?"is-on":""),liveTitle:M.computed(()=>(S.value?`Last updated ${D(S.value).format("HH:mm:ss")}. `:"")+(m.value||(a.value.live?"Refreshes every 30 seconds":"Live refresh is off - see settings")))};l.add(M.effect(()=>Y.write(a.value)));const Z=(e,t)=>{a.value=z({...a.peek(),[e]:t})};let A=0;async function k(){const e=c.peek(),t=++A;try{const[n,u,b,w]=await Promise.all([h("/analytics/stats?range="+encodeURIComponent(e)),h("/analytics/series?range="+encodeURIComponent(he(e))),h("/analytics/journeys?range="+encodeURIComponent(e)),h("/analytics/spikes")]);if(t!==A)return;i.value=n,f.value=u?.series??[],R.value=b,O.value=(w?.items??[]).map(o=>({...o,key:o.url})),T.value=e,L.value=new Date().toISOString(),y.value=""}catch{t===A&&(y.value="Could not refresh analytics. Previous figures, if shown, are retained.")}}let x=0;async function P(){const e=++x;try{const t=await h("/analytics/realtime");if(e!==x)return;v.value=t?.activeSessions??0,S.value=new Date().toISOString(),m.value="",ce("#/analytics")}catch{e===x&&(m.value="Live visitor count is unavailable. Retry to refresh it.")}}let G=!0;l.add(M.effect(()=>{if(c.value,G){G=!1;return}k()}));let $=null;l.add(M.effect(()=>{clearInterval($),$=a.value.live?setInterval(()=>{document.hidden||P()},we):null})),l.add(()=>clearInterval($));const W=s.querySelector(".an-chart");let C=null;l.add(()=>{C?.destroy(),C=null}),l.add(M.effect(()=>{const e=f.value,t=a.value.chart;if(!window.Chart||!W.isConnected)return;const n=getComputedStyle(document.documentElement),u=n.getPropertyValue("--dm-primary").trim()||"rgb(99, 102, 241)",b=n.getPropertyValue("--dm-text-muted").trim()||"#888",w=`color-mix(in srgb, ${n.getPropertyValue("--dm-border").trim()||"#888"} 45%, transparent)`;C?.destroy(),C=new window.Chart(W,{type:t,data:{labels:e.map(o=>ye(o.date).slice(0,5)),datasets:[{label:"Page views",data:e.map(o=>o.hits),borderColor:u,backgroundColor:t==="bar"?u:`color-mix(in srgb, ${u} 18%, transparent)`,fill:t==="line",tension:.3,borderWidth:2,pointRadius:e.length>40?0:2,borderRadius:3}]},options:{responsive:!0,maintainAspectRatio:!1,animation:!1,plugins:{legend:{display:!1}},scales:{y:{beginAtZero:!0,ticks:{precision:0,color:b},grid:{color:w}},x:{ticks:{maxRotation:0,autoSkip:!0,maxTicksLimit:10,color:b},grid:{display:!1}}}}})}));function Q(){const e=E.slideover({title:"Analytics settings",size:"sm",position:"right"}),t=Ee(`
21
+ </div>`;function Oe(){return window.Chart?Promise.resolve():new Promise((p,n)=>{const a=document.createElement("script");a.src=He,a.async=!0,a.onload=()=>p(),a.onerror=()=>n(new Error("chart.js load failed")),document.head.appendChild(a)})}function _e(p){const n=document.createElement("template");return n.innerHTML=p.trim(),n.content.firstElementChild}async function Ne(p){const n=v=>fetch("/api/analytics/export.csv?range="+encodeURIComponent(p),{headers:v?{Authorization:"Bearer "+v}:{}});let a=await n(be());if(a.status===401&&(a=await n(await he())),!a.ok)throw new Error("export failed");const i=await a.blob(),d=(a.headers.get("Content-Disposition")||"").match(/filename="?([^"]+)"?/),u=URL.createObjectURL(i),c=document.createElement("a");c.href=u,c.download=d?d[1]:"analytics.csv",document.body.appendChild(c),c.click(),c.remove(),URL.revokeObjectURL(u)}const l=we(/^#\/analytics\b/);export const analyticsView={templateUrl:"/admin/js/templates/analytics.html",async onMount(p){l.reset();const n=p.get(0);let a=!0;l.add(()=>{a=!1});const i=M.observable(oe.read()),d=M.observable(i.peek().range),u=M.observable("pages"),c=M.observable(""),v=M.observable(null),q=M.observable([]),g=M.observable(null),A=M.observable([]),h=M.observable(null),w=M.observable(""),f=M.observable(""),T=M.observable(null),x=M.observable(null),P=M.observable(null),$=M.observable(!0),F=M.observable(!!window.Chart);let G=new Set;const W=M.observable([]),J=M.observable("");l.add(M.effect(()=>{const e=v.value,{rows:t,matched:s}=xe(e?.top,{filter:c.value,limit:i.value.limit,total:e?.total});W.value=t,J.value=e?e.top.length?s?s>t.length?`Showing ${t.length} of ${s} pages - the cog changes how many.`:"":"No pages match that filter.":"No page views recorded in this range yet.":""}));const z=M.observable([]),K=M.observable([]),Y=M.observable([]);l.add(M.effect(()=>{const e=ke(g.value);z.value=e.entry,K.value=e.exit,Y.value=e.paths}));const le={range:d,section:u,filter:c,spikes:A,pageRows:W,pagesNote:J,entryRows:z,exitRows:K,pathRows:Y,loadError:w,liveError:f,loading:$,counts:M.computed(()=>Te(v.value,g.value,$.value?null:A.value)),freshness:M.computed(()=>T.value?`Last updated ${D(T.value).format("HH:mm:ss")} \xB7 ${V[P.value]||P.value}${w.value?" \xB7 Figures are stale":""}`:w.value?"Figures unavailable":"Loading figures\u2026"),kpi:M.computed(()=>Ee(v.value,g.value)),isAllTime:M.computed(()=>d.value==="all"),sessionsCount:M.computed(()=>g.value?.totalSessions??0),liveText:M.computed(()=>f.value?h.value===null?"Active now unavailable":`${h.value} active \xB7 stale`:h.value===null?"Active now: -":`${h.value} active now`),liveClass:M.computed(()=>!f.value&&h.value?"is-on":""),liveTitle:M.computed(()=>(x.value?`Last updated ${D(x.value).format("HH:mm:ss")}. `:"")+(f.value||(i.value.live?"Refreshes every 30 seconds":"Live refresh is off - see settings")))};l.add(M.effect(()=>oe.write(i.value)));const ie=(e,t)=>{i.value=ne({...i.peek(),[e]:t})};let Z=je(n.querySelector(".an-loading"),$e.rows);const Q=async()=>{const e=Z;Z=null,e&&await e.done(),a&&($.value=!1)};let j=0;async function S(){const e=d.peek(),t=++j;try{const[s,r,y,R]=await Promise.all([b("/analytics/stats?range="+encodeURIComponent(e)),b("/analytics/series?range="+encodeURIComponent(Le(e))),b("/analytics/journeys?range="+encodeURIComponent(e)),b("/analytics/spikes")]);if(await Q(),!a||t!==j)return;v.value=s,q.value=r?.series??[],g.value=y,A.value=(R?.items??[]).map(o=>({...o,key:o.url})),P.value=e,T.value=new Date().toISOString(),w.value=""}catch{await Q(),a&&t===j&&(w.value="Could not refresh analytics. Previous figures, if shown, are retained.")}}async function re(){try{const e=await b("/pages");a&&(G=new Set((Array.isArray(e)?e:[]).map(t=>t.urlPath).filter(Boolean)))}catch{}}let H=0;async function U(){const e=++H;try{const t=await b("/analytics/realtime");if(!a||e!==H)return;h.value=t?.activeSessions??0,x.value=new Date().toISOString(),f.value="",se("#/analytics")}catch{a&&e===H&&(f.value="Live visitor count is unavailable. Retry to refresh it.")}}let X=!0;l.add(M.effect(()=>{if(d.value,X){X=!1;return}S()}));let O=null;l.add(M.effect(()=>{clearInterval(O),O=i.value.live?setInterval(()=>{document.hidden||U()},Ie):null})),l.add(()=>clearInterval(O));const ee=n.querySelector(".an-chart");let C=null;l.add(()=>{C?.destroy(),C=null}),l.add(M.effect(()=>{const e=q.value,t=i.value.chart;if(!F.value||!window.Chart||!ee.isConnected)return;const s=getComputedStyle(document.documentElement),r=s.getPropertyValue("--dm-primary").trim()||"rgb(99, 102, 241)",y=s.getPropertyValue("--dm-text-muted").trim()||"#888",R=`color-mix(in srgb, ${s.getPropertyValue("--dm-border").trim()||"#888"} 45%, transparent)`;C?.destroy(),C=new window.Chart(ee,{type:t,data:{labels:e.map(o=>Pe(o.date).slice(0,5)),datasets:[{label:"Page views",data:e.map(o=>o.hits),borderColor:r,backgroundColor:t==="bar"?r:`color-mix(in srgb, ${r} 18%, transparent)`,fill:t==="line",tension:.3,borderWidth:2,pointRadius:e.length>40?0:2,borderRadius:3}]},options:{responsive:!0,maintainAspectRatio:!1,animation:!1,plugins:{legend:{display:!1}},scales:{y:{beginAtZero:!0,ticks:{precision:0,color:y},grid:{color:R}},x:{ticks:{maxRotation:0,autoSkip:!0,maxTicksLimit:10,color:y},grid:{display:!1}}}}})}));let m=null;l.add(()=>{try{m?.close()}catch{}});function ce(){if(!m){m=!0;try{de()}catch(e){throw m=null,e}}}function de(){const e=E.slideover({title:"Analytics settings",size:"sm",position:"right"}),t=_e(`
22
22
  <div class="an-panel-form">
23
23
  <div>
24
24
  <label class="form-label" for="an-set-range">Open on</label>
25
25
  <select id="an-set-range" class="form-input" data-model="range.value">
26
- ${be.map(o=>`<option value="${o}">${U[o]}</option>`).join("")}
26
+ ${Ce.map(o=>`<option value="${o}">${V[o]}</option>`).join("")}
27
27
  </select>
28
28
  </div>
29
29
  <div>
@@ -31,7 +31,7 @@ import{apiRequest as h,getToken as se,refreshAccessToken as ne}from"../api.js";i
31
31
  <span data-help="How many pages the Top pages list shows. The filter still searches all of them, and the CSV export always has every page."></span>
32
32
  </label>
33
33
  <select id="an-set-limit" class="form-input" data-model="limit.value">
34
- ${me.map(o=>`<option value="${o}">${o||"All"}</option>`).join("")}
34
+ ${Re.map(o=>`<option value="${o}">${o||"All"}</option>`).join("")}
35
35
  </select>
36
36
  </div>
37
37
  <div>
@@ -59,4 +59,4 @@ import{apiRequest as h,getToken as se,refreshAccessToken as ne}from"../api.js";i
59
59
  <span class="spacer"></span>
60
60
  <button type="button" class="btn btn-sm btn-primary" data-on-click="done">Done</button>
61
61
  </div>
62
- </div>`),n=(o,ae=j=>j)=>M.computed({read:()=>a.value[o],write:j=>Z(o,ae(j))}),u={range:n("range"),limit:n("limit",Number),chart:n("chart"),live:n("live"),confirmReset:n("confirmReset")};e.element.appendChild(t);const b=J(t),w=M.applyBindings(u,t,{methods:{done:()=>e.close(),reset:()=>{a.value={...ue}}}});le(e,()=>{w.dispose(),b()}),e.open()}oe(r,{id:"analytics-settings",icon:"settings",title:"Analytics settings",onClick:Q});const g=[...s.querySelectorAll(".an-list")];g[0].innerHTML=Me,g[1].innerHTML=H,g[2].innerHTML=H,g[3].innerHTML=H,g[4].innerHTML=Re,l.add(J(s,{position:"bottom"}));const X=M.applyBindings(K,s,{methods:{refresh:()=>{k(),P()},showPages:()=>{d.value="pages"},showJourneys:()=>{d.value="journeys"},showSpikes:()=>{d.value="spikes"}}});l.add(()=>X.dispose());const F=e=>new URL(e,location.origin).href,ee=E.contextMenu(s,{cursor:"glyph",match:".an-row",inherit:!1,density:"compact",items:e=>{const t=e.dataset.url;return[{label:"Open on the site",icon:"external-link",action:()=>window.open(F(t),"_blank","noopener")},{label:"Copy address",icon:"copy",action:async()=>{try{await navigator.clipboard.writeText(F(t)),E.toast("Address copied",{type:"success"})}catch{E.toast("The browser would not allow copying",{type:"error"})}}},{label:"Show only this page",icon:"filter",action:()=>{d.value="pages",p.value=t}}]}});l.add(()=>ee?.destroy?.());const te=E.contextMenu(s.querySelector(".an-bar-end"),{cursor:"glyph",match:".an-more",inherit:!1,density:"compact",items:()=>[{label:`Export ${U[c.peek()].toLowerCase()} as CSV`,icon:"download",action:async()=>{try{await Ce(c.peek())}catch{E.toast("Export failed.",{type:"error"})}}},{type:"divider"},{label:"Reset all statistics\u2026",icon:"trash",danger:!0,action:async()=>{if(!(a.peek().confirmReset&&!await E.confirm("Delete every analytics counter for this site? This cannot be undone.")))try{await h("/analytics/stats",{method:"DELETE"}),E.toast("Analytics reset.",{type:"success"}),k()}catch{E.toast("Reset failed.",{type:"error"})}}}]});l.add(()=>te?.destroy?.()),s.addEventListener("click",e=>{const t=e.target.closest(".an-kebab, .an-more");t&&ie(t)}),I.scan(s);try{await ke()}catch{E.toast("The chart library could not be loaded; figures still work.",{type:"warning"})}await Promise.all([k(),P()])}};
62
+ </div>`),s=(o,me=N=>N)=>M.computed({read:()=>i.value[o],write:N=>ie(o,me(N))}),r={range:s("range"),limit:s("limit",Number),chart:s("chart"),live:s("live"),confirmReset:s("confirmReset")};e.element.appendChild(t);const y=te(t),R=M.applyBindings(r,t,{methods:{done:()=>e.close(),reset:()=>{i.value={...Me}}}});ye(e,()=>{R.dispose(),y(),m===e&&(m=null)}),m=e,e.open()}fe(p,{id:"analytics-settings",icon:"settings",title:"Analytics settings",onClick:ce});const k=[...n.querySelectorAll(".an-list")];k[0].innerHTML=Ue,k[1].innerHTML=B,k[2].innerHTML=B,k[3].innerHTML=B,k[4].innerHTML=De,l.add(te(n,{position:"bottom"}));const ue=M.applyBindings(le,n,{methods:{refresh:()=>{S(),U()},showPages:()=>{u.value="pages"},showJourneys:()=>{u.value="journeys"},showSpikes:()=>{u.value="spikes"}}});l.add(()=>ue.dispose());const L=e=>Ae(e,location.origin),_=e=>{const t=L(e);t?window.open(t,"_blank","noopener"):E.toast("That address is not on this site, so it is not opened.",{type:"warning"})},pe=E.contextMenu(n,{cursor:"glyph",match:".an-row",inherit:!1,density:"compact",items:e=>{const t=e.dataset.url,s=Se(t,G);return[{header:t},{label:"Open on the site",icon:"external-link",disabled:!L(t),action:()=>_(t)},...s?[{label:"Edit the page",icon:"edit",action:()=>{location.hash=s}}]:[],{label:"Copy address",icon:"copy",disabled:!L(t),action:async()=>{const r=L(t);if(r)try{await navigator.clipboard.writeText(r),E.toast("Address copied.",{type:"success"})}catch{E.toast(r,{type:"info",duration:8e3})}}},{label:"Show only this page",icon:"filter",disabled:u.peek()==="pages"&&c.peek()===t,action:()=>{u.value="pages",c.value=t}}]}});l.add(()=>pe?.destroy?.());const ve=E.contextMenu(n.querySelector(".an-bar-end"),{cursor:"glyph",match:".an-more",inherit:!1,density:"compact",items:()=>[{label:`Export ${V[d.peek()].toLowerCase()} as CSV`,icon:"download",action:async()=>{try{await Ne(d.peek())}catch{E.toast("Export failed.",{type:"error"})}}},{type:"divider"},{label:"Reset all statistics\u2026",icon:"trash",danger:!0,action:async()=>{if(!(i.peek().confirmReset&&!await E.confirm("Delete every analytics counter for this site? This cannot be undone.")))try{if(await b("/analytics/stats",{method:"DELETE"}),!a)return;E.toast("Analytics reset.",{type:"success"}),se("#/analytics"),S()}catch{E.toast("Reset failed.",{type:"error"})}}}]});l.add(()=>ve?.destroy?.()),n.addEventListener("click",e=>{const t=e.target.closest(".an-kebab, .an-more");if(t){ae(t);return}const s=e.target.closest(".an-row");s?.dataset.url&&_(s.dataset.url)}),n.addEventListener("keydown",e=>{const t=e.target.closest(".an-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),ae(t)):e.key==="Enter"&&e.target===t&&t.dataset.url&&_(t.dataset.url))}),I.scan(n),Oe().then(()=>{a&&(F.value=!0)},()=>{a&&E.toast("The chart library could not be loaded; figures still work.",{type:"warning"})}),re(),await Promise.all([S(),U()])}};