From 32372c7545a6538df4d781dfcc57f52e40111c9b Mon Sep 17 00:00:00 2001 From: MOH Date: Fri, 3 Oct 2025 23:54:13 +0200 Subject: [PATCH] Version 1.2.0 --- floatina/assets/css/admin.css | 28 ++ floatina/assets/css/docked.css | 30 ++ floatina/assets/css/floatina.css | 399 ++++++++----------- floatina/assets/icons/bi-chat-dots.svg | 6 + floatina/assets/icons/bi-chat.svg | 3 + floatina/assets/icons/bi-house.svg | 3 + floatina/assets/icons/bi-mic.svg | 4 + floatina/assets/icons/bi-newspaper.svg | 5 + floatina/assets/icons/bi-question-circle.svg | 5 + floatina/assets/icons/bi-x.svg | 3 + floatina/assets/js/accordion.js | 35 ++ floatina/assets/js/admin.js | 16 + floatina/assets/js/core.js | 198 +++++++++ floatina/assets/js/docked.js | 164 ++++++++ floatina/assets/js/floatina.js | 294 -------------- floatina/floatina.php | 37 +- floatina/includes/class-ajax.php | 16 +- floatina/includes/class-assets.php | 213 ++++++++-- floatina/includes/class-cpts.php | 43 +- floatina/includes/class-meta.php | 26 ++ floatina/includes/class-settings.php | 289 ++++++++++---- floatina/includes/helpers.php | 45 ++- floatina/templates/loop-faq.php | 43 +- floatina/templates/loop-news.php | 42 +- floatina/templates/panel.php | 126 +++++- floatina/templates/tabs/tab-faq.php | 32 ++ floatina/templates/tabs/tab-home.php | 32 ++ floatina/templates/tabs/tab-news.php | 30 ++ floatina/templates/tabs/tab-text.php | 2 + floatina/templates/tabs/tab-voice.php | 2 + 30 files changed, 1434 insertions(+), 737 deletions(-) create mode 100644 floatina/assets/css/admin.css create mode 100644 floatina/assets/css/docked.css create mode 100644 floatina/assets/icons/bi-chat-dots.svg create mode 100644 floatina/assets/icons/bi-chat.svg create mode 100644 floatina/assets/icons/bi-house.svg create mode 100644 floatina/assets/icons/bi-mic.svg create mode 100644 floatina/assets/icons/bi-newspaper.svg create mode 100644 floatina/assets/icons/bi-question-circle.svg create mode 100644 floatina/assets/icons/bi-x.svg create mode 100644 floatina/assets/js/accordion.js create mode 100644 floatina/assets/js/admin.js create mode 100644 floatina/assets/js/core.js create mode 100644 floatina/assets/js/docked.js delete mode 100644 floatina/assets/js/floatina.js create mode 100644 floatina/includes/class-meta.php create mode 100644 floatina/templates/tabs/tab-faq.php create mode 100644 floatina/templates/tabs/tab-home.php create mode 100644 floatina/templates/tabs/tab-news.php create mode 100644 floatina/templates/tabs/tab-text.php create mode 100644 floatina/templates/tabs/tab-voice.php diff --git a/floatina/assets/css/admin.css b/floatina/assets/css/admin.css new file mode 100644 index 0000000..fcc2537 --- /dev/null +++ b/floatina/assets/css/admin.css @@ -0,0 +1,28 @@ +/************************************************************************************ + * Floatina – Admin styling + ************************************************************************************/ +.fi-admin { --fi-gap:14px; --fi-muted:#6b7280; } +.fi-admin .fi-admin-tabs { display:flex; gap:8px; margin:18px 0 16px; } +.fi-admin .fi-admin-tabs .fi-tab{ display:inline-flex; align-items:center; gap:8px; padding:8px 14px; border:1px solid #e5e7eb; border-radius:10px; background:#fff; text-decoration:none; color:#111827; } +.fi-admin .fi-admin-tabs .fi-tab.is-active{ background:#0ea5ff10; border-color:#93c5fd; box-shadow:0 6px 14px rgba(0,0,0,.06); } + +.fi-form .fi-field{ margin:18px 0; } +.fi-form .fi-label{ display:block; font-weight:600; margin-bottom:6px; } +.fi-form .description{ color:var(--fi-muted); margin:6px 0 0; } + +.fi-segment{ display:inline-flex; gap:6px; background:#f3f4f6; padding:4px; border-radius:12px; border:1px solid #e5e7eb; } +.fi-segment label{ display:inline-flex; align-items:center; } +.fi-segment input{ appearance:none; } +.fi-segment span{ display:inline-block; padding:6px 12px; border-radius:10px; } +.fi-segment input:checked + span{ background:#fff; border:1px solid #dbeafe; box-shadow:0 4px 10px rgba(0,0,0,.06); } + +.fi-toggle{ position:relative; display:inline-block; width:50px; height:28px; } +.fi-toggle input{ display:none; } +.fi-toggle .fi-tgl{ position:absolute; inset:0; background:#e5e7eb; border-radius:999px; transition:.2s ease; } +.fi-toggle .fi-tgl:after{ content:""; position:absolute; width:22px; height:22px; top:3px; left:3px; border-radius:50%; background:#fff; box-shadow:0 2px 6px rgba(0,0,0,.15); transition:.2s ease; } +.fi-toggle input:checked + .fi-tgl{ background:#60a5fa; } +.fi-toggle input:checked + .fi-tgl:after{ transform:translateX(22px); } + +.fi-grid2{ display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:16px; } +.fi-color{ width:220px; } +@media (max-width: 1024px){ .fi-grid2{ grid-template-columns:1fr; } } diff --git a/floatina/assets/css/docked.css b/floatina/assets/css/docked.css new file mode 100644 index 0000000..89bc760 --- /dev/null +++ b/floatina/assets/css/docked.css @@ -0,0 +1,30 @@ +/* path: floatina/assets/css/docked.css */ +/* === Vertical resize handle for Docked mode (visible on desktop & mobile) === */ +#floatina-panel .fi-resize-v{ + position:absolute; + left:8px; right:8px; top:-10px; + height:16px; + cursor:ns-resize; + border-radius:12px; + background:transparent; /* keep invisible; theme can draw grip if desired */ + z-index:2; +} +#floatina-panel.is-open .fi-resize-v{pointer-events:auto} + +/* === Shim overlay captures pointer events above iframes while resizing === */ +#floatina-panel .fi-shim{ + position:absolute; + inset:0; + pointer-events:none; + background:transparent; + z-index:3; +} +#floatina-panel .fi-shim.is-active{ pointer-events:auto; } + +/* === Optional: cursor feedback while resizing === */ +#floatina-panel.is-resizing{ user-select:none; } + +/* === Bigger handle on small screens for better touch target === */ +@media (max-width:640px){ + #floatina-panel .fi-resize-v{ top:-12px; height:20px; } +} diff --git a/floatina/assets/css/floatina.css b/floatina/assets/css/floatina.css index cf2a543..66753c6 100644 --- a/floatina/assets/css/floatina.css +++ b/floatina/assets/css/floatina.css @@ -1,254 +1,177 @@ /************************************************************************************ - * Floatina – core layout + * Floatina – Apple glass style (front-end) – HEX variables wired to settings ************************************************************************************/ -/* Inherit typography from theme */ -#floatina-panel, -#floatina-panel * { - font-family: inherit; +:root{ + --floatina-offset:16px; + --floatina-width:min(92vw,520px); + --floatina-height:72vh; + --floatina-radius-outer:24px; + --floatina-radius-inner:16px; + --floatina-padding:10px; + --floatina-fab:#1e88ff; + --floatina-accent:#1e66ff; + --floatina-border:#e6e8eb; + --floatina-nav-active:#eef5ff; + --floatina-grad-start:#ffffff; + --floatina-grad-end:#f5f7ff; + + /* === Runtime vars (updated by JS) === */ + --fi-nav-h: 0px; /* bottom nav height for embed iframes */ + --fi-panel-h: var(--floatina-height); /* current panel height */ + --fi-fab-h: 60px; /* FAB size so we نقدر نحسب مكانه فوق اللوحة */ } -/* CSS vars for theming via settings */ -:root { - --floatina-offset: 16px; - --floatina-width: min(92vw, 460px); - --floatina-height: 70vh; - --floatina-radius: 16px; - --floatina-shadow: 0 20px 50px rgba(0, 0, 0, 0.25); - --floatina-border: #e6e8eb; - --floatina-bg: #ffffff; - --floatina-soft: #f7f8fa; - --floatina-fab: #1e88ff; - --floatina-accent: #1e66ff; - --floatina-grad-start: #bfe0ff; - --floatina-grad-end: #eaf4ff; - --floatina-nav-active: #eef5ff; +/* === FAB === */ +#floatina-fab{ + position:fixed; + bottom:var(--floatina-offset); + width:var(--fi-fab-h); + height:var(--fi-fab-h); + border-radius:20px; + display:flex;align-items:center;justify-content:center; + background:var(--floatina-fab);color:#fff; + box-shadow:0 12px 28px rgba(0,0,0,.22), inset 0 1px 0 rgba(255,255,255,.4); + cursor:pointer;z-index:10000; + /* === no transition on top/right/left/bottom to follow instantly === */ + transition:transform .2s ease, box-shadow .2s ease; +} +#floatina-fab:hover{transform:translateY(-3px);box-shadow:0 16px 34px rgba(0,0,0,.26), inset 0 1px 0 rgba(255,255,255,.5)} +#floatina-fab img{width:26px;height:26px;display:block} +#floatina-fab.is-right{right:var(--floatina-offset)} +#floatina-fab.is-left{left:var(--floatina-offset)} + +/* === Panel (glass) === */ +#floatina-panel{ + position:fixed; + bottom:calc(var(--floatina-offset) + 72px); + width:var(--floatina-width); + max-height:var(--floatina-height); + border-radius:var(--floatina-radius-outer); + background:rgba(255,255,255,.65); + backdrop-filter:saturate(160%) blur(14px); + border:1px solid var(--floatina-border); + box-shadow:0 24px 60px rgba(0,0,0,.28); + overflow:hidden; + opacity:0; pointer-events:none; + transform:translateY(18px); + transition:opacity .25s ease, transform .25s ease; + z-index:9999; box-sizing:border-box; +} +#floatina-panel.is-open{opacity:1;transform:translateY(0);pointer-events:auto} +#floatina-panel.is-right{right:var(--floatina-offset)} +#floatina-panel.is-left{left:var(--floatina-offset)} + +/* === Hide any legacy header/close button === */ +.floatina-header,.floatina-close{display:none !important} + +/* === Body / Content === */ +.floatina-body{display:flex;flex-direction:column;height:calc(var(--floatina-height));background:transparent} +.floatina-content{flex:1;overflow:auto;position:relative;padding:var(--floatina-padding);min-height:0} +.floatina-content > *:first-child{margin-top:0} + +/* === Bottom nav === */ +.floatina-nav{display:flex;gap:8px;justify-content:space-between;border-top:1px solid var(--floatina-border);background:#ffffffcc;backdrop-filter:saturate(160%) blur(10px);padding:10px} +.floatina-tab{flex:1;display:flex;flex-direction:column;align-items:center;gap:6px;padding:10px 8px;border-radius:14px;font-size:12px;color:#0f172a;cursor:pointer;user-select:none;background:#fff;border:1px solid var(--floatina-border);transition:transform .15s ease, box-shadow .2s ease, background .2s ease} +.floatina-tab:hover{transform:translateY(-2px);box-shadow:0 8px 20px rgba(0,0,0,.12)} +.floatina-tab.is-active{background:var(--floatina-nav-active);color:var(--floatina-accent);border-color:var(--floatina-accent)} +.floatina-tab .fi-ico img{width:24px;height:24px;display:block} + +/* === Cards === */ +.fi-card{background:#fff;border:1px solid var(--floatina-border);border-radius:var(--floatina-radius-inner);padding:12px;margin:10px 0;box-shadow:0 10px 24px rgba(0,0,0,.08)} +.fi-card h4{margin:0 0 6px;font-size:14px} +.fi-muted{color:#475569;font-size:12px} + +/* === Buttons === */ +.fi-actions{display:flex;gap:8px;flex-wrap:wrap} +.fi-btn{display:inline-flex;align-items:center;justify-content:center;height:38px;padding:0 14px;border-radius:12px;background:#fff;border:1px solid var(--floatina-border);text-decoration:none;color:#0f172a;transition:transform .1s ease, box-shadow .2s ease} +.fi-btn:hover{transform:translateY(-1px);box-shadow:0 8px 20px rgba(0,0,0,.1)} + +/* === Iframe area (for non-embed panes) === */ +.floatina-iframe{position:relative;height:60vh;background:#fff;border:1px solid var(--floatina-border);border-radius:var(--floatina-radius-inner);overflow:hidden;box-shadow:0 12px 24px rgba(0,0,0,.08);z-index:0} +.floatina-iframe iframe{width:100%;height:100%;border:0;display:block;pointer-events:auto;position:relative;z-index:1} + +/* === EMBED MODE (Voice/Text) === */ +.floatina-content.fi-embed{padding:0;overflow:hidden} +.floatina-content.fi-embed .fi-pane{height:100%} +.floatina-content.fi-embed .floatina-iframe{ + height:calc(100% - var(--fi-nav-h) - env(safe-area-inset-bottom, 0px)); /* keeps nav visible */ + border-radius:0;border:0;box-shadow:none; } -/* FAB */ -#floatina-fab { - position: fixed; - bottom: var(--floatina-offset); - width: 56px; - height: 56px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - background: var(--floatina-fab, #1e88ff); - color: #fff; - box-shadow: 0 8px 22px rgba(0, 0, 0, 0.2); - cursor: pointer; - z-index: 9999; - transition: transform 0.2s ease; -} -#floatina-fab:hover { - transform: translateY(-2px); -} -#floatina-fab svg { - width: 26px; - height: 26px; - display: block; -} +/* === Loader === */ +.fi-loader{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;z-index:2;background:linear-gradient(180deg, rgba(255,255,255,.65), rgba(255,255,255,.35));backdrop-filter:blur(6px) saturate(140%)} +.fi-loader-inner{display:flex;flex-direction:column;align-items:center;gap:10px;padding:14px 18px;border-radius:16px;background:rgba(255,255,255,.75);border:1px solid var(--floatina-border);box-shadow:0 12px 28px rgba(0,0,0,.12)} +.fi-spinner{width:40px;height:40px;border-radius:50%;border:3px solid rgba(0,0,0,.12);border-top-color:var(--floatina-accent);animation:fi-spin 1s linear infinite} +.fi-loader-txt{font-size:12px;color:#334155} +@keyframes fi-spin{to{transform:rotate(360deg)}} -/* Left/Right placement */ -#floatina-fab.is-right { - right: var(--floatina-offset); -} -#floatina-fab.is-left { - left: var(--floatina-offset); -} -#floatina-panel.is-right { - right: var(--floatina-offset); -} -#floatina-panel.is-left { - left: var(--floatina-offset); -} +/* === Accordion === */ +.fi-accordion{display:block} +.fi-acc-item{background:#fff;border:1px solid var(--floatina-border);border-radius:var(--floatina-radius-inner);margin:8px 0;overflow:hidden} +.fi-acc-btn{width:100%;display:flex;align-items:center;justify-content:space-between;gap:12px;background:transparent;border:0;padding:14px 12px;font-weight:600;cursor:pointer} +.fi-acc-btn:hover{background:var(--floatina-nav-active)} +.fi-acc-ico::before{content:"+";display:inline-block;transition:transform .2s ease} +.fi-acc-btn[aria-expanded="true"] .fi-acc-ico::before{content:"–"} +.fi-acc-panel{max-height:0;overflow:hidden;transition:max-height .25s ease} +.fi-acc-inner{padding:0 12px 12px;color:#334155} -/* Panel shell */ -#floatina-panel { - position: fixed; - bottom: calc(var(--floatina-offset) + 66px); - width: var(--floatina-width); - max-height: var(--floatina-height); - border-radius: var(--floatina-radius); - background: var(--floatina-bg); - box-shadow: var(--floatina-shadow); - overflow: hidden; - opacity: 0; - pointer-events: none; - transform: translateY(16px); - transition: opacity 0.25s ease, transform 0.25s ease; - z-index: 9999; -} -#floatina-panel.is-open { - opacity: 1; - transform: translateY(0); - pointer-events: auto; -} +/* === News === */ +.fi-news-card{display:flex;gap:12px;align-items:stretch;text-decoration:none;background:#fff;border:1px solid var(--floatina-border);border-radius:var(--floatina-radius-inner);overflow:hidden;margin:10px 0;box-shadow:0 10px 24px rgba(0,0,0,.08);transition:transform .15s ease, box-shadow .2s ease} +.fi-news-card:hover{transform:translateY(-2px);box-shadow:0 16px 36px rgba(0,0,0,.14)} +.fi-news-media{min-width:96px;width:96px;background:#eaeef3;background-size:cover;background-position:center} +.fi-news-body{padding:12px 12px 12px 4px;display:flex;flex-direction:column;gap:6px} +.fi-news-title{font-size:14px;margin:0;color:#0f172a} +.fi-news-excerpt{font-size:12px;color:#475569;margin:0} +.fi-news-cta{margin-top:auto;font-size:12px;color:var(--floatina-accent)} -/* Header */ -.floatina-header { - position: relative; - background: linear-gradient( - 180deg, - var(--floatina-grad-start), - var(--floatina-grad-end) - ); - padding: 18px 16px 12px; -} -.floatina-title { - margin: 0; - font-size: 18px; - font-weight: 700; -} -.floatina-close { - position: absolute; - right: 12px; - top: 12px; - width: 28px; - height: 28px; - border-radius: 6px; - background: rgba(0, 0, 0, 0.05); - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; -} +/* === Tab panes === */ +.fi-pane{display:none;height:100%} +.fi-pane.is-active{display:block} -/* Body */ -.floatina-body { - display: flex; - flex-direction: column; - height: calc(var(--floatina-height) - 118px); - background: var(--floatina-soft); -} -.floatina-content { - flex: 1; - overflow: auto; - padding: 12px; -} +/* === Home hero === */ +.fi-hero{position:relative;border-radius:var(--floatina-radius-inner);padding:18px 16px 20px;background:linear-gradient(180deg, rgba(180,220,255,.95), rgba(236,246,255,.95));border:1px solid var(--floatina-border);box-shadow:0 16px 36px rgba(0,0,0,.10);margin:0 0 14px 0} +.fi-hero-title h1{margin:0 0 6px;font-size:28px;line-height:1.1;color:#0f172a;font-weight:800} +.fi-hero-title .fi-hero-sub{font-size:18px;color:#0f172a;opacity:.9} -/* Bottom nav */ -.floatina-nav { - display: flex; - gap: 6px; - justify-content: space-between; - border-top: 1px solid var(--floatina-border); - background: #fff; - padding: 10px; -} -.floatina-tab { - flex: 1; - display: flex; - flex-direction: column; - align-items: center; - gap: 4px; - padding: 8px 6px; - border-radius: 12px; - font-size: 12px; - color: #333; - cursor: pointer; - user-select: none; - background: #fff; - border: 1px solid var(--floatina-border); -} -.floatina-tab.is-active { - background: var(--floatina-nav-active); - color: var(--floatina-accent); - border-color: #b9d0ff; -} -.floatina-tab svg { - width: 20px; - height: 20px; - display: block; -} -.floatina-tab .fi-ico { - line-height: 0; -} +/* === Home list cards === */ +.fi-list{display:flex;flex-direction:column;gap:12px} +.fi-list-card{display:flex;align-items:center;justify-content:space-between;gap:14px;text-decoration:none;background:#fff;border:1px solid var(--floatina-border);border-radius:var(--floatina-radius-inner);padding:14px 16px;box-shadow:0 10px 24px rgba(0,0,0,.08);transition:transform .15s ease, box-shadow .2s ease, background .2s ease} +.fi-list-card:hover{transform:translateY(-1px);box-shadow:0 16px 36px rgba(0,0,0,.14);background:#fcfdff} +.fi-list-body{min-width:0} +.fi-list-title{font-size:16px;font-weight:700;color:#0f172a;margin-bottom:4px} +.fi-list-desc{font-size:14px;color:#475569;opacity:.95;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:60ch} +.fi-list-ico{width:22px;height:22px;display:grid;place-items:center;color:#0f172a;opacity:.85} -/* Cards + gradient */ -.fi-card { - background: #fff; - border: 1px solid var(--floatina-border); - border-radius: 12px; - padding: 12px; - margin: 8px 0; -} -.fi-card h4 { - margin: 0 0 6px; - font-size: 14px; -} -.fi-muted { - color: #6b7280; - font-size: 12px; -} -.fi-gradient { - background: linear-gradient(180deg, #dbeafe, #eef2ff); - border: none; -} +/* === Mobile (fix width + X above panel and follows resize) === */ +@media (max-width:640px){ + :root{--floatina-width:100vw;--floatina-height:85vh} -/* Buttons inside Home */ -.fi-actions { - display: flex; - gap: 8px; - flex-wrap: wrap; -} -.fi-btn { - display: inline-flex; - align-items: center; - justify-content: center; - height: 36px; - padding: 0 14px; - border-radius: 10px; - background: #fff; - border: 1px solid var(--floatina-border); - text-decoration: none; - color: #111; -} - -/* Iframe area */ -.floatina-iframe { - position: relative; - height: 60vh; - background: #fff; - border: 1px solid var(--floatina-border); - border-radius: 12px; - overflow: hidden; -} -.floatina-iframe iframe { - width: 100%; - height: 100%; - border: 0; - display: block; -} - -/* Mobile fullscreen behavior */ -@media (max-width: 640px) { - :root { - --floatina-width: 100vw; - --floatina-height: 100vh; + /* === Fill width symmetrically using safe-areas; ignore .is-left/.is-right offsets === */ + #floatina-panel, + #floatina-panel.is-left, + #floatina-panel.is-right{ + bottom:0; + left:env(safe-area-inset-left, 0px); + right:env(safe-area-inset-right, 0px); + width:auto; + max-height:100dvh; + border-radius:16px 16px 0 0; + transform:none; } - #floatina-panel { - bottom: 0; - right: 0; - left: 0; - width: 100vw; - max-height: 100vh; - border-radius: 0; - transform: translateY(12px); - } - #floatina-panel.is-open { - transform: translateY(0); - } - .floatina-body { - height: calc(100vh - 66px); - } - #floatina-fab { - bottom: 12px; - } -} -/* Color override from settings via inline vars */ -#floatina-fab[data-color] { - background: var(--floatina-fab-color, #1e88ff); + .floatina-body{height:calc(var(--floatina-height))} + + /* === FAB default (bottom-right) === */ + #floatina-fab{left:auto;right:calc(12px + env(safe-area-inset-right, 0px));bottom:calc(12px + env(safe-area-inset-bottom, 0px))} + + /* === FAB on open: sit 5px ABOVE panel's top and FOLLOW it (no transition) === */ + #floatina-fab.is-top{ + bottom:auto; + /* panelTop = 100dvh - panelH ; place FAB so bottom is 5px above panelTop => top = panelTop - FAB_H - 5px */ + top:calc(100dvh - var(--fi-panel-h, var(--floatina-height)) - var(--fi-fab-h) - 5px); + right:calc(12px + env(safe-area-inset-right, 0px)); + } + + .floatina-nav{padding:10px 10px calc(10px + env(safe-area-inset-bottom, 0px))} + .floatina-content{padding:10px} } diff --git a/floatina/assets/icons/bi-chat-dots.svg b/floatina/assets/icons/bi-chat-dots.svg new file mode 100644 index 0000000..7325f0d --- /dev/null +++ b/floatina/assets/icons/bi-chat-dots.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/floatina/assets/icons/bi-chat.svg b/floatina/assets/icons/bi-chat.svg new file mode 100644 index 0000000..845aa32 --- /dev/null +++ b/floatina/assets/icons/bi-chat.svg @@ -0,0 +1,3 @@ + + + diff --git a/floatina/assets/icons/bi-house.svg b/floatina/assets/icons/bi-house.svg new file mode 100644 index 0000000..215c854 --- /dev/null +++ b/floatina/assets/icons/bi-house.svg @@ -0,0 +1,3 @@ + + + diff --git a/floatina/assets/icons/bi-mic.svg b/floatina/assets/icons/bi-mic.svg new file mode 100644 index 0000000..c4cc720 --- /dev/null +++ b/floatina/assets/icons/bi-mic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/floatina/assets/icons/bi-newspaper.svg b/floatina/assets/icons/bi-newspaper.svg new file mode 100644 index 0000000..34e887b --- /dev/null +++ b/floatina/assets/icons/bi-newspaper.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/floatina/assets/icons/bi-question-circle.svg b/floatina/assets/icons/bi-question-circle.svg new file mode 100644 index 0000000..7ccc8fb --- /dev/null +++ b/floatina/assets/icons/bi-question-circle.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/floatina/assets/icons/bi-x.svg b/floatina/assets/icons/bi-x.svg new file mode 100644 index 0000000..e33ca52 --- /dev/null +++ b/floatina/assets/icons/bi-x.svg @@ -0,0 +1,3 @@ + + + diff --git a/floatina/assets/js/accordion.js b/floatina/assets/js/accordion.js new file mode 100644 index 0000000..e0969ae --- /dev/null +++ b/floatina/assets/js/accordion.js @@ -0,0 +1,35 @@ +/************************************************************************************ + * Floatina – Accordion controller + ************************************************************************************/ +(function (root) { + 'use strict'; + + function init(container) { + var scope = container || document; + var items = scope.querySelectorAll('.fi-acc-item'); + if (!items.length) return; + + items.forEach(function (item) { + var btn = item.querySelector('.fi-acc-btn'); + var panel = item.querySelector('.fi-acc-panel'); + if (!btn || !panel || btn.__fiBound) return; + + btn.__fiBound = true; + + btn.addEventListener('click', function () { + var expanded = btn.getAttribute('aria-expanded') === 'true'; + var next = !expanded; + + btn.setAttribute('aria-expanded', String(next)); + panel.style.maxHeight = next ? (panel.scrollHeight + 'px') : '0px'; + panel.setAttribute('aria-hidden', String(!next)); + }); + + btn.addEventListener('keydown', function (e) { + if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); btn.click(); } + }); + }); + } + + root.FloatinaAccordion = { init: init }; +})(window); diff --git a/floatina/assets/js/admin.js b/floatina/assets/js/admin.js new file mode 100644 index 0000000..2030ba7 --- /dev/null +++ b/floatina/assets/js/admin.js @@ -0,0 +1,16 @@ +/************************************************************************************ + * Floatina – Admin UI helpers (color pickers, tabs feel) + ************************************************************************************/ +(function($){ + $(function(){ + // Color pickers + $('.fi-color').wpColorPicker(); + + // Visual active state on tabs + const tabs = $('.fi-admin .fi-admin-tabs .fi-tab'); + tabs.on('click', function(){ + tabs.removeClass('is-active'); + $(this).addClass('is-active'); + }); + }); +})(jQuery); diff --git a/floatina/assets/js/core.js b/floatina/assets/js/core.js new file mode 100644 index 0000000..eff6761 --- /dev/null +++ b/floatina/assets/js/core.js @@ -0,0 +1,198 @@ +// path: floatina/assets/js/core.js +/************************************************************************************ + * Floatina – Core controller (FAB, tabs, iframe loader) + ************************************************************************************/ +(function () { + 'use strict'; + + var CFG = window.FLOATINA || {}; + var S = { open: false }; + var $ = function (sel, root) { return (root || document).querySelector(sel); }; + var $$ = function (sel, root) { return Array.from((root || document).querySelectorAll(sel)); }; + + // === Mobile detector === + function isMobile() { + try { + return (window.matchMedia && (window.matchMedia('(max-width: 640px)').matches || window.matchMedia('(pointer:coarse)').matches)); + } catch(e){ return false; } + } + + // === Compute bottom nav height and expose as CSS var for embed iframes === + function updateEmbedSafeArea() { + var c = $('#fi-content'); + if (!c) return; + var nav = $('.floatina-nav'); + var h = (c.classList.contains('fi-embed') && nav) ? nav.offsetHeight : 0; + c.style.setProperty('--fi-nav-h', (h || 0) + 'px'); + } + + function setEmbed(on) { + var c = $('#fi-content'); + if (c) { + c.classList.toggle('fi-embed', !!on); + updateEmbedSafeArea(); + } + } + + function showPane(id) { + $$('.fi-pane').forEach(function (p) { p.classList.remove('is-active'); }); + var pane = $('#' + id); + if (!pane) return; + pane.classList.add('is-active'); + + $$('.floatina-tab').forEach(function (t) { t.classList.remove('is-active'); }); + var btn = document.querySelector('.floatina-tab[data-target="' + id + '"]'); + if (btn) btn.classList.add('is-active'); + + if (pane.dataset && pane.dataset.kind) { + ensureIframe(pane); + } else { + setEmbed(false); + if (id === 'fi-pane-help' && window.FloatinaAccordion) { + window.FloatinaAccordion.init(pane); + } + } + + updateEmbedSafeArea(); + } + + function ensureIframe(pane) { + var url = pane.getAttribute('data-url') || ''; + var mode = pane.getAttribute('data-mode') || 'iframe'; + if (!url) { setEmbed(false); return; } + + if (mode === 'popup') { + setEmbed(false); + window.open(url, '_blank', 'noopener'); + return; + } + + setEmbed(true); + if (pane.__floatinaLoaded) { updateEmbedSafeArea(); return; } + + var container = pane.querySelector('[data-embed="container"]'); + if (!container) return; + + var loader = document.createElement('div'); + loader.className = 'fi-loader'; + loader.innerHTML = '
Loading…
'; + + var iframe = document.createElement('iframe'); + iframe.title = pane.dataset.kind || 'embed'; + iframe.loading = 'eager'; + iframe.allow = 'microphone; camera; autoplay; clipboard-read; clipboard-write; geolocation; display-capture; web-share; fullscreen'; + iframe.style.display = 'none'; + iframe.style.pointerEvents = 'auto'; + + container.appendChild(iframe); + container.appendChild(loader); + setTimeout(function(){ iframe.src = url; }, 0); + + var done = false; + var timer = null; + + function cleanup() { + done = true; + if (loader.parentNode) loader.parentNode.removeChild(loader); + if (timer) clearTimeout(timer); + } + + iframe.addEventListener('load', function () { + if (done) return; + cleanup(); + iframe.style.display = 'block'; + pane.__floatinaLoaded = true; + updateEmbedSafeArea(); + }); + + timer = setTimeout(function () { + if (done) return; + cleanup(); + setEmbed(false); + var card = document.createElement('div'); + card.className = 'fi-card'; + card.innerHTML = '

Cannot embed

The site refused to connect or took too long.
Open in new tab
'; + var a = card.querySelector('a'); a.href = url; + pane.innerHTML = ''; + pane.appendChild(card); + updateEmbedSafeArea(); + }, 10000); + } + + function bind() { + var fab = $('#floatina-fab'); + var panel = $('#floatina-panel'); + + if (fab) { + fab.addEventListener('click', function () { + S.open = !S.open; + panel.classList.toggle('is-open', S.open); + swapFabIcon(fab, S.open ? 'bi-x.svg' : 'bi-chat-dots.svg'); + fab.setAttribute('aria-expanded', S.open ? 'true' : 'false'); + + // === On mobile, float close button to the top when open === + if (isMobile()) { + fab.classList.toggle('is-top', S.open); + } + + if (S.open) updateEmbedSafeArea(); + }); + } + + $$('.floatina-tab').forEach(function (btn) { + btn.addEventListener('click', function () { + var target = btn.getAttribute('data-target'); + if (target) showPane(target); + }); + }); + + document.addEventListener('keydown', function (e) { + if (S.open && e.key === 'Escape') { + S.open = false; + panel.classList.remove('is-open'); + swapFabIcon(fab, 'bi-chat-dots.svg'); + if (fab) { + fab.setAttribute('aria-expanded', 'false'); + fab.classList.remove('is-top'); + } + } + }); + + window.addEventListener('resize', function () { + if (fab && isMobile() && S.open) fab.classList.add('is-top'); + updateEmbedSafeArea(); + }); + } + + function swapFabIcon(fab, file) { + if (!fab) return; + var img = fab.querySelector('img'); + if (!img) { + img = document.createElement('img'); + img.width = 24; img.height = 24; img.alt = ''; + fab.innerHTML = ''; fab.appendChild(img); + } + var map = (window.FLOATINA && window.FLOATINA.iconsMap) ? window.FLOATINA.iconsMap : {}; + img.src = map[file] || ''; + } + + document.addEventListener('DOMContentLoaded', function () { + bind(); + + var helpPane = $('#fi-pane-help'); + if (helpPane && helpPane.classList.contains('is-active') && window.FloatinaAccordion) { + window.FloatinaAccordion.init(helpPane); + } + + var active = document.querySelector('.floatina-tab.is-active'); + if (!active) { + var first = document.querySelector('.floatina-tab'); + if (first) { + var target = first.getAttribute('data-target'); + if (target) showPane(target); + } + } + + updateEmbedSafeArea(); + }); +})(); diff --git a/floatina/assets/js/docked.js b/floatina/assets/js/docked.js new file mode 100644 index 0000000..d54327a --- /dev/null +++ b/floatina/assets/js/docked.js @@ -0,0 +1,164 @@ +// path: floatina/assets/js/docked.js +/************************************************************************************ + * FloatinaDocked – vertical resize (desktop + mobile) + * - Drag handle at top edge + * - Shim over iframes while dragging + * - Persists height (localStorage -> cookie fallback) + * - Exposes --fi-panel-h CSS var globally so FAB can follow the panel + ************************************************************************************/ +(function () { + 'use strict'; + + /* === Storage helpers === */ + var KEY = 'floatina_docked_h_v1'; + var store = { + get: function () { + try { var v = localStorage.getItem(KEY); if (v) return parseInt(v, 10) || null; } catch(e){} + var m = document.cookie.match(new RegExp('(?:^|; )' + KEY.replace(/([.$?*|{}()[\]\\/+^])/g,'\\$1') + '=([^;]*)')); + return m ? parseInt(decodeURIComponent(m[1]), 10) || null : null; + }, + set: function (px) { + try { localStorage.setItem(KEY, String(px)); } catch(e){} + try { document.cookie = KEY + '=' + encodeURIComponent(String(px)) + '; path=/; SameSite=Lax; max-age='+(60*60*24*365); } catch(e){} + }, + clear: function () { + try { localStorage.removeItem(KEY); } catch(e){} + try { document.cookie = KEY + '=; path=/; max-age=0'; } catch(e){} + } + }; + + /* === Utils === */ + var clamp = function (v, min, max) { return Math.max(min, Math.min(max, v)); }; + var $ = function (sel, root) { return (root || document).querySelector(sel); }; + + /* === Media info (cap max height on small screens) === */ + var isSmall = false; + try { isSmall = window.matchMedia && window.matchMedia('(max-width: 640px)').matches; } catch(e){} + + /* === Public API === */ + var API = { + panelSel: '#floatina-panel', + handleCls: 'fi-resize-v', + shimCls: 'fi-shim', + minPx: 320, + maxVh: isSmall ? 0.98 : 0.92, + current: null, + + /* === Apply height (write to panel + global CSS var for FAB) === */ + apply: function () { + var panel = $(API.panelSel); + if (!panel) return; + var vhMax = Math.round(window.innerHeight * API.maxVh); + var h = clamp(API.current || panel.getBoundingClientRect().height, API.minPx, vhMax); + + panel.style.height = h + 'px'; + panel.style.setProperty('--floatina-height', h + 'px'); + + // === Make it global so CSS can position the FAB using calc(100dvh - var(--fi-panel-h)) + try { document.documentElement.style.setProperty('--fi-panel-h', h + 'px'); } catch(e){} + + document.dispatchEvent(new CustomEvent('floatina:docked:apply', { detail: { height: h } })); + }, + + setHeight: function (px, persist) { + API.current = px; + API.apply(); + if (persist) store.set(API.current); + }, + + reset: function () { + var panel = $(API.panelSel); + if (!panel) return; + store.clear(); + API.current = null; + panel.style.height = ''; + panel.style.removeProperty('--floatina-height'); + try { document.documentElement.style.removeProperty('--fi-panel-h'); } catch(e){} + document.dispatchEvent(new CustomEvent('floatina:docked:reset')); + }, + + init: function () { + var panel = $(API.panelSel); + if (!panel || panel.__fiDockedInit) return; + + /* === Inject handle and shim === */ + var handle = document.createElement('div'); + handle.className = API.handleCls; + handle.setAttribute('role', 'separator'); + handle.setAttribute('aria-label', 'Resize height'); + panel.appendChild(handle); + + var shim = document.createElement('div'); + shim.className = API.shimCls; + shim.setAttribute('aria-hidden', 'true'); + panel.appendChild(shim); + + /* === Restore persisted height === */ + var persisted = store.get(); + if (persisted && !isNaN(persisted)) { + API.current = persisted; + API.apply(); + } else { + // === Expose initial height (from CSS) so FAB knows where to sit + try { + var initH = panel.getBoundingClientRect().height; + document.documentElement.style.setProperty('--fi-panel-h', Math.round(initH) + 'px'); + } catch(e){} + } + + /* === Drag logic === */ + var dragging = false, startY = 0, startH = 0; + + handle.addEventListener('pointerdown', function (e) { + if (!panel.classList.contains('is-open')) return; + dragging = true; + startY = e.clientY; + startH = panel.getBoundingClientRect().height; + panel.classList.add('is-resizing'); + shim.classList.add('is-active'); + Array.from(panel.querySelectorAll('iframe')).forEach(function (f) { f.style.pointerEvents = 'none'; }); + handle.setPointerCapture(e.pointerId); + document.dispatchEvent(new CustomEvent('floatina:docked:resize:start', { detail: { start: startH } })); + e.preventDefault(); + }); + + handle.addEventListener('pointermove', function (e) { + if (!dragging) return; + var vhMax = Math.round(window.innerHeight * API.maxVh); + var delta = (startY - e.clientY); + API.current = clamp(startH + delta, API.minPx, vhMax); + API.apply(); + document.dispatchEvent(new CustomEvent('floatina:docked:resize', { detail: { height: API.current } })); + }); + + function finish() { + if (!dragging) return; + dragging = false; + panel.classList.remove('is-resizing'); + shim.classList.remove('is-active'); + Array.from(panel.querySelectorAll('iframe')).forEach(function (f) { f.style.pointerEvents = 'auto'; }); + store.set(API.current); + document.dispatchEvent(new CustomEvent('floatina:docked:resize:end', { detail: { height: API.current } })); + } + + handle.addEventListener('pointerup', finish); + handle.addEventListener('lostpointercapture', finish); + + /* === Keep within caps on viewport change === */ + window.addEventListener('resize', function () { + if (!panel.classList.contains('is-open')) return; + if (API.current == null) return; + var vhMax = Math.round(window.innerHeight * API.maxVh); + API.current = clamp(API.current, API.minPx, vhMax); + API.apply(); + }); + + panel.__fiDockedInit = true; + window.FloatinaDocked = API; + } + }; + + document.addEventListener('DOMContentLoaded', function () { + API.init(); + }); +})(); diff --git a/floatina/assets/js/floatina.js b/floatina/assets/js/floatina.js deleted file mode 100644 index 9190541..0000000 --- a/floatina/assets/js/floatina.js +++ /dev/null @@ -1,294 +0,0 @@ -/** - * Floatina – structured controller (no inline HTML/CSS) - * Modules: Config, State, Utils, Icons, View, Actions, Renderers. - */ -(function(){ - /* ========================== Config ========================== */ - const CFG = Object.freeze({ - brandName: (window.FLOATINA && window.FLOATINA.brand_name) || 'Assistant', - buttonColor: (window.FLOATINA && window.FLOATINA.button_color) || '#1e88ff', - position: (window.FLOATINA && window.FLOATINA.position) || 'right', // right|left - voiceURL: (window.FLOATINA && window.FLOATINA.voice_iframe) || '', - textURL: (window.FLOATINA && window.FLOATINA.text_iframe) || '', - voiceMode: (window.FLOATINA && window.FLOATINA.voice_mode) || 'iframe', // iframe|popup - textMode: (window.FLOATINA && window.FLOATINA.text_mode) || 'iframe', - ajaxNews: (window.FLOATINA && window.FLOATINA.ajax && window.FLOATINA.ajax.news) || '', - ajaxFAQ: (window.FLOATINA && window.FLOATINA.ajax && window.FLOATINA.ajax.faq) || '', - i18n: (window.FLOATINA && window.FLOATINA.strings) || {home:'Home',voice:'Voice',text:'Text',news:'News',help:'Help',open_new:'Open in new tab'} - }); - - /* ========================== State ========================== */ - const S = { - open: false, - tab: 'home' - }; - - /* ========================== Utils ========================== */ - const NS = 'http://www.w3.org/2000/svg'; - - /** Create DOM element */ - function el(tag, attrs = {}, children = []) { - const node = document.createElement(tag); - for (const k in attrs) { - if (k === 'class') node.className = attrs[k]; - else if (k === 'dataset') Object.entries(attrs[k]).forEach(([dk, dv]) => node.dataset[dk] = dv); - else if (k === 'aria') Object.entries(attrs[k]).forEach(([ak, av]) => node.setAttribute(`aria-${ak}`, av)); - else if (k === 'role') node.setAttribute('role', attrs[k]); - else if (k === 'html') node.innerHTML = attrs[k]; // avoid unless needed - else node.setAttribute(k, attrs[k]); - } - (Array.isArray(children) ? children : [children]).forEach(ch => { - if (ch == null) return; - if (typeof ch === 'string') node.appendChild(document.createTextNode(ch)); - else node.appendChild(ch); - }); - return node; - } - - /** Create SVG element */ - function svg(tag, attrs = {}, children = []) { - const node = document.createElementNS(NS, tag); - for (const k in attrs) node.setAttribute(k, attrs[k]); - (Array.isArray(children) ? children : [children]).forEach(ch => { if (ch) node.appendChild(ch); }); - return node; - } - - /** Clear element children */ - function clear(node){ while (node.firstChild) node.removeChild(node.firstChild); } - - /** Text */ - function t(key){ return CFG.i18n[key] || key; } - - /* ========================== Icons (pure SVG DOM) ========================== */ - function icoBubble(){ - return svg('svg', {viewBox:'0 0 16 16', 'aria-hidden':'true'}, [ - svg('path', {d:'M8 1a7 7 0 00-5.6 11.2L1.5 15l2.9-.9A7 7 0 108 1z', fill:'currentColor'}) - ]); - } - function icoX(){ - return svg('svg', {viewBox:'0 0 16 16', 'aria-hidden':'true'}, [ - svg('path', {d:'M3.5 3.5l9 9m0-9l-9 9', stroke:'currentColor','stroke-width':'1.75','stroke-linecap':'round', fill:'none'}) - ]); - } - function icoHome(){ - return svg('svg', {viewBox:'0 0 16 16','aria-hidden':'true'}, [ - svg('path', {d:'M8 3l6 5v6h-4V9H6v5H2V8l6-5z', fill:'currentColor'}) - ]); - } - function icoMic(){ - return svg('svg', {viewBox:'0 0 16 16','aria-hidden':'true'}, [ - svg('path', {d:'M8 11a3 3 0 003-3V5a3 3 0 10-6 0v3a3 3 0 003 3z', fill:'currentColor'}), - svg('path', {d:'M3 8a5 5 0 0010 0H11a3 3 0 11-6 0H3z', fill:'currentColor'}), - svg('path', {d:'M7 13h2v2H7z', fill:'currentColor'}) - ]); - } - function icoChat(){ - return svg('svg', {viewBox:'0 0 16 16','aria-hidden':'true'}, [ - svg('path', {d:'M2 2h12v9H6l-3 3V2z', fill:'currentColor'}) - ]); - } - function icoNews(){ - return svg('svg', {viewBox:'0 0 16 16','aria-hidden':'true'}, [ - svg('path', {d:'M1 6l12-4v8L1 6zm12 2h2V4h-2v4zM5 9l1 4h2L7 9z', fill:'currentColor'}) - ]); - } - function icoHelp(){ - return svg('svg', {viewBox:'0 0 16 16','aria-hidden':'true'}, [ - svg('path', {d:'M8 1a7 7 0 107 7A7 7 0 008 1zm1 11H7v-2h2zm1.6-5.2a2.4 2.4 0 00-4.6 1h2a.8.8 0 111.5-.3c0 .3-.2.5-.6.7A2.5 2.5 0 007 10h2a3.5 3.5 0 001.6-3.2z', fill:'currentColor'}) - ]); - } - - /* ========================== View (refs) ========================== */ - const V = { fab:null, panel:null, content:null, close:null, tabs:{} }; - - /* ========================== Build UI ========================== */ - function buildFab(){ - const b = el('button', {id:'floatina-fab', 'aria':{label:'Open Floatina'}}); - b.appendChild(icoBubble()); - if (CFG.position === 'left') b.classList.add('is-left'); else b.classList.add('is-right'); - if (CFG.buttonColor){ - b.dataset.color = '1'; - b.style.setProperty('--floatina-fab-color', CFG.buttonColor); - } - document.body.appendChild(b); - V.fab = b; - } - - function buildPanel(){ - const wrap = el('div', {id:'floatina-panel'}); - if (CFG.position === 'left') wrap.classList.add('is-left'); else wrap.classList.add('is-right'); - - // Header - const header = el('div', {class:'floatina-header'}); - const title = el('div', {class:'floatina-title'}, CFG.brandName); - const btnX = el('div', {class:'floatina-close', 'aria':{label:'Close'}}, icoX()); - header.appendChild(title); - header.appendChild(btnX); - - // Body - const body = el('div', {class:'floatina-body'}); - const content= el('div', {class:'floatina-content', id:'fi-content'}); - const nav = el('div', {class:'floatina-nav', role:'tablist'}); - - // Tabs - V.tabs.home = buildTab('home', icoHome(), t('home')); - V.tabs.voice = buildTab('voice', icoMic(), t('voice')); - V.tabs.text = buildTab('text', icoChat(), t('text')); - V.tabs.news = buildTab('news', icoNews(), t('news')); - V.tabs.help = buildTab('help', icoHelp(), t('help')); - nav.append(V.tabs.home, V.tabs.voice, V.tabs.text, V.tabs.news, V.tabs.help); - - body.appendChild(content); - body.appendChild(nav); - - wrap.append(header, body); - document.body.appendChild(wrap); - - V.panel = wrap; - V.content = content; - V.close = btnX; - } - - function buildTab(id, iconNode, label){ - const btn = el('button', {class:'floatina-tab', role:'tab', dataset:{tab:id}, 'aria':{controls:`panel-${id}`}}); - const icoWrap = el('span', {class:'fi-ico'}, iconNode); - const lbl = el('span', {class:'fi-lbl'}, label); - btn.append(icoWrap, lbl); - return btn; - } - - /* ========================== Actions ========================== */ - function open(){ - S.open = true; - V.panel.classList.add('is-open'); - V.fab.setAttribute('aria-expanded','true'); - V.fab.setAttribute('aria-label','Close Floatina'); - swapFabIcon(true); - } - function close(){ - S.open = false; - V.panel.classList.remove('is-open'); - V.fab.setAttribute('aria-expanded','false'); - V.fab.setAttribute('aria-label','Open Floatina'); - swapFabIcon(false); - } - function toggle(){ S.open ? close() : open(); } - - function swapFabIcon(showX){ - clear(V.fab); - V.fab.appendChild(showX ? icoX() : icoBubble()); - } - - function setActiveTab(id){ - Object.values(V.tabs).forEach(b => b.classList.remove('is-active')); - if (V.tabs[id]) V.tabs[id].classList.add('is-active'); - } - - /* ========================== Renderers ========================== */ - function render(tab){ - S.tab = tab; - setActiveTab(tab); - if (tab === 'home') return renderHome(); - if (tab === 'voice') return renderTarget('voice', CFG.voiceURL, CFG.voiceMode); - if (tab === 'text') return renderTarget('text', CFG.textURL, CFG.textMode); - if (tab === 'news') return renderPartial(CFG.ajaxNews); - if (tab === 'help') return renderPartial(CFG.ajaxFAQ); - } - - function renderHome(){ - clear(V.content); - - const card1 = el('div', {class:'fi-card fi-gradient'}); - card1.append(el('h4', {}, 'Welcome'), el('div', {class:'fi-muted'}, 'Quick links below.')); - - const card2 = el('div', {class:'fi-card'}); - const actions = el('div', {class:'fi-actions'}); - actions.append( - el('a', {class:'fi-btn', href:'/contact'}, 'Contact'), - el('a', {class:'fi-btn', href:'/about'}, 'About'), - el('a', {class:'fi-btn', href:'/support'}, 'Support') - ); - card2.append(actions); - - V.content.append(card1, card2); - } - - function renderTarget(kind, url, mode){ - clear(V.content); - - if (!url){ - const card = el('div', {class:'fi-card'}); - card.append(el('h4', {}, `Missing ${kind} URL`), el('div', {class:'fi-muted'}, 'Set it in Floatina settings.')); - V.content.append(card); - return; - } - - if (mode === 'popup'){ - const card = el('div', {class:'fi-card'}); - card.append( - el('h4', {}, kind === 'voice' ? 'Voice chat' : 'Text chat'), - el('div', {class:'fi-muted'}, 'Opens in a new window.'), - el('div', {class:'fi-actions'}, el('a', {class:'fi-btn', href:url, target:'_blank', rel:'noopener'}, t('open_new'))) - ); - // optional auto-open once per session - window.open(url, '_blank', 'noopener'); - V.content.append(card); - return; - } - - // iframe mode + fallback button - const wrap = el('div', {class:'floatina-iframe'}); - const iframe = el('iframe', {src:url, loading:'lazy', title:`${kind}`}); - wrap.append(iframe); - - const fb = el('div', {class:'fi-card'}, el('div', {class:'fi-actions'}, - el('a', {class:'fi-btn', href:url, target:'_blank', rel:'noopener'}, t('open_new')) - )); - - V.content.append(wrap, fb); - } - - function renderPartial(endpoint){ - clear(V.content); - const loader = el('div', {class:'fi-card'}, el('h4', {}, 'Loading…')); - V.content.append(loader); - if (!endpoint){ - clear(V.content); - const err = el('div', {class:'fi-card'}); - err.append(el('h4', {}, 'Error'), el('div', {class:'fi-muted'}, 'Endpoint missing.')); - V.content.append(err); - return; - } - fetch(endpoint, {credentials:'same-origin'}) - .then(r => r.text()) - .then(html => { V.content.innerHTML = html; }) - .catch(() => { - clear(V.content); - const err = el('div', {class:'fi-card'}); - err.append(el('h4', {}, 'Error'), el('div', {class:'fi-muted'}, 'Failed to load content.')); - V.content.append(err); - }); - } - - /* ========================== Bindings ========================== */ - function bind(){ - V.fab.addEventListener('click', toggle); - V.close.addEventListener('click', close); - - Object.values(V.tabs).forEach(btn => { - btn.addEventListener('click', () => render(btn.dataset.tab)); - }); - - document.addEventListener('keydown', e => { if (S.open && e.key === 'Escape') close(); }); - } - - /* ========================== Init ========================== */ - function init(){ - buildFab(); - buildPanel(); - bind(); - render('home'); - } - - document.addEventListener('DOMContentLoaded', init); -})(); diff --git a/floatina/floatina.php b/floatina/floatina.php index 94111e6..6184a21 100644 --- a/floatina/floatina.php +++ b/floatina/floatina.php @@ -1,53 +1,58 @@ floatina_opt('brand_name', 'Assistant'), - 'button_color' => floatina_opt('button_color', '#1e88ff'), - 'position' => floatina_opt('position', 'right'), - 'voice_iframe' => floatina_opt('voice_iframe', ''), - 'text_iframe' => floatina_opt('text_iframe', ''), - 'voice_mode' => floatina_opt('voice_mode', 'iframe'), // iframe|popup - 'text_mode' => floatina_opt('text_mode', 'iframe'), - 'strings' => ['home' => 'Home', 'voice' => 'Voice', 'text' => 'Text', 'news' => 'News', 'help' => 'Help', 'open_new' => 'Open in new tab'], - 'ajax' => [ - 'news' => add_query_arg(['action' => 'floatina_partial_news'], admin_url('admin-ajax.php')), - 'faq' => add_query_arg(['action' => 'floatina_partial_faq'], admin_url('admin-ajax.php')), - ], - ]; - wp_add_inline_script('floatina', 'window.FLOATINA=' . wp_json_encode($data) . ';', 'before'); + /* === Cache-busting helper (filemtime -> version) === */ + $asset_ver = function($abs_path) use ($ver) { + $t = @filemtime($abs_path); + return $t ? $t : $ver; + }; - // Inline CSS variables from settings - $vars = [ - '--floatina-fab:' . floatina_opt('button_color', '#1e88ff'), - '--floatina-accent:' . floatina_opt('accent_color', '#1e66ff'), - '--floatina-border:' . floatina_opt('border_color', '#e6e8eb'), - '--floatina-nav-active:' . floatina_opt('nav_active_bg', '#eef5ff'), - '--floatina-grad-start:' . floatina_opt('grad_start', '#bfe0ff'), - '--floatina-grad-end:' . floatina_opt('grad_end', '#eaf4ff'), - ]; - $css = ':root{' . implode(';', array_map('esc_html', $vars)) . ';}'; - wp_add_inline_style('floatina', $css); + /* === CSS: main === */ + $css_main_rel = 'assets/css/floatina.css'; + $css_main_path = $base_dir . $css_main_rel; + wp_register_style('floatina', $base_url . $css_main_rel, [], $asset_ver($css_main_path)); + wp_enqueue_style('floatina'); + + /* === JS: accordion + core === */ + $js_acc_rel = 'assets/js/accordion.js'; + $js_core_rel = 'assets/js/core.js'; + wp_register_script('floatina-accordion', $base_url . $js_acc_rel, [], $asset_ver($base_dir . $js_acc_rel), true); + wp_register_script('floatina-core', $base_url . $js_core_rel, ['floatina-accordion'], $asset_ver($base_dir . $js_core_rel), true); + wp_enqueue_script('floatina-core'); + + /* === Docked module (optional; only if files exist) === */ + $js_docked_rel = 'assets/js/docked.js'; + $css_docked_rel = 'assets/css/docked.css'; + if ( file_exists($base_dir . $js_docked_rel) ) { + wp_enqueue_script( + 'floatina-docked', + $base_url . $js_docked_rel, + ['floatina-core'], + $asset_ver($base_dir . $js_docked_rel), + true + ); + } + if ( file_exists($base_dir . $css_docked_rel) ) { + wp_enqueue_style( + 'floatina-docked', + $base_url . $css_docked_rel, + ['floatina'], + $asset_ver($base_dir . $css_docked_rel) + ); + } + + /* === Settings payload -> window.FLOATINA (icons + iframe modes) === */ + $settings = get_option('floatina_settings', []); + $settings = wp_parse_args($settings, [ + 'position' => 'right', + 'voice_iframe' => '', + 'text_iframe' => '', + 'voice_mode_popup' => 0, + 'text_mode_popup' => 0, + ]); + + $data = [ + 'position' => $settings['position'], + 'voice_iframe' => $settings['voice_iframe'], + 'text_iframe' => $settings['text_iframe'], + 'voice_mode' => $settings['voice_mode_popup'] ? 'popup' : 'iframe', + 'text_mode' => $settings['text_mode_popup'] ? 'popup' : 'iframe', + 'iconsMap' => [ + 'bi-x.svg' => $base_url . 'assets/icons/bi-x.svg', + 'bi-chat-dots.svg' => $base_url . 'assets/icons/bi-chat-dots.svg', + 'bi-house.svg' => $base_url . 'assets/icons/bi-house.svg', + 'bi-mic.svg' => $base_url . 'assets/icons/bi-mic.svg', + 'bi-newspaper.svg' => $base_url . 'assets/icons/bi-newspaper.svg', + 'bi-question-circle.svg' => $base_url . 'assets/icons/bi-question-circle.svg', + 'bi-chat.svg' => $base_url . 'assets/icons/bi-chat.svg', + ], + ]; + wp_add_inline_script('floatina-core', 'window.FLOATINA=' . wp_json_encode($data) . ';', 'before'); + + /* === CSS Variables from Style tab (HEX only, sanitized) === */ + $style = get_option('floatina_style', []); + $style = wp_parse_args($style, [ + 'button_color' => '#1e88ff', + 'accent_color' => '#1e66ff', + 'border_color' => '#e6e8eb', + 'nav_active_bg' => '#eef5ff', + 'grad_start' => '#ffffff', + 'grad_end' => '#f5f7ff', + 'icon_color' => '#0f172a', + 'text_color' => '#0f172a', + 'muted_color' => '#475569', + ]); + + $hex = function($val, $fallback){ $v = sanitize_hex_color($val); return $v ? $v : $fallback; }; + $css = ':root{' + . '--floatina-fab:' . $hex($style['button_color'], '#1e88ff') . ';' + . '--floatina-accent:' . $hex($style['accent_color'], '#1e66ff') . ';' + . '--floatina-border:' . $hex($style['border_color'], '#e6e8eb') . ';' + . '--floatina-nav-active:' . $hex($style['nav_active_bg'], '#eef5ff') . ';' + . '--floatina-grad-start:' . $hex($style['grad_start'], '#ffffff') . ';' + . '--floatina-grad-end:' . $hex($style['grad_end'], '#f5f7ff') . ';' + . '--floatina-icon:' . $hex($style['icon_color'], '#0f172a') . ';' + . '--floatina-text:' . $hex($style['text_color'], '#0f172a') . ';' + . '--floatina-muted:' . $hex($style['muted_color'], '#475569') . ';' + . '}'; + wp_add_inline_style('floatina', $css); +}); + +/************************************************************************************ + * ADMIN ASSETS (ONLY ON PLUGIN PAGE) + ************************************************************************************/ +add_action('admin_enqueue_scripts', function ($hook) { + if ($hook !== 'toplevel_page_floatina') return; + + $base_url = rtrim(FLOATINA_URL, '/') . '/'; + $base_dir = trailingslashit( dirname( dirname( __FILE__ ) ) ); + $ver = defined('FLOATINA_VER') ? FLOATINA_VER : '1.0.0'; + + $asset_ver = function($abs_path) use ($ver) { + $t = @filemtime($abs_path); + return $t ? $t : $ver; + }; + + wp_enqueue_style('wp-color-picker'); + wp_enqueue_script('wp-color-picker'); + + $css_admin_rel = 'assets/css/admin.css'; + $js_admin_rel = 'assets/js/admin.js'; + + wp_enqueue_style('floatina-admin', $base_url . $css_admin_rel, [], $asset_ver($base_dir . $css_admin_rel)); + wp_enqueue_script('floatina-admin', $base_url . $js_admin_rel, ['jquery', 'wp-color-picker'], $asset_ver($base_dir . $js_admin_rel), true); +}); + +/************************************************************************************ + * ADMIN MENU – keep only Settings & Style; remove Panel + ************************************************************************************/ +add_action('admin_menu', function () { + /* === Ensure parent menu exists === */ + global $admin_page_hooks, $submenu; + if ( empty($admin_page_hooks['floatina']) ) return; + + $parent = 'floatina'; + + /* === Settings & Style (no Panel) === */ + $hook_settings = add_submenu_page( + $parent, + __('Settings','floatina'), + __('Settings','floatina'), + 'manage_options', + 'floatina-settings', + '__return_null' /* === content rendered by redirect below === */ + ); + + $hook_style = add_submenu_page( + $parent, + __('Style','floatina'), + __('Style','floatina'), + 'manage_options', + 'floatina-style', + '__return_null' + ); + + /* === Redirect each submenu to the unified page with proper tab (content exists there) === */ + add_action('load-' . $hook_settings, function () { + wp_safe_redirect( add_query_arg(['page'=>'floatina','tab'=>'settings'], admin_url('admin.php')) ); + exit; + }); + add_action('load-' . $hook_style, function () { + wp_safe_redirect( add_query_arg(['page'=>'floatina','tab'=>'style'], admin_url('admin.php')) ); + exit; + }); +}, 99); + +/************************************************************************************ + * HIDE IN-PAGE TABS ON THE FLOATINA PAGE + * - We keep the content logic as-is (tab param), but hide the visual tabs. + ************************************************************************************/ +add_action('admin_head', function () { + if (!isset($_GET['page']) || $_GET['page'] !== 'floatina') return; + + /* === Inline CSS to hide any tab bar variants (WP default or custom) === */ + echo ''; }); diff --git a/floatina/includes/class-cpts.php b/floatina/includes/class-cpts.php index dfc560a..916d8a2 100644 --- a/floatina/includes/class-cpts.php +++ b/floatina/includes/class-cpts.php @@ -2,30 +2,29 @@ if (!defined('ABSPATH')) exit; /************************************************************************************ - * Register CPTs for News and FAQ under the same "Floatina" menu + * REGISTER CUSTOM POST TYPES ************************************************************************************/ -function floatina_register_cpts() -{ - $parent = 'floatina'; // matches settings menu slug +function floatina_register_cpts() { + $parent = 'floatina'; - register_post_type('floatina_news', [ - 'label' => 'Floatina News', - 'public' => true, - 'show_in_menu' => $parent, - 'menu_icon' => 'dashicons-megaphone', - 'supports' => ['title', 'editor', 'thumbnail', 'excerpt'], - 'has_archive' => false, - 'show_in_rest' => true, - ]); + register_post_type('floatina_news', [ + 'label' => 'Floatina News', + 'public' => true, + 'show_in_menu' => $parent, + 'menu_icon' => 'dashicons-megaphone', + 'supports' => ['title','editor','thumbnail','excerpt'], + 'has_archive' => false, + 'show_in_rest' => true, + ]); - register_post_type('floatina_faq', [ - 'label' => 'Floatina FAQ', - 'public' => true, - 'show_in_menu' => $parent, - 'menu_icon' => 'dashicons-editor-help', - 'supports' => ['title', 'editor'], - 'has_archive' => false, - 'show_in_rest' => true, - ]); + register_post_type('floatina_faq', [ + 'label' => 'Floatina FAQ', + 'public' => true, + 'show_in_menu' => $parent, + 'menu_icon' => 'dashicons-editor-help', + 'supports' => ['title','editor'], + 'has_archive' => false, + 'show_in_rest' => true, + ]); } add_action('init', 'floatina_register_cpts'); diff --git a/floatina/includes/class-meta.php b/floatina/includes/class-meta.php new file mode 100644 index 0000000..4c993de --- /dev/null +++ b/floatina/includes/class-meta.php @@ -0,0 +1,26 @@ +ID, '_floatina_news_link', true); + wp_nonce_field('floatina_news_link_nonce', 'floatina_news_link_nonce'); + echo '

'; + echo ''; + echo '

Leave empty to use the post permalink.

'; +} + +add_action('save_post_floatina_news', function($post_id){ + if (!isset($_POST['floatina_news_link_nonce']) || !wp_verify_nonce($_POST['floatina_news_link_nonce'], 'floatina_news_link_nonce')) return; + if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return; + if (!current_user_can('edit_post', $post_id)) return; + $url = isset($_POST['floatina_news_link_field']) ? esc_url_raw(trim($_POST['floatina_news_link_field'])) : ''; + if ($url) update_post_meta($post_id, '_floatina_news_link', $url); + else delete_post_meta($post_id, '_floatina_news_link'); +}); diff --git a/floatina/includes/class-settings.php b/floatina/includes/class-settings.php index 55cbafa..e102ad8 100644 --- a/floatina/includes/class-settings.php +++ b/floatina/includes/class-settings.php @@ -2,89 +2,216 @@ if (!defined('ABSPATH')) exit; /************************************************************************************ - * Admin menu + settings + * DEFAULTS ************************************************************************************/ -add_action('admin_menu', function () { - add_menu_page( - 'Floatina', - 'Floatina', - 'manage_options', - 'floatina', - 'floatina_render_settings', - 'dashicons-layout', - 81 - ); - add_submenu_page('floatina', 'Settings', 'Settings', 'manage_options', 'floatina', 'floatina_render_settings'); -}, 9); - -add_action('admin_init', function () { - register_setting('floatina_settings_group', 'floatina_settings'); - - /* ---------- General ---------- */ - add_settings_section('floatina_main', 'General', '__return_false', 'floatina'); - add_settings_field('position', 'Panel position', 'floatina_field_position', 'floatina', 'floatina_main'); - add_settings_field('brand_name', 'Brand name (header)', 'floatina_field_text', 'floatina', 'floatina_main', ['key' => 'brand_name', 'placeholder' => 'Assistant']); - - /* ---------- Chat targets ---------- */ - add_settings_section('floatina_chat', 'Chat targets', '__return_false', 'floatina'); - add_settings_field('voice_iframe', 'Voice URL', 'floatina_field_text', 'floatina', 'floatina_chat', ['key' => 'voice_iframe', 'placeholder' => 'https://...']); - add_settings_field('voice_mode', 'Voice mode', 'floatina_field_mode', 'floatina', 'floatina_chat', ['key' => 'voice_mode']); - add_settings_field('text_iframe', 'Text URL', 'floatina_field_text', 'floatina', 'floatina_chat', ['key' => 'text_iframe', 'placeholder' => 'https://...']); - add_settings_field('text_mode', 'Text mode', 'floatina_field_mode', 'floatina', 'floatina_chat', ['key' => 'text_mode']); - - /* ---------- Colors ---------- */ - add_settings_section('floatina_colors', 'Colors', '__return_false', 'floatina'); - add_settings_field('button_color', 'FAB color', 'floatina_field_text', 'floatina', 'floatina_colors', ['key' => 'button_color', 'placeholder' => '#1e88ff']); - add_settings_field('accent_color', 'Accent color', 'floatina_field_text', 'floatina', 'floatina_colors', ['key' => 'accent_color', 'placeholder' => '#1e66ff']); - add_settings_field('border_color', 'Border color', 'floatina_field_text', 'floatina', 'floatina_colors', ['key' => 'border_color', 'placeholder' => '#e6e8eb']); - add_settings_field('nav_active_bg', 'Tab active background', 'floatina_field_text', 'floatina', 'floatina_colors', ['key' => 'nav_active_bg', 'placeholder' => '#eef5ff']); - add_settings_field('grad_start', 'Header gradient start', 'floatina_field_text', 'floatina', 'floatina_colors', ['key' => 'grad_start', 'placeholder' => '#bfe0ff']); - add_settings_field('grad_end', 'Header gradient end', 'floatina_field_text', 'floatina', 'floatina_colors', ['key' => 'grad_end', 'placeholder' => '#eaf4ff']); -}); - -function floatina_field_text($args) -{ - $opts = get_option('floatina_settings', []); - $key = $args['key']; - $val = isset($opts[$key]) ? esc_attr($opts[$key]) : ''; - $ph = isset($args['placeholder']) ? esc_attr($args['placeholder']) : ''; - echo ''; +function floatina_defaults_settings() { + return [ + 'position' => 'right', + 'enable_home' => 1, + 'enable_voice' => 1, + 'enable_text' => 1, + 'enable_news' => 1, + 'enable_faq' => 1, + 'voice_iframe' => '', + 'voice_mode_popup' => 0, + 'text_iframe' => '', + 'text_mode_popup' => 0, + 'home_extra' => '', + ]; +} +function floatina_defaults_style() { + return [ + 'button_color' => '#1e88ff', + 'accent_color' => '#1e66ff', + 'border_color' => '#e6e8eb', + 'nav_active_bg' => '#eef5ff', + 'grad_start' => '#ffffff', + 'grad_end' => '#f5f7ff', + 'icon_color' => '#0f172a', + 'text_color' => '#0f172a', + 'muted_color' => '#475569', + ]; } -function floatina_field_position() -{ - $v = floatina_opt('position', 'right'); ?> - -    - -

Choose bottom-right or bottom-left placement.

- - -

Use Popup/New tab if the target site blocks iframes.

- -
-

Floatina

-
- -
-
-

Usage: Renders site-wide. Optional shortcode: [floatina].

-

Content: add items in Floatina News and Floatina FAQ. Typography inherits your theme.

-
-$v){ + if (array_key_exists($k,$input)) { + $val = sanitize_hex_color(trim((string)$input[$k])); + if ($val) $out[$k] = $val; + } + } + return $out; +} + +/************************************************************************************ + * ADMIN MENU + ************************************************************************************/ +add_action('admin_menu', function () { + add_menu_page('Floatina','Floatina','manage_options','floatina','floatina_render_settings','dashicons-layout',81); +}); + +/************************************************************************************ + * REGISTER SETTINGS + ************************************************************************************/ +add_action('admin_init', function () { + register_setting('floatina_group_settings', 'floatina_settings', [ + 'type'=>'array','sanitize_callback'=>'floatina_sanitize_settings' + ]); + register_setting('floatina_group_style', 'floatina_style', [ + 'type'=>'array','sanitize_callback'=>'floatina_sanitize_style' + ]); + + add_settings_section('floatina_section_settings','Settings','__return_false','floatina-settings'); + add_settings_section('floatina_section_style','Style','__return_false','floatina-style'); +}); + +/************************************************************************************ + * SETTINGS PAGE RENDERER + ************************************************************************************/ +function floatina_render_settings(){ + if (!current_user_can('manage_options')) return; + $tab = isset($_GET['tab']) ? sanitize_key($_GET['tab']) : 'settings'; + if (!in_array($tab, ['settings','style'], true)) $tab = 'settings'; + + $s = get_option('floatina_settings', floatina_defaults_settings()); + $c = get_option('floatina_style', floatina_defaults_style()); + ?> +
+

Floatina

+ +

+ Settings + Style +

+ + +
+ + +
+ +
+ + +
+

Choose bottom-right or bottom-left placement.

+
+ + 'Enable Home tab', + 'enable_voice' => 'Enable Voice tab', + 'enable_text' => 'Enable Text tab', + 'enable_news' => 'Enable News tab', + 'enable_faq' => 'Enable FAQ tab', + ]; + foreach ($toggles as $key=>$label): ?> +
+ + +
+ + +
+ +
+ + +
+
+ + +

