Version 1.2.0

This commit is contained in:
MOH 2025-10-03 23:54:13 +02:00
parent 4cd358493f
commit 32372c7545
30 changed files with 1434 additions and 737 deletions

View File

@ -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; } }

View File

@ -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; }
}

View File

@ -1,254 +1,177 @@
/************************************************************************************ /************************************************************************************
* Floatina core layout * Floatina Apple glass style (front-end) HEX variables wired to settings
************************************************************************************/ ************************************************************************************/
/* Inherit typography from theme */ :root{
#floatina-panel, --floatina-offset:16px;
#floatina-panel * { --floatina-width:min(92vw,520px);
font-family: inherit; --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 */ /* === FAB === */
:root { #floatina-fab{
--floatina-offset: 16px; position:fixed;
--floatina-width: min(92vw, 460px); bottom:var(--floatina-offset);
--floatina-height: 70vh; width:var(--fi-fab-h);
--floatina-radius: 16px; height:var(--fi-fab-h);
--floatina-shadow: 0 20px 50px rgba(0, 0, 0, 0.25); border-radius:20px;
--floatina-border: #e6e8eb; display:flex;align-items:center;justify-content:center;
--floatina-bg: #ffffff; background:var(--floatina-fab);color:#fff;
--floatina-soft: #f7f8fa; box-shadow:0 12px 28px rgba(0,0,0,.22), inset 0 1px 0 rgba(255,255,255,.4);
--floatina-fab: #1e88ff; cursor:pointer;z-index:10000;
--floatina-accent: #1e66ff; /* === no transition on top/right/left/bottom to follow instantly === */
--floatina-grad-start: #bfe0ff; transition:transform .2s ease, box-shadow .2s ease;
--floatina-grad-end: #eaf4ff; }
--floatina-nav-active: #eef5ff; #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 */ /* === Loader === */
#floatina-fab { .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%)}
position: fixed; .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)}
bottom: var(--floatina-offset); .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}
width: 56px; .fi-loader-txt{font-size:12px;color:#334155}
height: 56px; @keyframes fi-spin{to{transform:rotate(360deg)}}
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;
}
/* Left/Right placement */ /* === Accordion === */
#floatina-fab.is-right { .fi-accordion{display:block}
right: var(--floatina-offset); .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}
#floatina-fab.is-left { .fi-acc-btn:hover{background:var(--floatina-nav-active)}
left: var(--floatina-offset); .fi-acc-ico::before{content:"+";display:inline-block;transition:transform .2s ease}
} .fi-acc-btn[aria-expanded="true"] .fi-acc-ico::before{content:""}
#floatina-panel.is-right { .fi-acc-panel{max-height:0;overflow:hidden;transition:max-height .25s ease}
right: var(--floatina-offset); .fi-acc-inner{padding:0 12px 12px;color:#334155}
}
#floatina-panel.is-left {
left: var(--floatina-offset);
}
/* Panel shell */ /* === News === */
#floatina-panel { .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}
position: fixed; .fi-news-card:hover{transform:translateY(-2px);box-shadow:0 16px 36px rgba(0,0,0,.14)}
bottom: calc(var(--floatina-offset) + 66px); .fi-news-media{min-width:96px;width:96px;background:#eaeef3;background-size:cover;background-position:center}
width: var(--floatina-width); .fi-news-body{padding:12px 12px 12px 4px;display:flex;flex-direction:column;gap:6px}
max-height: var(--floatina-height); .fi-news-title{font-size:14px;margin:0;color:#0f172a}
border-radius: var(--floatina-radius); .fi-news-excerpt{font-size:12px;color:#475569;margin:0}
background: var(--floatina-bg); .fi-news-cta{margin-top:auto;font-size:12px;color:var(--floatina-accent)}
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;
}
/* Header */ /* === Tab panes === */
.floatina-header { .fi-pane{display:none;height:100%}
position: relative; .fi-pane.is-active{display:block}
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;
}
/* Body */ /* === Home hero === */
.floatina-body { .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}
display: flex; .fi-hero-title h1{margin:0 0 6px;font-size:28px;line-height:1.1;color:#0f172a;font-weight:800}
flex-direction: column; .fi-hero-title .fi-hero-sub{font-size:18px;color:#0f172a;opacity:.9}
height: calc(var(--floatina-height) - 118px);
background: var(--floatina-soft);
}
.floatina-content {
flex: 1;
overflow: auto;
padding: 12px;
}
/* Bottom nav */ /* === Home list cards === */
.floatina-nav { .fi-list{display:flex;flex-direction:column;gap:12px}
display: flex; .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}
gap: 6px; .fi-list-card:hover{transform:translateY(-1px);box-shadow:0 16px 36px rgba(0,0,0,.14);background:#fcfdff}
justify-content: space-between; .fi-list-body{min-width:0}
border-top: 1px solid var(--floatina-border); .fi-list-title{font-size:16px;font-weight:700;color:#0f172a;margin-bottom:4px}
background: #fff; .fi-list-desc{font-size:14px;color:#475569;opacity:.95;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:60ch}
padding: 10px; .fi-list-ico{width:22px;height:22px;display:grid;place-items:center;color:#0f172a;opacity:.85}
}
.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;
}
/* Cards + gradient */ /* === Mobile (fix width + X above panel and follows resize) === */
.fi-card { @media (max-width:640px){
background: #fff; :root{--floatina-width:100vw;--floatina-height:85vh}
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;
}
/* Buttons inside Home */ /* === Fill width symmetrically using safe-areas; ignore .is-left/.is-right offsets === */
.fi-actions { #floatina-panel,
display: flex; #floatina-panel.is-left,
gap: 8px; #floatina-panel.is-right{
flex-wrap: wrap; bottom:0;
} left:env(safe-area-inset-left, 0px);
.fi-btn { right:env(safe-area-inset-right, 0px);
display: inline-flex; width:auto;
align-items: center; max-height:100dvh;
justify-content: center; border-radius:16px 16px 0 0;
height: 36px; transform:none;
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;
} }
#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-body{height:calc(var(--floatina-height))}
#floatina-fab[data-color] {
background: var(--floatina-fab-color, #1e88ff); /* === 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}
} }

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor">
<path d="M2 2h12v9H6l-3 3V2z"/>
<circle cx="6" cy="7" r="1.1" fill="#fff"/>
<circle cx="8" cy="7" r="1.1" fill="#fff"/>
<circle cx="10" cy="7" r="1.1" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 261 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linejoin="round">
<path d="M2 3h12v7H7l-3 3v-3H2V3z"/>
</svg>

After

Width:  |  Height:  |  Size: 185 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linejoin="round">
<path d="M2 7.5L8 2l6 5.5V14a1 1 0 0 1-1 1h-3V9H6v6H3a1 1 0 0 1-1-1V7.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 224 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.25">
<rect x="5" y="2" width="6" height="8" rx="3"/>
<path d="M3 8a5 5 0 0 0 10 0M7 13h2v2H7z" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 241 B

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.25">
<rect x="2" y="3" width="10" height="10" rx="1.5"/>
<path d="M13 5h1v7a1 1 0 0 1-1 1H3"/>
<path d="M4.5 5.5h5M4.5 7.5h5M4.5 9.5h3" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 284 B

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.25">
<circle cx="8" cy="8" r="6.5"/>
<path d="M6.8 6.6a1.7 1.7 0 1 1 2.5 1.5c-.6.3-1 .6-1 .9V10" stroke-linecap="round"/>
<circle cx="8" cy="12" r=".8" fill="currentColor" stroke="none"/>
</svg>

After

Width:  |  Height:  |  Size: 311 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round">
<path d="M3.5 3.5l9 9m0-9l-9 9"/>
</svg>

After

Width:  |  Height:  |  Size: 181 B

View File

@ -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);

View File

@ -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);

198
floatina/assets/js/core.js Normal file
View File

@ -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 = '<div class="fi-loader-inner"><div class="fi-spinner"></div><div class="fi-loader-txt">Loading…</div></div>';
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 = '<h4>Cannot embed</h4><div class="fi-muted">The site refused to connect or took too long.</div><div class="fi-actions"><a class="fi-btn" target="_blank" rel="noopener">Open in new tab</a></div>';
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();
});
})();

