domma-cms 1.8.37 → 1.8.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/admin/css/admin.css +1 -1
- package/admin/index.html +2 -2
- package/admin/js/app.js +35 -9
- package/admin/js/lib/avatar.js +1 -1
- package/admin/js/lib/sidebar-count-cache.js +1 -0
- package/admin/js/lib/sidebar-renderer.js +4 -4
- package/admin/js/lib/site-settings-arrange.js +1 -1
- package/admin/js/lib/topbar-account.js +1 -0
- package/admin/js/lib/topbar-navigation.js +1 -0
- package/admin/js/lib/topbar-project.js +1 -0
- package/admin/js/lib/update-notice.js +1 -1
- package/admin/js/templates/settings.html +62 -19
- package/admin/js/views/settings.js +3 -5
- package/docs/screenshots/admin-navigation.png +0 -0
- package/docs/screenshots/admin-versions.png +0 -0
- package/docs/screenshots/site-settings-navigation.png +0 -0
- package/package.json +1 -1
- package/public/js/btt.js +1 -1
- package/server/routes/api/auth.js +2 -1
- package/server/routes/api/settings.js +1 -1
- package/server/services/frameworkVersions.js +11 -0
- package/server/services/renderer.js +2 -1
|
@@ -21,10 +21,9 @@
|
|
|
21
21
|
<button type="button" class="tab-item">SEO <i class="stx-dot" data-bind-hidden="!tabs.value.seo" title="Changed"></i></button>
|
|
22
22
|
<button type="button" class="tab-item">Footer <i class="stx-dot" data-bind-hidden="!tabs.value.footer" title="Changed"></i></button>
|
|
23
23
|
<button type="button" class="tab-item">Email <i class="stx-dot" data-bind-hidden="!tabs.value.email" title="Changed"></i><i class="stx-dot is-bad" data-bind-hidden="!noSmtp.value || tabs.value.email" title="No SMTP host"></i></button>
|
|
24
|
-
<button type="button" class="tab-item">
|
|
24
|
+
<button type="button" class="tab-item">Page navigation <i class="stx-dot" data-bind-hidden="!tabs.value.pageNavigation" title="Changed"></i></button>
|
|
25
25
|
<button type="button" class="tab-item">Cookie consent <i class="stx-dot" data-bind-hidden="!tabs.value.cookies" title="Changed"></i></button>
|
|
26
26
|
<button type="button" class="tab-item">Breadcrumbs <i class="stx-dot" data-bind-hidden="!tabs.value.breadcrumbs" title="Changed"></i></button>
|
|
27
|
-
<button type="button" class="tab-item">Custom CSS <i class="stx-dot" data-bind-hidden="!tabs.value.css" title="Changed"></i></button>
|
|
28
27
|
<button type="button" class="tab-item">Cache</button>
|
|
29
28
|
</div>
|
|
30
29
|
|
|
@@ -35,6 +34,7 @@
|
|
|
35
34
|
<span data-help-title="Needs attention" data-help="Settings that are saved but quietly wrong - the same list the badge on Site Settings in the sidebar counts. They go away once fixed and saved."></span></div>
|
|
36
35
|
<div class="stx-problems" data-each="problems.value key=text"></div>
|
|
37
36
|
</div>
|
|
37
|
+
<div class="stx-card-columns">
|
|
38
38
|
<div class="stx-card">
|
|
39
39
|
<div class="stx-card-title">Site</div>
|
|
40
40
|
<div class="stx-grid">
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
</div>
|
|
76
76
|
</div>
|
|
77
77
|
</div>
|
|
78
|
+
</div>
|
|
78
79
|
<div class="stx-card stx-moved">
|
|
79
80
|
<span data-icon="droplet"></span>
|
|
80
81
|
<div class="stx-moved-text">
|
|
@@ -87,6 +88,7 @@
|
|
|
87
88
|
|
|
88
89
|
<!-- Branding -->
|
|
89
90
|
<div class="tab-panel">
|
|
91
|
+
<div class="stx-card-columns">
|
|
90
92
|
<div class="stx-card"><div class="stx-card-title">Public site</div><div class="stx-grid">
|
|
91
93
|
<div><label class="form-label" for="stx-brandText">Brand name</label>
|
|
92
94
|
<input id="stx-brandText" type="text" class="form-input" placeholder="My Site" data-model="brandText.value">
|
|
@@ -120,10 +122,12 @@
|
|
|
120
122
|
<div data-brand-icon="adminBrandIcon"></div>
|
|
121
123
|
</div>
|
|
122
124
|
</div><p class="text-muted">The logo takes priority over the icon. Leave both empty to use the default icon.</p></div>
|
|
125
|
+
</div>
|
|
123
126
|
</div>
|
|
124
127
|
|
|
125
128
|
<!-- Favicon -->
|
|
126
129
|
<div class="tab-panel">
|
|
130
|
+
<div class="stx-card-columns">
|
|
127
131
|
<div class="stx-card"><div class="stx-card-title">Generate favicon.ico</div>
|
|
128
132
|
<p>Choose or upload an image. Generate a single ICO file with 16, 32, 48 and 256 pixel icons. Images keep their proportions with transparent padding.</p>
|
|
129
133
|
<label class="form-label" for="stx-favicon-source">Source image</label>
|
|
@@ -142,6 +146,7 @@
|
|
|
142
146
|
</div>
|
|
143
147
|
<p class="text-muted">Save to apply the browser icon. Leave it empty to use the public logo.</p>
|
|
144
148
|
</div>
|
|
149
|
+
</div>
|
|
145
150
|
</div>
|
|
146
151
|
|
|
147
152
|
<!-- SEO -->
|
|
@@ -170,6 +175,7 @@
|
|
|
170
175
|
|
|
171
176
|
<!-- Footer -->
|
|
172
177
|
<div class="tab-panel">
|
|
178
|
+
<div class="stx-card-columns">
|
|
173
179
|
<div class="stx-card">
|
|
174
180
|
<div class="stx-card-title">Footer</div>
|
|
175
181
|
<div class="stx-grid">
|
|
@@ -248,6 +254,7 @@
|
|
|
248
254
|
<input id="stx-s-yt" type="url" class="form-input" placeholder="https://youtube.com/@yourchannel" data-model="socialYoutube.value"></div>
|
|
249
255
|
</div>
|
|
250
256
|
</div>
|
|
257
|
+
</div>
|
|
251
258
|
</div>
|
|
252
259
|
|
|
253
260
|
<!-- Email -->
|
|
@@ -318,10 +325,11 @@
|
|
|
318
325
|
</div>
|
|
319
326
|
</div>
|
|
320
327
|
|
|
321
|
-
<!--
|
|
328
|
+
<!-- Page navigation -->
|
|
322
329
|
<div class="tab-panel">
|
|
330
|
+
<div class="stx-card-columns">
|
|
323
331
|
<div class="stx-card">
|
|
324
|
-
<div class="stx-card-title">Back to top
|
|
332
|
+
<div class="stx-card-title"><span class="stx-scroll-icon" data-icon="arrow-up"></span>Back to top
|
|
325
333
|
<span data-help="A button that appears once a visitor has scrolled down, and takes them back to the top of the page."></span></div>
|
|
326
334
|
<div class="stx-grid">
|
|
327
335
|
<div class="stx-setting"><label class="form-label" for="stx-btt-on">Show the button</label>
|
|
@@ -354,6 +362,41 @@
|
|
|
354
362
|
</div>
|
|
355
363
|
</div>
|
|
356
364
|
</div>
|
|
365
|
+
<div class="stx-card">
|
|
366
|
+
<div class="stx-card-title"><span class="stx-scroll-icon" data-icon="arrow-down"></span>Go to bottom
|
|
367
|
+
<span data-help="A matching down-arrow button that takes visitors to the bottom of the page. It hides when they are already near the bottom."></span></div>
|
|
368
|
+
<div class="stx-grid">
|
|
369
|
+
<div class="stx-setting"><label class="form-label" for="stx-gtb-on">Show the button</label>
|
|
370
|
+
<input id="stx-gtb-on" type="checkbox" class="form-switch" data-model="gtbEnabled.value"></div>
|
|
371
|
+
<div class="stx-setting"><label class="form-label" for="stx-gtb-smooth">Smooth scroll</label>
|
|
372
|
+
<input id="stx-gtb-smooth" type="checkbox" class="form-switch" data-model="gtbSmooth.value"></div>
|
|
373
|
+
<div class="stx-wide">
|
|
374
|
+
<label class="form-label" for="stx-gtb-label">Label <small class="stx-muted">(optional - empty is icon-only)</small></label>
|
|
375
|
+
<input id="stx-gtb-label" type="text" class="form-input" placeholder="Go to bottom" data-model="gtbLabel.value">
|
|
376
|
+
</div>
|
|
377
|
+
<div>
|
|
378
|
+
<label class="form-label" for="stx-gtb-thr">Hide near bottom (px)
|
|
379
|
+
<span data-help="How close to the bottom the visitor must be before this button hides."></span></label>
|
|
380
|
+
<input id="stx-gtb-thr" type="number" class="form-input" placeholder="300" data-model="gtbThreshold.value">
|
|
381
|
+
</div>
|
|
382
|
+
<div>
|
|
383
|
+
<label class="form-label" for="stx-gtb-pos">Position</label>
|
|
384
|
+
<select id="stx-gtb-pos" class="form-select" data-model="gtbPosition.value">
|
|
385
|
+
<option value="bottom-right">Bottom right</option>
|
|
386
|
+
<option value="bottom-left">Bottom left</option>
|
|
387
|
+
</select>
|
|
388
|
+
</div>
|
|
389
|
+
<div>
|
|
390
|
+
<label class="form-label" for="stx-gtb-off">Side offset (px)</label>
|
|
391
|
+
<input id="stx-gtb-off" type="number" class="form-input" placeholder="32" data-model="gtbOffset.value">
|
|
392
|
+
</div>
|
|
393
|
+
<div>
|
|
394
|
+
<label class="form-label" for="stx-gtb-boff">Bottom offset (px)</label>
|
|
395
|
+
<input id="stx-gtb-boff" type="number" class="form-input" placeholder="32" data-model="gtbBottomOffset.value">
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
398
|
+
</div>
|
|
399
|
+
</div>
|
|
357
400
|
</div>
|
|
358
401
|
|
|
359
402
|
<!-- Cookie consent -->
|
|
@@ -418,16 +461,22 @@
|
|
|
418
461
|
|
|
419
462
|
<!-- Breadcrumbs -->
|
|
420
463
|
<div class="tab-panel">
|
|
464
|
+
<div class="stx-card-columns">
|
|
421
465
|
<div class="stx-card">
|
|
422
466
|
<div class="stx-card-title">Breadcrumb trail
|
|
423
467
|
<span data-help-title="Breadcrumbs" data-help="A small frosted pill fixed to a corner of every public page, showing where the page sits (Home › Services › Web). A page can switch it off with “Show Breadcrumbs” in the page editor."></span></div>
|
|
424
468
|
<div class="stx-grid">
|
|
425
|
-
<div class="stx-wide stx-setting"><label class="form-label" for="stx-bc-on">Show breadcrumbs
|
|
469
|
+
<div class="stx-wide stx-setting"><label class="form-label" for="stx-bc-on">Show breadcrumbs</label>
|
|
426
470
|
<input id="stx-bc-on" type="checkbox" class="form-switch" data-model="bcEnabled.value"></div>
|
|
427
|
-
<div><label class="form-label" for="stx-bc-home">Home label</label>
|
|
471
|
+
<div class="stx-wide"><label class="form-label" for="stx-bc-home">Home label</label>
|
|
428
472
|
<input id="stx-bc-home" type="text" class="form-input" placeholder="Home" data-model="bcHomeLabel.value"></div>
|
|
429
|
-
|
|
430
|
-
|
|
473
|
+
</div>
|
|
474
|
+
<p class="stx-hint">Shows the path from Home to the current page. Individual pages can hide their trail in the page editor.</p>
|
|
475
|
+
</div>
|
|
476
|
+
<div class="stx-card">
|
|
477
|
+
<div class="stx-card-title">Placement</div>
|
|
478
|
+
<div class="stx-grid">
|
|
479
|
+
<div class="stx-wide">
|
|
431
480
|
<span class="form-label">Corner</span>
|
|
432
481
|
<div class="stx-corners" role="group" aria-label="Corner">
|
|
433
482
|
<button type="button" data-on-click="corner('TL')" data-bind-class="bcPosition.value === 'TL' && 'is-on'">↖ Top left</button>
|
|
@@ -436,7 +485,7 @@
|
|
|
436
485
|
<button type="button" data-on-click="corner('BR')" data-bind-class="bcPosition.value === 'BR' && 'is-on'">Bottom right ↘</button>
|
|
437
486
|
</div>
|
|
438
487
|
</div>
|
|
439
|
-
<div class="stx-pair">
|
|
488
|
+
<div class="stx-pair stx-wide">
|
|
440
489
|
<div><label class="form-label" for="stx-bc-x">From the side (px)</label>
|
|
441
490
|
<input id="stx-bc-x" type="number" class="form-input" min="0" max="500" placeholder="8" data-model="bcOffsetX.value"></div>
|
|
442
491
|
<div><label class="form-label" for="stx-bc-y">From the top/bottom (px)</label>
|
|
@@ -444,15 +493,6 @@
|
|
|
444
493
|
</div>
|
|
445
494
|
</div>
|
|
446
495
|
</div>
|
|
447
|
-
</div>
|
|
448
|
-
|
|
449
|
-
<!-- Custom CSS -->
|
|
450
|
-
<div class="tab-panel">
|
|
451
|
-
<div class="stx-card">
|
|
452
|
-
<div class="stx-card-title">Custom CSS
|
|
453
|
-
<span data-help="Added to the head of every public page, after the theme - so it has the last word. Saved with everything else (Save, or Ctrl+S). Up to 100 KB."></span></div>
|
|
454
|
-
<textarea class="form-input stx-css" spellcheck="false" placeholder="/* Add your custom CSS here */"></textarea>
|
|
455
|
-
<small class="stx-hint" data-bind-text="cssSize.value"></small>
|
|
456
496
|
</div>
|
|
457
497
|
</div>
|
|
458
498
|
|
|
@@ -502,6 +542,10 @@
|
|
|
502
542
|
.stx-card { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .75rem .9rem; display: flex; flex-direction: column; gap: .65rem; margin-top: .75rem; }
|
|
503
543
|
.stx-card-title { display: flex; align-items: center; gap: .4rem; font-weight: 600; flex-wrap: wrap; }
|
|
504
544
|
.stx-card-title .btn { display: inline-flex; align-items: center; gap: .3rem; font-weight: 500; }
|
|
545
|
+
.stx-card-columns { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 1rem; align-items: start; }
|
|
546
|
+
.stx-card-columns > .stx-card { min-width: 0; }
|
|
547
|
+
.stx-scroll-icon { display: inline-flex; padding: .35rem; border-radius: 50%; background: var(--dm-surface-raised); }
|
|
548
|
+
@media (max-width: 900px) { .stx-card-columns { grid-template-columns: minmax(0, 1fr); } }
|
|
505
549
|
.stx-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .75rem 1rem; }
|
|
506
550
|
.stx-wide { grid-column: 1 / -1; }
|
|
507
551
|
.stx-pair { display: grid; grid-template-columns: 1fr 1fr; gap: .75rem; }
|
|
@@ -530,7 +574,6 @@
|
|
|
530
574
|
.stx-corners button:nth-child(-n+2) { border-bottom: 1px solid var(--dm-border); }
|
|
531
575
|
.stx-corners button:hover { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); }
|
|
532
576
|
.stx-corners button.is-on { background: var(--dm-primary); color: var(--dm-primary-text); }
|
|
533
|
-
.stx-css { width: 100%; min-height: 420px; font-family: var(--dm-font-mono, monospace); font-size: var(--dm-text-sm); }
|
|
534
577
|
.stx-chips { display: flex; gap: .35rem; flex-wrap: wrap; }
|
|
535
578
|
.stx-chip { display: inline-flex; align-items: center; font-size: var(--dm-text-xs); font-weight: 600; padding: .1rem .5rem; border-radius: 999px; white-space: nowrap;
|
|
536
579
|
background: color-mix(in srgb, var(--dm-text) 10%, transparent); color: var(--dm-text-muted); }
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import{api as
|
|
1
|
+
import{api as j,apiRequest as C}from"../api.js";import{placeTools as Le}from"../lib/sidebar-grouping.js";import{addBannerAction as pe}from"../lib/plugin-chrome.js";import{helpPopovers as Oe}from"../lib/help-popover.js";import{openMenuAt as Y,sidebarCount as me}from"../lib/tool-kit.js";import{changedTabs as Re,describeCacheKey as G,draftOf as Ie,expiresIn as Ae,formFromSettings as $e,homeValue as Pe,payloadFromForm as je,siteUrlError as fe,settingsProblems as De,snapshot as he,TAB_FIELDS as He}from"../lib/site-settings-arrange.js";import{pickMedia as Be}from"../lib/media-picker.js";import{faviconGenerator as qe}from"../lib/favicon-generator.js";import{makeIconInput as Ue}from"../lib/shortcode-modal.js";import{fmtDateTime as _e}from"../lib/dates.js";const J=/^\/settings(\?.*)?$/,ve="#/settings",w="settings.draft",Fe=`
|
|
2
2
|
<div class="stx-problem">
|
|
3
3
|
<span class="stx-chip" data-bind-class="tone === 'danger' ? 'is-bad' : 'is-warn'">{{label}}</span>
|
|
4
4
|
<span>{{text}}</span>
|
|
5
5
|
<button type="button" class="btn btn-sm btn-ghost" data-go="{{tab}}">Go there</button>
|
|
6
|
-
</div>`,
|
|
6
|
+
</div>`,Ke=`
|
|
7
7
|
<div class="stx-row" data-id="{{id}}" tabindex="0">
|
|
8
8
|
<span class="stx-main"><strong>{{path}}</strong><small>{{meta}}</small></span>
|
|
9
9
|
<span class="stx-chip">{{expires}}</span>
|
|
10
10
|
<button type="button" class="stx-kebab" aria-label="Actions for {{path}}" title="Actions">\u22EE</button>
|
|
11
|
-
</div>`,Ge=["general","branding","favicon","seo","footer","email","backToTop","cookies","breadcrumbs","css","cache"];async function Je(l,p){const[r,L]=await Promise.all([H.get("/api/sidebar/menu").catch(()=>({items:[]})),H.get("/api/sidebar/registered-items").catch(()=>[])]),a=new Map([["/","Dashboard (built in)"]]),c=(C,m)=>{for(const i of C||[]){if(!i||i.hidden||i.restricted)continue;const f=typeof i.url=="string"?i.url:"";f.startsWith("#/")&&f.length>2&&!a.has(f.slice(1))&&a.set(f.slice(1),`${m?`${m} \u203A `:""}${i.text||f}`),c(i.items,i.url?m:i.text||m)}};c($e(r?.items||[],L||[]),""),a.has(p)||a.set(p,`${p} (not in the sidebar)`),l.replaceChildren();for(const[C,m]of a){const i=document.createElement("option");i.value=C,i.textContent=m,l.appendChild(i)}}let g=null,Ee=!1;function Qe(){Ee||!window.R?.use||(Ee=!0,R.use((l,p,r)=>!(Z.test(p?.path||"")&&!Z.test(l?.path||""))||!g?.dirty()?r():E.confirm("You have unsaved settings. Leave anyway? They are kept as a draft for your next visit (not the SMTP password).").then(a=>{a?r():history.replaceState(null,"","#/settings")})),M.subscribe("router:afterChange",({to:l}={})=>{if(g&&!Z.test(l?.path||"")){const p=g;g=null,p.dispose()}}))}export const settingsView={templateUrl:"/admin/js/templates/settings.html",async onMount(l){if(Qe(),g){const e=g;g=null,e.dispose()}const p=[],r=e=>(p.push(e),e);let L=!1;const a=l.get(0).querySelector(".stx")||l.get(0),c=()=>!L&&a.isConnected,C=E.loader(l.get(0),{type:"dots"});let m,i;try{[m,i]=await Promise.all([K.settings.get(),w("/settings/custom-css").then(e=>e?.css||"").catch(()=>null)])}catch(e){C.destroy(),E.toast(`Could not load the settings: ${e.message||e}`,{type:"error"});return}if(C.destroy(),!a.isConnected)return;const f=i!==null,ee=M.observable(m||{}),te=je(m||{},i||""),n={};for(const[e,t]of Object.entries(te))n[e]=M.observable(t);const _=()=>Object.fromEntries(Object.keys(n).map(e=>[e,n[e].value])),se=()=>Object.fromEntries(Object.keys(n).map(e=>[e,n[e].peek()])),y=M.observable(te),q=M.computed(()=>ke(y.value)),O=M.computed(()=>ke(_())!==q.value),A=M.observable(!1),$=M.observable(""),ne=M.observable(!1);K.plugins.policy().then(e=>{c()&&(ne.value=e?.signedOnly===!1)}).catch(()=>{});const U=M.observable(!1),ae=M.computed(()=>{const e=new Set(Be(_(),y.value));return Object.fromEntries(Object.keys(Ue).map(t=>[t,e.has(t)]))}),F=M.observable([]);r(M.effect(()=>{F.value=qe(_()).map(e=>({...e,label:e.tone==="danger"?"Mail":"Check"}))}));const oe=M.observable(""),P=M.observable(!1),B=M.observable(""),D=M.observable(""),v=M.observable(null),z=M.observable([]),N=M.observable(""),V=M.observable(!0),W=M.observable(""),re=M.observable([]);r(M.effect(()=>{const e=N.value.trim().toLowerCase(),t=Date.now();re.value=[...z.value].reverse().filter(s=>!e||String(s.key).toLowerCase().includes(e)||(s.tags||[]).some(o=>String(o).toLowerCase().includes(e))).map(s=>{const o=X(s.key);return{id:s.key,path:o.path,meta:[o.page?`as ${o.role}`:"not a page",(s.tags||[]).join(", ")].filter(Boolean).join(" \xB7 "),expires:He(s.expiresAt,t)}})}));const ie=ze(n.favicon,n.brandLogo,c),Se={...ie.data,...n,saving:A,dirty:O,tabs:ae,problems:F,testTo:oe,testing:P,testResult:B,testTone:D,cacheQuery:N,cacheLoading:V,cacheError:W,cacheRows:re,errorText:$,problemsText:M.computed(()=>F.value.length?"yes":""),siteUrlError:M.computed(()=>ye(n.siteUrl.value)),noSmtp:M.computed(()=>!String(n.smtpHost.value||"").trim()),mayAllowInsecureTLS:ne,dirtyEmail:M.computed(()=>ae.value.email),seoLength:M.computed(()=>{const e=String(n.seoDescription.value||"").trim().length;return e?`${e} characters${e>160?" - search engines usually cut it off after about 150":""}`:""}),cssSize:M.computed(()=>{if(!f)return"Could not load content/custom.css - reload to try again. It will not be saved until it loads.";const e=new Blob([String(n.css.value||"")]).size/1024;return`${e.toFixed(1)} KB of 100 KB${e>100?" - too big to save":""}`}),cacheOn:M.computed(()=>v.value?.enabled===!0),cacheState:M.computed(()=>{const e=v.value;return e?e.enabled?`On (${e.driver})`:"Off":"Status unknown"}),cacheSize:M.computed(()=>{const e=v.value;return e?.enabled&&e.size!==null&&e.size!==void 0?`${e.size}${e.maxItems?` of ${e.maxItems}`:""} entries`:""}),cacheHits:M.computed(()=>{const e=v.value?.hitRate;return e==null?"":`${Math.round(e*100)}% served from the cache`}),cacheToggleText:M.computed(()=>v.value?.enabled?"Turn off":"Turn on"),cacheEmpty:M.computed(()=>N.value.trim()?"Nothing matches.":v.value?.enabled?"The cache is empty - pages are cached as visitors open them.":"The cache is off.")};async function Y(){if(A.peek())return;const e=se(),t=y.peek(),s=String(e.css)!==String(t.css);if(s&&!f){E.toast("The custom CSS did not load, so it cannot be saved - reload the screen first.",{type:"error"});return}const o=ye(e.siteUrl);if(o){E.toast(`Site URL: ${o}`,{type:"error"});return}A.value=!0,$.value="";const h=String(e.title).trim()!==String(t.title).trim();try{await K.settings.save(_e(e,ee.peek(),{adminHomeReady:U.peek()})),s&&await w("/settings/custom-css",{method:"PUT",body:JSON.stringify({css:String(e.css)})});const d=await K.settings.get().catch(()=>null);if(!c())return;d&&(ee.value=d);const J={...e,smtpPass:""};n.smtpPass.value="",y.value=J;try{S.remove?S.remove(T):S.set(T,null)}catch{}document.dispatchEvent(new CustomEvent("dm:admin-home-changed")),(h||["adminBrandTitle","adminBrandLogo","adminBrandIcon"].some(ve=>e[ve]!==t[ve]))&&document.dispatchEvent(new CustomEvent("dm:sidebar-rebuild")),ge(we),E.toast("Settings saved.",{type:"success"})}catch(d){$.value=d.message||"Could not save the settings.",E.toast("Settings not saved - see the message at the top.",{type:"error"})}finally{A.value=!1}}async function Ce(){await E.confirm("Throw away your unsaved settings?")&&(ce(y.peek()),$.value="")}function ce(e){for(const[t,s]of Object.entries(e))n[t]&&(n[t].value=s);u&&u.getValue()!==String(n.css.peek())?u.setValue(String(n.css.peek())):!u&&b&&(b.value=String(n.css.peek()))}async function Me(){if(!P.peek()){P.value=!0,D.value="",B.value="Sending\u2026";try{const e=await w("/settings/test-email",{method:"POST",body:JSON.stringify({to:oe.peek().trim()||void 0})});if(!c())return;B.value=e?.message||"Test email sent.",D.value="is-ok"}catch(e){if(!c())return;B.value=e.message||"Could not send the test email.",D.value="is-bad"}finally{P.value=!1}}}async function j(){V.value=!0;try{const[e,t]=await Promise.all([w("/cache/status").catch(()=>null),w("/cache/keys")]);if(!c())return;v.value=e,z.value=Array.isArray(t?.entries)?t.entries:[],W.value=""}catch(e){if(!c())return;W.value=`Could not read the cache: ${e.message||e}`}finally{V.value=!1}}async function xe(){if(await E.confirm("Clear the whole response cache? The next visitor to each page renders it afresh.")){try{await w("/cache/clear",{method:"POST"}),E.toast("Cache cleared.",{type:"success"})}catch(e){E.toast(e.message||"Could not clear the cache.",{type:"error"})}c()&&await j()}}async function Te(){const e=v.peek()?.enabled===!0;if(!(e&&!await E.confirm("Turn the response cache off? Every visit will render its page afresh until it is turned back on. The choice is saved (config/cache.json)."))){try{v.value=await w("/cache/toggle",{method:"POST",body:JSON.stringify({enabled:!e})}),E.toast(e?"Cache turned off.":"Cache turned on.",{type:"success"}),ge(we)}catch(t){E.toast(t.message||"Could not change the cache.",{type:"error"})}c()&&await j()}}const le=e=>z.peek().find(t=>t.key===e);function Le(e){const t=le(e);if(!t)return[];const s=X(t.key);return[{header:s.path},{label:"Open the page",icon:"external-link",disabled:!s.page,action:()=>window.open(s.path,"_blank","noopener")},{label:"Copy key",icon:"copy",action:async()=>{try{await navigator.clipboard.writeText(t.key),E.toast("Key copied.",{type:"success"})}catch{E.toast("Could not copy - your browser blocked the clipboard.",{type:"warning"})}}},...s.page?[{label:"Copy address",icon:"link",action:async()=>{try{await navigator.clipboard.writeText(new URL(s.path,location.origin).href),E.toast("Address copied.",{type:"success"})}catch{E.toast("Could not copy - your browser blocked the clipboard.",{type:"warning"})}}}]:[]]}a.querySelector(".stx-problems").innerHTML=We;const x=a.querySelector(".stx-list");x.innerHTML=Ye;const Oe=E.tabs(a.querySelector(".stx-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),Re=M.applyBindings(Se,a,{methods:{...ie.methods,save:Y,discard:Ce,sendTest:Me,refreshCache:j,clearCache:xe,toggleCache:Te,pickBrandImage:async e=>{const t=await Fe({title:"Choose a branding image",current:n[e].peek()});t&&c()&&(n[e].value=t)},clearBrandImage:e=>{n[e].value=""},corner:e=>{n.bcPosition.value=e}}});r(()=>Re.dispose());for(const e of a.querySelectorAll("[data-brand-icon]")){const t=e.dataset.brandIcon,s=Ne("Choose an icon",n[t].peek());s.input.id=`stx-${t}`,s.input.classList.add("form-input");const o=()=>{n[t].value=s.input.value};s.input.addEventListener("input",o),r(()=>s.input.removeEventListener("input",o)),r(M.effect(()=>{s.input.value=n[t].value})),e.appendChild(s.el)}r(Pe(a,{position:"bottom"}));const Ie=e=>{const t=e.target.closest("[data-go]");if(!t)return;const s=Ge.indexOf(t.dataset.go);s>=0&&Oe.activate(s)};a.querySelector(".stx-problems").addEventListener("click",Ie);const G=a.querySelector("#stx-admin-home");Je(G,Ke(n.adminHome.peek())).then(()=>{c()&&(G.value=n.adminHome.peek(),U.value=!0)}).catch(()=>{});const b=a.querySelector(".stx-css");b.value=String(n.css.peek());let u=null;if(!f)b.disabled=!0;else if(E.editor)try{u=E.editor(b,{mode:"code",language:"css",lineNumbers:!0,showToolbar:!1,minHeight:420,placeholder:"/* Add your custom CSS here */",characterCount:!0,onChange:e=>{n.css.value=e}})}catch{u=null}if(u)u._editorEl&&u._editorEl.addEventListener("keydown",e=>{if(!(e.ctrlKey||e.metaKey)||e.key!=="c"&&e.key!=="x")return;const t=u._editorEl;if(t.selectionStart!==t.selectionEnd||typeof t.value!="string")return;e.preventDefault();const s=t.value,o=t.selectionStart,h=s.lastIndexOf(`
|
|
12
|
-
`,o-1)+1,d=s.indexOf(`
|
|
13
|
-
`,o),J=d===-1?s.slice(h):s.slice(h,d+1);navigator.clipboard?.writeText(J).catch(()=>{}),e.key==="x"?(t.value=s.slice(0,h)+(d===-1?"":s.slice(d+1)),t.selectionStart=t.selectionEnd=h,t.dispatchEvent(new Event("input",{bubbles:!0})),E.toast("Line cut",{type:"info",duration:1500})):E.toast("Line copied",{type:"info",duration:1500})});else{const e=()=>{n.css.value=b.value};b.addEventListener("input",e),r(()=>b.removeEventListener("input",e))}r(()=>{try{u?.destroy?.()}catch{}});const Ae=E.contextMenu(x,{cursor:"glyph",match:".stx-row",inherit:!1,density:"compact",items:e=>Le(e.dataset.id)});r(()=>Ae?.destroy?.());const ue=e=>{const t=le(e.dataset.id),s=t&&X(t.key);s?.page?window.open(s.path,"_blank","noopener"):Q(e)},de=e=>{const t=e.target.closest(".stx-row");if(!t)return;const s=e.target.closest(".stx-kebab");if(s){Q(s);return}ue(t)},pe=e=>{const t=e.target.closest(".stx-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),Q(t)):e.key==="Enter"&&e.target===t&&ue(t))};x.addEventListener("click",de),x.addEventListener("keydown",pe),r(()=>{x.removeEventListener("click",de),x.removeEventListener("keydown",pe)}),be(l,{id:"settings-view-site",icon:"external-link",title:"View site",onClick:()=>window.open("/","_blank","noopener")}),be(l,{id:"settings-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>Y()});const me=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||(e.preventDefault(),e.stopImmediatePropagation(),Y())};document.addEventListener("keydown",me,!0),r(()=>document.removeEventListener("keydown",me,!0));const fe=()=>{if(O.peek())try{S.set(T,{form:De(se()),base:q.peek(),at:new Date().toISOString()})}catch{}},he=e=>{O.peek()&&(fe(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",he),r(()=>window.removeEventListener("beforeunload",he)),g={dirty:()=>O.peek(),dispose(){L=!0,fe(),p.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(a),j();let k=null;try{k=S.get(T)}catch{}if(k?.form){try{S.remove?S.remove(T):S.set(T,null)}catch{}const e=k.base&&k.base!==q.peek(),t=k.at?` (${Ve(k.at)})`:"",s=e?`You left unsaved settings${t}, but the saved settings have changed since. Restore your version anyway?`:`You left unsaved settings${t}. Restore them?`;if(await E.confirm(s)&&c()){const o={...y.peek()};for(const[h,d]of Object.entries(k.form))h in o&&h!=="smtpPass"&&(o[h]=d);f||(o.css=y.peek().css),ce(o),U.peek()&&(G.value=n.adminHome.peek())}}}};
|
|
11
|
+
</div>`,Ne=["general","branding","favicon","seo","footer","email","pageNavigation","cookies","breadcrumbs","cache"];async function ze(l,u){const[r,k]=await Promise.all([H.get("/api/sidebar/menu").catch(()=>({items:[]})),H.get("/api/sidebar/registered-items").catch(()=>[])]),n=new Map([["/","Dashboard (built in)"]]),i=(b,d)=>{for(const o of b||[]){if(!o||o.hidden||o.restricted)continue;const m=typeof o.url=="string"?o.url:"";m.startsWith("#/")&&m.length>2&&!n.has(m.slice(1))&&n.set(m.slice(1),`${d?`${d} \u203A `:""}${o.text||m}`),i(o.items,o.url?d:o.text||d)}};i(Le(r?.items||[],k||[]),""),n.has(u)||n.set(u,`${u} (not in the sidebar)`),l.replaceChildren();for(const[b,d]of n){const o=document.createElement("option");o.value=b,o.textContent=d,l.appendChild(o)}}let h=null,be=!1;function Ve(){be||!window.R?.use||(be=!0,R.use((l,u,r)=>!(J.test(u?.path||"")&&!J.test(l?.path||""))||!h?.dirty()?r():E.confirm("You have unsaved settings. Leave anyway? They are kept as a draft for your next visit (not the SMTP password).").then(n=>{n?r():history.replaceState(null,"","#/settings")})),M.subscribe("router:afterChange",({to:l}={})=>{if(h&&!J.test(l?.path||"")){const u=h;h=null,u.dispose()}}))}export const settingsView={templateUrl:"/admin/js/templates/settings.html",async onMount(l){if(Ve(),h){const e=h;h=null,e.dispose()}const u=[],r=e=>(u.push(e),e);let k=!1;const n=l.get(0).querySelector(".stx")||l.get(0),i=()=>!k&&n.isConnected,b=E.loader(l.get(0),{type:"dots"});let d;try{d=await j.settings.get()}catch(e){b.destroy(),E.toast(`Could not load the settings: ${e.message||e}`,{type:"error"});return}if(b.destroy(),!n.isConnected)return;const o=M.observable(d||{}),m=$e(d||{}),a={};for(const[e,t]of Object.entries(m))a[e]=M.observable(t);const D=()=>Object.fromEntries(Object.keys(a).map(e=>[e,a[e].value])),Q=()=>Object.fromEntries(Object.keys(a).map(e=>[e,a[e].peek()])),g=M.observable(m),B=M.computed(()=>he(g.value)),x=M.computed(()=>he(D())!==B.value),T=M.observable(!1),L=M.observable(""),X=M.observable(!1);j.plugins.policy().then(e=>{i()&&(X.value=e?.signedOnly===!1)}).catch(()=>{});const q=M.observable(!1),Z=M.computed(()=>{const e=new Set(Re(D(),g.value));return Object.fromEntries(Object.keys(He).map(t=>[t,e.has(t)]))}),U=M.observable([]);r(M.effect(()=>{U.value=De(D()).map(e=>({...e,label:e.tone==="danger"?"Mail":"Check"}))}));const ee=M.observable(""),O=M.observable(!1),A=M.observable(""),$=M.observable(""),p=M.observable(null),_=M.observable([]),F=M.observable(""),K=M.observable(!0),N=M.observable(""),te=M.observable([]);r(M.effect(()=>{const e=F.value.trim().toLowerCase(),t=Date.now();te.value=[..._.value].reverse().filter(s=>!e||String(s.key).toLowerCase().includes(e)||(s.tags||[]).some(c=>String(c).toLowerCase().includes(e))).map(s=>{const c=G(s.key);return{id:s.key,path:c.path,meta:[c.page?`as ${c.role}`:"not a page",(s.tags||[]).join(", ")].filter(Boolean).join(" \xB7 "),expires:Ae(s.expiresAt,t)}})}));const se=qe(a.favicon,a.brandLogo,i),ge={...se.data,...a,saving:T,dirty:x,tabs:Z,problems:U,testTo:ee,testing:O,testResult:A,testTone:$,cacheQuery:F,cacheLoading:K,cacheError:N,cacheRows:te,errorText:L,problemsText:M.computed(()=>U.value.length?"yes":""),siteUrlError:M.computed(()=>fe(a.siteUrl.value)),noSmtp:M.computed(()=>!String(a.smtpHost.value||"").trim()),mayAllowInsecureTLS:X,dirtyEmail:M.computed(()=>Z.value.email),seoLength:M.computed(()=>{const e=String(a.seoDescription.value||"").trim().length;return e?`${e} characters${e>160?" - search engines usually cut it off after about 150":""}`:""}),cacheOn:M.computed(()=>p.value?.enabled===!0),cacheState:M.computed(()=>{const e=p.value;return e?e.enabled?`On (${e.driver})`:"Off":"Status unknown"}),cacheSize:M.computed(()=>{const e=p.value;return e?.enabled&&e.size!==null&&e.size!==void 0?`${e.size}${e.maxItems?` of ${e.maxItems}`:""} entries`:""}),cacheHits:M.computed(()=>{const e=p.value?.hitRate;return e==null?"":`${Math.round(e*100)}% served from the cache`}),cacheToggleText:M.computed(()=>p.value?.enabled?"Turn off":"Turn on"),cacheEmpty:M.computed(()=>F.value.trim()?"Nothing matches.":p.value?.enabled?"The cache is empty - pages are cached as visitors open them.":"The cache is off.")};async function z(){if(T.peek())return;const e=Q(),t=g.peek(),s=fe(e.siteUrl);if(s){E.toast(`Site URL: ${s}`,{type:"error"});return}T.value=!0,L.value="";const c=String(e.title).trim()!==String(t.title).trim();try{await j.settings.save(je(e,o.peek(),{adminHomeReady:q.peek()}));const f=await j.settings.get().catch(()=>null);if(!i())return;f&&(o.value=f);const W={...e,smtpPass:""};a.smtpPass.value="",g.value=W;try{S.remove?S.remove(w):S.set(w,null)}catch{}document.dispatchEvent(new CustomEvent("dm:admin-home-changed")),(c||["adminBrandTitle","adminBrandLogo","adminBrandIcon"].some(de=>e[de]!==t[de]))&&document.dispatchEvent(new CustomEvent("dm:sidebar-rebuild")),me(ve),E.toast("Settings saved.",{type:"success"})}catch(f){L.value=f.message||"Could not save the settings.",E.toast("Settings not saved - see the message at the top.",{type:"error"})}finally{T.value=!1}}async function ye(){await E.confirm("Throw away your unsaved settings?")&&(ae(g.peek()),L.value="")}function ae(e){for(const[t,s]of Object.entries(e))a[t]&&(a[t].value=s)}async function we(){if(!O.peek()){O.value=!0,$.value="",A.value="Sending\u2026";try{const e=await C("/settings/test-email",{method:"POST",body:JSON.stringify({to:ee.peek().trim()||void 0})});if(!i())return;A.value=e?.message||"Test email sent.",$.value="is-ok"}catch(e){if(!i())return;A.value=e.message||"Could not send the test email.",$.value="is-bad"}finally{O.value=!1}}}async function P(){K.value=!0;try{const[e,t]=await Promise.all([C("/cache/status").catch(()=>null),C("/cache/keys")]);if(!i())return;p.value=e,_.value=Array.isArray(t?.entries)?t.entries:[],N.value=""}catch(e){if(!i())return;N.value=`Could not read the cache: ${e.message||e}`}finally{K.value=!1}}async function ke(){if(await E.confirm("Clear the whole response cache? The next visitor to each page renders it afresh.")){try{await C("/cache/clear",{method:"POST"}),E.toast("Cache cleared.",{type:"success"})}catch(e){E.toast(e.message||"Could not clear the cache.",{type:"error"})}i()&&await P()}}async function Ee(){const e=p.peek()?.enabled===!0;if(!(e&&!await E.confirm("Turn the response cache off? Every visit will render its page afresh until it is turned back on. The choice is saved (config/cache.json)."))){try{p.value=await C("/cache/toggle",{method:"POST",body:JSON.stringify({enabled:!e})}),E.toast(e?"Cache turned off.":"Cache turned on.",{type:"success"}),me(ve)}catch(t){E.toast(t.message||"Could not change the cache.",{type:"error"})}i()&&await P()}}const ne=e=>_.peek().find(t=>t.key===e);function Se(e){const t=ne(e);if(!t)return[];const s=G(t.key);return[{header:s.path},{label:"Open the page",icon:"external-link",disabled:!s.page,action:()=>window.open(s.path,"_blank","noopener")},{label:"Copy key",icon:"copy",action:async()=>{try{await navigator.clipboard.writeText(t.key),E.toast("Key copied.",{type:"success"})}catch{E.toast("Could not copy - your browser blocked the clipboard.",{type:"warning"})}}},...s.page?[{label:"Copy address",icon:"link",action:async()=>{try{await navigator.clipboard.writeText(new URL(s.path,location.origin).href),E.toast("Address copied.",{type:"success"})}catch{E.toast("Could not copy - your browser blocked the clipboard.",{type:"warning"})}}}]:[]]}n.querySelector(".stx-problems").innerHTML=Fe;const y=n.querySelector(".stx-list");y.innerHTML=Ke;const Me=E.tabs(n.querySelector(".stx-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),Ce=M.applyBindings(ge,n,{methods:{...se.methods,save:z,discard:ye,sendTest:we,refreshCache:P,clearCache:ke,toggleCache:Ee,pickBrandImage:async e=>{const t=await Be({title:"Choose a branding image",current:a[e].peek()});t&&i()&&(a[e].value=t)},clearBrandImage:e=>{a[e].value=""},corner:e=>{a.bcPosition.value=e}}});r(()=>Ce.dispose());for(const e of n.querySelectorAll("[data-brand-icon]")){const t=e.dataset.brandIcon,s=Ue("Choose an icon",a[t].peek());s.input.id=`stx-${t}`,s.input.classList.add("form-input");const c=()=>{a[t].value=s.input.value};s.input.addEventListener("input",c),r(()=>s.input.removeEventListener("input",c)),r(M.effect(()=>{s.input.value=a[t].value})),e.appendChild(s.el)}r(Oe(n,{position:"bottom"}));const xe=e=>{const t=e.target.closest("[data-go]");if(!t)return;const s=Ne.indexOf(t.dataset.go);s>=0&&Me.activate(s)};n.querySelector(".stx-problems").addEventListener("click",xe);const V=n.querySelector("#stx-admin-home");ze(V,Pe(a.adminHome.peek())).then(()=>{i()&&(V.value=a.adminHome.peek(),q.value=!0)}).catch(()=>{});const Te=E.contextMenu(y,{cursor:"glyph",match:".stx-row",inherit:!1,density:"compact",items:e=>Se(e.dataset.id)});r(()=>Te?.destroy?.());const oe=e=>{const t=ne(e.dataset.id),s=t&&G(t.key);s?.page?window.open(s.path,"_blank","noopener"):Y(e)},re=e=>{const t=e.target.closest(".stx-row");if(!t)return;const s=e.target.closest(".stx-kebab");if(s){Y(s);return}oe(t)},ie=e=>{const t=e.target.closest(".stx-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),Y(t)):e.key==="Enter"&&e.target===t&&oe(t))};y.addEventListener("click",re),y.addEventListener("keydown",ie),r(()=>{y.removeEventListener("click",re),y.removeEventListener("keydown",ie)}),pe(l,{id:"settings-view-site",icon:"external-link",title:"View site",onClick:()=>window.open("/","_blank","noopener")}),pe(l,{id:"settings-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>z()});const ce=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||(e.preventDefault(),e.stopImmediatePropagation(),z())};document.addEventListener("keydown",ce,!0),r(()=>document.removeEventListener("keydown",ce,!0));const le=()=>{if(x.peek())try{S.set(w,{form:Ie(Q()),base:B.peek(),at:new Date().toISOString()})}catch{}},ue=e=>{x.peek()&&(le(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",ue),r(()=>window.removeEventListener("beforeunload",ue)),h={dirty:()=>x.peek(),dispose(){k=!0,le(),u.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(n),P();let v=null;try{v=S.get(w)}catch{}if(v?.form){try{S.remove?S.remove(w):S.set(w,null)}catch{}const e=v.base&&v.base!==B.peek(),t=v.at?` (${_e(v.at)})`:"",s=e?`You left unsaved settings${t}, but the saved settings have changed since. Restore your version anyway?`:`You left unsaved settings${t}. Restore them?`;if(await E.confirm(s)&&i()){const c={...g.peek()};for(const[f,W]of Object.entries(v.form))f in c&&f!=="smtpPass"&&(c[f]=W);ae(c),q.peek()&&(V.value=a.adminHome.peek())}}}};
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
package/public/js/btt.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(){"use strict";var
|
|
1
|
+
(function(){"use strict";var c=window.__CMS_SITE__||{},a=[],h="http://www.w3.org/2000/svg";function f(t,e){var n=Number(t);return t!=null&&t!==""&&Number.isFinite(n)?Math.max(0,n):e}function p(){return Math.max(0,document.documentElement.scrollHeight-window.innerHeight)}function v(t,e){if(!(!t||t.enabled===!1)){var n=e?"Go to bottom":"Back to top",s=e?"go-to-bottom-btn":"back-to-top-btn";if(!document.getElementById(s)){var o=document.createElement("button");o.id=s,o.type="button",o.setAttribute("aria-label",n),o.title=n;var r=document.createElementNS(h,"svg");Object.entries({viewBox:"0 0 24 24",width:"20",height:"20",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round","aria-hidden":"true"}).forEach(function(l){r.setAttribute(l[0],l[1])});var i=document.createElementNS(h,"path");if(i.setAttribute("d",e?"M12 5v14m-7-7 7 7 7-7":"M12 19V5M5 12l7-7 7 7"),r.appendChild(i),o.appendChild(r),t.label){var m=document.createElement("span");m.textContent=t.label,m.style.marginLeft="6px",o.appendChild(m)}var b=f(t.offset,32),x=f(t.bottomOffset,b),u=t.position!=="bottom-left";Object.assign(o.style,{position:"fixed",bottom:x+"px",right:u?b+"px":"auto",left:u?"auto":b+"px",zIndex:"9999",display:"flex",alignItems:"center",padding:t.label?"10px 16px":"10px",borderRadius:t.label?"999px":"50%",border:"none",cursor:"pointer",background:"rgba(0,0,0,0.5)",color:"#fff",backdropFilter:"blur(4px)",opacity:"0",transform:"translateY(12px)",transition:"opacity .25s, transform .25s",pointerEvents:"none"}),o.tabIndex=-1,document.body.appendChild(o),o.addEventListener("click",function(){var l=document.documentElement.classList.contains("dm-reduced-motion")||document.body.classList.contains("dm-reduced-motion")||window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches;window.scrollTo({top:e?p():0,behavior:t.smooth!==!1&&!l?"smooth":"auto"})}),a.push({btn:o,down:e,right:u,offset:b,bottomOffset:x,threshold:f(t.scrollThreshold,300)})}}}if(v(c.backToTop,!1),v(c.goToBottom,!0),!a.length)return;function d(){var t=p();if(a.forEach(function(r){var i=t>0&&(r.down?t-window.scrollY>r.threshold:window.scrollY>r.threshold);r.btn.style.opacity=i?"1":"0",r.btn.style.transform=i?"translateY(0)":"translateY(12px)",r.btn.style.pointerEvents=i?"auto":"none",r.btn.tabIndex=i?0:-1,r.btn.setAttribute("aria-hidden",String(!i))}),a.length===2){var e=a[0],n=a[1];if(e.right===n.right&&Math.abs(e.offset-n.offset)<Math.max(e.btn.offsetWidth,n.btn.offsetWidth)){var s=n.btn.offsetHeight||40,o=Math.abs(e.bottomOffset-n.bottomOffset)<s+12;e.btn.style.bottom=(o?n.bottomOffset+s+12:e.bottomOffset)+"px"}}}window.addEventListener("scroll",d,{passive:!0}),window.addEventListener("resize",d),typeof ResizeObserver=="function"&&new ResizeObserver(d).observe(document.body),d()})();
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* Password reset (the link, where it may point, the email) is services/passwordReset.js.
|
|
21
21
|
*/
|
|
22
22
|
import crypto from 'node:crypto';
|
|
23
|
+
import {frameworkVersions} from '../../services/frameworkVersions.js';
|
|
23
24
|
import {config, getConfig} from '../../config.js';
|
|
24
25
|
import {getRegisteredSidebarItems, hooks} from '../../services/hooks.js';
|
|
25
26
|
import {getMenu} from '../../services/menus.js';
|
|
@@ -254,7 +255,7 @@ export async function authRoutes(fastify) {
|
|
|
254
255
|
// Dashboard Pro's "Open on my boards" - for a user the admin does not confine.
|
|
255
256
|
if (!adminScope && isDashboardPro() && (await readPrefs(request.user)).home) adminHome = '/dashboards';
|
|
256
257
|
// adminBrand: the admin's name and mark for the top bar, sidebar header and tab title.
|
|
257
|
-
return {permissions, adminScope, adminHome, adminBrand: adminBrandFor(getConfig('site'))};
|
|
258
|
+
return {permissions, adminScope, adminHome, versions: frameworkVersions(), adminBrand: adminBrandFor(getConfig('site'))};
|
|
258
259
|
});
|
|
259
260
|
|
|
260
261
|
// GET /api/auth/permissions-registry - returns the full permissions registry,
|
|
@@ -108,7 +108,7 @@ export async function settingsRoutes(fastify) {
|
|
|
108
108
|
'title', 'tagline', 'description', 'logo', 'favicon',
|
|
109
109
|
'theme', 'adminTheme', 'fontFamily', 'fontSize',
|
|
110
110
|
'smtp', 'footer', 'analytics', 'social', 'locale',
|
|
111
|
-
'layoutOptions', 'seo', 'backToTop', 'cookieConsent', 'breadcrumbs', 'autoTheme',
|
|
111
|
+
'layoutOptions', 'seo', 'backToTop', 'goToBottom', 'cookieConsent', 'breadcrumbs', 'autoTheme',
|
|
112
112
|
'adminBrand',
|
|
113
113
|
// The screen `#/` (Dashboard) opens - see services/adminHome.js.
|
|
114
114
|
'adminHome',
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import {createRequire} from 'node:module';
|
|
2
|
+
const require = createRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
/** Domma bundles its pinned Reactive dependency into the served JS distribution. */
|
|
5
|
+
export function frameworkVersions() {
|
|
6
|
+
try {
|
|
7
|
+
const pkg = require('domma-js/package.json');
|
|
8
|
+
const reactive = pkg.dependencies?.['domma-reactive'] || pkg.devDependencies?.['domma-reactive'];
|
|
9
|
+
return {js: pkg.version, reactive: /^\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(reactive || '') ? reactive : null};
|
|
10
|
+
} catch { return {}; }
|
|
11
|
+
}
|
|
@@ -523,6 +523,7 @@ export async function renderPage(page, opts = {}) {
|
|
|
523
523
|
social: site.social || null
|
|
524
524
|
},
|
|
525
525
|
site.backToTop?.enabled ? { backToTop: site.backToTop } : {},
|
|
526
|
+
site.goToBottom?.enabled ? { goToBottom: site.goToBottom } : {},
|
|
526
527
|
site.cookieConsent?.enabled ? {cookieConsent: site.cookieConsent} : {},
|
|
527
528
|
autoTheme ? {autoTheme} : {}
|
|
528
529
|
)).replace(/<\/script>/gi, '<\\/script>'),
|
|
@@ -545,7 +546,7 @@ export async function renderPage(page, opts = {}) {
|
|
|
545
546
|
searchAssets.bodyTag,
|
|
546
547
|
switcherAssets.bodyTag,
|
|
547
548
|
buildAnalyticsTag(),
|
|
548
|
-
site.backToTop?.enabled
|
|
549
|
+
(site.backToTop?.enabled || site.goToBottom?.enabled) ? '<script src="/public/js/btt.js?v=20261022-page-navigation" defer></script>' : '',
|
|
549
550
|
site.cookieConsent?.enabled ? '<script src="/public/js/cookie-consent.js" defer></script>' : '',
|
|
550
551
|
(page.usedComponents || [])
|
|
551
552
|
.map(n => `<script type="module" src="/api/components/${n}.js"></script>`)
|