/* Copyright © 2026 K2B NETWORK SOLUTIONS LLC. All Rights Reserved. Proprietary and confidential. Unauthorized copying, distribution, or use of this file, via any medium, is strictly prohibited. Original work product. */ // K2B DMS — "JB", the dealership helper. RC34 (lane hlp): rewritten. // • DOCKED: a button in the top bar (window.JBHelperButton; on a phone's service screens, where the top bar steps aside, the same button // sits in the RO window's / My jobs' own title bar) opens the helper BESIDE the screen — a side panel on a laptop, a sheet along the // bottom on a phone. The panel is a column / row of the page grid (app.jsx renders window.JBHelperPanel inside .jb-shell): the screen // gets narrower or shorter and nothing is ever drawn over it. (Until RC33 JB floated over the screens and covered RO content on phones.) // • ANSWERS come from the server (POST /api/helper/ask): Claude through the store's key, under the owner's daily cap, with the knowledge // filtered for THIS login's role — or the server's built-in answers when the cap is reached / no key / AI off for the role, and it says // so. When the server can't be reached at all the page answers from the built-in answers it keeps (GET /api/helper/builtin, for this // login only, this tab only) and from the universal ones it carries (HelperPack.jsx.txt). // • "Open " buttons jump to the screen (only a screen the login's menu has); "Show me" walks through the screen: each step's // button gets a ring drawn AROUND it (the ring lets every click through), the step's words show in the helper, Next / Back / Stop. // A step the login can't use says so. The helper never presses anything and never changes data — it only explains. // • The two easter eggs are matched here, in the page: never sent to the server, never logged. const JBH_SKEY = "jb_helper_builtin_v1"; // sessionStorage (this tab): { uid, role, version, topics } — the built-in answers for THIS login const JBH_FIRST = { me: false, source: "intro", text: "Hi, I'm JB. Ask me how to do anything in the DMS — which screen, which button, in what order. I explain how it works; I never look up or change your records." }; /* ── one small store the button, the panel and the shell share ──────────────── */ const jbhStore = (() => { let st = { open: false, msgs: [JBH_FIRST], status: null, builtin: null, tour: null, busy: false, uid: null, egg: null }; const subs = new Set(); const set = (patch) => { st = Object.assign({}, st, typeof patch === "function" ? patch(st) : patch); subs.forEach((f) => { try { f(st); } catch (e) {} }); }; return { get: () => st, set, sub: (f) => { subs.add(f); return () => subs.delete(f); } }; })(); const useJbhStore = () => { const [s, setS] = React.useState(jbhStore.get()); React.useEffect(() => jbhStore.sub(setS), []); return s; }; const useJbHelperOpen = () => useJbhStore().open; const jbhUid = () => { try { return (window.DMS && DMS.userId && DMS.userId()) || ""; } catch (e) { return ""; } }; const jbhRole = () => { try { return (window.DMS && DMS.userRole && DMS.userRole()) || ""; } catch (e) { return ""; } }; // a different login on this PC never sees the last person's questions const jbhSameUser = () => { const u = jbhUid(); if (jbhStore.get().uid !== u) jbhStore.set({ uid: u, msgs: [JBH_FIRST], status: null, builtin: null, tour: null, busy: false }); }; const jbhReset = () => jbhStore.set({ open: false, msgs: [JBH_FIRST], status: null, builtin: null, tour: null, busy: false, uid: null }); const jbhToggle = (force) => { jbhSameUser(); const open = typeof force === "boolean" ? force : !jbhStore.get().open; jbhStore.set({ open, tour: open ? jbhStore.get().tour : null }); if (open) jbhLoad(); }; const jbhPush = (m) => jbhStore.set((s) => ({ msgs: s.msgs.concat(m).slice(-60) })); const jbhIsPhone = () => (window.innerWidth || 1200) < 900; const jbhCanGo = (r) => !!r && (typeof window.jbNavAllowed === "function" ? !!window.jbNavAllowed(r) : true); /* ── talking to the server ───────────────────────────────────────────────────── */ const jbhApi = (path, opts) => (typeof apiFetch === "function" ? apiFetch(path, opts) : Promise.reject(Object.assign(new Error("offline"), { status: 0 }))); const jbhOfflineSession = () => { try { return !!(window.jbIsOfflineSession && window.jbIsOfflineSession()); } catch (e) { return false; } }; async function jbhLoad() { const uid = jbhUid(); const role = jbhRole(); if (!jbhStore.get().builtin) { try { const c = JSON.parse(sessionStorage.getItem(JBH_SKEY) || "null"); if (c && c.uid === uid && c.role === role && Array.isArray(c.topics)) jbhStore.set({ builtin: c.topics, builtinVersion: c.version || "" }); } catch (e) {} } if (jbhOfflineSession()) { jbhStore.set({ status: { ok: false, ai: false, reason: "This is an offline session, so the helper answers from its built-in answers." } }); return; } let ver = ""; try { const s = await jbhApi("/helper/status"); ver = (s && s.version) || ""; jbhStore.set({ status: s }); } catch (e) { jbhStore.set({ status: { ok: false, ai: false, reason: e && e.status ? ((e.body && e.body.error) || "The helper is not available right now.") : "The server can't be reached right now, so the helper answers from its built-in answers." } }); } // fix round: the built-in answers are now every topic this login may see (for when the server can't be reached) - fetched once per // sign-in and knowledge version, not on every open if (jbhStore.get().builtin && ver && jbhStore.get().builtinVersion === ver) return; try { const b = await jbhApi("/helper/builtin"); if (b && Array.isArray(b.topics)) { jbhStore.set({ builtin: b.topics, builtinVersion: b.version || "" }); try { sessionStorage.setItem(JBH_SKEY, JSON.stringify({ uid, role, version: b.version, topics: b.topics })); } catch (e) {} } } catch (e) { /* keep what we had */ } } /* ── the built-in answers in the page (offline) ──────────────────────────────── */ const JBH_STOP = new Set("a an and are as at be but by can could do does did for from get got how i if in into is it its me my of on or our please should so that the their them then there these this to was we what when where which who why will with would you your about any some just need want tell know make way up out use using used".split(" ")); // RC34 fix round (r34-aud A1): the page splits words exactly like the server (src/lib/helper-words.js - the tables come in the built-in // pack as JB_HELPER_PACK.words): "two step sign in" / "2FA" -> signincode, "log out" -> signoff, "reconcile the bank" -> bankrec, ... let jbhW = null; function jbhWordsFn() { if (jbhW) return jbhW; const T = window.JB_HELPER_PACK && window.JB_HELPER_PACK.words; if (!T || !Array.isArray(T.phrases)) return (s) => String(s || "").toLowerCase().split(/[^a-z0-9]+/).filter((w) => w && !JBH_STOP.has(w)); const res = T.phrases.map((p) => [new RegExp(p[0], "g"), p[1]]); const canon = new Set(T.phrases.map((p) => p[1].trim())); const stop = new Set(T.stop || []); const syn = T.syn || {}; const stem = (w0) => { let w = w0; if (w.length <= 3 || /\d/.test(w)) return w; if (w.length > 5 && /ies$/.test(w)) w = w.slice(0, -3) + "y"; else if (w.length > 5 && /ied$/.test(w)) w = w.slice(0, -3) + "y"; else if (w.length > 6 && /ings?$/.test(w)) w = w.replace(/ings?$/, ""); else if (w.length > 5 && /ions?$/.test(w)) w = w.replace(/ions?$/, ""); else if (w.length > 4 && /ed$/.test(w) && !/eed$/.test(w)) w = w.slice(0, -2); else if (w.length > 4 && /(?:ss|us|is)$/.test(w)) { /* keep */ } else if (w.length > 4 && /(?:ches|shes|xes|sses)$/.test(w)) w = w.slice(0, -2); else if (w.length > 3 && /s$/.test(w)) w = w.slice(0, -1); if (w.length > 4 && /([b-df-hj-np-tv-z])\1$/.test(w) && !/(?:ll|ss)$/.test(w)) w = w.slice(0, -1); if (w.length > 4 && /e$/.test(w)) w = w.slice(0, -1); return w.length > 7 ? w.slice(0, 7) : w; }; jbhW = (s) => { let t = String(s || "").toLowerCase().replace(/[\u2018\u2019]/g, "'").replace(/[\u2010-\u2015]/g, "-"); for (const [re, rep] of res) t = t.replace(re, rep); const out = []; t.split(/[^a-z0-9&']+/).forEach((w0) => { const w = w0.replace(/^'+|'+$/g, "").replace(/'s$/, ""); if (!w || w === "&" || stop.has(w) || w.length > 40 || (w.length === 1 && !/\d/.test(w))) return; const st = canon.has(w) ? w : stem(w.replace(/'/g, "")); if (stop.has(st)) return; out.push(st); if (syn[w]) syn[w].forEach((x) => out.push(stem(x))); }); return out; }; return jbhW; } const jbhWords = (s) => jbhWordsFn()(s); function jbhLocal(q, route) { const pool = []; const seen = new Set(); (jbhStore.get().builtin || []).forEach((t) => { if (!seen.has(t.id)) { seen.add(t.id); pool.push(t); } }); ((window.JB_HELPER_PACK && window.JB_HELPER_PACK.topics) || []).forEach((t) => { if (!seen.has(t.id)) { seen.add(t.id); pool.push(Object.assign({ full: true }, t)); } }); const qw = new Set(jbhWords(q)); if (!qw.size) return null; let best = null; let bestS = 0; const ranked = []; for (const t of pool) { const phr = [t.q].concat(t.k || []).map((x) => new Set(jbhWords(x))); // the title and the other phrasings ("asks") const tw = new Set(); phr.forEach((p) => p.forEach((w) => tw.add(w))); const aw = new Set(jbhWords(t.a)); let s = 0; qw.forEach((w) => { if (tw.has(w)) s += 3; else if (aw.has(w)) s += 1; }); let cover = 0; phr.forEach((p) => { if (!p.size) return; let hit = 0; p.forEach((w) => { if (qw.has(w)) hit++; }); if (!hit) return; const pr = hit / p.size; const rc = hit / qw.size; cover = Math.max(cover, 2 * pr * rc / (pr + rc)); }); s += 6 * cover; // the question says (nearly) what the topic's title or one of its phrasings says = the right topic if (route && t.route === route) s *= 1.2; if (s > 0) ranked.push({ t, s }); if (s > bestS) { best = t; bestS = s; } } ranked.sort((x, y) => y.s - x.s); if (!best || bestS < 3) return { text: "I don't have a built-in answer for that one. Try other words — the name of the screen or the button helps.", related: ranked.slice(0, 3).map((x) => ({ id: x.t.id, q: x.t.q })) }; return { text: best.a, id: best.id, links: best.route ? [{ route: best.route, label: best.routeLabel }] : [], showMe: best.steps && best.steps.length ? { id: best.id, title: best.q, route: best.route, routeLabel: best.routeLabel, canUse: best.full !== false, steps: best.steps } : null, related: ranked.filter((x) => x.t.id !== best.id).slice(0, 3).map((x) => ({ id: x.t.id, q: x.t.q })) }; } /* ── the two easter eggs (local only) ────────────────────────────────────────── */ function jbhEgg(q) { const norm = q.toLowerCase().replace(/[^a-z ]/g, "").replace(/\s+/g, " ").trim(); if (norm.includes("who is the ceo")) { if (window.jbSound) { try { window.jbSound.scream(); setTimeout(() => window.jbSound.boom(), 3000); } catch (e) {} } jbhStore.set({ egg: { kind: "ceo", at: Date.now() } }); setTimeout(() => jbhStore.set({ egg: null }), 4600); return "☠️ You heard it. JARED BREWER. CEO. K2B NETWORK SOLUTIONS LLC. 💥"; } if (norm.includes("who does jared really love")) { jbhStore.set({ egg: { kind: "love", at: Date.now() } }); setTimeout(() => jbhStore.set({ egg: null }), 5200); return "💋 Only you, Ashley Brewer — my love, my dear!! 💖"; } return null; } /* ── asking ──────────────────────────────────────────────────────────────────── */ async function jbhAsk(q, route) { const text = String(q || "").trim(); if (!text || jbhStore.get().busy) return; jbhSameUser(); jbhPush({ me: true, text }); const egg = jbhEgg(text); if (egg) { jbhPush({ me: false, source: "egg", text: egg }); return; } const local = (note) => { const a = jbhLocal(text, route); jbhPush(Object.assign({ me: false, source: "builtin", note }, a || { text: "I don't have a built-in answer for that one." })); }; if (jbhOfflineSession()) { local("This is an offline session, so this is a built-in answer."); return; } jbhStore.set({ busy: true }); try { const r = await jbhApi("/helper/ask", { method: "POST", body: { question: text, screen: route || "" } }); jbhPush({ me: false, source: r.source, note: r.note, text: r.answer, links: r.links, showMe: r.showMe, related: r.related }); } catch (e) { if (e && e.status && e.body && e.body.error && e.status < 500) jbhPush({ me: false, source: "notice", text: e.body.error }); else local(e && e.status ? "The helper hit a problem on the server, so this is a built-in answer." : "The server can't be reached right now, so this is a built-in answer."); } finally { jbhStore.set({ busy: false }); } } async function jbhOpenTopic(id, route) { try { const r = await jbhApi("/helper/topic/" + encodeURIComponent(id)); const t = r.topic; jbhPush({ me: false, source: "topic", title: t.q, text: t.answer, links: t.links, showMe: t.showMe }); } catch (e) { const t = (jbhStore.get().builtin || []).concat((window.JB_HELPER_PACK && window.JB_HELPER_PACK.topics) || []).find((x) => x.id === id); if (t) jbhPush({ me: false, source: "builtin", title: t.q, text: t.a, links: t.route ? [{ route: t.route, label: t.routeLabel }] : [], showMe: t.steps && t.steps.length ? { id: t.id, title: t.q, route: t.route, routeLabel: t.routeLabel, canUse: t.full !== false, steps: t.steps } : null }); else jbhPush({ me: false, source: "notice", text: (e && e.body && e.body.error) || "That topic isn't available right now." }); } } /* ── "Show me": find each step's button on the real screen and ring it ───────── */ const jbhVisible = (el) => { try { if (!el || !el.getClientRects().length) return false; const cs = getComputedStyle(el); return cs.visibility !== "hidden" && cs.display !== "none"; } catch (e) { return false; } }; const jbhMine = (el) => !!(el && el.closest && el.closest("[data-jb-helper]")); const jbhClean = (s) => String(s || "").toLowerCase().replace(/[^a-z0-9&/+#$%.,()' -]+/g, " ").replace(/\s+/g, " ").trim(); function jbhFind(step) { let base = document; if (step.scope) { try { const sc = [...document.querySelectorAll(step.scope)].find(jbhVisible); if (sc) base = sc; } catch (e) {} } if (step.css) { try { return [...base.querySelectorAll(step.css)].find((el) => jbhVisible(el) && !jbhMine(el)) || null; } catch (e) { return null; } } const parts = String(step.text || "").split("*").map(jbhClean); const want = parts.join(" ").trim(); if (!want) return null; // fix round (r34-aud A4): "*" in a step's text = words that change (a month, a number): "Close * 20" finds "Close October 2026…" const wild = parts.length > 1 ? new RegExp(parts.map((x) => x.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".+?")) : null; const has = (txt) => (wild ? wild.test(txt) : txt.includes(want)); const hit = (el) => jbhVisible(el) && !jbhMine(el) && has(jbhClean(el.innerText || el.textContent || el.getAttribute("aria-label") || el.value)); const strong = [...base.querySelectorAll('button, a, [role="button"], [role="tab"], .nav-item, select, summary, label, input[type="button"], input[type="submit"]')].filter(hit); if (strong.length) return strong.sort((a, b) => (a.innerText || "").length - (b.innerText || "").length)[0]; const weak = [...base.querySelectorAll(".jb-screen span, .jb-screen div, .jb-screen td, .jb-screen th, .jb-screen li")].filter((el) => (el.innerText || "").length <= 80 && hit(el)); return weak.sort((a, b) => (a.innerText || "").length - (b.innerText || "").length)[0] || null; } let jbhTourTimer = null; function jbhTourLocate() { clearTimeout(jbhTourTimer); const t0 = Date.now(); const tick = () => { const tour = jbhStore.get().tour; if (!tour) return; const step = tour.steps[tour.i]; const el = jbhFind(step); if (el) { window.__jbhTarget = el; try { el.scrollIntoView({ block: "center", inline: "nearest" }); } catch (e) {} const disabled = !!(el.disabled || el.getAttribute("aria-disabled") === "true"); jbhStore.set({ tour: Object.assign({}, tour, { found: true, disabled }) }); return; } window.__jbhTarget = null; if (Date.now() - t0 < 4000) { jbhTourTimer = setTimeout(tick, 250); return; } jbhStore.set({ tour: Object.assign({}, tour, { found: false, disabled: false }) }); }; jbhStore.set({ tour: Object.assign({}, jbhStore.get().tour, { found: null }) }); tick(); } function jbhTourStart(sm, go, route) { if (!sm || !sm.steps || !sm.steps.length) return; if (sm.route && sm.route !== route) { if (!jbhCanGo(sm.route)) { jbhPush({ me: false, source: "notice", text: "That screen (" + (sm.routeLabel || sm.route) + ") isn't on your menu, so I can't show it to you. Ask your manager who does this part." }); return; } if (go) go(sm.route); } jbhStore.set({ tour: { id: sm.id, title: sm.title, steps: sm.steps, i: 0, canUse: sm.canUse !== false, found: null } }); setTimeout(jbhTourLocate, sm.route && sm.route !== route ? 600 : 0); } const jbhTourMove = (d) => { const t = jbhStore.get().tour; if (!t) return; const i = Math.max(0, Math.min(t.steps.length - 1, t.i + d)); if (i === t.i) return; jbhStore.set({ tour: Object.assign({}, t, { i, found: null }) }); jbhTourLocate(); }; const jbhTourStop = () => { clearTimeout(jbhTourTimer); window.__jbhTarget = null; jbhStore.set({ tour: null }); }; /* ── the little face (the button and the panel header) ───────────────────────── */ const JBHFace = ({ size = 22 }) => ( ); /* ── the top-bar button ──────────────────────────────────────────────────────── */ // tb = it sits in a window's own title bar (a phone's service screens, where the top bar steps aside — CSS shows it only there); dark = My jobs' dark bar; // side = the first item of the ☰ Menu on a phone's service screens (the RO title bar has no room left on a 360 px phone — CSS shows it only there) const JBHelperButton = ({ compact, tb, dark, side }) => { const s = useJbhStore(); if (side) return ( ); const base = tb ? { minWidth: 40, minHeight: dark ? 44 : 32, padding: "0 8px", borderRadius: 8, border: dark ? "1px solid rgba(255,255,255,.25)" : "1px solid var(--jb-border, #C8CCD2)", background: dark ? "rgba(255,255,255,.08)" : (s.open ? "#FFF4C9" : "var(--jb-surface, #FFF)"), color: dark ? "#FFF" : "var(--jb-navy, #11337A)", cursor: "pointer", alignItems: "center", justifyContent: "center", gap: 4, fontWeight: 700, flex: "0 0 auto" } : { display: "inline-flex", alignItems: "center", gap: 6, fontWeight: 700, background: s.open ? "#FFF4C9" : undefined }; return ( ); }; /* ── the panel (a column of the page on a laptop, a sheet along the bottom on a phone) ── */ const JBH_BTN = { fontSize: 12, padding: "4px 10px", borderRadius: 999, border: "1px solid #C8CCD2", background: "#F4F6F9", cursor: "pointer", color: "#22303F", fontFamily: "inherit" }; const JBHMessage = ({ m, go, route }) => { if (m.me) return
{m.text}
; const tag = m.source === "ai" ? "AI answer" : m.source === "builtin" ? "Built-in answer" : m.source === "refused" ? "Not something I can help with" : m.source === "topic" ? "Topic" : ""; return (
{tag &&
{tag}
} {m.title &&
{m.title}
} {m.note &&
{m.note}
}
{m.text}
{((m.links && m.links.length) || m.showMe) ? (
{(m.links || []).map((l) => { const ok = jbhCanGo(l.route); return ; })} {m.showMe && }
) : null} {m.related && m.related.length ? (
Related:
{m.related.map((r) => )}
) : null}
); }; const JBHTourBar = ({ tour }) => { const step = tour.steps[tour.i]; const why = !tour.canUse || step.canUse === false ? "" : tour.found === false ? "I can't find this one on your screen yet — it shows up after the step before (press the button I showed you first), or your sign-in doesn't have it." : tour.disabled ? "This button is greyed out for your sign-in right now." : ""; return (
Show me: {tour.title}
Step {tour.i + 1} of {tour.steps.length}. {step.note || (step.text ? "\"" + step.text + "\"" : "")}
{(!tour.canUse || step.canUse === false) &&
Your sign-in can't use this button — the screen just shows you where it is.
} {why &&
{why}
} {tour.found === null &&
Looking for it on the screen…
}
); }; const JBHelperPanel = ({ go, route }) => { const s = useJbhStore(); const [q, setQ] = React.useState(""); const logRef = React.useRef(null); React.useEffect(() => { jbhSameUser(); jbhLoad(); }, []); React.useEffect(() => { const el = logRef.current; if (el) el.scrollTop = el.scrollHeight; }, [s.msgs.length, s.busy]); const send = () => { const v = q.trim(); if (!v) return; setQ(""); jbhAsk(v, route); }; const st = s.status; const ai = !!(st && st.ai); const chips = (() => { const b = s.builtin || []; const here = b.filter((t) => t.route === route).slice(0, 3); const pick = (here.length ? here : b.slice(0, 3)).map((t) => t.q); return pick.length ? pick : ["How do I use the helper?", "What roles are there, and what does my role let me do?"]; })(); return ( ); }; /* ── what stays mounted: the ring of "Show me" and the easter eggs (never over a click: pointer-events none) ── */ const JBHelper = ({ go, route }) => { const s = useJbhStore(); const ringRef = React.useRef(null); React.useEffect(() => { if (s.open) jbhSameUser(); }, [route]); React.useEffect(() => { if (!s.tour) return undefined; let raf = 0; const loop = () => { const el = window.__jbhTarget; const ring = ringRef.current; if (ring) { if (el && el.isConnected && jbhVisible(el)) { const r = el.getBoundingClientRect(); ring.style.display = "block"; ring.style.left = (r.left - 4) + "px"; ring.style.top = (r.top - 4) + "px"; ring.style.width = (r.width + 8) + "px"; ring.style.height = (r.height + 8) + "px"; } else ring.style.display = "none"; } raf = requestAnimationFrame(loop); }; raf = requestAnimationFrame(loop); return () => cancelAnimationFrame(raf); }, [s.tour && s.tour.id, s.tour && s.tour.i]); React.useEffect(() => () => jbhTourStop(), []); const egg = s.egg; return ( <> {s.tour && ( )} {egg && egg.kind === "love" && ( )} {egg && egg.kind === "ceo" && ( )} ); }; /* ── the layout rules (in one place, so the panel never sits over the screen) ── */ (function jbhStyles() { try { if (document.getElementById("jbh-style")) return; const el = document.createElement("style"); el.id = "jbh-style"; el.textContent = ` .jb-helper-panel { display: flex; flex-direction: column; min-width: 0; min-height: 0; overflow: hidden; background: #FFF; font-family: var(--jb-font, system-ui); box-sizing: border-box; } @media screen and (min-width: 900px) { .jb-shell.jb-helper-open { grid-template-columns: 220px minmax(0, 1fr) min(380px, 30vw) !important; } .jb-shell.jb-helper-open > .jb-helper-panel { grid-column: 3; grid-row: 1; border-left: 1px solid var(--jb-border, #D8DDE5); } } @media screen and (min-width: 900px) and (max-width: 1099px) { .jb-shell.jb-helper-open.jb-svc-route { grid-template-columns: 56px minmax(0, 1fr) 340px !important; } } @media screen and (max-width: 899px) { .jb-shell.jb-helper-open { grid-template-rows: minmax(0, 1fr) minmax(0, 52%) !important; } .jb-shell.jb-helper-open > .jb-helper-panel { grid-column: 1 / -1; grid-row: 2; border-top: 2px solid #11337A; } } .jb-popshell > .jb-helper-panel { flex: 0 0 45%; border-top: 2px solid #11337A; } .jb-helper-tb { display: none !important; } @media screen and (max-width: 1439px) { .jb-topbar .jb-helper-btn .jbh-lbl { display: none; } .jb-topbar .jb-helper-btn { padding: 0 9px !important; } } @media screen and (max-width: 640px) { .jb-shell.jb-svc-route .jb-helper-tb { display: inline-flex !important; } } @media screen and (max-width: 379px) { .jb-shell.jb-svc-route [data-ro-screen] .jb-helper-tb { display: none !important; } } .jb-helper-side { display: none !important; } @media screen and (max-width: 640px) { .jb-shell.jb-svc-route.jb-svc-menu-open .jb-helper-side { display: flex !important; } } @media print { .jb-helper-panel, [data-jb-helper] { display: none !important; } } @keyframes jbh-pulse { 0%,100% { box-shadow: 0 0 0 4px rgba(255,176,0,.35), 0 0 18px rgba(255,176,0,.6); } 50% { box-shadow: 0 0 0 8px rgba(255,176,0,.15), 0 0 26px rgba(255,176,0,.8); } } @keyframes jbh-conf { 0% { transform: translateY(0) rotate(0deg); opacity: 1; } 100% { transform: translateY(112vh) rotate(680deg); opacity: 0.85; } } @keyframes jbh-out { 0% { opacity: 1; } 100% { opacity: 0; } }`; document.head.appendChild(el); } catch (e) {} })(); Object.assign(window, { JBHelper, JBHelperPanel, JBHelperButton, useJbHelperOpen, jbHelperToggle: jbhToggle, jbHelperReset: jbhReset, jbHelperStore: jbhStore });