View File

@ -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();
});
})();

View File

@ -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);
})();

View File

@ -1,53 +1,58 @@
<?php <?php
/** /**
* Plugin Name: Floatina * Plugin Name: Floatina
* Description: Floating action button with slide-up panel (Home, Voice, Text, News, Help). Mobile fullscreen. CPTs for News/FAQ. * Description: Floating action button with slide-up panel (Home, Voice, Text, News, Help). Mobile fullscreen. CPTs for News/FAQ.
* Version: 1.0.0 * Version: 1.2.0
* Author: Moh Farawati * Author: Moh Farawati
* Text Domain: floatina * Text Domain: floatina
*/ */
if (!defined('ABSPATH')) exit; if (!defined('ABSPATH')) exit;
define('FLOATINA_VER', '0.1.0'); /************************************************************************************
* CONSTANTS
************************************************************************************/
define('FLOATINA_VER', '1.2.0');
define('FLOATINA_DIR', plugin_dir_path(__FILE__)); define('FLOATINA_DIR', plugin_dir_path(__FILE__));
define('FLOATINA_URL', plugin_dir_url(__FILE__)); define('FLOATINA_URL', plugin_dir_url(__FILE__));
/************************************************************************************
* INCLUDES
************************************************************************************/
require_once FLOATINA_DIR . 'includes/helpers.php'; require_once FLOATINA_DIR . 'includes/helpers.php';
require_once FLOATINA_DIR . 'includes/class-assets.php'; require_once FLOATINA_DIR . 'includes/class-assets.php';
require_once FLOATINA_DIR . 'includes/class-cpts.php'; require_once FLOATINA_DIR . 'includes/class-cpts.php';
require_once FLOATINA_DIR . 'includes/class-settings.php'; require_once FLOATINA_DIR . 'includes/class-settings.php';
require_once FLOATINA_DIR . 'includes/class-ajax.php'; require_once FLOATINA_DIR . 'includes/class-ajax.php';
require_once FLOATINA_DIR . 'includes/class-meta.php';
/************************************************************************************ /************************************************************************************
* Bootstrap + i18n * I18N
************************************************************************************/ ************************************************************************************/
add_action('plugins_loaded', function () { add_action('plugins_loaded', function () {
load_plugin_textdomain('floatina', false, dirname(plugin_basename(__FILE__)) . '/languages'); load_plugin_textdomain('floatina', false, dirname(plugin_basename(__FILE__)) . '/languages');
}); });
/************************************************************************************ /************************************************************************************
* Activation: register CPTs and flush rules * ACTIVATION
************************************************************************************/ ************************************************************************************/
register_activation_hook(__FILE__, function () { register_activation_hook(__FILE__, function () {
floatina_register_cpts(); floatina_register_cpts();
flush_rewrite_rules(); flush_rewrite_rules();
}); });
/************************************************************************************ /************************************************************************************
* Render panel automatically in footer on frontend * FRONT-END RENDER (FOOTER)
************************************************************************************/ ************************************************************************************/
add_action('wp_footer', function () { add_action('wp_footer', function () {
if (is_admin()) return; if (is_admin()) return;
include FLOATINA_DIR . 'templates/panel.php'; include FLOATINA_DIR . 'templates/panel.php';
}, 999); }, 999);
/************************************************************************************ /************************************************************************************
* Optional shortcode [floatina] * SHORTCODE
************************************************************************************/ ************************************************************************************/
add_shortcode('floatina', function () { add_shortcode('floatina', function () {
ob_start(); ob_start();
include FLOATINA_DIR . 'templates/panel.php'; include FLOATINA_DIR . 'templates/panel.php';
return ob_get_clean(); return ob_get_clean();
}); });