Open in new tab when iframe is blocked.

+
+ +
+ + +
+
+ + +

Open in new tab when iframe is blocked.

+
+ +
+ +
+ + 'floatina_settings[home_extra]', + 'textarea_rows' => 6, + 'media_buttons' => true, + 'tinymce' => true, + ]); + ?> +

Optional block under the Hi card on Home. Leave empty to hide.

+
+ + +
+ + +
+ + +
+ 'FAB color','accent_color'=>'Accent color','border_color'=>'Border color', + 'nav_active_bg'=>'Tab active background','grad_start'=>'Header gradient start','grad_end'=>'Header gradient end', + 'icon_color'=>'Tab icon/text color','text_color'=>'Body text color','muted_color'=>'Muted text color' + ]; + foreach ($labels as $key=>$label): ?> +
+ + +
+ +
+ + +
+ +
+

Template missing

' . esc_html($file) . '
'; + } +} + +/************************************************************************************ + * OPTIONS SHORT HAND + ************************************************************************************/ +function floatina_opt($key, $default = '') { + $opts = get_option('floatina_settings', []); + return isset($opts[$key]) && $opts[$key] !== '' ? $opts[$key] : $default; +} +// ----- News link helpers ----- +if (!function_exists('floatina_news_link')) { + function floatina_news_link($post_id = 0) { + $post_id = $post_id ?: get_the_ID(); + // أولوية للمفتاح الجديد/الصحيح + $url = get_post_meta($post_id, '_floatina_news_link', true); + // توافق خلفي مع إصدارات قديمة استعملت المفتاح الخاطئ + if (!$url) $url = get_post_meta($post_id, 'floatina_news_url', true); + return $url ?: get_permalink($post_id); + } +} +if (!function_exists('floatina_is_external')) { + function floatina_is_external($url) { + $host = wp_parse_url($url, PHP_URL_HOST); + $home = wp_parse_url(home_url(), PHP_URL_HOST); + return $host && $home && strcasecmp($host, $home) !== 0; + } } diff --git a/floatina/templates/loop-faq.php b/floatina/templates/loop-faq.php index f2b2825..64421d8 100644 --- a/floatina/templates/loop-faq.php +++ b/floatina/templates/loop-faq.php @@ -1,26 +1,29 @@ 'floatina_faq', - 'post_status' => 'publish', - 'posts_per_page' => 20, - 'orderby' => ['menu_order' => 'ASC', 'date' => 'ASC'], + 'post_type' => 'floatina_faq', + 'post_status' => 'publish', + 'posts_per_page' => 20, + 'orderby' => ['menu_order'=>'ASC','date'=>'ASC'], ]); - -if ($q->have_posts()): - while ($q->have_posts()): $q->the_post(); ?> -
-

