Version 1.2.0
28
floatina/assets/css/admin.css
Normal 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; } }
|
||||
30
floatina/assets/css/docked.css
Normal 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; }
|
||||
}
|
||||
@ -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}
|
||||
}
|
||||
|
||||
6
floatina/assets/icons/bi-chat-dots.svg
Normal 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 |
3
floatina/assets/icons/bi-chat.svg
Normal 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 |
3
floatina/assets/icons/bi-house.svg
Normal 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 |
4
floatina/assets/icons/bi-mic.svg
Normal 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 |
5
floatina/assets/icons/bi-newspaper.svg
Normal 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 |
5
floatina/assets/icons/bi-question-circle.svg
Normal 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 |
3
floatina/assets/icons/bi-x.svg
Normal 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 |
35
floatina/assets/js/accordion.js
Normal 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);
|
||||
16
floatina/assets/js/admin.js
Normal 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
@ -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();
|
||||
});
|
||||
})();
|
||||
164
floatina/assets/js/docked.js
Normal 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();
|
||||
});
|
||||
})();
|
||||
@ -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);
|
||||
})();
|
||||
@ -1,53 +1,58 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Plugin Name: Floatina
|
||||
* 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
|
||||
* Text Domain: floatina
|
||||
*/
|
||||
|
||||
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_URL', plugin_dir_url(__FILE__));
|
||||
|
||||
/************************************************************************************
|
||||
* INCLUDES
|
||||
************************************************************************************/
|
||||
require_once FLOATINA_DIR . 'includes/helpers.php';
|
||||
require_once FLOATINA_DIR . 'includes/class-assets.php';
|
||||
require_once FLOATINA_DIR . 'includes/class-cpts.php';
|
||||
require_once FLOATINA_DIR . 'includes/class-settings.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 () {
|
||||
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 () {
|
||||
floatina_register_cpts();
|
||||
flush_rewrite_rules();
|
||||
floatina_register_cpts();
|
||||
flush_rewrite_rules();
|
||||
});
|
||||
|
||||
/************************************************************************************
|
||||
* Render panel automatically in footer on frontend
|
||||
* FRONT-END RENDER (FOOTER)
|
||||
************************************************************************************/
|
||||
add_action('wp_footer', function () {
|
||||
if (is_admin()) return;
|
||||
include FLOATINA_DIR . 'templates/panel.php';
|
||||
if (is_admin()) return;
|
||||
include FLOATINA_DIR . 'templates/panel.php';
|
||||
}, 999);
|
||||
|
||||
/************************************************************************************
|
||||
* Optional shortcode [floatina]
|
||||
* SHORTCODE
|
||||
************************************************************************************/
|
||||
add_shortcode('floatina', function () {
|
||||
ob_start();
|
||||
include FLOATINA_DIR . 'templates/panel.php';
|
||||
return ob_get_clean();
|
||||
ob_start();
|
||||
include FLOATINA_DIR . 'templates/panel.php';
|
||||
return ob_get_clean();
|
||||
});
|
||||
|
||||
@ -2,20 +2,18 @@
|
||||
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_nopriv_floatina_partial_news', 'floatina_partial_news');
|
||||
function floatina_partial_news()
|
||||
{
|
||||
include FLOATINA_DIR . 'templates/loop-news.php';
|
||||
wp_die();
|
||||
function floatina_partial_news(){
|
||||
include FLOATINA_DIR . 'templates/loop-news.php';
|
||||
wp_die();
|
||||
}
|
||||
|
||||
add_action('wp_ajax_floatina_partial_faq', 'floatina_partial_faq');
|
||||
add_action('wp_ajax_nopriv_floatina_partial_faq', 'floatina_partial_faq');
|
||||
function floatina_partial_faq()
|
||||
{
|
||||
include FLOATINA_DIR . 'templates/loop-faq.php';
|
||||
wp_die();
|
||||
function floatina_partial_faq(){
|
||||
include FLOATINA_DIR . 'templates/loop-faq.php';
|
||||
wp_die();
|
||||
}
|
||||
|
||||
@ -2,40 +2,191 @@
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
/************************************************************************************
|
||||
* Enqueue frontend assets and localize settings
|
||||
* FRONT-END ASSETS
|
||||
************************************************************************************/
|
||||
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');
|
||||
wp_enqueue_script('floatina');
|
||||
/* === Base paths / versions === */
|
||||
$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 = [
|
||||
'brand_name' => 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 '<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>';
|
||||
});
|
||||
|
||||
@ -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');
|
||||
|
||||
26
floatina/includes/class-meta.php
Normal 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');
|
||||
});
|
||||
@ -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 '<input type="text" name="floatina_settings[' . $key . ']" value="' . $val . '" placeholder="' . $ph . '" class="regular-text" />';
|
||||
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'); ?>
|
||||
<label><input type="radio" name="floatina_settings[position]" value="right" <?php checked($v, 'right'); ?> /> Right</label>
|
||||
|
||||
<label><input type="radio" name="floatina_settings[position]" value="left" <?php checked($v, 'left'); ?> /> Left</label>
|
||||
<p class="description">Choose bottom-right or bottom-left placement.</p>
|
||||
<?php }
|
||||
/************************************************************************************
|
||||
* SANITIZERS
|
||||
************************************************************************************/
|
||||
function floatina_sanitize_settings($input){
|
||||
$old = get_option('floatina_settings', []);
|
||||
$out = array_merge(floatina_defaults_settings(), is_array($old)?$old:[]);
|
||||
$chk = function($k){ return isset($_POST['floatina_settings'][$k]) ? 1 : 0; };
|
||||
|
||||
function floatina_field_mode($args)
|
||||
{
|
||||
$opts = get_option('floatina_settings', []);
|
||||
$key = $args['key'];
|
||||
$val = isset($opts[$key]) ? $opts[$key] : 'iframe'; ?>
|
||||
<select name="floatina_settings[<?php echo esc_attr($key); ?>]">
|
||||
<option value="iframe" <?php selected($val, 'iframe'); ?>>Iframe (embed)</option>
|
||||
<option value="popup" <?php selected($val, 'popup'); ?>>Popup/New tab</option>
|
||||
</select>
|
||||
<p class="description">Use Popup/New tab if the target site blocks iframes.</p>
|
||||
<?php }
|
||||
if (isset($input['position'])) {
|
||||
$out['position'] = in_array($input['position'], ['left','right'], true) ? $input['position'] : 'right';
|
||||
}
|
||||
$out['enable_home'] = $chk('enable_home');
|
||||
$out['enable_voice'] = $chk('enable_voice');
|
||||
$out['enable_text'] = $chk('enable_text');
|
||||
$out['enable_news'] = $chk('enable_news');
|
||||
$out['enable_faq'] = $chk('enable_faq');
|
||||
|
||||
function floatina_render_settings()
|
||||
{ ?>
|
||||
<div class="wrap">
|
||||
<h1>Floatina</h1>
|
||||
<form method="post" action="options.php">
|
||||
<?php
|
||||
settings_fields('floatina_settings_group');
|
||||
do_settings_sections('floatina');
|
||||
submit_button();
|
||||
?>
|
||||
</form>
|
||||
<hr>
|
||||
<p><strong>Usage:</strong> Renders site-wide. Optional shortcode: <code>[floatina]</code>.</p>
|
||||
<p>Content: add items in <em>Floatina News</em> and <em>Floatina FAQ</em>. Typography inherits your theme.</p>
|
||||
</div>
|
||||
<?php }
|
||||
if (array_key_exists('voice_iframe',$input)) $out['voice_iframe'] = esc_url_raw($input['voice_iframe']);
|
||||
$out['voice_mode_popup'] = $chk('voice_mode_popup');
|
||||
|
||||
if (array_key_exists('text_iframe',$input)) $out['text_iframe'] = esc_url_raw($input['text_iframe']);
|
||||
$out['text_mode_popup'] = $chk('text_mode_popup');
|
||||
|
||||
if (array_key_exists('home_extra',$input)) $out['home_extra'] = wp_kses_post($input['home_extra']);
|
||||
|
||||
return $out;
|
||||
}
|
||||
function floatina_sanitize_style($input){
|
||||
$old = get_option('floatina_style', []);
|
||||
$out = array_merge(floatina_defaults_style(), is_array($old)?$old:[]);
|
||||
foreach ($out as $k=>$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());
|
||||
?>
|
||||
<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
|
||||
}
|
||||
|
||||
@ -2,10 +2,45 @@
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
/************************************************************************************
|
||||
* Option getter with default
|
||||
* TEMPLATE HELPERS
|
||||
************************************************************************************/
|
||||
function floatina_opt($key, $default = '')
|
||||
{
|
||||
$opts = get_option('floatina_settings', []);
|
||||
return isset($opts[$key]) && $opts[$key] !== '' ? $opts[$key] : $default;
|
||||
if (!function_exists('floatina_locate_template')) {
|
||||
function floatina_locate_template($file) {
|
||||
$theme_path = trailingslashit(get_stylesheet_directory()) . 'floatina/' . trim($file, '/');
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,26 +1,29 @@
|
||||
<?php
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
/************************************************************************************
|
||||
* FAQ partial
|
||||
* FAQ ACCORDION (A11Y)
|
||||
************************************************************************************/
|
||||
$q = new WP_Query([
|
||||
'post_type' => '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(); ?>
|
||||
<div class="fi-card">
|
||||
<h4><?php echo esc_html(get_the_title()); ?></h4>
|
||||
<div class="fi-muted"><?php echo wp_kses_post(wpautop(get_the_content())); ?></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 class="fi-accordion" role="region" aria-label="FAQs">
|
||||
<?php if ($q->have_posts()): while($q->have_posts()): $q->the_post();
|
||||
$id = 'fi-acc-' . get_the_ID(); ?>
|
||||
<div class="fi-acc-item">
|
||||
<button class="fi-acc-btn" aria-expanded="false" aria-controls="<?php echo esc_attr($id); ?>">
|
||||
<span class="fi-acc-title"><?php echo esc_html(get_the_title()); ?></span>
|
||||
<span class="fi-acc-ico" aria-hidden="true"></span>
|
||||
</button>
|
||||
<div id="<?php echo esc_attr($id); ?>" class="fi-acc-panel" role="region" 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>
|
||||
|
||||
@ -1,28 +1,32 @@
|
||||
<?php
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
/************************************************************************************
|
||||
* News partial
|
||||
* NEWS CARDS LIST
|
||||
************************************************************************************/
|
||||
$q = new WP_Query([
|
||||
'post_type' => '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(); ?>
|
||||
<article class="fi-card">
|
||||
<h4><?php echo esc_html(get_the_title()); ?></h4>
|
||||
<div class="fi-muted"><?php echo esc_html(get_the_excerpt()); ?></div>
|
||||
<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();
|
||||
while ($q->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'); ?>
|
||||
|
||||
|
||||
|
||||
<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: ?>
|
||||
<div class="fi-card">
|
||||
<h4>No news</h4>
|
||||
<div class="fi-muted">Add posts in Floatina News.</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="fi-card">
|
||||
<h4>No news</h4>
|
||||
<div class="fi-muted">Add posts in Floatina News.</div>
|
||||
</div>
|
||||
<?php endif;
|
||||
|
||||
@ -1,2 +1,124 @@
|
||||
<?php if (!defined('ABSPATH')) exit; ?>
|
||||
<div id="floatina-root" aria-live="polite" aria-busy="false"></div>
|
||||
<?php
|
||||
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>
|
||||
|
||||
32
floatina/templates/tabs/tab-faq.php
Normal 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'); ?>
|
||||
32
floatina/templates/tabs/tab-home.php
Normal 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 we’ll 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>
|
||||
30
floatina/templates/tabs/tab-news.php
Normal 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; ?>
|
||||
2
floatina/templates/tabs/tab-text.php
Normal file
@ -0,0 +1,2 @@
|
||||
<?php if (!defined('ABSPATH')) exit; ?>
|
||||
<div class="floatina-iframe" data-embed="container"></div>
|
||||
2
floatina/templates/tabs/tab-voice.php
Normal file
@ -0,0 +1,2 @@
|
||||
<?php if (!defined('ABSPATH')) exit; ?>
|
||||
<div class="floatina-iframe" data-embed="container"></div>
|
||||