View File

@ -2,20 +2,18 @@
if (!defined('ABSPATH')) exit; if (!defined('ABSPATH')) exit;
/************************************************************************************ /************************************************************************************
* Public AJAX endpoints for News and FAQ partials * PUBLIC AJAX PARTIALS
************************************************************************************/ ************************************************************************************/
add_action('wp_ajax_floatina_partial_news', 'floatina_partial_news'); add_action('wp_ajax_floatina_partial_news', 'floatina_partial_news');
add_action('wp_ajax_nopriv_floatina_partial_news', 'floatina_partial_news'); add_action('wp_ajax_nopriv_floatina_partial_news', 'floatina_partial_news');
function floatina_partial_news() function floatina_partial_news(){
{ include FLOATINA_DIR . 'templates/loop-news.php';
include FLOATINA_DIR . 'templates/loop-news.php'; wp_die();
wp_die();
} }
add_action('wp_ajax_floatina_partial_faq', 'floatina_partial_faq'); add_action('wp_ajax_floatina_partial_faq', 'floatina_partial_faq');
add_action('wp_ajax_nopriv_floatina_partial_faq', 'floatina_partial_faq'); add_action('wp_ajax_nopriv_floatina_partial_faq', 'floatina_partial_faq');
function floatina_partial_faq() function floatina_partial_faq(){
{ include FLOATINA_DIR . 'templates/loop-faq.php';
include FLOATINA_DIR . 'templates/loop-faq.php'; wp_die();
wp_die();
} }

View File

