diff --git a/floatina/assets/css/admin.css b/floatina/assets/css/admin.css
new file mode 100644
index 0000000..fcc2537
--- /dev/null
+++ b/floatina/assets/css/admin.css
@@ -0,0 +1,28 @@
+/************************************************************************************
+ * Floatina – Admin styling
+ ************************************************************************************/
+.fi-admin { --fi-gap:14px; --fi-muted:#6b7280; }
+.fi-admin .fi-admin-tabs { display:flex; gap:8px; margin:18px 0 16px; }
+.fi-admin .fi-admin-tabs .fi-tab{ display:inline-flex; align-items:center; gap:8px; padding:8px 14px; border:1px solid #e5e7eb; border-radius:10px; background:#fff; text-decoration:none; color:#111827; }
+.fi-admin .fi-admin-tabs .fi-tab.is-active{ background:#0ea5ff10; border-color:#93c5fd; box-shadow:0 6px 14px rgba(0,0,0,.06); }
+
+.fi-form .fi-field{ margin:18px 0; }
+.fi-form .fi-label{ display:block; font-weight:600; margin-bottom:6px; }
+.fi-form .description{ color:var(--fi-muted); margin:6px 0 0; }
+
+.fi-segment{ display:inline-flex; gap:6px; background:#f3f4f6; padding:4px; border-radius:12px; border:1px solid #e5e7eb; }
+.fi-segment label{ display:inline-flex; align-items:center; }
+.fi-segment input{ appearance:none; }
+.fi-segment span{ display:inline-block; padding:6px 12px; border-radius:10px; }
+.fi-segment input:checked + span{ background:#fff; border:1px solid #dbeafe; box-shadow:0 4px 10px rgba(0,0,0,.06); }
+
+.fi-toggle{ position:relative; display:inline-block; width:50px; height:28px; }
+.fi-toggle input{ display:none; }
+.fi-toggle .fi-tgl{ position:absolute; inset:0; background:#e5e7eb; border-radius:999px; transition:.2s ease; }
+.fi-toggle .fi-tgl:after{ content:""; position:absolute; width:22px; height:22px; top:3px; left:3px; border-radius:50%; background:#fff; box-shadow:0 2px 6px rgba(0,0,0,.15); transition:.2s ease; }
+.fi-toggle input:checked + .fi-tgl{ background:#60a5fa; }
+.fi-toggle input:checked + .fi-tgl:after{ transform:translateX(22px); }
+
+.fi-grid2{ display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:16px; }
+.fi-color{ width:220px; }
+@media (max-width: 1024px){ .fi-grid2{ grid-template-columns:1fr; } }
diff --git a/floatina/assets/css/docked.css b/floatina/assets/css/docked.css
new file mode 100644
index 0000000..89bc760
--- /dev/null
+++ b/floatina/assets/css/docked.css
@@ -0,0 +1,30 @@
+/* path: floatina/assets/css/docked.css */
+/* === Vertical resize handle for Docked mode (visible on desktop & mobile) === */
+#floatina-panel .fi-resize-v{
+ position:absolute;
+ left:8px; right:8px; top:-10px;
+ height:16px;
+ cursor:ns-resize;
+ border-radius:12px;
+ background:transparent; /* keep invisible; theme can draw grip if desired */
+ z-index:2;
+}
+#floatina-panel.is-open .fi-resize-v{pointer-events:auto}
+
+/* === Shim overlay captures pointer events above iframes while resizing === */
+#floatina-panel .fi-shim{
+ position:absolute;
+ inset:0;
+ pointer-events:none;
+ background:transparent;
+ z-index:3;
+}
+#floatina-panel .fi-shim.is-active{ pointer-events:auto; }
+
+/* === Optional: cursor feedback while resizing === */
+#floatina-panel.is-resizing{ user-select:none; }
+
+/* === Bigger handle on small screens for better touch target === */
+@media (max-width:640px){
+ #floatina-panel .fi-resize-v{ top:-12px; height:20px; }
+}
diff --git a/floatina/assets/css/floatina.css b/floatina/assets/css/floatina.css
index cf2a543..66753c6 100644
--- a/floatina/assets/css/floatina.css
+++ b/floatina/assets/css/floatina.css
@@ -1,254 +1,177 @@
/************************************************************************************
- * Floatina – core layout
+ * Floatina – Apple glass style (front-end) – HEX variables wired to settings
************************************************************************************/
-/* Inherit typography from theme */
-#floatina-panel,
-#floatina-panel * {
- font-family: inherit;
+:root{
+ --floatina-offset:16px;
+ --floatina-width:min(92vw,520px);
+ --floatina-height:72vh;
+ --floatina-radius-outer:24px;
+ --floatina-radius-inner:16px;
+ --floatina-padding:10px;
+ --floatina-fab:#1e88ff;
+ --floatina-accent:#1e66ff;
+ --floatina-border:#e6e8eb;
+ --floatina-nav-active:#eef5ff;
+ --floatina-grad-start:#ffffff;
+ --floatina-grad-end:#f5f7ff;
+
+ /* === Runtime vars (updated by JS) === */
+ --fi-nav-h: 0px; /* bottom nav height for embed iframes */
+ --fi-panel-h: var(--floatina-height); /* current panel height */
+ --fi-fab-h: 60px; /* FAB size so we نقدر نحسب مكانه فوق اللوحة */
}
-/* CSS vars for theming via settings */
-:root {
- --floatina-offset: 16px;
- --floatina-width: min(92vw, 460px);
- --floatina-height: 70vh;
- --floatina-radius: 16px;
- --floatina-shadow: 0 20px 50px rgba(0, 0, 0, 0.25);
- --floatina-border: #e6e8eb;
- --floatina-bg: #ffffff;
- --floatina-soft: #f7f8fa;
- --floatina-fab: #1e88ff;
- --floatina-accent: #1e66ff;
- --floatina-grad-start: #bfe0ff;
- --floatina-grad-end: #eaf4ff;
- --floatina-nav-active: #eef5ff;
+/* === FAB === */
+#floatina-fab{
+ position:fixed;
+ bottom:var(--floatina-offset);
+ width:var(--fi-fab-h);
+ height:var(--fi-fab-h);
+ border-radius:20px;
+ display:flex;align-items:center;justify-content:center;
+ background:var(--floatina-fab);color:#fff;
+ box-shadow:0 12px 28px rgba(0,0,0,.22), inset 0 1px 0 rgba(255,255,255,.4);
+ cursor:pointer;z-index:10000;
+ /* === no transition on top/right/left/bottom to follow instantly === */
+ transition:transform .2s ease, box-shadow .2s ease;
+}
+#floatina-fab:hover{transform:translateY(-3px);box-shadow:0 16px 34px rgba(0,0,0,.26), inset 0 1px 0 rgba(255,255,255,.5)}
+#floatina-fab img{width:26px;height:26px;display:block}
+#floatina-fab.is-right{right:var(--floatina-offset)}
+#floatina-fab.is-left{left:var(--floatina-offset)}
+
+/* === Panel (glass) === */
+#floatina-panel{
+ position:fixed;
+ bottom:calc(var(--floatina-offset) + 72px);
+ width:var(--floatina-width);
+ max-height:var(--floatina-height);
+ border-radius:var(--floatina-radius-outer);
+ background:rgba(255,255,255,.65);
+ backdrop-filter:saturate(160%) blur(14px);
+ border:1px solid var(--floatina-border);
+ box-shadow:0 24px 60px rgba(0,0,0,.28);
+ overflow:hidden;
+ opacity:0; pointer-events:none;
+ transform:translateY(18px);
+ transition:opacity .25s ease, transform .25s ease;
+ z-index:9999; box-sizing:border-box;
+}
+#floatina-panel.is-open{opacity:1;transform:translateY(0);pointer-events:auto}
+#floatina-panel.is-right{right:var(--floatina-offset)}
+#floatina-panel.is-left{left:var(--floatina-offset)}
+
+/* === Hide any legacy header/close button === */
+.floatina-header,.floatina-close{display:none !important}
+
+/* === Body / Content === */
+.floatina-body{display:flex;flex-direction:column;height:calc(var(--floatina-height));background:transparent}
+.floatina-content{flex:1;overflow:auto;position:relative;padding:var(--floatina-padding);min-height:0}
+.floatina-content > *:first-child{margin-top:0}
+
+/* === Bottom nav === */
+.floatina-nav{display:flex;gap:8px;justify-content:space-between;border-top:1px solid var(--floatina-border);background:#ffffffcc;backdrop-filter:saturate(160%) blur(10px);padding:10px}
+.floatina-tab{flex:1;display:flex;flex-direction:column;align-items:center;gap:6px;padding:10px 8px;border-radius:14px;font-size:12px;color:#0f172a;cursor:pointer;user-select:none;background:#fff;border:1px solid var(--floatina-border);transition:transform .15s ease, box-shadow .2s ease, background .2s ease}
+.floatina-tab:hover{transform:translateY(-2px);box-shadow:0 8px 20px rgba(0,0,0,.12)}
+.floatina-tab.is-active{background:var(--floatina-nav-active);color:var(--floatina-accent);border-color:var(--floatina-accent)}
+.floatina-tab .fi-ico img{width:24px;height:24px;display:block}
+
+/* === Cards === */
+.fi-card{background:#fff;border:1px solid var(--floatina-border);border-radius:var(--floatina-radius-inner);padding:12px;margin:10px 0;box-shadow:0 10px 24px rgba(0,0,0,.08)}
+.fi-card h4{margin:0 0 6px;font-size:14px}
+.fi-muted{color:#475569;font-size:12px}
+
+/* === Buttons === */
+.fi-actions{display:flex;gap:8px;flex-wrap:wrap}
+.fi-btn{display:inline-flex;align-items:center;justify-content:center;height:38px;padding:0 14px;border-radius:12px;background:#fff;border:1px solid var(--floatina-border);text-decoration:none;color:#0f172a;transition:transform .1s ease, box-shadow .2s ease}
+.fi-btn:hover{transform:translateY(-1px);box-shadow:0 8px 20px rgba(0,0,0,.1)}
+
+/* === Iframe area (for non-embed panes) === */
+.floatina-iframe{position:relative;height:60vh;background:#fff;border:1px solid var(--floatina-border);border-radius:var(--floatina-radius-inner);overflow:hidden;box-shadow:0 12px 24px rgba(0,0,0,.08);z-index:0}
+.floatina-iframe iframe{width:100%;height:100%;border:0;display:block;pointer-events:auto;position:relative;z-index:1}
+
+/* === EMBED MODE (Voice/Text) === */
+.floatina-content.fi-embed{padding:0;overflow:hidden}
+.floatina-content.fi-embed .fi-pane{height:100%}
+.floatina-content.fi-embed .floatina-iframe{
+ height:calc(100% - var(--fi-nav-h) - env(safe-area-inset-bottom, 0px)); /* keeps nav visible */
+ border-radius:0;border:0;box-shadow:none;
}
-/* FAB */
-#floatina-fab {
- position: fixed;
- bottom: var(--floatina-offset);
- width: 56px;
- height: 56px;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- background: var(--floatina-fab, #1e88ff);
- color: #fff;
- box-shadow: 0 8px 22px rgba(0, 0, 0, 0.2);
- cursor: pointer;
- z-index: 9999;
- transition: transform 0.2s ease;
-}
-#floatina-fab:hover {
- transform: translateY(-2px);
-}
-#floatina-fab svg {
- width: 26px;
- height: 26px;
- display: block;
-}
+/* === Loader === */
+.fi-loader{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;z-index:2;background:linear-gradient(180deg, rgba(255,255,255,.65), rgba(255,255,255,.35));backdrop-filter:blur(6px) saturate(140%)}
+.fi-loader-inner{display:flex;flex-direction:column;align-items:center;gap:10px;padding:14px 18px;border-radius:16px;background:rgba(255,255,255,.75);border:1px solid var(--floatina-border);box-shadow:0 12px 28px rgba(0,0,0,.12)}
+.fi-spinner{width:40px;height:40px;border-radius:50%;border:3px solid rgba(0,0,0,.12);border-top-color:var(--floatina-accent);animation:fi-spin 1s linear infinite}
+.fi-loader-txt{font-size:12px;color:#334155}
+@keyframes fi-spin{to{transform:rotate(360deg)}}
-/* Left/Right placement */
-#floatina-fab.is-right {
- right: var(--floatina-offset);
-}
-#floatina-fab.is-left {
- left: var(--floatina-offset);
-}
-#floatina-panel.is-right {
- right: var(--floatina-offset);
-}
-#floatina-panel.is-left {
- left: var(--floatina-offset);
-}
+/* === Accordion === */
+.fi-accordion{display:block}
+.fi-acc-item{background:#fff;border:1px solid var(--floatina-border);border-radius:var(--floatina-radius-inner);margin:8px 0;overflow:hidden}
+.fi-acc-btn{width:100%;display:flex;align-items:center;justify-content:space-between;gap:12px;background:transparent;border:0;padding:14px 12px;font-weight:600;cursor:pointer}
+.fi-acc-btn:hover{background:var(--floatina-nav-active)}
+.fi-acc-ico::before{content:"+";display:inline-block;transition:transform .2s ease}
+.fi-acc-btn[aria-expanded="true"] .fi-acc-ico::before{content:"–"}
+.fi-acc-panel{max-height:0;overflow:hidden;transition:max-height .25s ease}
+.fi-acc-inner{padding:0 12px 12px;color:#334155}
-/* Panel shell */
-#floatina-panel {
- position: fixed;
- bottom: calc(var(--floatina-offset) + 66px);
- width: var(--floatina-width);
- max-height: var(--floatina-height);
- border-radius: var(--floatina-radius);
- background: var(--floatina-bg);
- box-shadow: var(--floatina-shadow);
- overflow: hidden;
- opacity: 0;
- pointer-events: none;
- transform: translateY(16px);
- transition: opacity 0.25s ease, transform 0.25s ease;
- z-index: 9999;
-}
-#floatina-panel.is-open {
- opacity: 1;
- transform: translateY(0);
- pointer-events: auto;
-}
+/* === News === */
+.fi-news-card{display:flex;gap:12px;align-items:stretch;text-decoration:none;background:#fff;border:1px solid var(--floatina-border);border-radius:var(--floatina-radius-inner);overflow:hidden;margin:10px 0;box-shadow:0 10px 24px rgba(0,0,0,.08);transition:transform .15s ease, box-shadow .2s ease}
+.fi-news-card:hover{transform:translateY(-2px);box-shadow:0 16px 36px rgba(0,0,0,.14)}
+.fi-news-media{min-width:96px;width:96px;background:#eaeef3;background-size:cover;background-position:center}
+.fi-news-body{padding:12px 12px 12px 4px;display:flex;flex-direction:column;gap:6px}
+.fi-news-title{font-size:14px;margin:0;color:#0f172a}
+.fi-news-excerpt{font-size:12px;color:#475569;margin:0}
+.fi-news-cta{margin-top:auto;font-size:12px;color:var(--floatina-accent)}
-/* Header */
-.floatina-header {
- position: relative;
- background: linear-gradient(
- 180deg,
- var(--floatina-grad-start),
- var(--floatina-grad-end)
- );
- padding: 18px 16px 12px;
-}
-.floatina-title {
- margin: 0;
- font-size: 18px;
- font-weight: 700;
-}
-.floatina-close {
- position: absolute;
- right: 12px;
- top: 12px;
- width: 28px;
- height: 28px;
- border-radius: 6px;
- background: rgba(0, 0, 0, 0.05);
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
-}
+/* === Tab panes === */
+.fi-pane{display:none;height:100%}
+.fi-pane.is-active{display:block}
-/* Body */
-.floatina-body {
- display: flex;
- flex-direction: column;
- height: calc(var(--floatina-height) - 118px);
- background: var(--floatina-soft);
-}
-.floatina-content {
- flex: 1;
- overflow: auto;
- padding: 12px;
-}
+/* === Home hero === */
+.fi-hero{position:relative;border-radius:var(--floatina-radius-inner);padding:18px 16px 20px;background:linear-gradient(180deg, rgba(180,220,255,.95), rgba(236,246,255,.95));border:1px solid var(--floatina-border);box-shadow:0 16px 36px rgba(0,0,0,.10);margin:0 0 14px 0}
+.fi-hero-title h1{margin:0 0 6px;font-size:28px;line-height:1.1;color:#0f172a;font-weight:800}
+.fi-hero-title .fi-hero-sub{font-size:18px;color:#0f172a;opacity:.9}
-/* Bottom nav */
-.floatina-nav {
- display: flex;
- gap: 6px;
- justify-content: space-between;
- border-top: 1px solid var(--floatina-border);
- background: #fff;
- padding: 10px;
-}
-.floatina-tab {
- flex: 1;
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 4px;
- padding: 8px 6px;
- border-radius: 12px;
- font-size: 12px;
- color: #333;
- cursor: pointer;
- user-select: none;
- background: #fff;
- border: 1px solid var(--floatina-border);
-}
-.floatina-tab.is-active {
- background: var(--floatina-nav-active);
- color: var(--floatina-accent);
- border-color: #b9d0ff;
-}
-.floatina-tab svg {
- width: 20px;
- height: 20px;
- display: block;
-}
-.floatina-tab .fi-ico {
- line-height: 0;
-}
+/* === Home list cards === */
+.fi-list{display:flex;flex-direction:column;gap:12px}
+.fi-list-card{display:flex;align-items:center;justify-content:space-between;gap:14px;text-decoration:none;background:#fff;border:1px solid var(--floatina-border);border-radius:var(--floatina-radius-inner);padding:14px 16px;box-shadow:0 10px 24px rgba(0,0,0,.08);transition:transform .15s ease, box-shadow .2s ease, background .2s ease}
+.fi-list-card:hover{transform:translateY(-1px);box-shadow:0 16px 36px rgba(0,0,0,.14);background:#fcfdff}
+.fi-list-body{min-width:0}
+.fi-list-title{font-size:16px;font-weight:700;color:#0f172a;margin-bottom:4px}
+.fi-list-desc{font-size:14px;color:#475569;opacity:.95;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:60ch}
+.fi-list-ico{width:22px;height:22px;display:grid;place-items:center;color:#0f172a;opacity:.85}
-/* Cards + gradient */
-.fi-card {
- background: #fff;
- border: 1px solid var(--floatina-border);
- border-radius: 12px;
- padding: 12px;
- margin: 8px 0;
-}
-.fi-card h4 {
- margin: 0 0 6px;
- font-size: 14px;
-}
-.fi-muted {
- color: #6b7280;
- font-size: 12px;
-}
-.fi-gradient {
- background: linear-gradient(180deg, #dbeafe, #eef2ff);
- border: none;
-}
+/* === Mobile (fix width + X above panel and follows resize) === */
+@media (max-width:640px){
+ :root{--floatina-width:100vw;--floatina-height:85vh}
-/* Buttons inside Home */
-.fi-actions {
- display: flex;
- gap: 8px;
- flex-wrap: wrap;
-}
-.fi-btn {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- height: 36px;
- padding: 0 14px;
- border-radius: 10px;
- background: #fff;
- border: 1px solid var(--floatina-border);
- text-decoration: none;
- color: #111;
-}
-
-/* Iframe area */
-.floatina-iframe {
- position: relative;
- height: 60vh;
- background: #fff;
- border: 1px solid var(--floatina-border);
- border-radius: 12px;
- overflow: hidden;
-}
-.floatina-iframe iframe {
- width: 100%;
- height: 100%;
- border: 0;
- display: block;
-}
-
-/* Mobile fullscreen behavior */
-@media (max-width: 640px) {
- :root {
- --floatina-width: 100vw;
- --floatina-height: 100vh;
+ /* === Fill width symmetrically using safe-areas; ignore .is-left/.is-right offsets === */
+ #floatina-panel,
+ #floatina-panel.is-left,
+ #floatina-panel.is-right{
+ bottom:0;
+ left:env(safe-area-inset-left, 0px);
+ right:env(safe-area-inset-right, 0px);
+ width:auto;
+ max-height:100dvh;
+ border-radius:16px 16px 0 0;
+ transform:none;
}
- #floatina-panel {
- bottom: 0;
- right: 0;
- left: 0;
- width: 100vw;
- max-height: 100vh;
- border-radius: 0;
- transform: translateY(12px);
- }
- #floatina-panel.is-open {
- transform: translateY(0);
- }
- .floatina-body {
- height: calc(100vh - 66px);
- }
- #floatina-fab {
- bottom: 12px;
- }
-}
-/* Color override from settings via inline vars */
-#floatina-fab[data-color] {
- background: var(--floatina-fab-color, #1e88ff);
+ .floatina-body{height:calc(var(--floatina-height))}
+
+ /* === FAB default (bottom-right) === */
+ #floatina-fab{left:auto;right:calc(12px + env(safe-area-inset-right, 0px));bottom:calc(12px + env(safe-area-inset-bottom, 0px))}
+
+ /* === FAB on open: sit 5px ABOVE panel's top and FOLLOW it (no transition) === */
+ #floatina-fab.is-top{
+ bottom:auto;
+ /* panelTop = 100dvh - panelH ; place FAB so bottom is 5px above panelTop => top = panelTop - FAB_H - 5px */
+ top:calc(100dvh - var(--fi-panel-h, var(--floatina-height)) - var(--fi-fab-h) - 5px);
+ right:calc(12px + env(safe-area-inset-right, 0px));
+ }
+
+ .floatina-nav{padding:10px 10px calc(10px + env(safe-area-inset-bottom, 0px))}
+ .floatina-content{padding:10px}
}
diff --git a/floatina/assets/icons/bi-chat-dots.svg b/floatina/assets/icons/bi-chat-dots.svg
new file mode 100644
index 0000000..7325f0d
--- /dev/null
+++ b/floatina/assets/icons/bi-chat-dots.svg
@@ -0,0 +1,6 @@
+
diff --git a/floatina/assets/icons/bi-chat.svg b/floatina/assets/icons/bi-chat.svg
new file mode 100644
index 0000000..845aa32
--- /dev/null
+++ b/floatina/assets/icons/bi-chat.svg
@@ -0,0 +1,3 @@
+
diff --git a/floatina/assets/icons/bi-house.svg b/floatina/assets/icons/bi-house.svg
new file mode 100644
index 0000000..215c854
--- /dev/null
+++ b/floatina/assets/icons/bi-house.svg
@@ -0,0 +1,3 @@
+
diff --git a/floatina/assets/icons/bi-mic.svg b/floatina/assets/icons/bi-mic.svg
new file mode 100644
index 0000000..c4cc720
--- /dev/null
+++ b/floatina/assets/icons/bi-mic.svg
@@ -0,0 +1,4 @@
+
diff --git a/floatina/assets/icons/bi-newspaper.svg b/floatina/assets/icons/bi-newspaper.svg
new file mode 100644
index 0000000..34e887b
--- /dev/null
+++ b/floatina/assets/icons/bi-newspaper.svg
@@ -0,0 +1,5 @@
+
diff --git a/floatina/assets/icons/bi-question-circle.svg b/floatina/assets/icons/bi-question-circle.svg
new file mode 100644
index 0000000..7ccc8fb
--- /dev/null
+++ b/floatina/assets/icons/bi-question-circle.svg
@@ -0,0 +1,5 @@
+
diff --git a/floatina/assets/icons/bi-x.svg b/floatina/assets/icons/bi-x.svg
new file mode 100644
index 0000000..e33ca52
--- /dev/null
+++ b/floatina/assets/icons/bi-x.svg
@@ -0,0 +1,3 @@
+
diff --git a/floatina/assets/js/accordion.js b/floatina/assets/js/accordion.js
new file mode 100644
index 0000000..e0969ae
--- /dev/null
+++ b/floatina/assets/js/accordion.js
@@ -0,0 +1,35 @@
+/************************************************************************************
+ * Floatina – Accordion controller
+ ************************************************************************************/
+(function (root) {
+ 'use strict';
+
+ function init(container) {
+ var scope = container || document;
+ var items = scope.querySelectorAll('.fi-acc-item');
+ if (!items.length) return;
+
+ items.forEach(function (item) {
+ var btn = item.querySelector('.fi-acc-btn');
+ var panel = item.querySelector('.fi-acc-panel');
+ if (!btn || !panel || btn.__fiBound) return;
+
+ btn.__fiBound = true;
+
+ btn.addEventListener('click', function () {
+ var expanded = btn.getAttribute('aria-expanded') === 'true';
+ var next = !expanded;
+
+ btn.setAttribute('aria-expanded', String(next));
+ panel.style.maxHeight = next ? (panel.scrollHeight + 'px') : '0px';
+ panel.setAttribute('aria-hidden', String(!next));
+ });
+
+ btn.addEventListener('keydown', function (e) {
+ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); btn.click(); }
+ });
+ });
+ }
+
+ root.FloatinaAccordion = { init: init };
+})(window);
diff --git a/floatina/assets/js/admin.js b/floatina/assets/js/admin.js
new file mode 100644
index 0000000..2030ba7
--- /dev/null
+++ b/floatina/assets/js/admin.js
@@ -0,0 +1,16 @@
+/************************************************************************************
+ * Floatina – Admin UI helpers (color pickers, tabs feel)
+ ************************************************************************************/
+(function($){
+ $(function(){
+ // Color pickers
+ $('.fi-color').wpColorPicker();
+
+ // Visual active state on tabs
+ const tabs = $('.fi-admin .fi-admin-tabs .fi-tab');
+ tabs.on('click', function(){
+ tabs.removeClass('is-active');
+ $(this).addClass('is-active');
+ });
+ });
+})(jQuery);
diff --git a/floatina/assets/js/core.js b/floatina/assets/js/core.js
new file mode 100644
index 0000000..eff6761
--- /dev/null
+++ b/floatina/assets/js/core.js
@@ -0,0 +1,198 @@
+// path: floatina/assets/js/core.js
+/************************************************************************************
+ * Floatina – Core controller (FAB, tabs, iframe loader)
+ ************************************************************************************/
+(function () {
+ 'use strict';
+
+ var CFG = window.FLOATINA || {};
+ var S = { open: false };
+ var $ = function (sel, root) { return (root || document).querySelector(sel); };
+ var $$ = function (sel, root) { return Array.from((root || document).querySelectorAll(sel)); };
+
+ // === Mobile detector ===
+ function isMobile() {
+ try {
+ return (window.matchMedia && (window.matchMedia('(max-width: 640px)').matches || window.matchMedia('(pointer:coarse)').matches));
+ } catch(e){ return false; }
+ }
+
+ // === Compute bottom nav height and expose as CSS var for embed iframes ===
+ function updateEmbedSafeArea() {
+ var c = $('#fi-content');
+ if (!c) return;
+ var nav = $('.floatina-nav');
+ var h = (c.classList.contains('fi-embed') && nav) ? nav.offsetHeight : 0;
+ c.style.setProperty('--fi-nav-h', (h || 0) + 'px');
+ }
+
+ function setEmbed(on) {
+ var c = $('#fi-content');
+ if (c) {
+ c.classList.toggle('fi-embed', !!on);
+ updateEmbedSafeArea();
+ }
+ }
+
+ function showPane(id) {
+ $$('.fi-pane').forEach(function (p) { p.classList.remove('is-active'); });
+ var pane = $('#' + id);
+ if (!pane) return;
+ pane.classList.add('is-active');
+
+ $$('.floatina-tab').forEach(function (t) { t.classList.remove('is-active'); });
+ var btn = document.querySelector('.floatina-tab[data-target="' + id + '"]');
+ if (btn) btn.classList.add('is-active');
+
+ if (pane.dataset && pane.dataset.kind) {
+ ensureIframe(pane);
+ } else {
+ setEmbed(false);
+ if (id === 'fi-pane-help' && window.FloatinaAccordion) {
+ window.FloatinaAccordion.init(pane);
+ }
+ }
+
+ updateEmbedSafeArea();
+ }
+
+ function ensureIframe(pane) {
+ var url = pane.getAttribute('data-url') || '';
+ var mode = pane.getAttribute('data-mode') || 'iframe';
+ if (!url) { setEmbed(false); return; }
+
+ if (mode === 'popup') {
+ setEmbed(false);
+ window.open(url, '_blank', 'noopener');
+ return;
+ }
+
+ setEmbed(true);
+ if (pane.__floatinaLoaded) { updateEmbedSafeArea(); return; }
+
+ var container = pane.querySelector('[data-embed="container"]');
+ if (!container) return;
+
+ var loader = document.createElement('div');
+ loader.className = 'fi-loader';
+ loader.innerHTML = '
';
+
+ var iframe = document.createElement('iframe');
+ iframe.title = pane.dataset.kind || 'embed';
+ iframe.loading = 'eager';
+ iframe.allow = 'microphone; camera; autoplay; clipboard-read; clipboard-write; geolocation; display-capture; web-share; fullscreen';
+ iframe.style.display = 'none';
+ iframe.style.pointerEvents = 'auto';
+
+ container.appendChild(iframe);
+ container.appendChild(loader);
+ setTimeout(function(){ iframe.src = url; }, 0);
+
+ var done = false;
+ var timer = null;
+
+ function cleanup() {
+ done = true;
+ if (loader.parentNode) loader.parentNode.removeChild(loader);
+ if (timer) clearTimeout(timer);
+ }
+
+ iframe.addEventListener('load', function () {
+ if (done) return;
+ cleanup();
+ iframe.style.display = 'block';
+ pane.__floatinaLoaded = true;
+ updateEmbedSafeArea();
+ });
+
+ timer = setTimeout(function () {
+ if (done) return;
+ cleanup();
+ setEmbed(false);
+ var card = document.createElement('div');
+ card.className = 'fi-card';
+ card.innerHTML = 'Cannot embed
The site refused to connect or took too long.
';
+ var a = card.querySelector('a'); a.href = url;
+ pane.innerHTML = '';
+ pane.appendChild(card);
+ updateEmbedSafeArea();
+ }, 10000);
+ }
+
+ function bind() {
+ var fab = $('#floatina-fab');
+ var panel = $('#floatina-panel');
+
+ if (fab) {
+ fab.addEventListener('click', function () {
+ S.open = !S.open;
+ panel.classList.toggle('is-open', S.open);
+ swapFabIcon(fab, S.open ? 'bi-x.svg' : 'bi-chat-dots.svg');
+ fab.setAttribute('aria-expanded', S.open ? 'true' : 'false');
+
+ // === On mobile, float close button to the top when open ===
+ if (isMobile()) {
+ fab.classList.toggle('is-top', S.open);
+ }
+
+ if (S.open) updateEmbedSafeArea();
+ });
+ }
+
+ $$('.floatina-tab').forEach(function (btn) {
+ btn.addEventListener('click', function () {
+ var target = btn.getAttribute('data-target');
+ if (target) showPane(target);
+ });
+ });
+
+ document.addEventListener('keydown', function (e) {
+ if (S.open && e.key === 'Escape') {
+ S.open = false;
+ panel.classList.remove('is-open');
+ swapFabIcon(fab, 'bi-chat-dots.svg');
+ if (fab) {
+ fab.setAttribute('aria-expanded', 'false');
+ fab.classList.remove('is-top');
+ }
+ }
+ });
+
+ window.addEventListener('resize', function () {
+ if (fab && isMobile() && S.open) fab.classList.add('is-top');
+ updateEmbedSafeArea();
+ });
+ }
+
+ function swapFabIcon(fab, file) {
+ if (!fab) return;
+ var img = fab.querySelector('img');
+ if (!img) {
+ img = document.createElement('img');
+ img.width = 24; img.height = 24; img.alt = '';
+ fab.innerHTML = ''; fab.appendChild(img);
+ }
+ var map = (window.FLOATINA && window.FLOATINA.iconsMap) ? window.FLOATINA.iconsMap : {};
+ img.src = map[file] || '';
+ }
+
+ document.addEventListener('DOMContentLoaded', function () {
+ bind();
+
+ var helpPane = $('#fi-pane-help');
+ if (helpPane && helpPane.classList.contains('is-active') && window.FloatinaAccordion) {
+ window.FloatinaAccordion.init(helpPane);
+ }
+
+ var active = document.querySelector('.floatina-tab.is-active');
+ if (!active) {
+ var first = document.querySelector('.floatina-tab');
+ if (first) {
+ var target = first.getAttribute('data-target');
+ if (target) showPane(target);
+ }
+ }
+
+ updateEmbedSafeArea();
+ });
+})();
diff --git a/floatina/assets/js/docked.js b/floatina/assets/js/docked.js
new file mode 100644
index 0000000..d54327a
--- /dev/null
+++ b/floatina/assets/js/docked.js
@@ -0,0 +1,164 @@
+// path: floatina/assets/js/docked.js
+/************************************************************************************
+ * FloatinaDocked – vertical resize (desktop + mobile)
+ * - Drag handle at top edge
+ * - Shim over iframes while dragging
+ * - Persists height (localStorage -> cookie fallback)
+ * - Exposes --fi-panel-h CSS var globally so FAB can follow the panel
+ ************************************************************************************/
+(function () {
+ 'use strict';
+
+ /* === Storage helpers === */
+ var KEY = 'floatina_docked_h_v1';
+ var store = {
+ get: function () {
+ try { var v = localStorage.getItem(KEY); if (v) return parseInt(v, 10) || null; } catch(e){}
+ var m = document.cookie.match(new RegExp('(?:^|; )' + KEY.replace(/([.$?*|{}()[\]\\/+^])/g,'\\$1') + '=([^;]*)'));
+ return m ? parseInt(decodeURIComponent(m[1]), 10) || null : null;
+ },
+ set: function (px) {
+ try { localStorage.setItem(KEY, String(px)); } catch(e){}
+ try { document.cookie = KEY + '=' + encodeURIComponent(String(px)) + '; path=/; SameSite=Lax; max-age='+(60*60*24*365); } catch(e){}
+ },
+ clear: function () {
+ try { localStorage.removeItem(KEY); } catch(e){}
+ try { document.cookie = KEY + '=; path=/; max-age=0'; } catch(e){}
+ }
+ };
+
+ /* === Utils === */
+ var clamp = function (v, min, max) { return Math.max(min, Math.min(max, v)); };
+ var $ = function (sel, root) { return (root || document).querySelector(sel); };
+
+ /* === Media info (cap max height on small screens) === */
+ var isSmall = false;
+ try { isSmall = window.matchMedia && window.matchMedia('(max-width: 640px)').matches; } catch(e){}
+
+ /* === Public API === */
+ var API = {
+ panelSel: '#floatina-panel',
+ handleCls: 'fi-resize-v',
+ shimCls: 'fi-shim',
+ minPx: 320,
+ maxVh: isSmall ? 0.98 : 0.92,
+ current: null,
+
+ /* === Apply height (write to panel + global CSS var for FAB) === */
+ apply: function () {
+ var panel = $(API.panelSel);
+ if (!panel) return;
+ var vhMax = Math.round(window.innerHeight * API.maxVh);
+ var h = clamp(API.current || panel.getBoundingClientRect().height, API.minPx, vhMax);
+
+ panel.style.height = h + 'px';
+ panel.style.setProperty('--floatina-height', h + 'px');
+
+ // === Make it global so CSS can position the FAB using calc(100dvh - var(--fi-panel-h))
+ try { document.documentElement.style.setProperty('--fi-panel-h', h + 'px'); } catch(e){}
+
+ document.dispatchEvent(new CustomEvent('floatina:docked:apply', { detail: { height: h } }));
+ },
+
+ setHeight: function (px, persist) {
+ API.current = px;
+ API.apply();
+ if (persist) store.set(API.current);
+ },
+
+ reset: function () {
+ var panel = $(API.panelSel);
+ if (!panel) return;
+ store.clear();
+ API.current = null;
+ panel.style.height = '';
+ panel.style.removeProperty('--floatina-height');
+ try { document.documentElement.style.removeProperty('--fi-panel-h'); } catch(e){}
+ document.dispatchEvent(new CustomEvent('floatina:docked:reset'));
+ },
+
+ init: function () {
+ var panel = $(API.panelSel);
+ if (!panel || panel.__fiDockedInit) return;
+
+ /* === Inject handle and shim === */
+ var handle = document.createElement('div');
+ handle.className = API.handleCls;
+ handle.setAttribute('role', 'separator');
+ handle.setAttribute('aria-label', 'Resize height');
+ panel.appendChild(handle);
+
+ var shim = document.createElement('div');
+ shim.className = API.shimCls;
+ shim.setAttribute('aria-hidden', 'true');
+ panel.appendChild(shim);
+
+ /* === Restore persisted height === */
+ var persisted = store.get();
+ if (persisted && !isNaN(persisted)) {
+ API.current = persisted;
+ API.apply();
+ } else {
+ // === Expose initial height (from CSS) so FAB knows where to sit
+ try {
+ var initH = panel.getBoundingClientRect().height;
+ document.documentElement.style.setProperty('--fi-panel-h', Math.round(initH) + 'px');
+ } catch(e){}
+ }
+
+ /* === Drag logic === */
+ var dragging = false, startY = 0, startH = 0;
+
+ handle.addEventListener('pointerdown', function (e) {
+ if (!panel.classList.contains('is-open')) return;
+ dragging = true;
+ startY = e.clientY;
+ startH = panel.getBoundingClientRect().height;
+ panel.classList.add('is-resizing');
+ shim.classList.add('is-active');
+ Array.from(panel.querySelectorAll('iframe')).forEach(function (f) { f.style.pointerEvents = 'none'; });
+ handle.setPointerCapture(e.pointerId);
+ document.dispatchEvent(new CustomEvent('floatina:docked:resize:start', { detail: { start: startH } }));
+ e.preventDefault();
+ });
+
+ handle.addEventListener('pointermove', function (e) {
+ if (!dragging) return;
+ var vhMax = Math.round(window.innerHeight * API.maxVh);
+ var delta = (startY - e.clientY);
+ API.current = clamp(startH + delta, API.minPx, vhMax);
+ API.apply();
+ document.dispatchEvent(new CustomEvent('floatina:docked:resize', { detail: { height: API.current } }));
+ });
+
+ function finish() {
+ if (!dragging) return;
+ dragging = false;
+ panel.classList.remove('is-resizing');
+ shim.classList.remove('is-active');
+ Array.from(panel.querySelectorAll('iframe')).forEach(function (f) { f.style.pointerEvents = 'auto'; });
+ store.set(API.current);
+ document.dispatchEvent(new CustomEvent('floatina:docked:resize:end', { detail: { height: API.current } }));
+ }
+
+ handle.addEventListener('pointerup', finish);
+ handle.addEventListener('lostpointercapture', finish);
+
+ /* === Keep within caps on viewport change === */
+ window.addEventListener('resize', function () {
+ if (!panel.classList.contains('is-open')) return;
+ if (API.current == null) return;
+ var vhMax = Math.round(window.innerHeight * API.maxVh);
+ API.current = clamp(API.current, API.minPx, vhMax);
+ API.apply();
+ });
+
+ panel.__fiDockedInit = true;
+ window.FloatinaDocked = API;
+ }
+ };
+
+ document.addEventListener('DOMContentLoaded', function () {
+ API.init();
+ });
+})();
diff --git a/floatina/assets/js/floatina.js b/floatina/assets/js/floatina.js
deleted file mode 100644
index 9190541..0000000
--- a/floatina/assets/js/floatina.js
+++ /dev/null
@@ -1,294 +0,0 @@
-/**
- * Floatina – structured controller (no inline HTML/CSS)
- * Modules: Config, State, Utils, Icons, View, Actions, Renderers.
- */
-(function(){
- /* ========================== Config ========================== */
- const CFG = Object.freeze({
- brandName: (window.FLOATINA && window.FLOATINA.brand_name) || 'Assistant',
- buttonColor: (window.FLOATINA && window.FLOATINA.button_color) || '#1e88ff',
- position: (window.FLOATINA && window.FLOATINA.position) || 'right', // right|left
- voiceURL: (window.FLOATINA && window.FLOATINA.voice_iframe) || '',
- textURL: (window.FLOATINA && window.FLOATINA.text_iframe) || '',
- voiceMode: (window.FLOATINA && window.FLOATINA.voice_mode) || 'iframe', // iframe|popup
- textMode: (window.FLOATINA && window.FLOATINA.text_mode) || 'iframe',
- ajaxNews: (window.FLOATINA && window.FLOATINA.ajax && window.FLOATINA.ajax.news) || '',
- ajaxFAQ: (window.FLOATINA && window.FLOATINA.ajax && window.FLOATINA.ajax.faq) || '',
- i18n: (window.FLOATINA && window.FLOATINA.strings) || {home:'Home',voice:'Voice',text:'Text',news:'News',help:'Help',open_new:'Open in new tab'}
- });
-
- /* ========================== State ========================== */
- const S = {
- open: false,
- tab: 'home'
- };
-
- /* ========================== Utils ========================== */
- const NS = 'http://www.w3.org/2000/svg';
-
- /** Create DOM element */
- function el(tag, attrs = {}, children = []) {
- const node = document.createElement(tag);
- for (const k in attrs) {
- if (k === 'class') node.className = attrs[k];
- else if (k === 'dataset') Object.entries(attrs[k]).forEach(([dk, dv]) => node.dataset[dk] = dv);
- else if (k === 'aria') Object.entries(attrs[k]).forEach(([ak, av]) => node.setAttribute(`aria-${ak}`, av));
- else if (k === 'role') node.setAttribute('role', attrs[k]);
- else if (k === 'html') node.innerHTML = attrs[k]; // avoid unless needed
- else node.setAttribute(k, attrs[k]);
- }
- (Array.isArray(children) ? children : [children]).forEach(ch => {
- if (ch == null) return;
- if (typeof ch === 'string') node.appendChild(document.createTextNode(ch));
- else node.appendChild(ch);
- });
- return node;
- }
-
- /** Create SVG element */
- function svg(tag, attrs = {}, children = []) {
- const node = document.createElementNS(NS, tag);
- for (const k in attrs) node.setAttribute(k, attrs[k]);
- (Array.isArray(children) ? children : [children]).forEach(ch => { if (ch) node.appendChild(ch); });
- return node;
- }
-
- /** Clear element children */
- function clear(node){ while (node.firstChild) node.removeChild(node.firstChild); }
-
- /** Text */
- function t(key){ return CFG.i18n[key] || key; }
-
- /* ========================== Icons (pure SVG DOM) ========================== */
- function icoBubble(){
- return svg('svg', {viewBox:'0 0 16 16', 'aria-hidden':'true'}, [
- svg('path', {d:'M8 1a7 7 0 00-5.6 11.2L1.5 15l2.9-.9A7 7 0 108 1z', fill:'currentColor'})
- ]);
- }
- function icoX(){
- return svg('svg', {viewBox:'0 0 16 16', 'aria-hidden':'true'}, [
- svg('path', {d:'M3.5 3.5l9 9m0-9l-9 9', stroke:'currentColor','stroke-width':'1.75','stroke-linecap':'round', fill:'none'})
- ]);
- }
- function icoHome(){
- return svg('svg', {viewBox:'0 0 16 16','aria-hidden':'true'}, [
- svg('path', {d:'M8 3l6 5v6h-4V9H6v5H2V8l6-5z', fill:'currentColor'})
- ]);
- }
- function icoMic(){
- return svg('svg', {viewBox:'0 0 16 16','aria-hidden':'true'}, [
- svg('path', {d:'M8 11a3 3 0 003-3V5a3 3 0 10-6 0v3a3 3 0 003 3z', fill:'currentColor'}),
- svg('path', {d:'M3 8a5 5 0 0010 0H11a3 3 0 11-6 0H3z', fill:'currentColor'}),
- svg('path', {d:'M7 13h2v2H7z', fill:'currentColor'})
- ]);
- }
- function icoChat(){
- return svg('svg', {viewBox:'0 0 16 16','aria-hidden':'true'}, [
- svg('path', {d:'M2 2h12v9H6l-3 3V2z', fill:'currentColor'})
- ]);
- }
- function icoNews(){
- return svg('svg', {viewBox:'0 0 16 16','aria-hidden':'true'}, [
- svg('path', {d:'M1 6l12-4v8L1 6zm12 2h2V4h-2v4zM5 9l1 4h2L7 9z', fill:'currentColor'})
- ]);
- }
- function icoHelp(){
- return svg('svg', {viewBox:'0 0 16 16','aria-hidden':'true'}, [
- svg('path', {d:'M8 1a7 7 0 107 7A7 7 0 008 1zm1 11H7v-2h2zm1.6-5.2a2.4 2.4 0 00-4.6 1h2a.8.8 0 111.5-.3c0 .3-.2.5-.6.7A2.5 2.5 0 007 10h2a3.5 3.5 0 001.6-3.2z', fill:'currentColor'})
- ]);
- }
-
- /* ========================== View (refs) ========================== */
- const V = { fab:null, panel:null, content:null, close:null, tabs:{} };
-
- /* ========================== Build UI ========================== */
- function buildFab(){
- const b = el('button', {id:'floatina-fab', 'aria':{label:'Open Floatina'}});
- b.appendChild(icoBubble());
- if (CFG.position === 'left') b.classList.add('is-left'); else b.classList.add('is-right');
- if (CFG.buttonColor){
- b.dataset.color = '1';
- b.style.setProperty('--floatina-fab-color', CFG.buttonColor);
- }
- document.body.appendChild(b);
- V.fab = b;
- }
-
- function buildPanel(){
- const wrap = el('div', {id:'floatina-panel'});
- if (CFG.position === 'left') wrap.classList.add('is-left'); else wrap.classList.add('is-right');
-
- // Header
- const header = el('div', {class:'floatina-header'});
- const title = el('div', {class:'floatina-title'}, CFG.brandName);
- const btnX = el('div', {class:'floatina-close', 'aria':{label:'Close'}}, icoX());
- header.appendChild(title);
- header.appendChild(btnX);
-
- // Body
- const body = el('div', {class:'floatina-body'});
- const content= el('div', {class:'floatina-content', id:'fi-content'});
- const nav = el('div', {class:'floatina-nav', role:'tablist'});
-
- // Tabs
- V.tabs.home = buildTab('home', icoHome(), t('home'));
- V.tabs.voice = buildTab('voice', icoMic(), t('voice'));
- V.tabs.text = buildTab('text', icoChat(), t('text'));
- V.tabs.news = buildTab('news', icoNews(), t('news'));
- V.tabs.help = buildTab('help', icoHelp(), t('help'));
- nav.append(V.tabs.home, V.tabs.voice, V.tabs.text, V.tabs.news, V.tabs.help);
-
- body.appendChild(content);
- body.appendChild(nav);
-
- wrap.append(header, body);
- document.body.appendChild(wrap);
-
- V.panel = wrap;
- V.content = content;
- V.close = btnX;
- }
-
- function buildTab(id, iconNode, label){
- const btn = el('button', {class:'floatina-tab', role:'tab', dataset:{tab:id}, 'aria':{controls:`panel-${id}`}});
- const icoWrap = el('span', {class:'fi-ico'}, iconNode);
- const lbl = el('span', {class:'fi-lbl'}, label);
- btn.append(icoWrap, lbl);
- return btn;
- }
-
- /* ========================== Actions ========================== */
- function open(){
- S.open = true;
- V.panel.classList.add('is-open');
- V.fab.setAttribute('aria-expanded','true');
- V.fab.setAttribute('aria-label','Close Floatina');
- swapFabIcon(true);
- }
- function close(){
- S.open = false;
- V.panel.classList.remove('is-open');
- V.fab.setAttribute('aria-expanded','false');
- V.fab.setAttribute('aria-label','Open Floatina');
- swapFabIcon(false);
- }
- function toggle(){ S.open ? close() : open(); }
-
- function swapFabIcon(showX){
- clear(V.fab);
- V.fab.appendChild(showX ? icoX() : icoBubble());
- }
-
- function setActiveTab(id){
- Object.values(V.tabs).forEach(b => b.classList.remove('is-active'));
- if (V.tabs[id]) V.tabs[id].classList.add('is-active');
- }
-
- /* ========================== Renderers ========================== */
- function render(tab){
- S.tab = tab;
- setActiveTab(tab);
- if (tab === 'home') return renderHome();
- if (tab === 'voice') return renderTarget('voice', CFG.voiceURL, CFG.voiceMode);
- if (tab === 'text') return renderTarget('text', CFG.textURL, CFG.textMode);
- if (tab === 'news') return renderPartial(CFG.ajaxNews);
- if (tab === 'help') return renderPartial(CFG.ajaxFAQ);
- }
-
- function renderHome(){
- clear(V.content);
-
- const card1 = el('div', {class:'fi-card fi-gradient'});
- card1.append(el('h4', {}, 'Welcome'), el('div', {class:'fi-muted'}, 'Quick links below.'));
-
- const card2 = el('div', {class:'fi-card'});
- const actions = el('div', {class:'fi-actions'});
- actions.append(
- el('a', {class:'fi-btn', href:'/contact'}, 'Contact'),
- el('a', {class:'fi-btn', href:'/about'}, 'About'),
- el('a', {class:'fi-btn', href:'/support'}, 'Support')
- );
- card2.append(actions);
-
- V.content.append(card1, card2);
- }
-
- function renderTarget(kind, url, mode){
- clear(V.content);
-
- if (!url){
- const card = el('div', {class:'fi-card'});
- card.append(el('h4', {}, `Missing ${kind} URL`), el('div', {class:'fi-muted'}, 'Set it in Floatina settings.'));
- V.content.append(card);
- return;
- }
-
- if (mode === 'popup'){
- const card = el('div', {class:'fi-card'});
- card.append(
- el('h4', {}, kind === 'voice' ? 'Voice chat' : 'Text chat'),
- el('div', {class:'fi-muted'}, 'Opens in a new window.'),
- el('div', {class:'fi-actions'}, el('a', {class:'fi-btn', href:url, target:'_blank', rel:'noopener'}, t('open_new')))
- );
- // optional auto-open once per session
- window.open(url, '_blank', 'noopener');
- V.content.append(card);
- return;
- }
-
- // iframe mode + fallback button
- const wrap = el('div', {class:'floatina-iframe'});
- const iframe = el('iframe', {src:url, loading:'lazy', title:`${kind}`});
- wrap.append(iframe);
-
- const fb = el('div', {class:'fi-card'}, el('div', {class:'fi-actions'},
- el('a', {class:'fi-btn', href:url, target:'_blank', rel:'noopener'}, t('open_new'))
- ));
-
- V.content.append(wrap, fb);
- }
-
- function renderPartial(endpoint){
- clear(V.content);
- const loader = el('div', {class:'fi-card'}, el('h4', {}, 'Loading…'));
- V.content.append(loader);
- if (!endpoint){
- clear(V.content);
- const err = el('div', {class:'fi-card'});
- err.append(el('h4', {}, 'Error'), el('div', {class:'fi-muted'}, 'Endpoint missing.'));
- V.content.append(err);
- return;
- }
- fetch(endpoint, {credentials:'same-origin'})
- .then(r => r.text())
- .then(html => { V.content.innerHTML = html; })
- .catch(() => {
- clear(V.content);
- const err = el('div', {class:'fi-card'});
- err.append(el('h4', {}, 'Error'), el('div', {class:'fi-muted'}, 'Failed to load content.'));
- V.content.append(err);
- });
- }
-
- /* ========================== Bindings ========================== */
- function bind(){
- V.fab.addEventListener('click', toggle);
- V.close.addEventListener('click', close);
-
- Object.values(V.tabs).forEach(btn => {
- btn.addEventListener('click', () => render(btn.dataset.tab));
- });
-
- document.addEventListener('keydown', e => { if (S.open && e.key === 'Escape') close(); });
- }
-
- /* ========================== Init ========================== */
- function init(){
- buildFab();
- buildPanel();
- bind();
- render('home');
- }
-
- document.addEventListener('DOMContentLoaded', init);
-})();
diff --git a/floatina/floatina.php b/floatina/floatina.php
index 94111e6..6184a21 100644
--- a/floatina/floatina.php
+++ b/floatina/floatina.php
@@ -1,53 +1,58 @@
floatina_opt('brand_name', 'Assistant'),
- 'button_color' => floatina_opt('button_color', '#1e88ff'),
- 'position' => floatina_opt('position', 'right'),
- 'voice_iframe' => floatina_opt('voice_iframe', ''),
- 'text_iframe' => floatina_opt('text_iframe', ''),
- 'voice_mode' => floatina_opt('voice_mode', 'iframe'), // iframe|popup
- 'text_mode' => floatina_opt('text_mode', 'iframe'),
- 'strings' => ['home' => 'Home', 'voice' => 'Voice', 'text' => 'Text', 'news' => 'News', 'help' => 'Help', 'open_new' => 'Open in new tab'],
- 'ajax' => [
- 'news' => add_query_arg(['action' => 'floatina_partial_news'], admin_url('admin-ajax.php')),
- 'faq' => add_query_arg(['action' => 'floatina_partial_faq'], admin_url('admin-ajax.php')),
- ],
- ];
- wp_add_inline_script('floatina', 'window.FLOATINA=' . wp_json_encode($data) . ';', 'before');
+ /* === Cache-busting helper (filemtime -> version) === */
+ $asset_ver = function($abs_path) use ($ver) {
+ $t = @filemtime($abs_path);
+ return $t ? $t : $ver;
+ };
- // Inline CSS variables from settings
- $vars = [
- '--floatina-fab:' . floatina_opt('button_color', '#1e88ff'),
- '--floatina-accent:' . floatina_opt('accent_color', '#1e66ff'),
- '--floatina-border:' . floatina_opt('border_color', '#e6e8eb'),
- '--floatina-nav-active:' . floatina_opt('nav_active_bg', '#eef5ff'),
- '--floatina-grad-start:' . floatina_opt('grad_start', '#bfe0ff'),
- '--floatina-grad-end:' . floatina_opt('grad_end', '#eaf4ff'),
- ];
- $css = ':root{' . implode(';', array_map('esc_html', $vars)) . ';}';
- wp_add_inline_style('floatina', $css);
+ /* === CSS: main === */
+ $css_main_rel = 'assets/css/floatina.css';
+ $css_main_path = $base_dir . $css_main_rel;
+ wp_register_style('floatina', $base_url . $css_main_rel, [], $asset_ver($css_main_path));
+ wp_enqueue_style('floatina');
+
+ /* === JS: accordion + core === */
+ $js_acc_rel = 'assets/js/accordion.js';
+ $js_core_rel = 'assets/js/core.js';
+ wp_register_script('floatina-accordion', $base_url . $js_acc_rel, [], $asset_ver($base_dir . $js_acc_rel), true);
+ wp_register_script('floatina-core', $base_url . $js_core_rel, ['floatina-accordion'], $asset_ver($base_dir . $js_core_rel), true);
+ wp_enqueue_script('floatina-core');
+
+ /* === Docked module (optional; only if files exist) === */
+ $js_docked_rel = 'assets/js/docked.js';
+ $css_docked_rel = 'assets/css/docked.css';
+ if ( file_exists($base_dir . $js_docked_rel) ) {
+ wp_enqueue_script(
+ 'floatina-docked',
+ $base_url . $js_docked_rel,
+ ['floatina-core'],
+ $asset_ver($base_dir . $js_docked_rel),
+ true
+ );
+ }
+ if ( file_exists($base_dir . $css_docked_rel) ) {
+ wp_enqueue_style(
+ 'floatina-docked',
+ $base_url . $css_docked_rel,
+ ['floatina'],
+ $asset_ver($base_dir . $css_docked_rel)
+ );
+ }
+
+ /* === Settings payload -> window.FLOATINA (icons + iframe modes) === */
+ $settings = get_option('floatina_settings', []);
+ $settings = wp_parse_args($settings, [
+ 'position' => 'right',
+ 'voice_iframe' => '',
+ 'text_iframe' => '',
+ 'voice_mode_popup' => 0,
+ 'text_mode_popup' => 0,
+ ]);
+
+ $data = [
+ 'position' => $settings['position'],
+ 'voice_iframe' => $settings['voice_iframe'],
+ 'text_iframe' => $settings['text_iframe'],
+ 'voice_mode' => $settings['voice_mode_popup'] ? 'popup' : 'iframe',
+ 'text_mode' => $settings['text_mode_popup'] ? 'popup' : 'iframe',
+ 'iconsMap' => [
+ 'bi-x.svg' => $base_url . 'assets/icons/bi-x.svg',
+ 'bi-chat-dots.svg' => $base_url . 'assets/icons/bi-chat-dots.svg',
+ 'bi-house.svg' => $base_url . 'assets/icons/bi-house.svg',
+ 'bi-mic.svg' => $base_url . 'assets/icons/bi-mic.svg',
+ 'bi-newspaper.svg' => $base_url . 'assets/icons/bi-newspaper.svg',
+ 'bi-question-circle.svg' => $base_url . 'assets/icons/bi-question-circle.svg',
+ 'bi-chat.svg' => $base_url . 'assets/icons/bi-chat.svg',
+ ],
+ ];
+ wp_add_inline_script('floatina-core', 'window.FLOATINA=' . wp_json_encode($data) . ';', 'before');
+
+ /* === CSS Variables from Style tab (HEX only, sanitized) === */
+ $style = get_option('floatina_style', []);
+ $style = wp_parse_args($style, [
+ 'button_color' => '#1e88ff',
+ 'accent_color' => '#1e66ff',
+ 'border_color' => '#e6e8eb',
+ 'nav_active_bg' => '#eef5ff',
+ 'grad_start' => '#ffffff',
+ 'grad_end' => '#f5f7ff',
+ 'icon_color' => '#0f172a',
+ 'text_color' => '#0f172a',
+ 'muted_color' => '#475569',
+ ]);
+
+ $hex = function($val, $fallback){ $v = sanitize_hex_color($val); return $v ? $v : $fallback; };
+ $css = ':root{'
+ . '--floatina-fab:' . $hex($style['button_color'], '#1e88ff') . ';'
+ . '--floatina-accent:' . $hex($style['accent_color'], '#1e66ff') . ';'
+ . '--floatina-border:' . $hex($style['border_color'], '#e6e8eb') . ';'
+ . '--floatina-nav-active:' . $hex($style['nav_active_bg'], '#eef5ff') . ';'
+ . '--floatina-grad-start:' . $hex($style['grad_start'], '#ffffff') . ';'
+ . '--floatina-grad-end:' . $hex($style['grad_end'], '#f5f7ff') . ';'
+ . '--floatina-icon:' . $hex($style['icon_color'], '#0f172a') . ';'
+ . '--floatina-text:' . $hex($style['text_color'], '#0f172a') . ';'
+ . '--floatina-muted:' . $hex($style['muted_color'], '#475569') . ';'
+ . '}';
+ wp_add_inline_style('floatina', $css);
+});
+
+/************************************************************************************
+ * ADMIN ASSETS (ONLY ON PLUGIN PAGE)
+ ************************************************************************************/
+add_action('admin_enqueue_scripts', function ($hook) {
+ if ($hook !== 'toplevel_page_floatina') return;
+
+ $base_url = rtrim(FLOATINA_URL, '/') . '/';
+ $base_dir = trailingslashit( dirname( dirname( __FILE__ ) ) );
+ $ver = defined('FLOATINA_VER') ? FLOATINA_VER : '1.0.0';
+
+ $asset_ver = function($abs_path) use ($ver) {
+ $t = @filemtime($abs_path);
+ return $t ? $t : $ver;
+ };
+
+ wp_enqueue_style('wp-color-picker');
+ wp_enqueue_script('wp-color-picker');
+
+ $css_admin_rel = 'assets/css/admin.css';
+ $js_admin_rel = 'assets/js/admin.js';
+
+ wp_enqueue_style('floatina-admin', $base_url . $css_admin_rel, [], $asset_ver($base_dir . $css_admin_rel));
+ wp_enqueue_script('floatina-admin', $base_url . $js_admin_rel, ['jquery', 'wp-color-picker'], $asset_ver($base_dir . $js_admin_rel), true);
+});
+
+/************************************************************************************
+ * ADMIN MENU – keep only Settings & Style; remove Panel
+ ************************************************************************************/
+add_action('admin_menu', function () {
+ /* === Ensure parent menu exists === */
+ global $admin_page_hooks, $submenu;
+ if ( empty($admin_page_hooks['floatina']) ) return;
+
+ $parent = 'floatina';
+
+ /* === Settings & Style (no Panel) === */
+ $hook_settings = add_submenu_page(
+ $parent,
+ __('Settings','floatina'),
+ __('Settings','floatina'),
+ 'manage_options',
+ 'floatina-settings',
+ '__return_null' /* === content rendered by redirect below === */
+ );
+
+ $hook_style = add_submenu_page(
+ $parent,
+ __('Style','floatina'),
+ __('Style','floatina'),
+ 'manage_options',
+ 'floatina-style',
+ '__return_null'
+ );
+
+ /* === Redirect each submenu to the unified page with proper tab (content exists there) === */
+ add_action('load-' . $hook_settings, function () {
+ wp_safe_redirect( add_query_arg(['page'=>'floatina','tab'=>'settings'], admin_url('admin.php')) );
+ exit;
+ });
+ add_action('load-' . $hook_style, function () {
+ wp_safe_redirect( add_query_arg(['page'=>'floatina','tab'=>'style'], admin_url('admin.php')) );
+ exit;
+ });
+}, 99);
+
+/************************************************************************************
+ * HIDE IN-PAGE TABS ON THE FLOATINA PAGE
+ * - We keep the content logic as-is (tab param), but hide the visual tabs.
+ ************************************************************************************/
+add_action('admin_head', function () {
+ if (!isset($_GET['page']) || $_GET['page'] !== 'floatina') return;
+
+ /* === Inline CSS to hide any tab bar variants (WP default or custom) === */
+ echo '';
});
diff --git a/floatina/includes/class-cpts.php b/floatina/includes/class-cpts.php
index dfc560a..916d8a2 100644
--- a/floatina/includes/class-cpts.php
+++ b/floatina/includes/class-cpts.php
@@ -2,30 +2,29 @@
if (!defined('ABSPATH')) exit;
/************************************************************************************
- * Register CPTs for News and FAQ under the same "Floatina" menu
+ * REGISTER CUSTOM POST TYPES
************************************************************************************/
-function floatina_register_cpts()
-{
- $parent = 'floatina'; // matches settings menu slug
+function floatina_register_cpts() {
+ $parent = 'floatina';
- register_post_type('floatina_news', [
- 'label' => 'Floatina News',
- 'public' => true,
- 'show_in_menu' => $parent,
- 'menu_icon' => 'dashicons-megaphone',
- 'supports' => ['title', 'editor', 'thumbnail', 'excerpt'],
- 'has_archive' => false,
- 'show_in_rest' => true,
- ]);
+ register_post_type('floatina_news', [
+ 'label' => 'Floatina News',
+ 'public' => true,
+ 'show_in_menu' => $parent,
+ 'menu_icon' => 'dashicons-megaphone',
+ 'supports' => ['title','editor','thumbnail','excerpt'],
+ 'has_archive' => false,
+ 'show_in_rest' => true,
+ ]);
- register_post_type('floatina_faq', [
- 'label' => 'Floatina FAQ',
- 'public' => true,
- 'show_in_menu' => $parent,
- 'menu_icon' => 'dashicons-editor-help',
- 'supports' => ['title', 'editor'],
- 'has_archive' => false,
- 'show_in_rest' => true,
- ]);
+ register_post_type('floatina_faq', [
+ 'label' => 'Floatina FAQ',
+ 'public' => true,
+ 'show_in_menu' => $parent,
+ 'menu_icon' => 'dashicons-editor-help',
+ 'supports' => ['title','editor'],
+ 'has_archive' => false,
+ 'show_in_rest' => true,
+ ]);
}
add_action('init', 'floatina_register_cpts');
diff --git a/floatina/includes/class-meta.php b/floatina/includes/class-meta.php
new file mode 100644
index 0000000..4c993de
--- /dev/null
+++ b/floatina/includes/class-meta.php
@@ -0,0 +1,26 @@
+ID, '_floatina_news_link', true);
+ wp_nonce_field('floatina_news_link_nonce', 'floatina_news_link_nonce');
+ echo '';
+ echo '';
+ echo 'Leave empty to use the post permalink.
';
+}
+
+add_action('save_post_floatina_news', function($post_id){
+ if (!isset($_POST['floatina_news_link_nonce']) || !wp_verify_nonce($_POST['floatina_news_link_nonce'], 'floatina_news_link_nonce')) return;
+ if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;
+ if (!current_user_can('edit_post', $post_id)) return;
+ $url = isset($_POST['floatina_news_link_field']) ? esc_url_raw(trim($_POST['floatina_news_link_field'])) : '';
+ if ($url) update_post_meta($post_id, '_floatina_news_link', $url);
+ else delete_post_meta($post_id, '_floatina_news_link');
+});
diff --git a/floatina/includes/class-settings.php b/floatina/includes/class-settings.php
index 55cbafa..e102ad8 100644
--- a/floatina/includes/class-settings.php
+++ b/floatina/includes/class-settings.php
@@ -2,89 +2,216 @@
if (!defined('ABSPATH')) exit;
/************************************************************************************
- * Admin menu + settings
+ * DEFAULTS
************************************************************************************/
-add_action('admin_menu', function () {
- add_menu_page(
- 'Floatina',
- 'Floatina',
- 'manage_options',
- 'floatina',
- 'floatina_render_settings',
- 'dashicons-layout',
- 81
- );
- add_submenu_page('floatina', 'Settings', 'Settings', 'manage_options', 'floatina', 'floatina_render_settings');
-}, 9);
-
-add_action('admin_init', function () {
- register_setting('floatina_settings_group', 'floatina_settings');
-
- /* ---------- General ---------- */
- add_settings_section('floatina_main', 'General', '__return_false', 'floatina');
- add_settings_field('position', 'Panel position', 'floatina_field_position', 'floatina', 'floatina_main');
- add_settings_field('brand_name', 'Brand name (header)', 'floatina_field_text', 'floatina', 'floatina_main', ['key' => 'brand_name', 'placeholder' => 'Assistant']);
-
- /* ---------- Chat targets ---------- */
- add_settings_section('floatina_chat', 'Chat targets', '__return_false', 'floatina');
- add_settings_field('voice_iframe', 'Voice URL', 'floatina_field_text', 'floatina', 'floatina_chat', ['key' => 'voice_iframe', 'placeholder' => 'https://...']);
- add_settings_field('voice_mode', 'Voice mode', 'floatina_field_mode', 'floatina', 'floatina_chat', ['key' => 'voice_mode']);
- add_settings_field('text_iframe', 'Text URL', 'floatina_field_text', 'floatina', 'floatina_chat', ['key' => 'text_iframe', 'placeholder' => 'https://...']);
- add_settings_field('text_mode', 'Text mode', 'floatina_field_mode', 'floatina', 'floatina_chat', ['key' => 'text_mode']);
-
- /* ---------- Colors ---------- */
- add_settings_section('floatina_colors', 'Colors', '__return_false', 'floatina');
- add_settings_field('button_color', 'FAB color', 'floatina_field_text', 'floatina', 'floatina_colors', ['key' => 'button_color', 'placeholder' => '#1e88ff']);
- add_settings_field('accent_color', 'Accent color', 'floatina_field_text', 'floatina', 'floatina_colors', ['key' => 'accent_color', 'placeholder' => '#1e66ff']);
- add_settings_field('border_color', 'Border color', 'floatina_field_text', 'floatina', 'floatina_colors', ['key' => 'border_color', 'placeholder' => '#e6e8eb']);
- add_settings_field('nav_active_bg', 'Tab active background', 'floatina_field_text', 'floatina', 'floatina_colors', ['key' => 'nav_active_bg', 'placeholder' => '#eef5ff']);
- add_settings_field('grad_start', 'Header gradient start', 'floatina_field_text', 'floatina', 'floatina_colors', ['key' => 'grad_start', 'placeholder' => '#bfe0ff']);
- add_settings_field('grad_end', 'Header gradient end', 'floatina_field_text', 'floatina', 'floatina_colors', ['key' => 'grad_end', 'placeholder' => '#eaf4ff']);
-});
-
-function floatina_field_text($args)
-{
- $opts = get_option('floatina_settings', []);
- $key = $args['key'];
- $val = isset($opts[$key]) ? esc_attr($opts[$key]) : '';
- $ph = isset($args['placeholder']) ? esc_attr($args['placeholder']) : '';
- echo '';
+function floatina_defaults_settings() {
+ return [
+ 'position' => 'right',
+ 'enable_home' => 1,
+ 'enable_voice' => 1,
+ 'enable_text' => 1,
+ 'enable_news' => 1,
+ 'enable_faq' => 1,
+ 'voice_iframe' => '',
+ 'voice_mode_popup' => 0,
+ 'text_iframe' => '',
+ 'text_mode_popup' => 0,
+ 'home_extra' => '',
+ ];
+}
+function floatina_defaults_style() {
+ return [
+ 'button_color' => '#1e88ff',
+ 'accent_color' => '#1e66ff',
+ 'border_color' => '#e6e8eb',
+ 'nav_active_bg' => '#eef5ff',
+ 'grad_start' => '#ffffff',
+ 'grad_end' => '#f5f7ff',
+ 'icon_color' => '#0f172a',
+ 'text_color' => '#0f172a',
+ 'muted_color' => '#475569',
+ ];
}
-function floatina_field_position()
-{
- $v = floatina_opt('position', 'right'); ?>
-
-
-
- Choose bottom-right or bottom-left placement.
-
-
- Use Popup/New tab if the target site blocks iframes.
-
-
-
Floatina
-
-
-
Usage: Renders site-wide. Optional shortcode: [floatina].
-
Content: add items in Floatina News and Floatina FAQ. Typography inherits your theme.
-
-$v){
+ if (array_key_exists($k,$input)) {
+ $val = sanitize_hex_color(trim((string)$input[$k]));
+ if ($val) $out[$k] = $val;
+ }
+ }
+ return $out;
+}
+
+/************************************************************************************
+ * ADMIN MENU
+ ************************************************************************************/
+add_action('admin_menu', function () {
+ add_menu_page('Floatina','Floatina','manage_options','floatina','floatina_render_settings','dashicons-layout',81);
+});
+
+/************************************************************************************
+ * REGISTER SETTINGS
+ ************************************************************************************/
+add_action('admin_init', function () {
+ register_setting('floatina_group_settings', 'floatina_settings', [
+ 'type'=>'array','sanitize_callback'=>'floatina_sanitize_settings'
+ ]);
+ register_setting('floatina_group_style', 'floatina_style', [
+ 'type'=>'array','sanitize_callback'=>'floatina_sanitize_style'
+ ]);
+
+ add_settings_section('floatina_section_settings','Settings','__return_false','floatina-settings');
+ add_settings_section('floatina_section_style','Style','__return_false','floatina-style');
+});
+
+/************************************************************************************
+ * SETTINGS PAGE RENDERER
+ ************************************************************************************/
+function floatina_render_settings(){
+ if (!current_user_can('manage_options')) return;
+ $tab = isset($_GET['tab']) ? sanitize_key($_GET['tab']) : 'settings';
+ if (!in_array($tab, ['settings','style'], true)) $tab = 'settings';
+
+ $s = get_option('floatina_settings', floatina_defaults_settings());
+ $c = get_option('floatina_style', floatina_defaults_style());
+ ?>
+
+
Floatina
+
+
+
+
+
+
+
+
+
+
+ Template missing
' . esc_html($file) . '
';
+ }
+}
+
+/************************************************************************************
+ * OPTIONS SHORT HAND
+ ************************************************************************************/
+function floatina_opt($key, $default = '') {
+ $opts = get_option('floatina_settings', []);
+ return isset($opts[$key]) && $opts[$key] !== '' ? $opts[$key] : $default;
+}
+// ----- News link helpers -----
+if (!function_exists('floatina_news_link')) {
+ function floatina_news_link($post_id = 0) {
+ $post_id = $post_id ?: get_the_ID();
+ // أولوية للمفتاح الجديد/الصحيح
+ $url = get_post_meta($post_id, '_floatina_news_link', true);
+ // توافق خلفي مع إصدارات قديمة استعملت المفتاح الخاطئ
+ if (!$url) $url = get_post_meta($post_id, 'floatina_news_url', true);
+ return $url ?: get_permalink($post_id);
+ }
+}
+if (!function_exists('floatina_is_external')) {
+ function floatina_is_external($url) {
+ $host = wp_parse_url($url, PHP_URL_HOST);
+ $home = wp_parse_url(home_url(), PHP_URL_HOST);
+ return $host && $home && strcasecmp($host, $home) !== 0;
+ }
}
diff --git a/floatina/templates/loop-faq.php b/floatina/templates/loop-faq.php
index f2b2825..64421d8 100644
--- a/floatina/templates/loop-faq.php
+++ b/floatina/templates/loop-faq.php
@@ -1,26 +1,29 @@
'floatina_faq',
- 'post_status' => 'publish',
- 'posts_per_page' => 20,
- 'orderby' => ['menu_order' => 'ASC', 'date' => 'ASC'],
+ 'post_type' => 'floatina_faq',
+ 'post_status' => 'publish',
+ 'posts_per_page' => 20,
+ 'orderby' => ['menu_order'=>'ASC','date'=>'ASC'],
]);
-
-if ($q->have_posts()):
- while ($q->have_posts()): $q->the_post(); ?>
-
-
-
-
No FAQs
-
Add posts in Floatina FAQ.
-
-
\ No newline at end of file
+?>
+
+have_posts()): while($q->have_posts()): $q->the_post();
+ $id = 'fi-acc-' . get_the_ID(); ?>
+
+
+
No FAQs
Add posts in Floatina FAQ.
+
+
diff --git a/floatina/templates/loop-news.php b/floatina/templates/loop-news.php
index 208362b..f542d21 100644
--- a/floatina/templates/loop-news.php
+++ b/floatina/templates/loop-news.php
@@ -1,28 +1,32 @@
'floatina_news',
- 'post_status' => 'publish',
- 'posts_per_page' => 8,
- 'orderby' => 'date',
- 'order' => 'DESC',
+ 'post_type' => 'floatina_news',
+ 'post_status' => 'publish',
+ 'posts_per_page' => 8,
+ 'orderby' => 'date',
+ 'order' => 'DESC',
]);
if ($q->have_posts()):
- while ($q->have_posts()): $q->the_post(); ?>
-
-
-
-
-
- have_posts()): $q->the_post();
+ $url = get_post_meta(get_the_ID(), '_floatina_news_link', true);
+ $href = $url ? $url : get_permalink();
+ $thumb = get_the_post_thumbnail_url(get_the_ID(), 'medium'); ?>
+
+
+
+ >
+
+
-
-
No news
-
Add posts in Floatina News.
-
-
\ No newline at end of file
+
+
No news
+
Add posts in Floatina News.
+
+
-
\ No newline at end of file
+
+ * 2) plugin/templates/tabs/
+ * 3) (legacy) theme/floatina/
+ * 4) (legacy) plugin/templates/
+ */
+if (!function_exists('floatina_panel_include')) {
+ function floatina_panel_include($rel_file) {
+ $rel_file = ltrim(str_replace(['..', '\\'], ['', '/'], $rel_file), '/');
+
+ $theme_base = trailingslashit(get_stylesheet_directory()) . 'floatina/';
+ $plugin_base = trailingslashit(dirname(__FILE__)); // .../templates/
+ $plugin_tabs = $plugin_base . 'tabs/';
+
+ $theme_tabs = $theme_base . 'tabs/' . basename($rel_file);
+ $plugin_tabs_file = $plugin_tabs . basename($rel_file);
+ if (file_exists($theme_tabs)) { include $theme_tabs; return; }
+ if (file_exists($plugin_tabs_file)) { include $plugin_tabs_file; return; }
+
+ // Legacy fallback (keeps older themes working)
+ $theme_old = $theme_base . basename($rel_file);
+ $plugin_old = $plugin_base . basename($rel_file);
+ if (file_exists($theme_old)) { include $theme_old; return; }
+ if (file_exists($plugin_old)) { include $plugin_old; return; }
+
+ if (current_user_can('manage_options')) {
+ echo '';
+ echo '
Template missing
';
+ echo '
Could not find: ' . esc_html($rel_file) . ' in templates/tabs/ or legacy paths.
';
+ echo '
';
+ }
+ }
+}
+
+/* === Panel bootstrap (FAB + body + nav) =================================== */
+$pos = floatina_opt('position','right');
+$enable = [
+ 'home' => (int) floatina_opt('enable_home', 1),
+ 'voice' => (int) floatina_opt('enable_voice',1),
+ 'text' => (int) floatina_opt('enable_text', 1),
+ 'news' => (int) floatina_opt('enable_news', 1),
+ 'faq' => (int) floatina_opt('enable_faq', 1),
+];
+
+$voice_url = floatina_opt('voice_iframe','');
+$text_url = floatina_opt('text_iframe','');
+$voice_mode = floatina_opt('voice_mode_popup',0) ? 'popup' : 'iframe';
+$text_mode = floatina_opt('text_mode_popup',0) ? 'popup' : 'iframe';
+
+/* === Panes definition (labels are translatable) =========================== */
+$panes = [];
+if ($enable['home']) $panes[] = ['id'=>'fi-pane-home', 'lbl'=>__('Home','floatina'), 'icon'=>'bi-house.svg', 'kind'=>null, 'tmpl'=>'tab-home.php'];
+if ($enable['voice']) $panes[] = ['id'=>'fi-pane-voice', 'lbl'=>__('Voice','floatina'),'icon'=>'bi-mic.svg', 'kind'=>'voice','tmpl'=>'tab-voice.php','url'=>$voice_url,'mode'=>$voice_mode];
+if ($enable['text']) $panes[] = ['id'=>'fi-pane-text', 'lbl'=>__('Text','floatina'), 'icon'=>'bi-chat-dots.svg', 'kind'=>'text', 'tmpl'=>'tab-text.php', 'url'=>$text_url, 'mode'=>$text_mode];
+if ($enable['news']) $panes[] = ['id'=>'fi-pane-news', 'lbl'=>__('News','floatina'), 'icon'=>'bi-newspaper.svg', 'kind'=>null, 'tmpl'=>'tab-news.php'];
+if ($enable['faq']) $panes[] = ['id'=>'fi-pane-help', 'lbl'=>__('Help','floatina'), 'icon'=>'bi-question-circle.svg', 'kind'=>null, 'tmpl'=>'tab-faq.php'];
+
+/* === Icons helper ========================================================= */
+$icon = function($file){
+ return esc_url(rtrim(FLOATINA_URL,'/') . '/assets/icons/' . ltrim($file,'/'));
+};
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/floatina/templates/tabs/tab-faq.php b/floatina/templates/tabs/tab-faq.php
new file mode 100644
index 0000000..01a1fc5
--- /dev/null
+++ b/floatina/templates/tabs/tab-faq.php
@@ -0,0 +1,32 @@
+ 'floatina_faq',
+ 'post_status' => 'publish',
+ 'posts_per_page' => 20,
+ 'orderby' => ['menu_order'=>'ASC','date'=>'ASC'],
+]);
+?>
+
+have_posts()): while($q->have_posts()): $q->the_post(); ?>
+
+
+
No FAQs
Add posts in Floatina FAQ.
+
+
+
+
diff --git a/floatina/templates/tabs/tab-home.php b/floatina/templates/tabs/tab-home.php
new file mode 100644
index 0000000..ee73fda
--- /dev/null
+++ b/floatina/templates/tabs/tab-home.php
@@ -0,0 +1,32 @@
+
+
+
+
Hi
+
How can we help you today?
+
+
+
+
+
+
+
+
diff --git a/floatina/templates/tabs/tab-news.php b/floatina/templates/tabs/tab-news.php
new file mode 100644
index 0000000..eeee70d
--- /dev/null
+++ b/floatina/templates/tabs/tab-news.php
@@ -0,0 +1,30 @@
+ 'floatina_news',
+ 'post_status' => 'publish',
+ 'posts_per_page' => 8,
+ 'orderby' => 'date',
+ 'order' => 'DESC',
+]);
+
+if ($q->have_posts()):
+ while ($q->have_posts()): $q->the_post();
+ $url = get_post_meta(get_the_ID(),'floatina_news_url',true);
+ $href = $url ? $url : get_permalink();
+ $thumb = get_the_post_thumbnail_url(get_the_ID(),'medium');
+ $excerpt = get_the_excerpt();
+ ?>
+
+
+
+
+
+ No news
Add posts in Floatina News.
+
diff --git a/floatina/templates/tabs/tab-text.php b/floatina/templates/tabs/tab-text.php
new file mode 100644
index 0000000..269ae10
--- /dev/null
+++ b/floatina/templates/tabs/tab-text.php
@@ -0,0 +1,2 @@
+
+
diff --git a/floatina/templates/tabs/tab-voice.php b/floatina/templates/tabs/tab-voice.php
new file mode 100644
index 0000000..269ae10
--- /dev/null
+++ b/floatina/templates/tabs/tab-voice.php
@@ -0,0 +1,2 @@
+
+