Add configurable widget online effects

This commit is contained in:
rajchales-monito
2026-07-24 14:02:37 +02:00
parent 8178b4a4e7
commit e3270201b8
5 changed files with 98 additions and 7 deletions
+27
View File
@@ -179,6 +179,33 @@ label { display: grid; gap: 6px; color: var(--muted); font-size: 13px; }
font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace; font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
text-transform: lowercase; text-transform: lowercase;
} }
.effect-settings {
margin: 0;
padding: 10px 12px;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px 10px;
border: 1px solid var(--line);
border-radius: 8px;
background: #f8fbf9;
}
.effect-settings legend {
padding: 0 4px;
color: var(--muted);
font-size: 13px;
}
.effect-settings label {
display: flex;
align-items: center;
gap: 7px;
min-width: 0;
color: var(--ink);
font-size: 12px;
}
.effect-settings input {
width: auto;
margin: 0;
}
.list-tools { .list-tools {
padding: 10px 14px; padding: 10px 14px;
border-bottom: 1px solid var(--line); border-bottom: 1px solid var(--line);
+8
View File
@@ -110,6 +110,14 @@
<label>Pozice mobil <select name="mobileSide"><option>right</option><option>left</option></select></label> <label>Pozice mobil <select name="mobileSide"><option>right</option><option>left</option></select></label>
<label>Mobil bottom px <input name="mobileBottom" type="number"></label> <label>Mobil bottom px <input name="mobileBottom" type="number"></label>
<label>Mobil offset px <input name="mobileOffset" type="number"></label> <label>Mobil offset px <input name="mobileOffset" type="number"></label>
<fieldset class="effect-settings">
<legend>Online efekt tlacitka</legend>
<label><input name="launcherBreathe" type="checkbox"> jemne dychani</label>
<label><input name="launcherRing" type="checkbox"> svetelny prstenec</label>
<label><input name="launcherHoverLabel" type="checkbox"> napis pri najeti</label>
<label><input name="launcherWave" type="checkbox"> jednorazova vlna</label>
<label><input name="launcherAvatar" type="checkbox"> mini avatar styl</label>
</fieldset>
<label class="switch setting-switch"> <label class="switch setting-switch">
<input name="pulseOnAdminReply" type="checkbox"> <input name="pulseOnAdminReply" type="checkbox">
<span>Pulzovani widgetu pri odpovedi</span> <span>Pulzovani widgetu pri odpovedi</span>
+12
View File
@@ -79,6 +79,13 @@ $("#settingsForm").addEventListener("submit", async (event) => {
settings.mobile.side = form.get("mobileSide"); settings.mobile.side = form.get("mobileSide");
settings.mobile.bottomPx = Number(form.get("mobileBottom")); settings.mobile.bottomPx = Number(form.get("mobileBottom"));
settings.mobile.sideOffsetPx = Number(form.get("mobileOffset")); settings.mobile.sideOffsetPx = Number(form.get("mobileOffset"));
settings.launcherEffects = {
breathe: form.get("launcherBreathe") === "on",
ring: form.get("launcherRing") === "on",
hoverLabel: form.get("launcherHoverLabel") === "on",
wave: form.get("launcherWave") === "on",
avatar: form.get("launcherAvatar") === "on"
};
settings.notifications = settings.notifications || {}; settings.notifications = settings.notifications || {};
settings.notifications.pulseOnAdminReply = form.get("pulseOnAdminReply") === "on"; settings.notifications.pulseOnAdminReply = form.get("pulseOnAdminReply") === "on";
await saveSite({ settings, isOnline: state.site.isOnline }); await saveSite({ settings, isOnline: state.site.isOnline });
@@ -324,6 +331,11 @@ function renderSite() {
form.mobileSide.value = s.mobile.side || "right"; form.mobileSide.value = s.mobile.side || "right";
form.mobileBottom.value = s.mobile.bottomPx || 14; form.mobileBottom.value = s.mobile.bottomPx || 14;
form.mobileOffset.value = s.mobile.sideOffsetPx || 12; form.mobileOffset.value = s.mobile.sideOffsetPx || 12;
form.launcherBreathe.checked = s.launcherEffects?.breathe !== false;
form.launcherRing.checked = s.launcherEffects?.ring !== false;
form.launcherHoverLabel.checked = s.launcherEffects?.hoverLabel !== false;
form.launcherWave.checked = s.launcherEffects?.wave !== false;
form.launcherAvatar.checked = s.launcherEffects?.avatar !== false;
form.pulseOnAdminReply.checked = s.notifications?.pulseOnAdminReply !== false; form.pulseOnAdminReply.checked = s.notifications?.pulseOnAdminReply !== false;
} }
+43 -7
View File
@@ -25,8 +25,9 @@
root.innerHTML = ` root.innerHTML = `
<style>${styles(settings)}</style> <style>${styles(settings)}</style>
<button class="mf-launcher" type="button" aria-label="${copy.title}"> <button class="mf-launcher ${launcherEffectClasses(settings, site.isOnline)}" type="button" aria-label="${copy.title}">
<span></span> <span class="mf-launcher-icon"></span>
<span class="mf-launcher-label">${escapeHtml(site.isOnline ? "Jsme online" : "Offline")}</span>
</button> </button>
<section class="mf-panel" aria-live="polite"> <section class="mf-panel" aria-live="polite">
<header> <header>
@@ -413,10 +414,23 @@
}; };
} }
function launcherEffectClasses(settings, isOnline) {
const effects = settings.launcherEffects || {};
return [
isOnline ? "mf-online" : "mf-offline",
effects.breathe ? "mf-effect-breathe" : "",
effects.ring ? "mf-effect-ring" : "",
effects.hoverLabel ? "mf-effect-hover-label" : "",
effects.wave ? "mf-effect-wave" : "",
effects.avatar ? "mf-effect-avatar" : ""
].filter(Boolean).join(" ");
}
function styles(settings) { function styles(settings) {
const c = settings.colors; const c = settings.colors;
const desktop = settings.desktop; const desktop = settings.desktop;
const mobile = settings.mobile; const mobile = settings.mobile;
const labelOffset = desktop.side === "left" ? "left: 68px; right: auto;" : "right: 68px; left: auto;";
return ` return `
:host { all: initial; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: ${c.text}; } :host { all: initial; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: ${c.text}; }
* { box-sizing: border-box; } * { box-sizing: border-box; }
@@ -424,12 +438,21 @@
.mf-launcher { .mf-launcher {
position: fixed; z-index: 2147483000; ${desktop.side}: ${desktop.sideOffsetPx}px; bottom: ${desktop.bottomPx}px; position: fixed; z-index: 2147483000; ${desktop.side}: ${desktop.sideOffsetPx}px; bottom: ${desktop.bottomPx}px;
width: 58px; height: 58px; border: 0; border-radius: 50%; background: ${c.brand}; color: ${c.brandText}; width: 58px; height: 58px; border: 0; border-radius: 50%; background: ${c.brand}; color: ${c.brandText};
box-shadow: 0 12px 30px rgb(0 0 0 / 22%); cursor: pointer; box-shadow: 0 12px 30px rgb(0 0 0 / 22%); cursor: pointer; overflow: visible;
} }
.mf-launcher span, .mf-launcher span:before, .mf-launcher span:after { display: block; background: ${c.brandText}; height: 3px; border-radius: 4px; content: ""; } .mf-launcher-icon, .mf-launcher-icon:before, .mf-launcher-icon:after { display: block; background: ${c.brandText}; height: 3px; border-radius: 4px; content: ""; }
.mf-launcher span { width: 26px; margin: 0 auto; } .mf-launcher-icon { width: 26px; margin: 0 auto; }
.mf-launcher span:before { transform: translateY(-8px); } .mf-launcher-icon:before { transform: translateY(-8px); }
.mf-launcher span:after { transform: translateY(5px); width: 18px; } .mf-launcher-icon:after { transform: translateY(5px); width: 18px; }
.mf-launcher-label { position: absolute; ${labelOffset} top: 50%; transform: translateY(-50%) translateX(8px); opacity: 0; pointer-events: none; white-space: nowrap; border-radius: 999px; padding: 7px 10px; background: ${c.text}; color: white; font-size: 12px; box-shadow: 0 10px 24px rgb(0 0 0 / 18%); transition: opacity .18s ease, transform .18s ease; }
.mf-effect-hover-label:hover .mf-launcher-label { opacity: 1; transform: translateY(-50%) translateX(0); }
.mf-online.mf-effect-breathe { animation: mfBreathe 2.8s ease-in-out infinite; }
.mf-online.mf-effect-ring:before { position: absolute; inset: -5px; border: 2px solid ${c.brand}; border-radius: 50%; opacity: .62; content: ""; animation: mfRing 2.2s ease-out infinite; }
.mf-online.mf-effect-wave:after { position: absolute; inset: 0; border-radius: 50%; content: ""; box-shadow: 0 0 0 0 rgb(15 143 111 / 42%); animation: mfWaveOnce 1.7s ease-out 1 .6s; pointer-events: none; }
.mf-effect-avatar .mf-launcher-icon { width: 26px; height: 20px; border-radius: 12px 12px 10px 10px; background: rgb(255 255 255 / 24%); border: 2px solid ${c.brandText}; position: relative; }
.mf-effect-avatar .mf-launcher-icon:before { position: absolute; left: 6px; top: 6px; width: 4px; height: 4px; border-radius: 50%; background: ${c.brandText}; transform: none; box-shadow: 10px 0 0 ${c.brandText}; }
.mf-effect-avatar .mf-launcher-icon:after { position: absolute; left: 7px; top: 13px; width: 12px; height: 2px; border-radius: 999px; background: ${c.brandText}; transform: none; opacity: .88; }
.mf-offline { filter: grayscale(.2) saturate(.72); opacity: .86; }
.mf-launcher.mf-notify { animation: mfPulse 1.15s ease-out infinite; } .mf-launcher.mf-notify { animation: mfPulse 1.15s ease-out infinite; }
.mf-panel { .mf-panel {
position: fixed; z-index: 2147483001; ${desktop.side}: ${desktop.sideOffsetPx}px; bottom: ${desktop.bottomPx + 72}px; position: fixed; z-index: 2147483001; ${desktop.side}: ${desktop.sideOffsetPx}px; bottom: ${desktop.bottomPx + 72}px;
@@ -480,6 +503,19 @@
70% { box-shadow: 0 12px 30px rgb(0 0 0 / 22%), 0 0 0 14px rgb(15 143 111 / 0%); transform: scale(1.04); } 70% { box-shadow: 0 12px 30px rgb(0 0 0 / 22%), 0 0 0 14px rgb(15 143 111 / 0%); transform: scale(1.04); }
100% { box-shadow: 0 12px 30px rgb(0 0 0 / 22%), 0 0 0 0 rgb(15 143 111 / 0%); transform: scale(1); } 100% { box-shadow: 0 12px 30px rgb(0 0 0 / 22%), 0 0 0 0 rgb(15 143 111 / 0%); transform: scale(1); }
} }
@keyframes mfBreathe {
0%, 100% { transform: scale(1); box-shadow: 0 12px 30px rgb(0 0 0 / 22%); }
50% { transform: scale(1.035); box-shadow: 0 16px 36px rgb(0 0 0 / 20%), 0 0 0 8px rgb(15 143 111 / 10%); }
}
@keyframes mfRing {
0% { transform: scale(.92); opacity: .58; }
70% { transform: scale(1.22); opacity: 0; }
100% { transform: scale(.92); opacity: 0; }
}
@keyframes mfWaveOnce {
0% { transform: scale(1); opacity: .7; }
100% { transform: scale(1.58); opacity: 0; }
}
@media (max-width: 640px) { @media (max-width: 640px) {
.mf-launcher { ${mobile.side}: ${mobile.sideOffsetPx}px; bottom: ${mobile.bottomPx}px; } .mf-launcher { ${mobile.side}: ${mobile.sideOffsetPx}px; bottom: ${mobile.bottomPx}px; }
.mf-panel { ${mobile.side}: ${mobile.sideOffsetPx}px; bottom: ${mobile.bottomPx + 68}px; width: min(${mobile.widthPx}px, calc(100vw - 24px)); max-height: calc(100vh - 96px); } .mf-panel { ${mobile.side}: ${mobile.sideOffsetPx}px; bottom: ${mobile.bottomPx + 68}px; width: min(${mobile.widthPx}px, calc(100vw - 24px)); max-height: calc(100vh - 96px); }
+8
View File
@@ -263,6 +263,13 @@ function defaultSettings() {
}, },
desktop: { side: "right", bottomPx: 22, sideOffsetPx: 22, widthPx: 370 }, desktop: { side: "right", bottomPx: 22, sideOffsetPx: 22, widthPx: 370 },
mobile: { side: "right", bottomPx: 14, sideOffsetPx: 12, widthPx: 340 }, mobile: { side: "right", bottomPx: 14, sideOffsetPx: 12, widthPx: 340 },
launcherEffects: {
breathe: true,
ring: true,
hoverLabel: true,
wave: true,
avatar: true
},
notifications: { pulseOnAdminReply: true } notifications: { pulseOnAdminReply: true }
}; };
} }
@@ -844,6 +851,7 @@ function formatAttachment(row) {
function formatSite(site) { function formatSite(site) {
const settings = parseJson(site.settings_json, defaultSettings()); const settings = parseJson(site.settings_json, defaultSettings());
settings.notifications = { ...defaultSettings().notifications, ...(settings.notifications || {}) }; settings.notifications = { ...defaultSettings().notifications, ...(settings.notifications || {}) };
settings.launcherEffects = { ...defaultSettings().launcherEffects, ...(settings.launcherEffects || {}) };
return { return {
id: site.id, id: site.id,
siteKey: site.site_key, siteKey: site.site_key,