-
-
- -
-

No FAQs

-
Add posts in Floatina FAQ.
-
- \ No newline at end of file +?> +
+have_posts()): while($q->have_posts()): $q->the_post(); + $id = 'fi-acc-' . get_the_ID(); ?> +
+ + +
+ +

No FAQs

Add posts in Floatina FAQ.
+ +
diff --git a/floatina/templates/loop-news.php b/floatina/templates/loop-news.php index 208362b..f542d21 100644 --- a/floatina/templates/loop-news.php +++ b/floatina/templates/loop-news.php @@ -1,28 +1,32 @@ 'floatina_news', - 'post_status' => 'publish', - 'posts_per_page' => 8, - 'orderby' => 'date', - 'order' => 'DESC', + 'post_type' => 'floatina_news', + 'post_status' => 'publish', + 'posts_per_page' => 8, + 'orderby' => 'date', + 'order' => 'DESC', ]); if ($q->have_posts()): - while ($q->have_posts()): $q->the_post(); ?> -
-

-
-
Open
-
- have_posts()): $q->the_post(); + $url = get_post_meta(get_the_ID(), '_floatina_news_link', true); + $href = $url ? $url : get_permalink(); + $thumb = get_the_post_thumbnail_url(get_the_ID(), 'medium'); ?> + + + + > + + -
-