@ -2,40 +2,191 @@
if (!defined('ABSPATH')) exit; if (!defined('ABSPATH')) exit;
/************************************************************************************ /************************************************************************************
* Enqueue frontend assets and localize settings * FRONT-END ASSETS
************************************************************************************/ ************************************************************************************/
add_action('wp_enqueue_scripts', function () { add_action('wp_enqueue_scripts', function () {
wp_register_style('floatina', FLOATINA_URL . 'assets/css/floatina.css', [], FLOATINA_VER);
wp_register_script('floatina', FLOATINA_URL . 'assets/js/floatina.js', [], FLOATINA_VER, true);
wp_enqueue_style('floatina'); /* === Base paths / versions === */
wp_enqueue_script('floatina'); $base_url = rtrim(FLOATINA_URL, '/') . '/';
$base_dir = trailingslashit( dirname( dirname( __FILE__ ) ) ); // /wp-content/plugins/floatina/
$ver = defined('FLOATINA_VER') ? FLOATINA_VER : '1.0.0';
$data = [ /* === Cache-busting helper (filemtime -> version) === */
'brand_name' => floatina_opt('brand_name', 'Assistant'), $asset_ver = function($abs_path) use ($ver) {
'button_color' => floatina_opt('button_color', '#1e88ff'), $t = @filemtime($abs_path);
'position' => floatina_opt('position', 'right'), return $t ? $t : $ver;
'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');
// Inline CSS variables from settings /* === CSS: main === */
$vars = [ $css_main_rel = 'assets/css/floatina.css';
'--floatina-fab:' . floatina_opt('button_color', '#1e88ff'), $css_main_path = $base_dir . $css_main_rel;
'--floatina-accent:' . floatina_opt('accent_color', '#1e66ff'), wp_register_style('floatina', $base_url . $css_main_rel, [], $asset_ver($css_main_path));
'--floatina-border:' . floatina_opt('border_color', '#e6e8eb'), wp_enqueue_style('floatina');
'--floatina-nav-active:' . floatina_opt('nav_active_bg', '#eef5ff'),
'--floatina-grad-start:' . floatina_opt('grad_start', '#bfe0ff'), /* === JS: accordion + core === */
'--floatina-grad-end:' . floatina_opt('grad_end', '#eaf4ff'), $js_acc_rel = 'assets/js/accordion.js';
]; $js_core_rel = 'assets/js/core.js';
$css = ':root{' . implode(';', array_map('esc_html', $vars)) . ';}'; wp_register_script('floatina-accordion', $base_url . $js_acc_rel, [], $asset_ver($base_dir . $js_acc_rel), true);
wp_add_inline_style('floatina', $css); 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 '<style>
/* hide WP nav-tab bar if used */
.wrap .nav-tab-wrapper{ display:none !important; }
/* hide custom tab wrappers (sane guesses) */
.wrap .fi-admin-tabs, .wrap .floatina-tabs{ display:none !important; }
/* hide direct tab links if printed as buttons/anchors */
.wrap a[href*="page=floatina&tab="]{ display:none !important; }
</style>';
}); });

View File

@ -2,30 +2,29 @@
if (!defined('ABSPATH')) exit; if (!defined('ABSPATH')) exit;
/************************************************************************************ /************************************************************************************
* Register CPTs for News and FAQ under the same "Floatina" menu * REGISTER CUSTOM POST TYPES
************************************************************************************/ ************************************************************************************/
function floatina_register_cpts() function floatina_register_cpts() {
{ $parent = 'floatina';
$parent = 'floatina'; // matches settings menu slug
register_post_type('floatina_news', [ register_post_type('floatina_news', [
'label' => 'Floatina News', 'label' => 'Floatina News',
'public' => true, 'public' => true,
'show_in_menu' => $parent, 'show_in_menu' => $parent,
'menu_icon' => 'dashicons-megaphone', 'menu_icon' => 'dashicons-megaphone',
'supports' => ['title', 'editor', 'thumbnail', 'excerpt'], 'supports' => ['title','editor','thumbnail','excerpt'],
'has_archive' => false, 'has_archive' => false,
'show_in_rest' => true, 'show_in_rest' => true,
]); ]);
register_post_type('floatina_faq', [ register_post_type('floatina_faq', [
'label' => 'Floatina FAQ', 'label' => 'Floatina FAQ',
'public' => true, 'public' => true,
'show_in_menu' => $parent, 'show_in_menu' => $parent,
'menu_icon' => 'dashicons-editor-help', 'menu_icon' => 'dashicons-editor-help',
'supports' => ['title', 'editor'], 'supports' => ['title','editor'],
'has_archive' => false, 'has_archive' => false,
'show_in_rest' => true, 'show_in_rest' => true,
]); ]);
} }
add_action('init', 'floatina_register_cpts'); add_action('init', 'floatina_register_cpts');

View File

@ -0,0 +1,26 @@
<?php
if (!defined('ABSPATH')) exit;
/************************************************************************************
* META BOX: NEWS LINK (SINGLE SOURCE OF TRUTH)
************************************************************************************/
add_action('add_meta_boxes', function(){
add_meta_box('floatina_news_link', 'News Link', 'floatina_news_link_box', 'floatina_news', 'side', 'high');
});
function floatina_news_link_box($post){
$val = get_post_meta($post->ID, '_floatina_news_link', true);
wp_nonce_field('floatina_news_link_nonce', 'floatina_news_link_nonce');
echo '<p><label for="floatina_news_link_field">Target URL</label></p>';
echo '<input type="url" id="floatina_news_link_field" name="floatina_news_link_field" value="' . esc_attr($val) . '" placeholder="https://your-site/page" style="width:100%;">';
echo '<p class="description">Leave empty to use the post permalink.</p>';
}
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');
});

View File

