Version 1.0.0

This commit is contained in:
MOH 2025-10-03 23:53:09 +02:00
commit 4cd358493f
12 changed files with 851 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,254 @@
/************************************************************************************
* Floatina core layout
************************************************************************************/
/* Inherit typography from theme */
#floatina-panel,
#floatina-panel * {
font-family: inherit;
}
/* 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: 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;
}
/* 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);
}
/* 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;
}
/* 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;
}
/* 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;
}
/* 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;
}
/* 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;
}
/* 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;
}
#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);
}

View File

@ -0,0 +1,294 @@
/**
* 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);
})();

53
floatina/floatina.php Normal file
View File

@ -0,0 +1,53 @@
<?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
* Author: Moh Farawati
* Text Domain: floatina
*/
if (!defined('ABSPATH')) exit;
define('FLOATINA_VER', '0.1.0');
define('FLOATINA_DIR', plugin_dir_path(__FILE__));
define('FLOATINA_URL', plugin_dir_url(__FILE__));
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';
/************************************************************************************
* Bootstrap + i18n
************************************************************************************/
add_action('plugins_loaded', function () {
load_plugin_textdomain('floatina', false, dirname(plugin_basename(__FILE__)) . '/languages');
});
/************************************************************************************
* Activation: register CPTs and flush rules
************************************************************************************/
register_activation_hook(__FILE__, function () {
floatina_register_cpts();
flush_rewrite_rules();
});
/************************************************************************************
* Render panel automatically in footer on frontend
************************************************************************************/
add_action('wp_footer', function () {
if (is_admin()) return;
include FLOATINA_DIR . 'templates/panel.php';
}, 999);
/************************************************************************************
* Optional shortcode [floatina]
************************************************************************************/
add_shortcode('floatina', function () {
ob_start();
include FLOATINA_DIR . 'templates/panel.php';
return ob_get_clean();
});

View File

@ -0,0 +1,21 @@
<?php
if (!defined('ABSPATH')) exit;
/************************************************************************************
* Public AJAX endpoints for News and FAQ 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();
}
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();
}

View File

@ -0,0 +1,41 @@
<?php
if (!defined('ABSPATH')) exit;
/************************************************************************************
* Enqueue frontend assets and localize settings
************************************************************************************/
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');
$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');
// 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);
});

View File

@ -0,0 +1,31 @@
<?php
if (!defined('ABSPATH')) exit;
/************************************************************************************
* Register CPTs for News and FAQ under the same "Floatina" menu
************************************************************************************/
function floatina_register_cpts()
{
$parent = 'floatina'; // matches settings menu slug
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,
]);
}
add_action('init', 'floatina_register_cpts');

View File

@ -0,0 +1,90 @@
<?php
if (!defined('ABSPATH')) exit;
/************************************************************************************
* Admin menu + settings
************************************************************************************/
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_field_position()
{
$v = floatina_opt('position', 'right'); ?>
<label><input type="radio" name="floatina_settings[position]" value="right" <?php checked($v, 'right'); ?> /> Right</label>
&nbsp;&nbsp;
<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 }
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 }
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 }

View File

@ -0,0 +1,11 @@
<?php
if (!defined('ABSPATH')) exit;
/************************************************************************************
* Option getter with default
************************************************************************************/
function floatina_opt($key, $default = '')
{
$opts = get_option('floatina_settings', []);
return isset($opts[$key]) && $opts[$key] !== '' ? $opts[$key] : $default;
}

View File

@ -0,0 +1,26 @@
<?php
if (!defined('ABSPATH')) exit;
/************************************************************************************
* FAQ partial
************************************************************************************/
$q = new WP_Query([
'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; ?>

View File

@ -0,0 +1,28 @@
<?php
if (!defined('ABSPATH')) exit;
/************************************************************************************
* News partial
************************************************************************************/
$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(); ?>
<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();
else: ?>
<div class="fi-card">
<h4>No news</h4>
<div class="fi-muted">Add posts in Floatina News.</div>
</div>
<?php endif; ?>

View File

@ -0,0 +1,2 @@
<?php if (!defined('ABSPATH')) exit; ?>
<div id="floatina-root" aria-live="polite" aria-busy="false"></div>