No news

-
Add posts in Floatina News.
-
- \ No newline at end of file +
+

No news

+
Add posts in Floatina News.
+
+ -
\ No newline at end of file + + * 2) plugin/templates/tabs/ + * 3) (legacy) theme/floatina/ + * 4) (legacy) plugin/templates/ + */ +if (!function_exists('floatina_panel_include')) { + function floatina_panel_include($rel_file) { + $rel_file = ltrim(str_replace(['..', '\\'], ['', '/'], $rel_file), '/'); + + $theme_base = trailingslashit(get_stylesheet_directory()) . 'floatina/'; + $plugin_base = trailingslashit(dirname(__FILE__)); // .../templates/ + $plugin_tabs = $plugin_base . 'tabs/'; + + $theme_tabs = $theme_base . 'tabs/' . basename($rel_file); + $plugin_tabs_file = $plugin_tabs . basename($rel_file); + if (file_exists($theme_tabs)) { include $theme_tabs; return; } + if (file_exists($plugin_tabs_file)) { include $plugin_tabs_file; return; } + + // Legacy fallback (keeps older themes working) + $theme_old = $theme_base . basename($rel_file); + $plugin_old = $plugin_base . basename($rel_file); + if (file_exists($theme_old)) { include $theme_old; return; } + if (file_exists($plugin_old)) { include $plugin_old; return; } + + if (current_user_can('manage_options')) { + echo '
'; + echo '

Template missing

'; + echo '
Could not find: ' . esc_html($rel_file) . ' in templates/tabs/ or legacy paths.
'; + echo '
'; + } + } +} + +/* === Panel bootstrap (FAB + body + nav) =================================== */ +$pos = floatina_opt('position','right'); +$enable = [ + 'home' => (int) floatina_opt('enable_home', 1), + 'voice' => (int) floatina_opt('enable_voice',1), + 'text' => (int) floatina_opt('enable_text', 1), + 'news' => (int) floatina_opt('enable_news', 1), + 'faq' => (int) floatina_opt('enable_faq', 1), +]; + +$voice_url = floatina_opt('voice_iframe',''); +$text_url = floatina_opt('text_iframe',''); +$voice_mode = floatina_opt('voice_mode_popup',0) ? 'popup' : 'iframe'; +$text_mode = floatina_opt('text_mode_popup',0) ? 'popup' : 'iframe'; + +/* === Panes definition (labels are translatable) =========================== */ +$panes = []; +if ($enable['home']) $panes[] = ['id'=>'fi-pane-home', 'lbl'=>__('Home','floatina'), 'icon'=>'bi-house.svg', 'kind'=>null, 'tmpl'=>'tab-home.php']; +if ($enable['voice']) $panes[] = ['id'=>'fi-pane-voice', 'lbl'=>__('Voice','floatina'),'icon'=>'bi-mic.svg', 'kind'=>'voice','tmpl'=>'tab-voice.php','url'=>$voice_url,'mode'=>$voice_mode]; +if ($enable['text']) $panes[] = ['id'=>'fi-pane-text', 'lbl'=>__('Text','floatina'), 'icon'=>'bi-chat-dots.svg', 'kind'=>'text', 'tmpl'=>'tab-text.php', 'url'=>$text_url, 'mode'=>$text_mode]; +if ($enable['news']) $panes[] = ['id'=>'fi-pane-news', 'lbl'=>__('News','floatina'), 'icon'=>'bi-newspaper.svg', 'kind'=>null, 'tmpl'=>'tab-news.php']; +if ($enable['faq']) $panes[] = ['id'=>'fi-pane-help', 'lbl'=>__('Help','floatina'), 'icon'=>'bi-question-circle.svg', 'kind'=>null, 'tmpl'=>'tab-faq.php']; + +/* === Icons helper ========================================================= */ +$icon = function($file){ + return esc_url(rtrim(FLOATINA_URL,'/') . '/assets/icons/' . ltrim($file,'/')); +}; +?> + + + + + +
+
+ + +
+ +
> + + +
+ +
+ + +
+ + + +
+ +
+
diff --git a/floatina/templates/tabs/tab-faq.php b/floatina/templates/tabs/tab-faq.php new file mode 100644 index 0000000..01a1fc5 --- /dev/null +++ b/floatina/templates/tabs/tab-faq.php @@ -0,0 +1,32 @@ + 'floatina_faq', + 'post_status' => 'publish', + 'posts_per_page' => 20, + 'orderby' => ['menu_order'=>'ASC','date'=>'ASC'], +]); +?> +
+have_posts()): while($q->have_posts()): $q->the_post(); ?> +
+ + +
+ +