@ -2,89 +2,216 @@
if (!defined('ABSPATH')) exit; if (!defined('ABSPATH')) exit;
/************************************************************************************ /************************************************************************************
* Admin menu + settings * DEFAULTS
************************************************************************************/ ************************************************************************************/
add_action('admin_menu', function () { function floatina_defaults_settings() {
add_menu_page( return [
'Floatina', 'position' => 'right',
'Floatina', 'enable_home' => 1,
'manage_options', 'enable_voice' => 1,
'floatina', 'enable_text' => 1,
'floatina_render_settings', 'enable_news' => 1,
'dashicons-layout', 'enable_faq' => 1,
81 'voice_iframe' => '',
); 'voice_mode_popup' => 0,
add_submenu_page('floatina', 'Settings', 'Settings', 'manage_options', 'floatina', 'floatina_render_settings'); 'text_iframe' => '',
}, 9); 'text_mode_popup' => 0,
'home_extra' => '',
add_action('admin_init', function () { ];
register_setting('floatina_settings_group', 'floatina_settings'); }
function floatina_defaults_style() {
/* ---------- General ---------- */ return [
add_settings_section('floatina_main', 'General', '__return_false', 'floatina'); 'button_color' => '#1e88ff',
add_settings_field('position', 'Panel position', 'floatina_field_position', 'floatina', 'floatina_main'); 'accent_color' => '#1e66ff',
add_settings_field('brand_name', 'Brand name (header)', 'floatina_field_text', 'floatina', 'floatina_main', ['key' => 'brand_name', 'placeholder' => 'Assistant']); 'border_color' => '#e6e8eb',
'nav_active_bg' => '#eef5ff',
/* ---------- Chat targets ---------- */ 'grad_start' => '#ffffff',
add_settings_section('floatina_chat', 'Chat targets', '__return_false', 'floatina'); 'grad_end' => '#f5f7ff',
add_settings_field('voice_iframe', 'Voice URL', 'floatina_field_text', 'floatina', 'floatina_chat', ['key' => 'voice_iframe', 'placeholder' => 'https://...']); 'icon_color' => '#0f172a',
add_settings_field('voice_mode', 'Voice mode', 'floatina_field_mode', 'floatina', 'floatina_chat', ['key' => 'voice_mode']); 'text_color' => '#0f172a',
add_settings_field('text_iframe', 'Text URL', 'floatina_field_text', 'floatina', 'floatina_chat', ['key' => 'text_iframe', 'placeholder' => 'https://...']); 'muted_color' => '#475569',
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 '<input type="text" name="floatina_settings[' . $key . ']" value="' . $val . '" placeholder="' . $ph . '" class="regular-text" />';
} }
function floatina_field_position() /************************************************************************************
{ * SANITIZERS
$v = floatina_opt('position', 'right'); ?> ************************************************************************************/
<label><input type="radio" name="floatina_settings[position]" value="right" <?php checked($v, 'right'); ?> /> Right</label> function floatina_sanitize_settings($input){
&nbsp;&nbsp; $old = get_option('floatina_settings', []);
<label><input type="radio" name="floatina_settings[position]" value="left" <?php checked($v, 'left'); ?> /> Left</label> $out = array_merge(floatina_defaults_settings(), is_array($old)?$old:[]);
<p class="description">Choose bottom-right or bottom-left placement.</p> $chk = function($k){ return isset($_POST['floatina_settings'][$k]) ? 1 : 0; };
<?php }
function floatina_field_mode($args) if (isset($input['position'])) {
{ $out['position'] = in_array($input['position'], ['left','right'], true) ? $input['position'] : 'right';
$opts = get_option('floatina_settings', []); }
$key = $args['key']; $out['enable_home'] = $chk('enable_home');
$val = isset($opts[$key]) ? $opts[$key] : 'iframe'; ?> $out['enable_voice'] = $chk('enable_voice');
<select name="floatina_settings[<?php echo esc_attr($key); ?>]"> $out['enable_text'] = $chk('enable_text');
<option value="iframe" <?php selected($val, 'iframe'); ?>>Iframe (embed)</option> $out['enable_news'] = $chk('enable_news');
<option value="popup" <?php selected($val, 'popup'); ?>>Popup/New tab</option> $out['enable_faq'] = $chk('enable_faq');
</select>
<p class="description">Use Popup/New tab if the target site blocks iframes.</p>
<?php }
function floatina_render_settings() if (array_key_exists('voice_iframe',$input)) $out['voice_iframe'] = esc_url_raw($input['voice_iframe']);
{ ?> $out['voice_mode_popup'] = $chk('voice_mode_popup');
<div class="wrap">
<h1>Floatina</h1> if (array_key_exists('text_iframe',$input)) $out['text_iframe'] = esc_url_raw($input['text_iframe']);
<form method="post" action="options.php"> $out['text_mode_popup'] = $chk('text_mode_popup');
<?php
settings_fields('floatina_settings_group'); if (array_key_exists('home_extra',$input)) $out['home_extra'] = wp_kses_post($input['home_extra']);
do_settings_sections('floatina');
submit_button(); return $out;
?> }
</form> function floatina_sanitize_style($input){
<hr> $old = get_option('floatina_style', []);
<p><strong>Usage:</strong> Renders site-wide. Optional shortcode: <code>[floatina]</code>.</p> $out = array_merge(floatina_defaults_style(), is_array($old)?$old:[]);
<p>Content: add items in <em>Floatina News</em> and <em>Floatina FAQ</em>. Typography inherits your theme.</p> foreach ($out as $k=>$v){
</div> if (array_key_exists($k,$input)) {
<?php } $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());
?>
<div class="wrap fi-admin">
<h1>Floatina</h1>
<h2 class="fi-admin-tabs">
<a class="fi-tab <?php echo $tab==='settings'?'is-active':''; ?>" href="<?php echo esc_url(admin_url('admin.php?page=floatina&tab=settings')); ?>">Settings</a>
<a class="fi-tab <?php echo $tab==='style'?'is-active':''; ?>" href="<?php echo esc_url(admin_url('admin.php?page=floatina&tab=style')); ?>">Style</a>
</h2>
<?php if ($tab==='settings'): ?>
<form method="post" action="options.php" class="fi-form">
<?php settings_fields('floatina_group_settings'); ?>
<div class="fi-field">
<label class="fi-label">Panel position</label>
<div class="fi-segment">
<label><input type="radio" name="floatina_settings[position]" value="right" <?php checked($s['position'],'right'); ?>><span>Right</span></label>
<label><input type="radio" name="floatina_settings[position]" value="left" <?php checked($s['position'],'left'); ?>><span>Left</span></label>
</div>
<p class="description">Choose bottom-right or bottom-left placement.</p>
</div>
<?php
$toggles = [
'enable_home' => '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): ?>
<div class="fi-field">
<label class="fi-label"><?php echo esc_html($label); ?></label>
<label class="fi-toggle">
<input type="checkbox" name="floatina_settings[<?php echo esc_attr($key); ?>]" value="1" <?php checked( (int)$s[$key], 1 ); ?> />
<span class="fi-tgl"></span>
</label>
</div>
<?php endforeach; ?>
<hr class="fi-sep" />
<div class="fi-field">
<label class="fi-label">Voice URL</label>
<input type="url" class="regular-text" name="floatina_settings[voice_iframe]" value="<?php echo esc_attr($s['voice_iframe']); ?>" />
</div>
<div class="fi-field">
<label class="fi-label">Voice mode (Popup if blocked)</label>
<label class="fi-toggle">
<input type="checkbox" name="floatina_settings[voice_mode_popup]" value="1" <?php checked($s['voice_mode_popup'],1); ?> />
<span class="fi-tgl"></span>
</label>
<p class="description">Open in new tab when iframe is blocked.</p>
</div>
<div class="fi-field">
<label class="fi-label">Text URL</label>
<input type="url" class="regular-text" name="floatina_settings[text_iframe]" value="<?php echo esc_attr($s['text_iframe']); ?>" />
</div>
<div class="fi-field">
<label class="fi-label">Text mode (Popup if blocked)</label>
<label class="fi-toggle">
<input type="checkbox" name="floatina_settings[text_mode_popup]" value="1" <?php checked($s['text_mode_popup'],1); ?> />
<span class="fi-tgl"></span>
</label>
<p class="description">Open in new tab when iframe is blocked.</p>
</div>
<hr class="fi-sep" />
<div class="fi-field">
<label class="fi-label">Home extra block</label>
<?php
wp_editor($s['home_extra'], 'floatina_home_extra', [
'textarea_name' => 'floatina_settings[home_extra]',
'textarea_rows' => 6,
'media_buttons' => true,
'tinymce' => true,
]);
?>
<p class="description">Optional block under the Hi card on Home. Leave empty to hide.</p>
</div>
<?php submit_button('Save settings'); ?>
</form>
<?php else: ?>
<form method="post" action="options.php" class="fi-form">
<?php settings_fields('floatina_group_style'); ?>
<div class="fi-grid2">
<?php
$labels = [
'button_color'=>'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): ?>
<div class="fi-field">
<label class="fi-label"><?php echo esc_html($label); ?></label>
<input type="text" class="fi-color" name="floatina_style[<?php echo esc_attr($key); ?>]" value="<?php echo esc_attr($c[$key]); ?>">
</div>
<?php endforeach; ?>
</div>
<?php submit_button('Save style'); ?>
</form>
<?php endif; ?>
</div>
<?php
}

View File

@ -2,10 +2,45 @@
if (!defined('ABSPATH')) exit; if (!defined('ABSPATH')) exit;
/************************************************************************************ /************************************************************************************
* Option getter with default * TEMPLATE HELPERS
************************************************************************************/ ************************************************************************************/
function floatina_opt($key, $default = '') if (!function_exists('floatina_locate_template')) {
{ function floatina_locate_template($file) {
$opts = get_option('floatina_settings', []); $theme_path = trailingslashit(get_stylesheet_directory()) . 'floatina/' . trim($file, '/');
return isset($opts[$key]) && $opts[$key] !== '' ? $opts[$key] : $default; if (file_exists($theme_path)) return $theme_path;
return trailingslashit(FLOATINA_DIR) . 'templates/' . trim($file, '/');
}
}
if (!function_exists('floatina_include_template')) {
function floatina_include_template($file) {
$p = floatina_locate_template($file);
if (file_exists($p)) include $p;
else echo '<div class="fi-card"><h4>Template missing</h4><div class="fi-muted">' . esc_html($file) . '</div></div>';
}
}
/************************************************************************************
* 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;
}
} }

View File

@ -1,26 +1,29 @@
<?php <?php
if (!defined('ABSPATH')) exit; if (!defined('ABSPATH')) exit;
/************************************************************************************ /************************************************************************************
* FAQ partial * FAQ ACCORDION (A11Y)
************************************************************************************/ ************************************************************************************/
$q = new WP_Query([ $q = new WP_Query([
'post_type' => 'floatina_faq', 'post_type' => 'floatina_faq',
'post_status' => 'publish', 'post_status' => 'publish',
'posts_per_page' => 20, 'posts_per_page' => 20,
'orderby' => ['menu_order' => 'ASC', 'date' => 'ASC'], 'orderby' => ['menu_order'=>'ASC','date'=>'ASC'],
]); ]);
?>
if ($q->have_posts()): <div class="fi-accordion" role="region" aria-label="FAQs">
while ($q->have_posts()): $q->the_post(); ?> <?php if ($q->have_posts()): while($q->have_posts()): $q->the_post();
<div class="fi-card"> $id = 'fi-acc-' . get_the_ID(); ?>
<h4><?php echo esc_html(get_the_title()); ?></h4> <div class="fi-acc-item">
<div class="fi-muted"><?php echo wp_kses_post(wpautop(get_the_content())); ?></div> <button class="fi-acc-btn" aria-expanded="false" aria-controls="<?php echo esc_attr($id); ?>">
</div> <span class="fi-acc-title"><?php echo esc_html(get_the_title()); ?></span>
<?php endwhile; <span class="fi-acc-ico" aria-hidden="true"></span>
wp_reset_postdata(); </button>
else: ?> <div id="<?php echo esc_attr($id); ?>" class="fi-acc-panel" role="region" aria-hidden="true">
<div class="fi-card"> <div class="fi-acc-inner"><?php echo wp_kses_post(wpautop(get_the_content())); ?></div>
<h4>No FAQs</h4> </div>
<div class="fi-muted">Add posts in Floatina FAQ.</div> </div>
</div> <?php endwhile; wp_reset_postdata(); else: ?>
<?php endif; ?> <div class="fi-card"><h4>No FAQs</h4><div class="fi-muted">Add posts in Floatina FAQ.</div></div>
<?php endif; ?>
</div>

View File

@ -1,28 +1,32 @@
<?php <?php
if (!defined('ABSPATH')) exit; if (!defined('ABSPATH')) exit;
/************************************************************************************ /************************************************************************************
* News partial * NEWS CARDS LIST
************************************************************************************/ ************************************************************************************/
$q = new WP_Query([ $q = new WP_Query([
'post_type' => 'floatina_news', 'post_type' => 'floatina_news',
'post_status' => 'publish', 'post_status' => 'publish',
'posts_per_page' => 8, 'posts_per_page' => 8,
'orderby' => 'date', 'orderby' => 'date',
'order' => 'DESC', 'order' => 'DESC',
]); ]);
if ($q->have_posts()): if ($q->have_posts()):
while ($q->have_posts()): $q->the_post(); ?> while ($q->have_posts()): $q->the_post();
<article class="fi-card"> $url = get_post_meta(get_the_ID(), '_floatina_news_link', true);
<h4><?php echo esc_html(get_the_title()); ?></h4> $href = $url ? $url : get_permalink();
<div class="fi-muted"><?php echo esc_html(get_the_excerpt()); ?></div> $thumb = get_the_post_thumbnail_url(get_the_ID(), 'medium'); ?>
<div style="margin-top:8px;"><a href="<?php echo esc_url(get_permalink()); ?>" target="_blank" rel="noopener">Open</a></div>
</article>
<?php endwhile;
wp_reset_postdata(); <a class="fi-news-card" href="<?php echo esc_url($href); ?>" <?php echo $ext ? 'target="_blank" rel="noopener noreferrer"' : ''; ?>>
<?php endwhile;
wp_reset_postdata();
else: ?> else: ?>
<div class="fi-card"> <div class="fi-card">
<h4>No news</h4> <h4>No news</h4>
<div class="fi-muted">Add posts in Floatina News.</div> <div class="fi-muted">Add posts in Floatina News.</div>
</div> </div>
<?php endif; ?> <?php endif;

View File

@ -1,2 +1,124 @@
<?php if (!defined('ABSPATH')) exit; ?> <?php
<div id="floatina-root" aria-live="polite" aria-busy="false"></div> if (!defined('ABSPATH')) exit;
/* === Local template loader (tabs-first, no forwarders) ===================== */
/* Search order:
* 1) theme/floatina/tabs/<file>
* 2) plugin/templates/tabs/<file>
* 3) (legacy) theme/floatina/<file>
* 4) (legacy) plugin/templates/<file>
*/
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 '<div class="fi-card" style="border-color:#dc2626">';
echo '<h4 style="margin:0 0 6px;color:#dc2626">Template missing</h4>';
echo '<div class="fi-muted">Could not find: <code>' . esc_html($rel_file) . '</code> in <code>templates/tabs/</code> or legacy paths.</div>';
echo '</div>';
}
}
}
/* === 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,'/'));
};
?>
<!-- === FAB (open/close) === -->
<button id="floatina-fab"
class="<?php echo $pos==='left' ? 'is-left' : 'is-right'; ?>"
aria-expanded="false"
aria-label="<?php echo esc_attr__('Open Floatina','floatina'); ?>">
<img src="<?php echo $icon('bi-chat-dots.svg'); ?>" alt="" width="24" height="24" />
</button>
<!-- === Panel container === -->
<div id="floatina-panel" class="<?php echo $pos==='left' ? 'is-left' : 'is-right'; ?>">
<div class="floatina-body">
<!-- === Content panes === -->
<div class="floatina-content" id="fi-content">
<?php
$first = true;
foreach ($panes as $p):
$attrs = '';
if (!empty($p['kind'])) {
$u = isset($p['url']) ? esc_url($p['url']) : '';
$m = isset($p['mode']) ? esc_attr($p['mode']) : 'iframe';
$attrs = ' data-kind="'.esc_attr($p['kind']).'" data-url="'.$u.'" data-mode="'.$m.'"';
}
?>
<section id="<?php echo esc_attr($p['id']); ?>"
class="fi-pane<?php echo $first ? ' is-active' : ''; ?>"
role="tabpanel"<?php echo $attrs; ?>>
<?php /* === Include from templates/tabs/ with legacy fallback === */ ?>
<?php floatina_panel_include($p['tmpl']); ?>
</section>
<?php
$first = false;
endforeach;
?>
</div>
<!-- === Bottom nav === -->
<div class="floatina-nav" role="tablist">
<?php
$first = true;
foreach ($panes as $p): ?>
<button class="floatina-tab<?php echo $first ? ' is-active' : ''; ?>"
id="<?php echo esc_attr(str_replace('pane','tab',$p['id'])); ?>"
role="tab"
data-target="<?php echo esc_attr($p['id']); ?>">
<span class="fi-ico">
<img class="fi-ico-img" src="<?php echo $icon($p['icon']); ?>" alt="" width="24" height="24" />
</span>
<span class="fi-lbl"><?php echo esc_html($p['lbl']); ?></span>
</button>
<?php
$first = false;
endforeach; ?>
</div>
</div>
</div>

View File

@ -0,0 +1,32 @@
<?php
if (!defined('ABSPATH')) exit;
/* Hook for adding custom content above the accordion from the theme */
do_action('floatina_faq_before');
$q = new WP_Query([
'post_type' => 'floatina_faq',
'post_status' => 'publish',
'posts_per_page' => 20,
'orderby' => ['menu_order'=>'ASC','date'=>'ASC'],
]);
?>
<div class="fi-accordion">
<?php if ($q->have_posts()): while($q->have_posts()): $q->the_post(); ?>
<div class="fi-acc-item">
<button class="fi-acc-btn" aria-expanded="false">
<span><?php echo esc_html(get_the_title()); ?></span>
<span class="fi-acc-ico" aria-hidden="true"></span>
</button>
<div class="fi-acc-panel" aria-hidden="true">
<div class="fi-acc-inner">
<?php echo wp_kses_post(wpautop(get_the_content())); ?>
</div>
</div>
</div>
<?php endwhile; wp_reset_postdata(); else: ?>
<div class="fi-card"><h4>No FAQs</h4><div class="fi-muted">Add posts in Floatina FAQ.</div></div>
<?php endif; ?>
</div>
<?php do_action('floatina_faq_after'); ?>

View File

@ -0,0 +1,32 @@
<?php
if (!defined('ABSPATH')) exit;
$extra = floatina_opt('home_extra', '');
?>
<div class="fi-hero">
<div class="fi-hero-title">
<h1>Hi</h1>
<div class="fi-hero-sub">How can we help you today?</div>
</div>
</div>
<?php if (!empty($extra)): ?>
<div class="fi-card"><?php echo wp_kses_post($extra); ?></div>
<?php endif; ?>
<div class="fi-list">
<a class="fi-list-card" href="/contact">
<div class="fi-list-body">
<div class="fi-list-title">Contact Support</div>
<div class="fi-list-desc">Not live chat, but well get back to you asap.</div>
</div>
<div class="fi-list-ico">?</div>
</a>
<a class="fi-list-card" href="/upgrade">
<div class="fi-list-body">
<div class="fi-list-title">Lifetime License Upgrade Form</div>
<div class="fi-list-desc">Upgrade lower to a higher Lifetime tier.</div>
</div>
<div class="fi-list-ico"></div>
</a>
</div>

View File

@ -0,0 +1,30 @@
<?php
if (!defined('ABSPATH')) exit;
$q = new WP_Query([
'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();
$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();
?>
<a class="fi-news-card" href="<?php echo esc_url($href); ?>" target="_blank" rel="noopener">
<div class="fi-news-media" style="<?php echo $thumb?'background-image:url('.esc_url($thumb).')':''; ?>"></div>
<div class="fi-news-body">
<h4 class="fi-news-title"><?php echo esc_html(get_the_title()); ?></h4>
<p class="fi-news-excerpt"><?php echo esc_html($excerpt); ?></p>
<span class="fi-news-cta">Open </span>
</div>
</a>
<?php endwhile; wp_reset_postdata();
else: ?>
<div class="fi-card"><h4>No news</h4><div class="fi-muted">Add posts in Floatina News.</div></div>
<?php endif; ?>

View File

@ -0,0 +1,2 @@
<?php if (!defined('ABSPATH')) exit; ?>
<div class="floatina-iframe" data-embed="container"></div>

View File

@ -0,0 +1,2 @@
<?php if (!defined('ABSPATH')) exit; ?>
<div class="floatina-iframe" data-embed="container"></div>