No FAQs

Add posts in Floatina FAQ.
+ +
+ + diff --git a/floatina/templates/tabs/tab-home.php b/floatina/templates/tabs/tab-home.php new file mode 100644 index 0000000..ee73fda --- /dev/null +++ b/floatina/templates/tabs/tab-home.php @@ -0,0 +1,32 @@ + +
+
+

Hi

+
How can we help you today?
+
+
+ + +
+ + +
+ +
+
Contact Support
+
Not live chat, but we’ll get back to you asap.
+
+
?
+
+ + +
+
Lifetime License Upgrade Form
+
Upgrade lower to a higher Lifetime tier.
+
+
+
+
diff --git a/floatina/templates/tabs/tab-news.php b/floatina/templates/tabs/tab-news.php new file mode 100644 index 0000000..eeee70d --- /dev/null +++ b/floatina/templates/tabs/tab-news.php @@ -0,0 +1,30 @@ + 'floatina_news', + 'post_status' => 'publish', + 'posts_per_page' => 8, + 'orderby' => 'date', + 'order' => 'DESC', +]); + +if ($q->have_posts()): + while ($q->have_posts()): $q->the_post(); + $url = get_post_meta(get_the_ID(),'floatina_news_url',true); + $href = $url ? $url : get_permalink(); + $thumb = get_the_post_thumbnail_url(get_the_ID(),'medium'); + $excerpt = get_the_excerpt(); + ?> + +
+
+

+

+ Open → +
+
+ +

No news

Add posts in Floatina News.
+ diff --git a/floatina/templates/tabs/tab-text.php b/floatina/templates/tabs/tab-text.php new file mode 100644 index 0000000..269ae10 --- /dev/null +++ b/floatina/templates/tabs/tab-text.php @@ -0,0 +1,2 @@ + +
diff --git a/floatina/templates/tabs/tab-voice.php b/floatina/templates/tabs/tab-voice.php new file mode 100644 index 0000000..269ae10 --- /dev/null +++ b/floatina/templates/tabs/tab-voice.php @@ -0,0 +1,2 @@ + +