init
This commit is contained in:
2350
theme/assets/css/main.css
Normal file
2350
theme/assets/css/main.css
Normal file
File diff suppressed because it is too large
Load Diff
612
theme/assets/js/main.js
Normal file
612
theme/assets/js/main.js
Normal file
@@ -0,0 +1,612 @@
|
||||
/**
|
||||
* OTS Theme — Main JS
|
||||
*/
|
||||
|
||||
/* ── Theme toggle (runs before DOMContentLoaded to prevent flash) ── */
|
||||
(function() {
|
||||
const saved = localStorage.getItem('oribi-theme');
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const theme = saved || (prefersDark ? 'dark' : 'light');
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
})();
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
/* ── Sticky header ──────────────────────────────────────── */
|
||||
const header = document.getElementById('site-header');
|
||||
if (header) {
|
||||
window.addEventListener('scroll', () => {
|
||||
header.classList.toggle('scrolled', window.scrollY > 40);
|
||||
}, { passive: true });
|
||||
}
|
||||
|
||||
/* ── Mobile nav toggle ──────────────────────────────────── */
|
||||
const toggle = document.getElementById('nav-toggle');
|
||||
const nav = document.getElementById('site-nav');
|
||||
if (toggle && nav) {
|
||||
toggle.addEventListener('click', () => {
|
||||
toggle.classList.toggle('open');
|
||||
nav.classList.toggle('open');
|
||||
const expanded = toggle.getAttribute('aria-expanded') === 'true';
|
||||
toggle.setAttribute('aria-expanded', !expanded);
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Mobile sub-menu accordion ──────────────────────────── */
|
||||
if (nav) {
|
||||
nav.addEventListener('click', (e) => {
|
||||
// Only act in mobile (nav-toggle visible = mobile view)
|
||||
if (!toggle || getComputedStyle(toggle).display === 'none') return;
|
||||
|
||||
const parentLi = e.target.closest('.menu-item-has-children');
|
||||
if (!parentLi) return;
|
||||
|
||||
// If the click is on the parent link itself (not a child link), toggle
|
||||
const clickedLink = e.target.closest('a');
|
||||
const subMenu = parentLi.querySelector(':scope > .sub-menu');
|
||||
if (!subMenu) return;
|
||||
|
||||
// If they clicked a sub-menu link, let it navigate normally
|
||||
if (clickedLink && subMenu.contains(clickedLink)) return;
|
||||
|
||||
// Toggle this item; collapse siblings
|
||||
const isOpen = parentLi.classList.contains('submenu-open');
|
||||
parentLi.closest('.nav-menu')
|
||||
.querySelectorAll('.menu-item-has-children.submenu-open')
|
||||
.forEach(li => li.classList.remove('submenu-open'));
|
||||
|
||||
if (!isOpen) {
|
||||
parentLi.classList.add('submenu-open');
|
||||
// Prevent the parent anchor from navigating when toggling
|
||||
if (clickedLink && parentLi.contains(clickedLink) && !subMenu.contains(clickedLink)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Scroll-to-top ──────────────────────────────────────── */
|
||||
const scrollBtn = document.getElementById('scroll-top');
|
||||
if (scrollBtn) {
|
||||
window.addEventListener('scroll', () => {
|
||||
scrollBtn.classList.toggle('visible', window.scrollY > 600);
|
||||
}, { passive: true });
|
||||
scrollBtn.addEventListener('click', () => {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Light / Dark theme toggle ─────────────────────────── */
|
||||
const themeToggle = document.getElementById('theme-toggle');
|
||||
if (themeToggle) {
|
||||
themeToggle.addEventListener('click', () => {
|
||||
const current = document.documentElement.getAttribute('data-theme') || 'light';
|
||||
const next = current === 'dark' ? 'light' : 'dark';
|
||||
document.documentElement.setAttribute('data-theme', next);
|
||||
localStorage.setItem('oribi-theme', next);
|
||||
themeToggle.setAttribute('aria-label',
|
||||
next === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Contact form (AJAX) ────────────────────────────────── */
|
||||
const form = document.getElementById('contact-form');
|
||||
const notice = document.getElementById('form-notice');
|
||||
if (form && typeof oribiAjax !== 'undefined') {
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
notice.className = 'form-notice';
|
||||
notice.style.display = 'none';
|
||||
|
||||
const data = new FormData(form);
|
||||
data.append('action', 'oribi_contact');
|
||||
data.append('nonce', oribiAjax.nonce);
|
||||
|
||||
try {
|
||||
const res = await fetch(oribiAjax.url, { method: 'POST', body: data });
|
||||
const json = await res.json();
|
||||
notice.textContent = json.data;
|
||||
notice.className = 'form-notice ' + (json.success ? 'success' : 'error');
|
||||
notice.style.display = 'block';
|
||||
if (json.success) form.reset();
|
||||
} catch {
|
||||
notice.textContent = 'Something went wrong. Please try again.';
|
||||
notice.className = 'form-notice error';
|
||||
notice.style.display = 'block';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Animate cards on scroll ────────────────────────────── */
|
||||
const cards = document.querySelectorAll('.oribi-card, .feature-card, .industry-card, .pricing-card, .value-card, .platform-row');
|
||||
if (cards.length && 'IntersectionObserver' in window) {
|
||||
cards.forEach(c => { c.style.opacity = '0'; c.style.transform = 'translateY(24px)'; c.style.transition = 'opacity .5s ease, transform .5s ease'; });
|
||||
const io = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.style.opacity = '1';
|
||||
entry.target.style.transform = 'translateY(0)';
|
||||
io.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}, { threshold: 0.1 });
|
||||
cards.forEach(c => io.observe(c));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/* -- Datacenter hero background canvas ----------------------------------------- */
|
||||
(function () {
|
||||
const canvas = document.getElementById('dc-canvas');
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
/*
|
||||
* Performance:
|
||||
* 1. bgCanvas -- static geometry painted once per resize, blit each frame.
|
||||
* 2. LED draws batched by colour -- one shadowBlur setup per colour (~4/frame).
|
||||
* 3. 30 fps cap.
|
||||
* 4. Visibility API + IntersectionObserver pause rAF when hidden/off-screen.
|
||||
*/
|
||||
|
||||
/* -- colour palette -------------------------------------------------------- */
|
||||
const ROOM_TOP = '#020509';
|
||||
const ROOM_BOT = '#030b08';
|
||||
const RACK_SHELL = '#111b2e'; /* outer frame -- dark navy */
|
||||
const RACK_SHELL2 = '#0c1422'; /* outer frame shadow side */
|
||||
const RACK_FACE = '#0d1728'; /* inner face panel background */
|
||||
const RAIL_FACE = '#141f35'; /* mounting rail column face */
|
||||
const RAIL_EDGE = '#1c2d4a'; /* rail inner-edge highlight */
|
||||
const SCREW_COL = '#0a1220'; /* screw/nut recesses on rails */
|
||||
const SRV_FACE = '#1a2840'; /* server 1U face -- lighter than rack */
|
||||
const SRV_STRIPE = '#1f3050'; /* top-edge highlight stripe */
|
||||
const SRV_SHADOW = '#0e1a2b'; /* bottom-edge shadow line */
|
||||
const SRV_OFF = '#0d1624'; /* unpowered slot */
|
||||
const PWRBTN_RING = '#243654'; /* power button outer ring */
|
||||
const PWRBTN_FACE = '#101e30'; /* power button recessed face */
|
||||
const VENT_SLOT = '#0b1421'; /* vent/louver slots */
|
||||
const BAY_SLOT = '#09111e'; /* drive bay recesses */
|
||||
const BAY_EDGE = '#1d2f48'; /* drive bay raised edge */
|
||||
const PATCH_BODY = '#111d30'; /* patch unit body */
|
||||
const PATCH_PORT = '#070d18'; /* patch port holes */
|
||||
const PATCH_LBL = '#0b1422'; /* patch label strip */
|
||||
const CAB_TROUGH = '#080f1c'; /* cable management trough */
|
||||
const LED_OFF = '#182438'; /* unlit LED placeholder */
|
||||
const LED_COLORS = { green: '#00f07a', amber: '#ffb200', red: '#ff3838', blue: '#00aaff' };
|
||||
|
||||
/* -- depth layers (back -> front)
|
||||
railW = thickness of side mounting-rail columns
|
||||
padTop = vertical padding inside rack before first unit -- */
|
||||
const LAYERS = [
|
||||
{ alpha: 0.28, yShift: 0.12, rackW: 54, rackGap: 18, unitH: 12, unitGap: 1, railW: 5, padTop: 7, ledSz: 2, ledCols: 3, ledRows: 2 },
|
||||
{ alpha: 0.55, yShift: 0.05, rackW: 80, rackGap: 28, unitH: 18, unitGap: 2, railW: 7, padTop: 9, ledSz: 3, ledCols: 3, ledRows: 2 },
|
||||
{ alpha: 1.00, yShift: 0.00, rackW: 112, rackGap: 40, unitH: 25, unitGap: 2, railW: 10, padTop: 11, ledSz: 4, ledCols: 4, ledRows: 2 },
|
||||
];
|
||||
|
||||
/* -- shared LED-position helpers (same formula in paintBg & drawLEDs) ----- */
|
||||
function ledOriginX(ux, uw, ledCols, ledSz) {
|
||||
return ux + uw - ledCols * (ledSz + 3) - 3;
|
||||
}
|
||||
function ledOriginY(uy, unitH, ledRows, ledSz) {
|
||||
return uy + ((unitH - ledRows * (ledSz + 2) + 1) / 2 | 0);
|
||||
}
|
||||
|
||||
/* -- state ----------------------------------------------------------------- */
|
||||
let layers = [];
|
||||
const bgCanvas = document.createElement('canvas');
|
||||
const bgCtx = bgCanvas.getContext('2d');
|
||||
let scanY = 0;
|
||||
let lastTs = 0;
|
||||
let rafId;
|
||||
let W = 1, H = 1;
|
||||
|
||||
const FRAME_MS = 1000 / 30; /* 30 fps throttle */
|
||||
let fpsDebt = 0;
|
||||
|
||||
/* -- build rack data for one layer ---------------------------------------- */
|
||||
function buildLayer(def) {
|
||||
const { rackW, rackGap, unitH, unitGap, railW, padTop, ledCols, ledRows, yShift } = def;
|
||||
const numRacks = Math.ceil(W / (rackW + rackGap)) + 2;
|
||||
const racks = [];
|
||||
|
||||
for (let r = 0; r < numRacks; r++) {
|
||||
const rx = r * (rackW + rackGap) - rackGap;
|
||||
const numUnits = Math.floor((H * (1 - Math.abs(yShift) * 2) - padTop * 2) / (unitH + unitGap));
|
||||
const units = [];
|
||||
const activity = {
|
||||
timer: Math.random() * 8000, period: 5000 + Math.random() * 12000,
|
||||
active: false, burstTimer: 0, burstLength: 0,
|
||||
};
|
||||
|
||||
for (let u = 0; u < numUnits; u++) {
|
||||
const leds = [];
|
||||
for (let l = 0; l < ledCols * ledRows; l++) {
|
||||
const rnd = Math.random();
|
||||
const type = rnd < 0.68 ? 'green' : rnd < 0.86 ? 'amber' : rnd < 0.96 ? 'red' : 'blue';
|
||||
leds.push({ type, on: Math.random() > 0.2,
|
||||
blinkPeriod: 400 + Math.random() * 5000,
|
||||
timer: Math.random() * 5000 });
|
||||
}
|
||||
units.push({ leds, powered: Math.random() > 0.08 });
|
||||
}
|
||||
|
||||
racks.push({ rx, units, activity, patchRows: 2 + Math.floor(Math.random() * 2) });
|
||||
}
|
||||
return racks;
|
||||
}
|
||||
|
||||
/* -- paint static geometry to bgCanvas (once per resize) ------------------ */
|
||||
function paintBg() {
|
||||
bgCanvas.width = W;
|
||||
bgCanvas.height = H;
|
||||
|
||||
/* room background */
|
||||
const bg = bgCtx.createLinearGradient(0, 0, 0, H);
|
||||
bg.addColorStop(0, ROOM_TOP);
|
||||
bg.addColorStop(0.5, '#05080f');
|
||||
bg.addColorStop(1, ROOM_BOT);
|
||||
bgCtx.fillStyle = bg;
|
||||
bgCtx.fillRect(0, 0, W, H);
|
||||
|
||||
/* ceiling fluorescent bar */
|
||||
const ceil = bgCtx.createLinearGradient(0, 0, 0, H * 0.10);
|
||||
ceil.addColorStop(0, 'rgba(140,190,255,0.13)');
|
||||
ceil.addColorStop(1, 'rgba(140,190,255,0)');
|
||||
bgCtx.fillStyle = ceil;
|
||||
bgCtx.fillRect(0, 0, W, H * 0.10);
|
||||
|
||||
/* floor glow */
|
||||
const flr = bgCtx.createLinearGradient(0, H * 0.84, 0, H);
|
||||
flr.addColorStop(0, 'rgba(0,220,110,0)');
|
||||
flr.addColorStop(1, 'rgba(0,180,90,0.10)');
|
||||
bgCtx.fillStyle = flr;
|
||||
bgCtx.fillRect(0, H * 0.84, W, H * 0.16);
|
||||
|
||||
for (const layer of layers) {
|
||||
const { def, racks } = layer;
|
||||
const { rackW, unitH, unitGap, railW, padTop, ledSz, ledCols, ledRows, alpha, yShift } = def;
|
||||
const layerY = H * yShift;
|
||||
const uw = rackW - 2 * railW;
|
||||
|
||||
bgCtx.save();
|
||||
bgCtx.globalAlpha = alpha;
|
||||
|
||||
for (const rack of racks) {
|
||||
const { rx, units, patchRows } = rack;
|
||||
const totalRows = units.length + patchRows;
|
||||
const rackH = totalRows * (unitH + unitGap) - unitGap + padTop * 2 + 6;
|
||||
const ry = Math.floor((H - rackH) / 2) + layerY;
|
||||
const ux = rx + railW;
|
||||
|
||||
/* 1. rack shadow (cartoon depth) */
|
||||
bgCtx.fillStyle = RACK_SHELL2;
|
||||
bgCtx.fillRect(rx + 2, ry + 2, rackW, rackH);
|
||||
|
||||
/* 2. rack outer shell */
|
||||
bgCtx.fillStyle = RACK_SHELL;
|
||||
bgCtx.fillRect(rx, ry, rackW, rackH);
|
||||
|
||||
/* cartoon top/left highlights */
|
||||
bgCtx.fillStyle = 'rgba(255,255,255,0.13)';
|
||||
bgCtx.fillRect(rx, ry, rackW, 1);
|
||||
bgCtx.fillStyle = 'rgba(255,255,255,0.07)';
|
||||
bgCtx.fillRect(rx, ry, 1, rackH);
|
||||
|
||||
/* 3. inner face panel */
|
||||
bgCtx.fillStyle = RACK_FACE;
|
||||
bgCtx.fillRect(ux, ry + 1, uw, rackH - 2);
|
||||
|
||||
/* 4. mounting rail columns */
|
||||
[rx, rx + rackW - railW].forEach(function(cx) {
|
||||
bgCtx.fillStyle = RAIL_FACE;
|
||||
bgCtx.fillRect(cx, ry, railW, rackH);
|
||||
|
||||
/* inner-edge accent */
|
||||
const edgeX = (cx === rx) ? cx + railW - 1 : cx;
|
||||
bgCtx.fillStyle = RAIL_EDGE;
|
||||
bgCtx.fillRect(edgeX, ry, 1, rackH);
|
||||
|
||||
/* screw holes */
|
||||
const screwX = cx + (railW / 2 | 0) - 1;
|
||||
const screwSz = Math.max(2, railW * 0.28 | 0);
|
||||
let sy = ry + padTop;
|
||||
while (sy < ry + rackH - padTop) {
|
||||
bgCtx.fillStyle = SCREW_COL;
|
||||
bgCtx.fillRect(screwX, sy, screwSz, screwSz);
|
||||
sy += unitH + unitGap;
|
||||
}
|
||||
});
|
||||
|
||||
/* 5. top cap brace */
|
||||
bgCtx.fillStyle = 'rgba(255,255,255,0.05)';
|
||||
bgCtx.fillRect(ux, ry, uw, padTop);
|
||||
|
||||
/* 6. cable management trough */
|
||||
bgCtx.fillStyle = CAB_TROUGH;
|
||||
bgCtx.fillRect(ux, ry + rackH - padTop - 2, uw, padTop + 2);
|
||||
bgCtx.fillStyle = 'rgba(255,255,255,0.04)';
|
||||
bgCtx.fillRect(ux, ry + rackH - padTop - 2, uw, 1);
|
||||
|
||||
/* 7. patch panel rows */
|
||||
for (let p = 0; p < patchRows; p++) {
|
||||
const py = ry + padTop + p * (unitH + unitGap);
|
||||
|
||||
bgCtx.fillStyle = PATCH_BODY;
|
||||
bgCtx.fillRect(ux, py, uw, unitH);
|
||||
|
||||
/* top bevel + bottom shadow */
|
||||
bgCtx.fillStyle = 'rgba(255,255,255,0.08)';
|
||||
bgCtx.fillRect(ux, py, uw, 1);
|
||||
bgCtx.fillStyle = 'rgba(0,0,0,0.38)';
|
||||
bgCtx.fillRect(ux, py + unitH - 1, uw, 1);
|
||||
|
||||
/* label strip (left ~18%) */
|
||||
const lblW = Math.max(6, uw * 0.18 | 0);
|
||||
bgCtx.fillStyle = PATCH_LBL;
|
||||
bgCtx.fillRect(ux + 2, py + 2, lblW, unitH - 4);
|
||||
|
||||
/* RJ-45 style port holes */
|
||||
const portsX = ux + lblW + 4;
|
||||
const portsW = uw - lblW - 6;
|
||||
const portW = Math.max(3, unitH * 0.50 | 0);
|
||||
const portH = Math.max(2, unitH * 0.38 | 0);
|
||||
const portY = py + ((unitH - portH) / 2 | 0);
|
||||
const numPort = Math.floor(portsW / (portW + 2));
|
||||
bgCtx.fillStyle = PATCH_PORT;
|
||||
for (let pp = 0; pp < numPort; pp++) {
|
||||
bgCtx.fillRect(portsX + pp * (portW + 2), portY, portW, portH);
|
||||
}
|
||||
}
|
||||
|
||||
/* 8. server units */
|
||||
for (let u = 0; u < units.length; u++) {
|
||||
const unit = units[u];
|
||||
const uy = ry + padTop + (u + patchRows) * (unitH + unitGap);
|
||||
|
||||
if (!unit.powered) {
|
||||
/* empty / powered-off slot */
|
||||
bgCtx.fillStyle = SRV_OFF;
|
||||
bgCtx.fillRect(ux, uy, uw, unitH);
|
||||
bgCtx.fillStyle = 'rgba(0,0,0,0.32)';
|
||||
bgCtx.fillRect(ux, uy + unitH - 1, uw, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* server face */
|
||||
bgCtx.fillStyle = SRV_FACE;
|
||||
bgCtx.fillRect(ux, uy, uw, unitH);
|
||||
|
||||
/* top highlight stripe */
|
||||
bgCtx.fillStyle = SRV_STRIPE;
|
||||
bgCtx.fillRect(ux, uy, uw, 1);
|
||||
|
||||
/* bottom shadow line */
|
||||
bgCtx.fillStyle = SRV_SHADOW;
|
||||
bgCtx.fillRect(ux, uy + unitH - 1, uw, 1);
|
||||
|
||||
/* -- power button (left section) -- */
|
||||
if (unitH >= 12) {
|
||||
const pBtnR = Math.max(2, unitH * 0.18 | 0);
|
||||
const pBtnX = ux + railW + pBtnR + 1;
|
||||
const pBtnY = uy + (unitH / 2 | 0) - pBtnR;
|
||||
/* outer ring */
|
||||
bgCtx.fillStyle = PWRBTN_RING;
|
||||
bgCtx.fillRect(pBtnX - 1, pBtnY - 1, pBtnR * 2 + 2, pBtnR * 2 + 2);
|
||||
/* inner recess */
|
||||
bgCtx.fillStyle = PWRBTN_FACE;
|
||||
bgCtx.fillRect(pBtnX, pBtnY, pBtnR * 2, pBtnR * 2);
|
||||
}
|
||||
|
||||
/* -- vent / louver slots -- */
|
||||
if (unitH >= 14) {
|
||||
const pBtnR = Math.max(2, unitH * 0.18 | 0);
|
||||
const ventStartX = ux + railW + pBtnR * 2 + 5;
|
||||
const ventH = Math.max(3, unitH - 6);
|
||||
const ventY = uy + 3;
|
||||
const numVents = Math.min(5, Math.floor(uw * 0.10 / 3));
|
||||
bgCtx.fillStyle = VENT_SLOT;
|
||||
for (let v = 0; v < numVents; v++) {
|
||||
bgCtx.fillRect(ventStartX + v * 3, ventY, 1, ventH);
|
||||
}
|
||||
}
|
||||
|
||||
/* -- drive bays (centre section) -- */
|
||||
const ledPanelW = ledCols * (ledSz + 3) + 5;
|
||||
const rightStop = ux + uw - ledPanelW;
|
||||
const leftStop = ux + Math.max(
|
||||
railW * 2 + (unitH * 0.18 | 0) * 2 + (unitH >= 14 ? 14 : 4) + 4,
|
||||
uw * 0.22 | 0
|
||||
);
|
||||
const bayAreaW = rightStop - leftStop;
|
||||
|
||||
if (bayAreaW > 8 && unitH >= 10) {
|
||||
const bayH = Math.max(3, unitH * 0.46 | 0);
|
||||
const bayW = Math.max(4, Math.min(11, bayAreaW / 5 | 0));
|
||||
const bayY = uy + ((unitH - bayH) / 2 | 0);
|
||||
const numBays = Math.min(8, Math.floor(bayAreaW / (bayW + 2)));
|
||||
|
||||
for (let b = 0; b < numBays; b++) {
|
||||
const bx = leftStop + b * (bayW + 2);
|
||||
/* bay recess */
|
||||
bgCtx.fillStyle = BAY_SLOT;
|
||||
bgCtx.fillRect(bx, bayY, bayW, bayH);
|
||||
/* bay top-edge highlight */
|
||||
bgCtx.fillStyle = BAY_EDGE;
|
||||
bgCtx.fillRect(bx, bayY, bayW, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/* -- off-state LED panel (right section) -- */
|
||||
const lox = ledOriginX(ux, uw, ledCols, ledSz);
|
||||
const loy = ledOriginY(uy, unitH, ledRows, ledSz);
|
||||
/* panel inset background */
|
||||
bgCtx.fillStyle = 'rgba(0,0,0,0.28)';
|
||||
bgCtx.fillRect(lox - 2, uy + 2, ledCols * (ledSz + 3) + 1, unitH - 4);
|
||||
/* placeholder dots */
|
||||
bgCtx.fillStyle = LED_OFF;
|
||||
for (let row = 0; row < ledRows; row++) {
|
||||
for (let col = 0; col < ledCols; col++) {
|
||||
bgCtx.fillRect(
|
||||
lox + col * (ledSz + 3),
|
||||
loy + row * (ledSz + 2),
|
||||
ledSz, ledSz
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bgCtx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
/* -- collect LED state & draw all lit LEDs batched by colour -------------- */
|
||||
function drawLEDs(dt) {
|
||||
const buckets = { green: [], amber: [], red: [], blue: [] };
|
||||
|
||||
for (const layer of layers) {
|
||||
const { def, racks } = layer;
|
||||
const { rackW, unitH, unitGap, railW, padTop, ledSz, ledCols, ledRows, alpha, yShift } = def;
|
||||
const uw = rackW - 2 * railW;
|
||||
const layerY = H * yShift;
|
||||
|
||||
for (const rack of racks) {
|
||||
const { rx, units, activity, patchRows } = rack;
|
||||
const totalRows = units.length + patchRows;
|
||||
const rackH = totalRows * (unitH + unitGap) - unitGap + padTop * 2 + 6;
|
||||
const ry = Math.floor((H - rackH) / 2) + layerY;
|
||||
const ux = rx + railW;
|
||||
|
||||
/* rack status strip on left rail */
|
||||
buckets['green'].push({
|
||||
x: rx + 1, y: ry + padTop, w: 2, h: rackH - padTop * 2,
|
||||
a: alpha * 0.28,
|
||||
});
|
||||
|
||||
/* per-unit LEDs */
|
||||
for (let u = 0; u < units.length; u++) {
|
||||
const unit = units[u];
|
||||
if (!unit.powered) continue;
|
||||
|
||||
const uy = ry + padTop + (u + patchRows) * (unitH + unitGap);
|
||||
const lox = ledOriginX(ux, uw, ledCols, ledSz);
|
||||
const loy = ledOriginY(uy, unitH, ledRows, ledSz);
|
||||
|
||||
for (let row = 0; row < ledRows; row++) {
|
||||
for (let col = 0; col < ledCols; col++) {
|
||||
const led = unit.leds[row * ledCols + col];
|
||||
led.timer += dt;
|
||||
if (led.timer >= led.blinkPeriod) {
|
||||
led.timer = 0;
|
||||
if (led.type === 'amber' || Math.random() < 0.2) led.on = !led.on;
|
||||
}
|
||||
if (!led.on) continue;
|
||||
buckets[led.type].push({
|
||||
x: lox + col * (ledSz + 3),
|
||||
y: loy + row * (ledSz + 2),
|
||||
w: ledSz, h: ledSz, a: alpha * 0.92,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* one shadowBlur setup per colour */
|
||||
for (const [key, rects] of Object.entries(buckets)) {
|
||||
if (!rects.length) continue;
|
||||
const color = LED_COLORS[key];
|
||||
ctx.shadowColor = color;
|
||||
ctx.shadowBlur = 8;
|
||||
ctx.fillStyle = color;
|
||||
for (const r of rects) {
|
||||
ctx.globalAlpha = r.a;
|
||||
ctx.fillRect(r.x, r.y, r.w, r.h);
|
||||
}
|
||||
}
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.shadowColor = 'transparent';
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
/* -- main render loop ------------------------------------------------------ */
|
||||
function draw(ts) {
|
||||
rafId = requestAnimationFrame(draw);
|
||||
|
||||
const elapsed = ts - lastTs;
|
||||
lastTs = ts;
|
||||
fpsDebt += elapsed;
|
||||
if (fpsDebt < FRAME_MS) return;
|
||||
const dt = Math.min(fpsDebt, 80);
|
||||
fpsDebt = fpsDebt % FRAME_MS;
|
||||
|
||||
/* blit static geometry */
|
||||
ctx.drawImage(bgCanvas, 0, 0);
|
||||
|
||||
/* mid-aisle ambient glow */
|
||||
const aisle = ctx.createLinearGradient(0, H * 0.46, 0, H * 0.58);
|
||||
aisle.addColorStop(0, 'rgba(0,80,180,0)');
|
||||
aisle.addColorStop(0.5, 'rgba(0,80,180,0.022)');
|
||||
aisle.addColorStop(1, 'rgba(0,80,180,0)');
|
||||
ctx.fillStyle = aisle;
|
||||
ctx.fillRect(0, H * 0.44, W, H * 0.16);
|
||||
|
||||
/* lit LEDs */
|
||||
drawLEDs(dt);
|
||||
|
||||
/* sweeping scan line */
|
||||
scanY = (scanY + 0.28 * (dt / 16)) % (H + 200);
|
||||
const sg = ctx.createLinearGradient(0, scanY - 110, 0, scanY + 110);
|
||||
sg.addColorStop(0, 'rgba(60,180,255,0)');
|
||||
sg.addColorStop(0.45, 'rgba(60,180,255,0.025)');
|
||||
sg.addColorStop(0.5, 'rgba(60,180,255,0.06)');
|
||||
sg.addColorStop(0.55, 'rgba(60,180,255,0.025)');
|
||||
sg.addColorStop(1, 'rgba(60,180,255,0)');
|
||||
ctx.fillStyle = sg;
|
||||
ctx.fillRect(0, scanY - 110, W, 220);
|
||||
|
||||
/* vignette */
|
||||
const vig = ctx.createRadialGradient(W/2, H/2, H * 0.25, W/2, H/2, H * 0.88);
|
||||
vig.addColorStop(0, 'rgba(0,0,0,0)');
|
||||
vig.addColorStop(1, 'rgba(0,0,0,0.60)');
|
||||
ctx.fillStyle = vig;
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
}
|
||||
|
||||
/* -- lifecycle helpers ----------------------------------------------------- */
|
||||
function start() {
|
||||
if (!rafId) { lastTs = performance.now(); rafId = requestAnimationFrame(draw); }
|
||||
}
|
||||
function stop() {
|
||||
cancelAnimationFrame(rafId); rafId = null;
|
||||
}
|
||||
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
document.hidden ? stop() : start();
|
||||
});
|
||||
|
||||
if ('IntersectionObserver' in window) {
|
||||
new IntersectionObserver(entries => {
|
||||
entries.forEach(e => e.isIntersecting ? start() : stop());
|
||||
}, { threshold: 0.01 }).observe(canvas);
|
||||
}
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
canvas.width = canvas.offsetWidth || canvas.parentElement.offsetWidth;
|
||||
canvas.height = canvas.offsetHeight || canvas.parentElement.offsetHeight;
|
||||
W = canvas.width;
|
||||
H = canvas.height;
|
||||
layers = LAYERS.map(def => ({ def, racks: buildLayer(def) }));
|
||||
paintBg();
|
||||
}, { passive: true });
|
||||
|
||||
/* -- init ------------------------------------------------------------------ */
|
||||
canvas.width = canvas.offsetWidth || canvas.parentElement.offsetWidth;
|
||||
canvas.height = canvas.offsetHeight || canvas.parentElement.offsetHeight;
|
||||
W = canvas.width;
|
||||
H = canvas.height;
|
||||
layers = LAYERS.map(def => ({ def, racks: buildLayer(def) }));
|
||||
paintBg();
|
||||
start();
|
||||
})();
|
||||
173
theme/assets/svg/hero-infrastructure.svg
Normal file
173
theme/assets/svg/hero-infrastructure.svg
Normal file
@@ -0,0 +1,173 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 520 440" fill="none">
|
||||
<!-- ─── Definitions ─────────────────────────────────────── -->
|
||||
<defs>
|
||||
<!-- LED glow filters -->
|
||||
<filter id="glow-green" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur in="SourceGraphic" stdDeviation="2.5" result="blur"/>
|
||||
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
|
||||
</filter>
|
||||
<filter id="glow-accent" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur in="SourceGraphic" stdDeviation="3" result="blur"/>
|
||||
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
|
||||
</filter>
|
||||
<filter id="glow-primary" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur in="SourceGraphic" stdDeviation="2" result="blur"/>
|
||||
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- ─── Central Cloud / Hub ─────────────────────────────── -->
|
||||
<g class="infra-hub">
|
||||
<rect x="195" y="30" width="130" height="80" rx="12" fill="#141c2e" stroke="#00757c" stroke-width="1.5"/>
|
||||
<!-- Cloud icon -->
|
||||
<path d="M240 60 a14 14 0 0 1 26 0 a10 10 0 0 1 9 10h-44a10 10 0 0 1 9-10z" fill="none" stroke="#00757c" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<text x="260" y="92" text-anchor="middle" fill="rgba(255,255,255,.6)" font-size="9" font-family="Inter, sans-serif" font-weight="600" letter-spacing=".08em">CLOUD</text>
|
||||
</g>
|
||||
|
||||
<!-- ─── Connection Lines (animated) ─────────────────────── -->
|
||||
<g class="infra-connections" stroke="#00757c" stroke-width="1" opacity=".5">
|
||||
<!-- Hub → Left Rack -->
|
||||
<line class="conn-line" x1="195" y1="90" x2="95" y2="160"/>
|
||||
<!-- Hub → Center Rack -->
|
||||
<line class="conn-line" x1="260" y1="110" x2="260" y2="160"/>
|
||||
<!-- Hub → Right Rack -->
|
||||
<line class="conn-line" x1="325" y1="90" x2="425" y2="160"/>
|
||||
<!-- Left → Center -->
|
||||
<line class="conn-line" x1="135" y1="265" x2="220" y2="265"/>
|
||||
<!-- Center → Right -->
|
||||
<line class="conn-line" x1="300" y1="265" x2="385" y2="265"/>
|
||||
<!-- Hub → Shield -->
|
||||
<line class="conn-line" x1="260" y1="350" x2="260" y2="380"/>
|
||||
</g>
|
||||
|
||||
<!-- ─── Data flow particles ─────────────────────────────── -->
|
||||
<g class="infra-particles">
|
||||
<circle class="data-particle dp-1" r="2.5" fill="#00757c" filter="url(#glow-accent)"/>
|
||||
<circle class="data-particle dp-2" r="2.5" fill="#00757c" filter="url(#glow-accent)"/>
|
||||
<circle class="data-particle dp-3" r="2.5" fill="#00757c" filter="url(#glow-accent)"/>
|
||||
<circle class="data-particle dp-4" r="2" fill="#D83302" filter="url(#glow-primary)"/>
|
||||
<circle class="data-particle dp-5" r="2" fill="#D83302" filter="url(#glow-primary)"/>
|
||||
</g>
|
||||
|
||||
<!-- ─── Server Rack 1 (Left) ────────────────────────────── -->
|
||||
<g class="infra-rack rack-left">
|
||||
<rect x="55" y="160" width="80" height="140" rx="6" fill="#0f1623" stroke="rgba(255,255,255,.1)" stroke-width="1"/>
|
||||
<!-- Rack label -->
|
||||
<text x="95" y="178" text-anchor="middle" fill="rgba(255,255,255,.35)" font-size="8" font-family="Inter, sans-serif" font-weight="600" letter-spacing=".1em">SRV-01</text>
|
||||
<!-- Server units -->
|
||||
<rect x="65" y="186" width="60" height="12" rx="2" fill="#141c2e" stroke="rgba(255,255,255,.06)" stroke-width=".5"/>
|
||||
<rect x="65" y="202" width="60" height="12" rx="2" fill="#141c2e" stroke="rgba(255,255,255,.06)" stroke-width=".5"/>
|
||||
<rect x="65" y="218" width="60" height="12" rx="2" fill="#141c2e" stroke="rgba(255,255,255,.06)" stroke-width=".5"/>
|
||||
<rect x="65" y="234" width="60" height="12" rx="2" fill="#141c2e" stroke="rgba(255,255,255,.06)" stroke-width=".5"/>
|
||||
<rect x="65" y="250" width="60" height="12" rx="2" fill="#141c2e" stroke="rgba(255,255,255,.06)" stroke-width=".5"/>
|
||||
<rect x="65" y="266" width="60" height="12" rx="2" fill="#141c2e" stroke="rgba(255,255,255,.06)" stroke-width=".5"/>
|
||||
<rect x="65" y="282" width="60" height="12" rx="2" fill="#141c2e" stroke="rgba(255,255,255,.06)" stroke-width=".5"/>
|
||||
<!-- LEDs -->
|
||||
<circle class="led led-green" cx="70" cy="192" r="2" fill="#22c55e" filter="url(#glow-green)"/>
|
||||
<circle class="led led-green" cx="70" cy="208" r="2" fill="#22c55e" filter="url(#glow-green)"/>
|
||||
<circle class="led led-amber" cx="70" cy="224" r="2" fill="#f59e0b"/>
|
||||
<circle class="led led-green" cx="70" cy="240" r="2" fill="#22c55e" filter="url(#glow-green)"/>
|
||||
<circle class="led led-green" cx="70" cy="256" r="2" fill="#22c55e" filter="url(#glow-green)"/>
|
||||
<circle class="led led-green" cx="70" cy="272" r="2" fill="#22c55e" filter="url(#glow-green)"/>
|
||||
<circle class="led led-green" cx="70" cy="288" r="2" fill="#22c55e" filter="url(#glow-green)"/>
|
||||
<!-- Drive bays (small lines) -->
|
||||
<line x1="80" y1="190" x2="120" y2="190" stroke="rgba(255,255,255,.08)" stroke-width="1"/>
|
||||
<line x1="80" y1="194" x2="110" y2="194" stroke="rgba(255,255,255,.05)" stroke-width="1"/>
|
||||
<line x1="80" y1="206" x2="120" y2="206" stroke="rgba(255,255,255,.08)" stroke-width="1"/>
|
||||
<line x1="80" y1="210" x2="115" y2="210" stroke="rgba(255,255,255,.05)" stroke-width="1"/>
|
||||
<line x1="80" y1="222" x2="118" y2="222" stroke="rgba(255,255,255,.08)" stroke-width="1"/>
|
||||
<line x1="80" y1="226" x2="108" y2="226" stroke="rgba(255,255,255,.05)" stroke-width="1"/>
|
||||
</g>
|
||||
|
||||
<!-- ─── Server Rack 2 (Center — primary) ───────────────── -->
|
||||
<g class="infra-rack rack-center">
|
||||
<rect x="220" y="160" width="80" height="188" rx="6" fill="#0f1623" stroke="#D83302" stroke-width="1.5"/>
|
||||
<!-- Rack label -->
|
||||
<text x="260" y="178" text-anchor="middle" fill="rgba(255,255,255,.35)" font-size="8" font-family="Inter, sans-serif" font-weight="600" letter-spacing=".1em">SRV-02</text>
|
||||
<!-- Server units -->
|
||||
<rect x="230" y="186" width="60" height="12" rx="2" fill="#141c2e" stroke="rgba(255,255,255,.06)" stroke-width=".5"/>
|
||||
<rect x="230" y="202" width="60" height="12" rx="2" fill="#141c2e" stroke="rgba(255,255,255,.06)" stroke-width=".5"/>
|
||||
<rect x="230" y="218" width="60" height="12" rx="2" fill="#141c2e" stroke="rgba(255,255,255,.06)" stroke-width=".5"/>
|
||||
<rect x="230" y="234" width="60" height="12" rx="2" fill="#141c2e" stroke="rgba(255,255,255,.06)" stroke-width=".5"/>
|
||||
<rect x="230" y="250" width="60" height="12" rx="2" fill="#141c2e" stroke="rgba(255,255,255,.06)" stroke-width=".5"/>
|
||||
<rect x="230" y="266" width="60" height="12" rx="2" fill="#141c2e" stroke="rgba(255,255,255,.06)" stroke-width=".5"/>
|
||||
<rect x="230" y="282" width="60" height="12" rx="2" fill="#141c2e" stroke="rgba(255,255,255,.06)" stroke-width=".5"/>
|
||||
<rect x="230" y="298" width="60" height="12" rx="2" fill="#141c2e" stroke="rgba(255,255,255,.06)" stroke-width=".5"/>
|
||||
<rect x="230" y="314" width="60" height="12" rx="2" fill="#141c2e" stroke="rgba(255,255,255,.06)" stroke-width=".5"/>
|
||||
<rect x="230" y="330" width="60" height="12" rx="2" fill="#141c2e" stroke="rgba(255,255,255,.06)" stroke-width=".5"/>
|
||||
<!-- LEDs -->
|
||||
<circle class="led led-green" cx="235" cy="192" r="2" fill="#22c55e" filter="url(#glow-green)"/>
|
||||
<circle class="led led-green" cx="235" cy="208" r="2" fill="#22c55e" filter="url(#glow-green)"/>
|
||||
<circle class="led led-green" cx="235" cy="224" r="2" fill="#22c55e" filter="url(#glow-green)"/>
|
||||
<circle class="led led-green" cx="235" cy="240" r="2" fill="#22c55e" filter="url(#glow-green)"/>
|
||||
<circle class="led led-green" cx="235" cy="256" r="2" fill="#22c55e" filter="url(#glow-green)"/>
|
||||
<circle class="led led-green" cx="235" cy="272" r="2" fill="#22c55e" filter="url(#glow-green)"/>
|
||||
<circle class="led led-green" cx="235" cy="288" r="2" fill="#22c55e" filter="url(#glow-green)"/>
|
||||
<circle class="led led-green" cx="235" cy="304" r="2" fill="#22c55e" filter="url(#glow-green)"/>
|
||||
<circle class="led led-green" cx="235" cy="320" r="2" fill="#22c55e" filter="url(#glow-green)"/>
|
||||
<circle class="led led-green" cx="235" cy="336" r="2" fill="#22c55e" filter="url(#glow-green)"/>
|
||||
<!-- Drive bays -->
|
||||
<line x1="245" y1="190" x2="285" y2="190" stroke="rgba(255,255,255,.08)" stroke-width="1"/>
|
||||
<line x1="245" y1="194" x2="275" y2="194" stroke="rgba(255,255,255,.05)" stroke-width="1"/>
|
||||
<line x1="245" y1="206" x2="285" y2="206" stroke="rgba(255,255,255,.08)" stroke-width="1"/>
|
||||
<line x1="245" y1="210" x2="280" y2="210" stroke="rgba(255,255,255,.05)" stroke-width="1"/>
|
||||
<line x1="245" y1="222" x2="283" y2="222" stroke="rgba(255,255,255,.08)" stroke-width="1"/>
|
||||
<line x1="245" y1="226" x2="270" y2="226" stroke="rgba(255,255,255,.05)" stroke-width="1"/>
|
||||
</g>
|
||||
|
||||
<!-- ─── Server Rack 3 (Right) ──────────────────────────── -->
|
||||
<g class="infra-rack rack-right">
|
||||
<rect x="385" y="160" width="80" height="140" rx="6" fill="#0f1623" stroke="rgba(255,255,255,.1)" stroke-width="1"/>
|
||||
<!-- Rack label -->
|
||||
<text x="425" y="178" text-anchor="middle" fill="rgba(255,255,255,.35)" font-size="8" font-family="Inter, sans-serif" font-weight="600" letter-spacing=".1em">SRV-03</text>
|
||||
<!-- Server units -->
|
||||
<rect x="395" y="186" width="60" height="12" rx="2" fill="#141c2e" stroke="rgba(255,255,255,.06)" stroke-width=".5"/>
|
||||
<rect x="395" y="202" width="60" height="12" rx="2" fill="#141c2e" stroke="rgba(255,255,255,.06)" stroke-width=".5"/>
|
||||
<rect x="395" y="218" width="60" height="12" rx="2" fill="#141c2e" stroke="rgba(255,255,255,.06)" stroke-width=".5"/>
|
||||
<rect x="395" y="234" width="60" height="12" rx="2" fill="#141c2e" stroke="rgba(255,255,255,.06)" stroke-width=".5"/>
|
||||
<rect x="395" y="250" width="60" height="12" rx="2" fill="#141c2e" stroke="rgba(255,255,255,.06)" stroke-width=".5"/>
|
||||
<rect x="395" y="266" width="60" height="12" rx="2" fill="#141c2e" stroke="rgba(255,255,255,.06)" stroke-width=".5"/>
|
||||
<rect x="395" y="282" width="60" height="12" rx="2" fill="#141c2e" stroke="rgba(255,255,255,.06)" stroke-width=".5"/>
|
||||
<!-- LEDs -->
|
||||
<circle class="led led-green" cx="400" cy="192" r="2" fill="#22c55e" filter="url(#glow-green)"/>
|
||||
<circle class="led led-green" cx="400" cy="208" r="2" fill="#22c55e" filter="url(#glow-green)"/>
|
||||
<circle class="led led-green" cx="400" cy="224" r="2" fill="#22c55e" filter="url(#glow-green)"/>
|
||||
<circle class="led led-green" cx="400" cy="240" r="2" fill="#22c55e" filter="url(#glow-green)"/>
|
||||
<circle class="led led-amber" cx="400" cy="256" r="2" fill="#f59e0b"/>
|
||||
<circle class="led led-green" cx="400" cy="272" r="2" fill="#22c55e" filter="url(#glow-green)"/>
|
||||
<circle class="led led-green" cx="400" cy="288" r="2" fill="#22c55e" filter="url(#glow-green)"/>
|
||||
<!-- Drive bays -->
|
||||
<line x1="410" y1="190" x2="450" y2="190" stroke="rgba(255,255,255,.08)" stroke-width="1"/>
|
||||
<line x1="410" y1="194" x2="440" y2="194" stroke="rgba(255,255,255,.05)" stroke-width="1"/>
|
||||
<line x1="410" y1="206" x2="450" y2="206" stroke="rgba(255,255,255,.08)" stroke-width="1"/>
|
||||
<line x1="410" y1="210" x2="445" y2="210" stroke="rgba(255,255,255,.05)" stroke-width="1"/>
|
||||
</g>
|
||||
|
||||
<!-- ─── Shield (Security) ──────────────────────────────── -->
|
||||
<g class="infra-shield">
|
||||
<path d="M260 385 l-28 12 v22 c0 12 10 22 28 30 c18-8 28-18 28-30 v-22 z" fill="#141c2e" stroke="#00757c" stroke-width="1.5"/>
|
||||
<path d="M253 411 l5 5 l10-10" fill="none" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
|
||||
<!-- ─── Status badges ──────────────────────────────────── -->
|
||||
<!-- Badge: All Systems Operational -->
|
||||
<g class="infra-badge badge-status">
|
||||
<rect x="352" y="370" width="148" height="32" rx="16" fill="#141c2e" stroke="rgba(255,255,255,.1)" stroke-width="1"/>
|
||||
<circle class="led led-green" cx="370" cy="386" r="4" fill="#22c55e" filter="url(#glow-green)"/>
|
||||
<text x="382" y="390" fill="rgba(255,255,255,.7)" font-size="10" font-family="Inter, sans-serif" font-weight="600">All systems online</text>
|
||||
</g>
|
||||
|
||||
<!-- Badge: Monitoring Active -->
|
||||
<g class="infra-badge badge-monitor">
|
||||
<rect x="20" y="115" width="132" height="32" rx="16" fill="#141c2e" stroke="rgba(255,255,255,.1)" stroke-width="1"/>
|
||||
<circle class="led led-accent" cx="38" cy="131" r="4" fill="#00757c" filter="url(#glow-accent)"/>
|
||||
<text x="50" y="135" fill="rgba(255,255,255,.7)" font-size="10" font-family="Inter, sans-serif" font-weight="600">Monitoring active</text>
|
||||
</g>
|
||||
|
||||
<!-- Badge: 99.9% Uptime -->
|
||||
<g class="infra-badge badge-uptime">
|
||||
<rect x="390" y="115" width="118" height="32" rx="16" fill="#141c2e" stroke="rgba(255,255,255,.1)" stroke-width="1"/>
|
||||
<circle class="led led-primary" cx="408" cy="131" r="4" fill="#D83302" filter="url(#glow-primary)"/>
|
||||
<text x="420" y="135" fill="rgba(255,255,255,.7)" font-size="10" font-family="Inter, sans-serif" font-weight="600">99.9% uptime</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 13 KiB |
295
theme/blocks/editor.css
Normal file
295
theme/blocks/editor.css
Normal file
@@ -0,0 +1,295 @@
|
||||
/**
|
||||
* OTS Theme — Editor-specific overrides
|
||||
* Loaded only inside the Gutenberg block editor.
|
||||
*/
|
||||
|
||||
/* ── Font variables for editor preview ── */
|
||||
.editor-styles-wrapper {
|
||||
--font-heading: var(--wp--preset--font-family--heading, var(--font-sans));
|
||||
--font-sans: var(--wp--preset--font-family--sans, 'Inter', system-ui, -apple-system, sans-serif);
|
||||
}
|
||||
.editor-styles-wrapper h1,
|
||||
.editor-styles-wrapper h2,
|
||||
.editor-styles-wrapper h3,
|
||||
.editor-styles-wrapper h4,
|
||||
.editor-styles-wrapper h5,
|
||||
.editor-styles-wrapper h6 {
|
||||
font-family: var(--font-heading, var(--font-sans));
|
||||
}
|
||||
|
||||
/* Full-width blocks fill the editor canvas */
|
||||
.editor-styles-wrapper [data-type^="oribi/"] {
|
||||
max-width: none !important;
|
||||
margin-left: calc(-1 * var(--wp--custom--spacing--outer, 0px)) !important;
|
||||
margin-right: calc(-1 * var(--wp--custom--spacing--outer, 0px)) !important;
|
||||
}
|
||||
|
||||
/* Decorative overlays must not eat pointer events */
|
||||
.editor-styles-wrapper .hero-bg-grid,
|
||||
.editor-styles-wrapper .hero-glow,
|
||||
.editor-styles-wrapper .hero-glow-2 {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* RichText placeholders inside dark sections */
|
||||
.editor-styles-wrapper .hero [data-rich-text-placeholder]::after,
|
||||
.editor-styles-wrapper .cta-banner [data-rich-text-placeholder]::after,
|
||||
.editor-styles-wrapper .page-hero [data-rich-text-placeholder]::after {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
/* Force hero container to be clickable above decorative layers */
|
||||
.editor-styles-wrapper .hero .container,
|
||||
.editor-styles-wrapper .cta-banner .container,
|
||||
.editor-styles-wrapper .page-hero .container {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* Prevent cursor issues on static elements used as RichText wrappers */
|
||||
.editor-styles-wrapper .btn.btn-primary,
|
||||
.editor-styles-wrapper .btn.btn-ghost {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
/* Inline-editable stat values and labels */
|
||||
.editor-styles-wrapper .hero-stat-value[contenteditable],
|
||||
.editor-styles-wrapper .hero-stat-label[contenteditable] {
|
||||
cursor: text;
|
||||
min-width: 40px;
|
||||
outline: none;
|
||||
}
|
||||
.editor-styles-wrapper .hero-stat-value:focus,
|
||||
.editor-styles-wrapper .hero-stat-label:focus {
|
||||
outline: 1px dashed rgba(255,255,255,.25);
|
||||
outline-offset: 2px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* Secondary button placeholder visibility */
|
||||
.editor-styles-wrapper .btn.btn-ghost[data-rich-text-placeholder]::after {
|
||||
color: rgba(255,255,255,.3);
|
||||
}
|
||||
|
||||
/* Device visual shouldn't eat clicks from editable text */
|
||||
.editor-styles-wrapper .hero-devices {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Grid / card hover effects are distracting while editing */
|
||||
.editor-styles-wrapper .oribi-card:hover,
|
||||
.editor-styles-wrapper .feature-card:hover,
|
||||
.editor-styles-wrapper .pricing-card:hover {
|
||||
transform: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Ensure platform-row visuals don't overlap editable text */
|
||||
.editor-styles-wrapper .platform-visual {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ── Animated hero editor overrides ───────────────────────── */
|
||||
.editor-styles-wrapper .hero-animated .hero-particles,
|
||||
.editor-styles-wrapper .page-hero-animated .hero-particles,
|
||||
.editor-styles-wrapper .hero-animated__glow {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.editor-styles-wrapper .hero-animated .container,
|
||||
.editor-styles-wrapper .page-hero-animated .container {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.editor-styles-wrapper .hero-animated [data-rich-text-placeholder]::after,
|
||||
.editor-styles-wrapper .page-hero-animated [data-rich-text-placeholder]::after {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
/* Contact form placeholder in editor */
|
||||
.editor-styles-wrapper .contact-form-wrap {
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
/* Remove default block outline in favour of section styling */
|
||||
.editor-styles-wrapper [data-type^="oribi/"] > .block-editor-block-list__block-edit > [data-block] {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* ── InnerBlocks child styling ─────────────────────────────────────────── */
|
||||
|
||||
/* Child blocks inside grids: remove block-list spacing */
|
||||
.editor-styles-wrapper [data-type="oribi/feature-section"] .block-editor-inner-blocks,
|
||||
.editor-styles-wrapper [data-type="oribi/value-section"] .block-editor-inner-blocks,
|
||||
.editor-styles-wrapper [data-type="oribi/addon-section"] .block-editor-inner-blocks,
|
||||
.editor-styles-wrapper [data-type="oribi/image-section"] .block-editor-inner-blocks,
|
||||
.editor-styles-wrapper [data-type="oribi/stat-section"] .block-editor-inner-blocks,
|
||||
.editor-styles-wrapper [data-type="oribi/link-section"] .block-editor-inner-blocks,
|
||||
.editor-styles-wrapper [data-type="oribi/pricing-section"] .block-editor-inner-blocks,
|
||||
.editor-styles-wrapper [data-type="oribi/platform-section"] .block-editor-inner-blocks,
|
||||
.editor-styles-wrapper [data-type="oribi/trust-section"] .block-editor-inner-blocks {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Feature cards grid: children lay out inline */
|
||||
.editor-styles-wrapper .grid-2 > .block-editor-inner-blocks > .block-editor-block-list__layout,
|
||||
.editor-styles-wrapper .grid-3 > .block-editor-inner-blocks > .block-editor-block-list__layout,
|
||||
.editor-styles-wrapper .grid-4 > .block-editor-inner-blocks > .block-editor-block-list__layout {
|
||||
display: grid;
|
||||
gap: 2rem;
|
||||
}
|
||||
.editor-styles-wrapper .grid-2 > .block-editor-inner-blocks > .block-editor-block-list__layout {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
.editor-styles-wrapper .grid-3 > .block-editor-inner-blocks > .block-editor-block-list__layout {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
.editor-styles-wrapper .grid-4 > .block-editor-inner-blocks > .block-editor-block-list__layout {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
/* Pricing cards grid */
|
||||
.editor-styles-wrapper .pricing-grid > .block-editor-inner-blocks > .block-editor-block-list__layout {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
/* Trust items: stack vertically with gap */
|
||||
.editor-styles-wrapper [data-type="oribi/trust-section"] .grid-2 > div:first-child .block-editor-block-list__layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
/* ── Oribi Icon Picker ─────────────────────────────────────── */
|
||||
.oribi-icon-picker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.oribi-icon-current {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 7px 10px;
|
||||
background: #f0f0f1;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
color: #1e1e1e;
|
||||
min-height: 36px;
|
||||
}
|
||||
.oribi-icon-current i {
|
||||
font-size: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.oribi-icon-current-label {
|
||||
font-family: monospace;
|
||||
font-size: 11px;
|
||||
color: #555;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.oribi-icon-clear {
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
background: none;
|
||||
border: 1px solid #cc1818;
|
||||
border-radius: 3px;
|
||||
color: #cc1818;
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
padding: 2px 6px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.oribi-icon-clear:hover {
|
||||
background: #cc1818;
|
||||
color: #fff;
|
||||
}
|
||||
.oribi-icon-search {
|
||||
width: 100%;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid #949494;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
}
|
||||
.oribi-icon-search:focus {
|
||||
border-color: #3858e9;
|
||||
box-shadow: 0 0 0 2px rgba(56, 88, 233, .2);
|
||||
}
|
||||
.oribi-icon-count {
|
||||
font-size: 11px;
|
||||
color: #757575;
|
||||
text-align: right;
|
||||
line-height: 1;
|
||||
}
|
||||
.oribi-icon-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 3px;
|
||||
max-height: 252px;
|
||||
overflow-y: auto;
|
||||
padding: 4px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background: #fafafa;
|
||||
}
|
||||
.oribi-icon-grid::-webkit-scrollbar { width: 6px; }
|
||||
.oribi-icon-grid::-webkit-scrollbar-track { background: #f0f0f0; border-radius: 3px; }
|
||||
.oribi-icon-grid::-webkit-scrollbar-thumb { background: #bbb; border-radius: 3px; }
|
||||
.oribi-icon-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 3px;
|
||||
padding: 7px 2px 5px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
min-width: 0;
|
||||
transition: background .12s, border-color .12s, color .12s;
|
||||
color: #1e1e1e;
|
||||
}
|
||||
.oribi-icon-cell:hover {
|
||||
background: #e8f0fe;
|
||||
border-color: #3858e9;
|
||||
color: #3858e9;
|
||||
}
|
||||
.oribi-icon-cell.is-active {
|
||||
background: #3858e9;
|
||||
border-color: #3858e9;
|
||||
color: #fff;
|
||||
}
|
||||
.oribi-icon-cell i {
|
||||
font-size: 17px;
|
||||
line-height: 1;
|
||||
}
|
||||
.oribi-icon-cell span {
|
||||
font-size: 8.5px;
|
||||
color: inherit;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
text-align: center;
|
||||
opacity: 0.75;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.oribi-icon-empty {
|
||||
font-size: 12px;
|
||||
color: #757575;
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
1774
theme/blocks/editor.js
Normal file
1774
theme/blocks/editor.js
Normal file
File diff suppressed because it is too large
Load Diff
1548
theme/blocks/index.php
Normal file
1548
theme/blocks/index.php
Normal file
File diff suppressed because it is too large
Load Diff
29
theme/functions.php
Normal file
29
theme/functions.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
/**
|
||||
* Oribi Tech — Theme Bootstrap
|
||||
*
|
||||
* @package OTS_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
define( 'ORIBI_VERSION', wp_get_theme()->get( 'Version' ) );
|
||||
define( 'ORIBI_DIR', get_template_directory() );
|
||||
define( 'ORIBI_URI', get_template_directory_uri() );
|
||||
|
||||
/* ── Includes ──────────────────────────────────────────────── */
|
||||
require_once ORIBI_DIR . '/inc/setup.php';
|
||||
require_once ORIBI_DIR . '/inc/enqueue.php';
|
||||
require_once ORIBI_DIR . '/inc/ajax.php';
|
||||
|
||||
/* ── Customisable design-token system ──────────────────────── */
|
||||
require_once ORIBI_DIR . '/inc/theme-defaults.php';
|
||||
require_once ORIBI_DIR . '/inc/font-manager.php';
|
||||
require_once ORIBI_DIR . '/inc/theme-generator.php';
|
||||
require_once ORIBI_DIR . '/inc/theme-settings.php';
|
||||
|
||||
/* ── Custom Blocks (render callbacks + editor assets) ──────── */
|
||||
require_once ORIBI_DIR . '/blocks/index.php';
|
||||
64
theme/inc/ajax.php
Normal file
64
theme/inc/ajax.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
/**
|
||||
* AJAX Contact Form Handler
|
||||
*
|
||||
* Receives submissions from the oribi/contact-section block form,
|
||||
* validates input, and sends an email via wp_mail().
|
||||
*
|
||||
* @package OTS_Theme
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
add_action( 'wp_ajax_oribi_contact', 'oribi_handle_contact' );
|
||||
add_action( 'wp_ajax_nopriv_oribi_contact', 'oribi_handle_contact' );
|
||||
|
||||
/**
|
||||
* Process the contact form AJAX request.
|
||||
*/
|
||||
function oribi_handle_contact() {
|
||||
|
||||
// Verify nonce
|
||||
if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'oribi_contact_nonce' ) ) {
|
||||
wp_send_json_error( 'Security check failed. Please refresh the page and try again.' );
|
||||
}
|
||||
|
||||
$name = isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : '';
|
||||
$email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';
|
||||
$interest = isset( $_POST['interest'] ) ? sanitize_text_field( wp_unslash( $_POST['interest'] ) ) : '';
|
||||
$message = isset( $_POST['message'] ) ? sanitize_textarea_field( wp_unslash( $_POST['message'] ) ) : '';
|
||||
|
||||
// Validate required fields
|
||||
if ( empty( $name ) || empty( $email ) || empty( $message ) ) {
|
||||
wp_send_json_error( 'Please fill in all required fields.' );
|
||||
}
|
||||
|
||||
if ( ! is_email( $email ) ) {
|
||||
wp_send_json_error( 'Please enter a valid email address.' );
|
||||
}
|
||||
|
||||
// Build the email
|
||||
$to = get_option( 'admin_email' );
|
||||
$subject = sprintf( '[OTS Theme] New inquiry from %s', $name );
|
||||
$body = sprintf(
|
||||
"Name: %s\nEmail: %s\nInterested In: %s\n\nMessage:\n%s",
|
||||
$name,
|
||||
$email,
|
||||
$interest ? $interest : 'Not specified',
|
||||
$message
|
||||
);
|
||||
$headers = [
|
||||
'Content-Type: text/plain; charset=UTF-8',
|
||||
sprintf( 'Reply-To: %s <%s>', $name, $email ),
|
||||
];
|
||||
|
||||
$sent = wp_mail( $to, $subject, $body, $headers );
|
||||
|
||||
if ( $sent ) {
|
||||
wp_send_json_success( "Thanks! We'll get back to you shortly." );
|
||||
} else {
|
||||
wp_send_json_error( 'Something went wrong. Please try again or email us directly.' );
|
||||
}
|
||||
}
|
||||
110
theme/inc/enqueue.php
Normal file
110
theme/inc/enqueue.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
/**
|
||||
* Asset Enqueuing — frontend styles, scripts, and editor additions.
|
||||
*
|
||||
* @package OTS_Theme
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/* ── Frontend assets ───────────────────────────────────────── */
|
||||
add_action( 'wp_enqueue_scripts', function () {
|
||||
|
||||
// Main stylesheet (supplements theme.json generated styles)
|
||||
wp_enqueue_style(
|
||||
'oribi-main',
|
||||
ORIBI_URI . '/assets/css/main.css',
|
||||
[],
|
||||
ORIBI_VERSION
|
||||
);
|
||||
|
||||
// Main JS — dark mode, sticky header, mobile nav, scroll animations
|
||||
wp_enqueue_script(
|
||||
'oribi-main',
|
||||
ORIBI_URI . '/assets/js/main.js',
|
||||
[],
|
||||
ORIBI_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
// Localize AJAX endpoint for the contact form
|
||||
wp_localize_script( 'oribi-main', 'oribiAjax', [
|
||||
'url' => admin_url( 'admin-ajax.php' ),
|
||||
'nonce' => wp_create_nonce( 'oribi_contact_nonce' ),
|
||||
] );
|
||||
|
||||
// Threaded comments (if ever enabled)
|
||||
if ( is_singular() && comments_open() && get_option( 'thread_comments' ) ) {
|
||||
wp_enqueue_script( 'comment-reply' );
|
||||
}
|
||||
} );
|
||||
|
||||
/* ── Font Awesome 6 Free (CDN) ─────────────────────────────── */
|
||||
add_action( 'wp_enqueue_scripts', function () {
|
||||
wp_enqueue_style(
|
||||
'font-awesome',
|
||||
'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css',
|
||||
[],
|
||||
null
|
||||
);
|
||||
} );
|
||||
|
||||
add_action( 'enqueue_block_editor_assets', function () {
|
||||
wp_enqueue_style(
|
||||
'font-awesome',
|
||||
'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css',
|
||||
[],
|
||||
null
|
||||
);
|
||||
} );
|
||||
|
||||
/* ── Google Fonts — dynamic based on theme settings ────────── */
|
||||
add_action( 'wp_enqueue_scripts', 'oribi_enqueue_selected_fonts' );
|
||||
add_action( 'enqueue_block_editor_assets', 'oribi_enqueue_selected_fonts' );
|
||||
|
||||
/**
|
||||
* Enqueue Google Fonts stylesheets for the selected body & heading fonts.
|
||||
*/
|
||||
function oribi_enqueue_selected_fonts() {
|
||||
if ( ! function_exists( 'oribi_get_setting' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$slugs = array_unique( array_filter( [
|
||||
oribi_get_setting( 'font_family' ),
|
||||
oribi_get_setting( 'font_heading' ),
|
||||
] ) );
|
||||
|
||||
foreach ( $slugs as $slug ) {
|
||||
$url = oribi_get_google_font_url( $slug );
|
||||
if ( $url ) {
|
||||
wp_enqueue_style(
|
||||
'oribi-gf-' . $slug,
|
||||
$url,
|
||||
[],
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Generated theme CSS (colour / font / spacing overrides) ─ */
|
||||
add_action( 'wp_enqueue_scripts', 'oribi_enqueue_generated_css', 20 );
|
||||
add_action( 'enqueue_block_editor_assets', 'oribi_enqueue_generated_css', 20 );
|
||||
|
||||
/**
|
||||
* Enqueue the dynamically generated CSS file that applies design-token
|
||||
* overrides from the admin settings page.
|
||||
*/
|
||||
function oribi_enqueue_generated_css() {
|
||||
if ( ! function_exists( 'oribi_generated_css_url' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$url = oribi_generated_css_url();
|
||||
$ver = get_theme_mod( 'oribi_css_version', '1' );
|
||||
|
||||
wp_enqueue_style( 'oribi-custom-tokens', $url, [ 'oribi-main' ], $ver );
|
||||
}
|
||||
260
theme/inc/font-manager.php
Normal file
260
theme/inc/font-manager.php
Normal file
@@ -0,0 +1,260 @@
|
||||
<?php
|
||||
/**
|
||||
* Font Manager — Bridges the WordPress Font Library with theme settings.
|
||||
*
|
||||
* Uses the built-in Font Library (WP 6.5+) for registration, discovery,
|
||||
* and @font-face generation. The admin settings page uses the helpers
|
||||
* in this file to populate font-family dropdowns.
|
||||
*
|
||||
* @package OTS_Theme
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a curated set of Google Fonts with WordPress's Font Library
|
||||
* so they appear immediately in the admin settings page dropdown.
|
||||
*
|
||||
* Admins can add more fonts via Appearance → Editor → Fonts at any time.
|
||||
*/
|
||||
add_action( 'after_setup_theme', function () {
|
||||
|
||||
/*
|
||||
* WordPress 6.5+ ships wp_register_font_family() / wp_register_font_face()
|
||||
* via the Fonts API (WP_Fonts or WP_Webfonts depending on merge iteration).
|
||||
* We wrap the calls so the theme degrades gracefully on older installs.
|
||||
*/
|
||||
if ( ! function_exists( 'wp_register_webfont_provider' ) && ! class_exists( 'WP_Fonts' ) ) {
|
||||
// Font Library is available in WP 6.5+; on older versions the
|
||||
// Google Fonts CDN link in enqueue.php serves as the fallback.
|
||||
return;
|
||||
}
|
||||
} );
|
||||
|
||||
/**
|
||||
* Return every font family available to this WordPress installation.
|
||||
*
|
||||
* Sources (merged, de-duped by slug):
|
||||
* 1. theme.json → settings.typography.fontFamilies
|
||||
* 2. Font Library → any fonts the user installed via Site Editor
|
||||
* 3. Bundled list → a small curated set of popular Google Fonts
|
||||
*
|
||||
* @return array<int,array{slug:string,name:string,fontFamily:string}>
|
||||
*/
|
||||
function oribi_get_available_fonts() {
|
||||
|
||||
$fonts = [];
|
||||
|
||||
/* ── 1. theme.json registered families ─────────────────── */
|
||||
$theme_json = WP_Theme_JSON_Resolver::get_merged_data()->get_data();
|
||||
if ( ! empty( $theme_json['settings']['typography']['fontFamilies']['theme'] ) ) {
|
||||
foreach ( $theme_json['settings']['typography']['fontFamilies']['theme'] as $f ) {
|
||||
$fonts[ $f['slug'] ] = [
|
||||
'slug' => $f['slug'],
|
||||
'name' => $f['name'] ?? $f['slug'],
|
||||
'fontFamily' => $f['fontFamily'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 2. Font Library (user-installed via Site Editor) ───── */
|
||||
$font_families = oribi_query_font_library();
|
||||
foreach ( $font_families as $f ) {
|
||||
$slug = sanitize_title( $f['slug'] ?? $f['name'] );
|
||||
if ( ! isset( $fonts[ $slug ] ) ) {
|
||||
$fonts[ $slug ] = [
|
||||
'slug' => $slug,
|
||||
'name' => $f['name'],
|
||||
'fontFamily' => $f['fontFamily'] ?? "'{$f['name']}', sans-serif",
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 3. Bundled Google Fonts (always shown as available) ── */
|
||||
$bundled = oribi_get_bundled_google_fonts();
|
||||
foreach ( $bundled as $slug => $data ) {
|
||||
if ( ! isset( $fonts[ $slug ] ) ) {
|
||||
$fonts[ $slug ] = $data;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort alphabetically by display name.
|
||||
uasort( $fonts, function ( $a, $b ) {
|
||||
return strcasecmp( $a['name'], $b['name'] );
|
||||
} );
|
||||
|
||||
return array_values( $fonts );
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the WP Font Library (wp_font_family post type) for installed fonts.
|
||||
*
|
||||
* @return array<int,array{slug:string,name:string,fontFamily:string}>
|
||||
*/
|
||||
function oribi_query_font_library() {
|
||||
|
||||
$results = [];
|
||||
|
||||
// Font Library stores each font family as a wp_font_family post (WP 6.5+).
|
||||
$query = new WP_Query( [
|
||||
'post_type' => 'wp_font_family',
|
||||
'posts_per_page' => 100,
|
||||
'post_status' => 'publish',
|
||||
'no_found_rows' => true,
|
||||
] );
|
||||
|
||||
if ( $query->have_posts() ) {
|
||||
foreach ( $query->posts as $post ) {
|
||||
$content = json_decode( $post->post_content, true );
|
||||
if ( $content ) {
|
||||
$results[] = [
|
||||
'slug' => $content['slug'] ?? sanitize_title( $post->post_title ),
|
||||
'name' => $post->post_title,
|
||||
'fontFamily' => $content['fontFamily'] ?? "'{$post->post_title}', sans-serif",
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
wp_reset_postdata();
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a curated set of popular Google Fonts bundled with the theme.
|
||||
*
|
||||
* These are always shown in the font selector even if the user hasn't
|
||||
* explicitly installed them via the Font Library. WordPress will load
|
||||
* them from Google Fonts CDN when selected.
|
||||
*
|
||||
* @return array<string,array{slug:string,name:string,fontFamily:string,googleUrl:string}>
|
||||
*/
|
||||
function oribi_get_bundled_google_fonts() {
|
||||
|
||||
return [
|
||||
'inter' => [
|
||||
'slug' => 'inter',
|
||||
'name' => 'Inter',
|
||||
'fontFamily' => "'Inter', system-ui, -apple-system, sans-serif",
|
||||
'googleUrl' => 'https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap',
|
||||
],
|
||||
'roboto' => [
|
||||
'slug' => 'roboto',
|
||||
'name' => 'Roboto',
|
||||
'fontFamily' => "'Roboto', system-ui, sans-serif",
|
||||
'googleUrl' => 'https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700;900&display=swap',
|
||||
],
|
||||
'open-sans' => [
|
||||
'slug' => 'open-sans',
|
||||
'name' => 'Open Sans',
|
||||
'fontFamily' => "'Open Sans', system-ui, sans-serif",
|
||||
'googleUrl' => 'https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700;800&display=swap',
|
||||
],
|
||||
'poppins' => [
|
||||
'slug' => 'poppins',
|
||||
'name' => 'Poppins',
|
||||
'fontFamily' => "'Poppins', system-ui, sans-serif",
|
||||
'googleUrl' => 'https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700;800;900&display=swap',
|
||||
],
|
||||
'lato' => [
|
||||
'slug' => 'lato',
|
||||
'name' => 'Lato',
|
||||
'fontFamily' => "'Lato', system-ui, sans-serif",
|
||||
'googleUrl' => 'https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700;900&display=swap',
|
||||
],
|
||||
'montserrat' => [
|
||||
'slug' => 'montserrat',
|
||||
'name' => 'Montserrat',
|
||||
'fontFamily' => "'Montserrat', system-ui, sans-serif",
|
||||
'googleUrl' => 'https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700;800;900&display=swap',
|
||||
],
|
||||
'source-sans-3' => [
|
||||
'slug' => 'source-sans-3',
|
||||
'name' => 'Source Sans 3',
|
||||
'fontFamily' => "'Source Sans 3', system-ui, sans-serif",
|
||||
'googleUrl' => 'https://fonts.googleapis.com/css2?family=Source+Sans+3:wght@300;400;500;600;700;800;900&display=swap',
|
||||
],
|
||||
'nunito' => [
|
||||
'slug' => 'nunito',
|
||||
'name' => 'Nunito',
|
||||
'fontFamily' => "'Nunito', system-ui, sans-serif",
|
||||
'googleUrl' => 'https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;500;600;700;800;900&display=swap',
|
||||
],
|
||||
'raleway' => [
|
||||
'slug' => 'raleway',
|
||||
'name' => 'Raleway',
|
||||
'fontFamily' => "'Raleway', system-ui, sans-serif",
|
||||
'googleUrl' => 'https://fonts.googleapis.com/css2?family=Raleway:wght@300;400;500;600;700;800;900&display=swap',
|
||||
],
|
||||
'dm-sans' => [
|
||||
'slug' => 'dm-sans',
|
||||
'name' => 'DM Sans',
|
||||
'fontFamily' => "'DM Sans', system-ui, sans-serif",
|
||||
'googleUrl' => 'https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&display=swap',
|
||||
],
|
||||
'work-sans' => [
|
||||
'slug' => 'work-sans',
|
||||
'name' => 'Work Sans',
|
||||
'fontFamily' => "'Work Sans', system-ui, sans-serif",
|
||||
'googleUrl' => 'https://fonts.googleapis.com/css2?family=Work+Sans:wght@300;400;500;600;700;800;900&display=swap',
|
||||
],
|
||||
'plus-jakarta-sans' => [
|
||||
'slug' => 'plus-jakarta-sans',
|
||||
'name' => 'Plus Jakarta Sans',
|
||||
'fontFamily' => "'Plus Jakarta Sans', system-ui, sans-serif",
|
||||
'googleUrl' => 'https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap',
|
||||
],
|
||||
'system-ui' => [
|
||||
'slug' => 'system-ui',
|
||||
'name' => 'System UI (no download)',
|
||||
'fontFamily' => "system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif",
|
||||
'googleUrl' => '',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a font slug to its CSS font-family value.
|
||||
*
|
||||
* Checks (in order): bundled list → Font Library → theme.json.
|
||||
*
|
||||
* @param string $slug Font slug (e.g. 'inter', 'roboto').
|
||||
* @return string CSS font-family value.
|
||||
*/
|
||||
function oribi_get_font_family_css( $slug ) {
|
||||
|
||||
if ( empty( $slug ) ) {
|
||||
$slug = 'inter';
|
||||
}
|
||||
|
||||
// Bundled fonts (includes Google Fonts URL for enqueue).
|
||||
$bundled = oribi_get_bundled_google_fonts();
|
||||
if ( isset( $bundled[ $slug ] ) ) {
|
||||
return $bundled[ $slug ]['fontFamily'];
|
||||
}
|
||||
|
||||
// Search all available fonts.
|
||||
$fonts = oribi_get_available_fonts();
|
||||
foreach ( $fonts as $f ) {
|
||||
if ( $f['slug'] === $slug ) {
|
||||
return $f['fontFamily'];
|
||||
}
|
||||
}
|
||||
|
||||
// Ultimate fallback.
|
||||
return "'Inter', system-ui, -apple-system, sans-serif";
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Google Fonts stylesheet URL for a given font slug,
|
||||
* or empty string if not a bundled Google Font.
|
||||
*
|
||||
* @param string $slug Font slug.
|
||||
* @return string URL or ''.
|
||||
*/
|
||||
function oribi_get_google_font_url( $slug ) {
|
||||
$bundled = oribi_get_bundled_google_fonts();
|
||||
return isset( $bundled[ $slug ] ) ? $bundled[ $slug ]['googleUrl'] : '';
|
||||
}
|
||||
63
theme/inc/setup.php
Normal file
63
theme/inc/setup.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
/**
|
||||
* Theme Setup — registers supports, menus, patterns, and editor styles.
|
||||
*
|
||||
* @package OTS_Theme
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/* ── Theme supports ────────────────────────────────────────── */
|
||||
add_action( 'after_setup_theme', function () {
|
||||
|
||||
// FSE / block essentials
|
||||
add_theme_support( 'wp-block-styles' );
|
||||
add_theme_support( 'responsive-embeds' );
|
||||
add_theme_support( 'editor-styles' );
|
||||
|
||||
// Classic fallback supports (still respected by block themes)
|
||||
add_theme_support( 'automatic-feed-links' );
|
||||
add_theme_support( 'title-tag' );
|
||||
add_theme_support( 'post-thumbnails' );
|
||||
add_theme_support( 'custom-logo', [
|
||||
'height' => 40,
|
||||
'width' => 180,
|
||||
'flex-height' => true,
|
||||
'flex-width' => true,
|
||||
] );
|
||||
add_theme_support( 'html5', [
|
||||
'search-form',
|
||||
'comment-form',
|
||||
'comment-list',
|
||||
'gallery',
|
||||
'caption',
|
||||
'style',
|
||||
'script',
|
||||
] );
|
||||
|
||||
// Load main.css inside the block editor so previews match the frontend
|
||||
add_editor_style( 'assets/css/main.css' );
|
||||
|
||||
// Navigation menus (used by oribi/site-header block)
|
||||
register_nav_menus( [
|
||||
'primary' => __( 'Primary Menu', 'ots-theme' ),
|
||||
'footer' => __( 'Footer Menu', 'ots-theme' ),
|
||||
] );
|
||||
} );
|
||||
|
||||
/* ── Block pattern categories ──────────────────────────────── */
|
||||
add_action( 'init', function () {
|
||||
register_block_pattern_category( 'oribi-pages', [
|
||||
'label' => __( 'Oribi Tech — Pages', 'ots-theme' ),
|
||||
] );
|
||||
register_block_pattern_category( 'oribi-sections', [
|
||||
'label' => __( 'Oribi Tech — Sections', 'ots-theme' ),
|
||||
] );
|
||||
} );
|
||||
|
||||
/* ── Remove core block patterns if desired ─────────────────── */
|
||||
add_action( 'after_setup_theme', function () {
|
||||
remove_theme_support( 'core-block-patterns' );
|
||||
} );
|
||||
111
theme/inc/theme-defaults.php
Normal file
111
theme/inc/theme-defaults.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
/**
|
||||
* Theme Defaults — Provides default values for all customizable design tokens.
|
||||
*
|
||||
* These defaults match the original hardcoded values so existing sites
|
||||
* see no visual change when upgrading.
|
||||
*
|
||||
* @package OTS_Theme
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the full array of default design-token values.
|
||||
*
|
||||
* Keys mirror the theme-mod names used throughout the customisation system.
|
||||
*
|
||||
* @return array<string,string>
|
||||
*/
|
||||
function oribi_get_theme_defaults() {
|
||||
|
||||
return [
|
||||
|
||||
/* ── Light-mode colour palette ──────────────────────── */
|
||||
'color_primary' => '#D83302',
|
||||
'color_primary_dk' => '#B52B02',
|
||||
'color_primary_lt' => '#FEF0EB',
|
||||
'color_accent' => '#00757c',
|
||||
'color_accent_dk' => '#005a60',
|
||||
'color_accent_lt' => '#E6F4F5',
|
||||
'color_dark' => '#0D1321',
|
||||
'color_dark_2' => '#1A2236',
|
||||
'color_text' => '#2D3748',
|
||||
'color_text_muted' => '#718096',
|
||||
'color_border' => '#E2E8F0',
|
||||
'color_bg' => '#FFFFFF',
|
||||
'color_bg_alt' => '#FFF8F5',
|
||||
|
||||
/* ── Dark-mode colour palette ───────────────────────── */
|
||||
'dark_primary' => '#FF6B3D',
|
||||
'dark_primary_dk' => '#D83302',
|
||||
'dark_primary_lt' => 'rgba(216,51,2,0.15)',
|
||||
'dark_accent' => '#00757c',
|
||||
'dark_accent_dk' => '#005a60',
|
||||
'dark_accent_lt' => 'rgba(0,117,124,0.15)',
|
||||
'dark_dark' => '#E2E8F0',
|
||||
'dark_dark_2' => '#CBD5E0',
|
||||
'dark_text' => '#CBD5E0',
|
||||
'dark_text_muted' => '#A0AEC0',
|
||||
'dark_border' => '#2D3748',
|
||||
'dark_bg' => '#0F1724',
|
||||
'dark_bg_alt' => '#151F30',
|
||||
'dark_bg_dark' => '#0A0F1A',
|
||||
'dark_heading' => '#F7FAFC',
|
||||
'dark_card_bg' => '#151F30',
|
||||
|
||||
/* ── Typography ─────────────────────────────────────── */
|
||||
'font_family' => 'inter', // slug from WP Font Library
|
||||
'font_heading' => '', // empty = same as body
|
||||
|
||||
/* ── Border radii ───────────────────────────────────── */
|
||||
'radius_sm' => '6',
|
||||
'radius_md' => '12',
|
||||
'radius_lg' => '20',
|
||||
'radius_xl' => '32',
|
||||
|
||||
/* ── Spacing / layout ───────────────────────────────── */
|
||||
'container_max' => '1200',
|
||||
'container_pad_min' => '1',
|
||||
'container_pad_max' => '2',
|
||||
'wide_size' => '1536',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a single design-token value, respecting saved theme-mod overrides.
|
||||
*
|
||||
* @param string $key Token name (e.g. 'color_primary').
|
||||
* @return string
|
||||
*/
|
||||
function oribi_get_setting( $key ) {
|
||||
$defaults = oribi_get_theme_defaults();
|
||||
$default = isset( $defaults[ $key ] ) ? $defaults[ $key ] : '';
|
||||
return get_theme_mod( 'oribi_' . $key, $default );
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the current set of defaults into theme-mods (one-time migration).
|
||||
*
|
||||
* Called the first time the settings page is loaded, so that existing sites
|
||||
* start with their current visual identity already stored.
|
||||
*/
|
||||
function oribi_maybe_seed_defaults() {
|
||||
|
||||
if ( get_theme_mod( 'oribi_defaults_seeded' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$defaults = oribi_get_theme_defaults();
|
||||
|
||||
foreach ( $defaults as $key => $value ) {
|
||||
// Only set if the user hasn't already saved a value.
|
||||
if ( false === get_theme_mod( 'oribi_' . $key, false ) ) {
|
||||
set_theme_mod( 'oribi_' . $key, $value );
|
||||
}
|
||||
}
|
||||
|
||||
set_theme_mod( 'oribi_defaults_seeded', true );
|
||||
}
|
||||
306
theme/inc/theme-generator.php
Normal file
306
theme/inc/theme-generator.php
Normal file
@@ -0,0 +1,306 @@
|
||||
<?php
|
||||
/**
|
||||
* Theme Generator — Builds and caches a CSS file from saved design tokens.
|
||||
*
|
||||
* Reads theme-mods written by the admin settings page and produces a
|
||||
* static CSS file in the uploads directory. The file is enqueued after
|
||||
* main.css so that custom values override defaults.
|
||||
*
|
||||
* @package OTS_Theme
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the absolute filesystem path of the generated CSS file.
|
||||
*
|
||||
* Multi-site aware — each site gets its own file.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function oribi_generated_css_path() {
|
||||
$upload_dir = wp_upload_dir();
|
||||
$blog_id = get_current_blog_id();
|
||||
return trailingslashit( $upload_dir['basedir'] ) . "oribi-theme-{$blog_id}-custom.css";
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the public URL of the generated CSS file.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function oribi_generated_css_url() {
|
||||
$upload_dir = wp_upload_dir();
|
||||
$blog_id = get_current_blog_id();
|
||||
return trailingslashit( $upload_dir['baseurl'] ) . "oribi-theme-{$blog_id}-custom.css";
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: convert a hex colour like #D83302 to its "r,g,b" string.
|
||||
*
|
||||
* @param string $hex Hex colour with or without leading #.
|
||||
* @return string e.g. "216,51,2"
|
||||
*/
|
||||
function oribi_hex_to_rgb( $hex ) {
|
||||
$hex = ltrim( $hex, '#' );
|
||||
if ( strlen( $hex ) === 3 ) {
|
||||
$hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
|
||||
}
|
||||
if ( strlen( $hex ) !== 6 ) {
|
||||
return '0,0,0';
|
||||
}
|
||||
$r = hexdec( substr( $hex, 0, 2 ) );
|
||||
$g = hexdec( substr( $hex, 2, 2 ) );
|
||||
$b = hexdec( substr( $hex, 4, 2 ) );
|
||||
return "{$r},{$g},{$b}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the CSS string from current theme-mod values.
|
||||
*
|
||||
* @return string Complete CSS content.
|
||||
*/
|
||||
function oribi_build_css() {
|
||||
|
||||
$s = 'oribi_get_setting'; // shorthand
|
||||
|
||||
// Gather values.
|
||||
$primary = $s( 'color_primary' );
|
||||
$primary_dk = $s( 'color_primary_dk' );
|
||||
$primary_lt = $s( 'color_primary_lt' );
|
||||
$accent = $s( 'color_accent' );
|
||||
$accent_dk = $s( 'color_accent_dk' );
|
||||
$accent_lt = $s( 'color_accent_lt' );
|
||||
$dark = $s( 'color_dark' );
|
||||
$dark_2 = $s( 'color_dark_2' );
|
||||
$text = $s( 'color_text' );
|
||||
$text_muted = $s( 'color_text_muted' );
|
||||
$border = $s( 'color_border' );
|
||||
$bg = $s( 'color_bg' );
|
||||
$bg_alt = $s( 'color_bg_alt' );
|
||||
|
||||
// Dark mode.
|
||||
$dk_primary = $s( 'dark_primary' );
|
||||
$dk_primary_dk = $s( 'dark_primary_dk' );
|
||||
$dk_primary_lt = $s( 'dark_primary_lt' );
|
||||
$dk_accent = $s( 'dark_accent' );
|
||||
$dk_accent_dk = $s( 'dark_accent_dk' );
|
||||
$dk_accent_lt = $s( 'dark_accent_lt' );
|
||||
$dk_dark = $s( 'dark_dark' );
|
||||
$dk_dark_2 = $s( 'dark_dark_2' );
|
||||
$dk_text = $s( 'dark_text' );
|
||||
$dk_text_muted = $s( 'dark_text_muted' );
|
||||
$dk_border = $s( 'dark_border' );
|
||||
$dk_bg = $s( 'dark_bg' );
|
||||
$dk_bg_alt = $s( 'dark_bg_alt' );
|
||||
$dk_bg_dark = $s( 'dark_bg_dark' );
|
||||
$dk_heading = $s( 'dark_heading' );
|
||||
$dk_card_bg = $s( 'dark_card_bg' );
|
||||
|
||||
// Typography.
|
||||
$body_font_slug = $s( 'font_family' );
|
||||
$heading_font_slug = $s( 'font_heading' );
|
||||
$body_font_css = oribi_get_font_family_css( $body_font_slug );
|
||||
$heading_font_css = $heading_font_slug ? oribi_get_font_family_css( $heading_font_slug ) : $body_font_css;
|
||||
|
||||
// Border radii.
|
||||
$r_sm = intval( $s( 'radius_sm' ) );
|
||||
$r_md = intval( $s( 'radius_md' ) );
|
||||
$r_lg = intval( $s( 'radius_lg' ) );
|
||||
$r_xl = intval( $s( 'radius_xl' ) );
|
||||
|
||||
// Layout.
|
||||
$c_max = intval( $s( 'container_max' ) );
|
||||
$c_pad_min = floatval( $s( 'container_pad_min' ) );
|
||||
$c_pad_max = floatval( $s( 'container_pad_max' ) );
|
||||
$wide = intval( $s( 'wide_size' ) );
|
||||
|
||||
$primary_rgb = oribi_hex_to_rgb( $primary );
|
||||
$accent_rgb = oribi_hex_to_rgb( $accent );
|
||||
$dk_primary_rgb = oribi_hex_to_rgb( $dk_primary );
|
||||
|
||||
// Build CSS.
|
||||
$css = <<<CSS
|
||||
/* ================================================================
|
||||
OTS Theme — Generated Theme Overrides
|
||||
Generated: %s
|
||||
================================================================ */
|
||||
|
||||
/* ── WordPress preset colour overrides ─────────────────────────── */
|
||||
:root {
|
||||
--wp--preset--color--primary: {$primary};
|
||||
--wp--preset--color--primary-dk: {$primary_dk};
|
||||
--wp--preset--color--primary-lt: {$primary_lt};
|
||||
--wp--preset--color--accent: {$accent};
|
||||
--wp--preset--color--accent-dk: {$accent_dk};
|
||||
--wp--preset--color--accent-lt: {$accent_lt};
|
||||
--wp--preset--color--dark: {$dark};
|
||||
--wp--preset--color--dark-2: {$dark_2};
|
||||
--wp--preset--color--text: {$text};
|
||||
--wp--preset--color--text-muted: {$text_muted};
|
||||
--wp--preset--color--border: {$border};
|
||||
--wp--preset--color--bg: {$bg};
|
||||
--wp--preset--color--bg-alt: {$bg_alt};
|
||||
}
|
||||
|
||||
/* ── Light-mode aliases (consumed by main.css) ─────────────────── */
|
||||
:root,
|
||||
[data-theme="light"] {
|
||||
--color-primary: {$primary};
|
||||
--color-primary-dk: {$primary_dk};
|
||||
--color-primary-lt: {$primary_lt};
|
||||
--color-primary-rgb: {$primary_rgb};
|
||||
--color-accent: {$accent};
|
||||
--color-accent-dk: {$accent_dk};
|
||||
--color-accent-lt: {$accent_lt};
|
||||
--color-accent-rgb: {$accent_rgb};
|
||||
--color-dark: {$dark};
|
||||
--color-dark-2: {$dark_2};
|
||||
--color-text: {$text};
|
||||
--color-text-muted: {$text_muted};
|
||||
--color-border: {$border};
|
||||
--color-bg: {$bg};
|
||||
--color-bg-alt: {$bg_alt};
|
||||
--color-bg-dark: {$dark};
|
||||
--color-heading: {$dark};
|
||||
--header-scrolled-bg: rgba(255,255,255,.97);
|
||||
--header-scrolled-text: {$text};
|
||||
--card-bg: {$bg};
|
||||
--form-bg: {$bg_alt};
|
||||
--form-bg-focus: {$bg};
|
||||
|
||||
/* ── Typography ── */
|
||||
--font-sans: {$body_font_css};
|
||||
--font-heading: {$heading_font_css};
|
||||
|
||||
/* ── Border radii ── */
|
||||
--radius-sm: {$r_sm}px;
|
||||
--radius-md: {$r_md}px;
|
||||
--radius-lg: {$r_lg}px;
|
||||
--radius-xl: {$r_xl}px;
|
||||
--wp--custom--radius--sm: {$r_sm}px;
|
||||
--wp--custom--radius--md: {$r_md}px;
|
||||
--wp--custom--radius--lg: {$r_lg}px;
|
||||
--wp--custom--radius--xl: {$r_xl}px;
|
||||
|
||||
/* ── Layout ── */
|
||||
--container-max: {$c_max}px;
|
||||
--container-pad: clamp({$c_pad_min}rem, 5vw, {$c_pad_max}rem);
|
||||
--wp--custom--container--max: {$c_max}px;
|
||||
--wp--custom--container--pad: clamp({$c_pad_min}rem, 5vw, {$c_pad_max}rem);
|
||||
}
|
||||
|
||||
/* ── Dark mode overrides ───────────────────────────────────────── */
|
||||
[data-theme="dark"] {
|
||||
--wp--custom--dark--primary: {$dk_primary};
|
||||
--wp--custom--dark--primary-dk: {$dk_primary_dk};
|
||||
--wp--custom--dark--primary-lt: {$dk_primary_lt};
|
||||
--wp--custom--dark--accent: {$dk_accent};
|
||||
--wp--custom--dark--accent-dk: {$dk_accent_dk};
|
||||
--wp--custom--dark--accent-lt: {$dk_accent_lt};
|
||||
--wp--custom--dark--dark: {$dk_dark};
|
||||
--wp--custom--dark--dark-2: {$dk_dark_2};
|
||||
--wp--custom--dark--text: {$dk_text};
|
||||
--wp--custom--dark--text-muted: {$dk_text_muted};
|
||||
--wp--custom--dark--border: {$dk_border};
|
||||
--wp--custom--dark--bg: {$dk_bg};
|
||||
--wp--custom--dark--bg-alt: {$dk_bg_alt};
|
||||
--wp--custom--dark--bg-dark: {$dk_bg_dark};
|
||||
--wp--custom--dark--heading: {$dk_heading};
|
||||
--wp--custom--dark--card-bg: {$dk_card_bg};
|
||||
|
||||
--color-primary: {$dk_primary};
|
||||
--color-primary-dk: {$dk_primary_dk};
|
||||
--color-primary-lt: {$dk_primary_lt};
|
||||
--color-primary-rgb: {$dk_primary_rgb};
|
||||
--color-accent: {$dk_accent};
|
||||
--color-accent-dk: {$dk_accent_dk};
|
||||
--color-accent-lt: {$dk_accent_lt};
|
||||
--color-dark: {$dk_dark};
|
||||
--color-dark-2: {$dk_dark_2};
|
||||
--color-text: {$dk_text};
|
||||
--color-text-muted: {$dk_text_muted};
|
||||
--color-border: {$dk_border};
|
||||
--color-bg: {$dk_bg};
|
||||
--color-bg-alt: {$dk_bg_alt};
|
||||
--color-bg-dark: {$dk_bg_dark};
|
||||
--color-heading: {$dk_heading};
|
||||
--header-scrolled-bg: rgba(15,23,36,.97);
|
||||
--header-scrolled-text: {$dk_text};
|
||||
--card-bg: {$dk_card_bg};
|
||||
--form-bg: {$dk_card_bg};
|
||||
--form-bg-focus: #1A2538;
|
||||
}
|
||||
|
||||
/* ── Typography application ────────────────────────────────────── */
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6,
|
||||
.wp-block-heading {
|
||||
font-family: var(--font-heading, var(--font-sans));
|
||||
}
|
||||
|
||||
/* ── WordPress layout overrides ────────────────────────────────── */
|
||||
.wp-site-blocks > .wp-block-group,
|
||||
.wp-site-blocks > .alignfull {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
body > .is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)) {
|
||||
max-width: {$c_max}px;
|
||||
}
|
||||
|
||||
CSS;
|
||||
|
||||
return sprintf( $css, gmdate( 'Y-m-d H:i:s' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the generated CSS file to disk.
|
||||
*
|
||||
* @return bool|WP_Error True on success, WP_Error on failure.
|
||||
*/
|
||||
function oribi_write_generated_css() {
|
||||
|
||||
$css = oribi_build_css();
|
||||
$path = oribi_generated_css_path();
|
||||
|
||||
// Ensure directory exists.
|
||||
$dir = dirname( $path );
|
||||
if ( ! file_exists( $dir ) ) {
|
||||
wp_mkdir_p( $dir );
|
||||
}
|
||||
|
||||
// Use WP_Filesystem for safe file writing.
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
global $wp_filesystem;
|
||||
if ( ! WP_Filesystem() ) {
|
||||
return new WP_Error( 'fs', __( 'Could not initialise filesystem.', 'ots-theme' ) );
|
||||
}
|
||||
|
||||
$result = $wp_filesystem->put_contents( $path, $css, FS_CHMOD_FILE );
|
||||
if ( ! $result ) {
|
||||
return new WP_Error( 'write', __( 'Could not write generated CSS file.', 'ots-theme' ) );
|
||||
}
|
||||
|
||||
// Bump version number so browsers cache-bust.
|
||||
$version = intval( get_theme_mod( 'oribi_css_version', 0 ) ) + 1;
|
||||
set_theme_mod( 'oribi_css_version', $version );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerate the CSS file if it doesn't exist yet (e.g. first page load).
|
||||
*
|
||||
* Hooked early so the file is ready before wp_enqueue_scripts fires.
|
||||
*/
|
||||
add_action( 'init', function () {
|
||||
if ( ! file_exists( oribi_generated_css_path() ) ) {
|
||||
oribi_write_generated_css();
|
||||
}
|
||||
} );
|
||||
732
theme/inc/theme-settings.php
Normal file
732
theme/inc/theme-settings.php
Normal file
@@ -0,0 +1,732 @@
|
||||
<?php
|
||||
/**
|
||||
* Theme Settings — Custom admin page for configuring colours, fonts,
|
||||
* spacing, and border-radius design tokens.
|
||||
*
|
||||
* Appearance → Theme Design Settings
|
||||
*
|
||||
* @package OTS_Theme
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/* ── Register the admin page ───────────────────────────────────── */
|
||||
add_action( 'admin_menu', function () {
|
||||
add_theme_page(
|
||||
__( 'Theme Design Settings', 'ots-theme' ),
|
||||
__( 'Theme Design', 'ots-theme' ),
|
||||
'edit_theme_options',
|
||||
'oribi-theme-settings',
|
||||
'oribi_render_settings_page'
|
||||
);
|
||||
} );
|
||||
|
||||
/* ── Enqueue admin-only assets ─────────────────────────────────── */
|
||||
add_action( 'admin_enqueue_scripts', function ( $hook ) {
|
||||
|
||||
if ( 'appearance_page_oribi-theme-settings' !== $hook ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// WordPress colour picker.
|
||||
wp_enqueue_style( 'wp-color-picker' );
|
||||
wp_enqueue_script( 'wp-color-picker' );
|
||||
|
||||
// Google Fonts for preview.
|
||||
wp_enqueue_style(
|
||||
'oribi-admin-google-fonts',
|
||||
'https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Roboto:wght@400;700&family=Open+Sans:wght@400;700&family=Poppins:wght@400;600;700&family=Lato:wght@400;700&family=Montserrat:wght@400;600;700&family=Source+Sans+3:wght@400;600;700&family=Nunito:wght@400;600;700&family=Raleway:wght@400;600;700&family=DM+Sans:wght@400;500;700&family=Work+Sans:wght@400;500;600;700&family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap',
|
||||
[],
|
||||
null
|
||||
);
|
||||
|
||||
// Inline CSS + JS for the settings page.
|
||||
wp_add_inline_style( 'wp-color-picker', oribi_admin_inline_css() );
|
||||
wp_add_inline_script( 'wp-color-picker', oribi_admin_inline_js(), 'after' );
|
||||
} );
|
||||
|
||||
/* ── Handle form submission ────────────────────────────────────── */
|
||||
add_action( 'admin_init', function () {
|
||||
|
||||
if (
|
||||
! isset( $_POST['oribi_settings_nonce'] ) ||
|
||||
! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['oribi_settings_nonce'] ) ), 'oribi_save_settings' )
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! current_user_can( 'edit_theme_options' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$defaults = oribi_get_theme_defaults();
|
||||
|
||||
// Determine action — save or reset.
|
||||
$action = isset( $_POST['oribi_action'] ) ? sanitize_text_field( wp_unslash( $_POST['oribi_action'] ) ) : 'save';
|
||||
|
||||
if ( 'reset' === $action ) {
|
||||
foreach ( $defaults as $key => $value ) {
|
||||
set_theme_mod( 'oribi_' . $key, $value );
|
||||
}
|
||||
} else {
|
||||
// Sanitize & save each setting.
|
||||
foreach ( $defaults as $key => $default ) {
|
||||
$posted = isset( $_POST[ 'oribi_' . $key ] )
|
||||
? wp_unslash( $_POST[ 'oribi_' . $key ] ) // phpcs:ignore
|
||||
: $default;
|
||||
|
||||
// Determine sanitisation method by key prefix/type.
|
||||
if ( strpos( $key, 'color_' ) === 0 || strpos( $key, 'dark_' ) === 0 ) {
|
||||
// Colour values (hex or rgba).
|
||||
$posted = oribi_sanitize_color( $posted );
|
||||
} elseif ( strpos( $key, 'font_' ) === 0 ) {
|
||||
$posted = sanitize_text_field( $posted );
|
||||
} elseif ( strpos( $key, 'radius_' ) === 0 || strpos( $key, 'container_' ) === 0 || $key === 'wide_size' ) {
|
||||
$posted = sanitize_text_field( $posted );
|
||||
} else {
|
||||
$posted = sanitize_text_field( $posted );
|
||||
}
|
||||
|
||||
set_theme_mod( 'oribi_' . $key, $posted );
|
||||
}
|
||||
}
|
||||
|
||||
// Regenerate CSS.
|
||||
$result = oribi_write_generated_css();
|
||||
if ( is_wp_error( $result ) ) {
|
||||
add_settings_error( 'oribi_settings', 'css_error', $result->get_error_message(), 'error' );
|
||||
} else {
|
||||
$msg = 'reset' === $action
|
||||
? __( 'Settings reset to defaults. CSS regenerated.', 'ots-theme' )
|
||||
: __( 'Settings saved. CSS regenerated.', 'ots-theme' );
|
||||
add_settings_error( 'oribi_settings', 'saved', $msg, 'success' );
|
||||
}
|
||||
|
||||
// Store errors/notices in transient so they survive the redirect.
|
||||
set_transient( 'oribi_settings_notices', get_settings_errors( 'oribi_settings' ), 30 );
|
||||
|
||||
// PRG redirect.
|
||||
wp_safe_redirect( admin_url( 'themes.php?page=oribi-theme-settings' ) );
|
||||
exit;
|
||||
} );
|
||||
|
||||
/**
|
||||
* Sanitize a colour value (hex or rgba).
|
||||
*
|
||||
* @param string $value Raw colour value.
|
||||
* @return string Sanitized colour.
|
||||
*/
|
||||
function oribi_sanitize_color( $value ) {
|
||||
$value = trim( $value );
|
||||
|
||||
// Allow rgba(...) values (used for dark mode light tints).
|
||||
if ( preg_match( '/^rgba?\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*(,\s*[\d.]+\s*)?\)$/', $value ) ) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
// Standard hex.
|
||||
return sanitize_hex_color( $value ) ?? '#000000';
|
||||
}
|
||||
|
||||
/* ── Render the settings page ──────────────────────────────────── */
|
||||
function oribi_render_settings_page() {
|
||||
|
||||
// Seed defaults on first visit.
|
||||
oribi_maybe_seed_defaults();
|
||||
|
||||
// Show any saved notices.
|
||||
$notices = get_transient( 'oribi_settings_notices' );
|
||||
if ( $notices ) {
|
||||
delete_transient( 'oribi_settings_notices' );
|
||||
foreach ( $notices as $notice ) {
|
||||
printf(
|
||||
'<div class="notice notice-%s is-dismissible"><p>%s</p></div>',
|
||||
esc_attr( $notice['type'] ),
|
||||
esc_html( $notice['message'] )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$s = 'oribi_get_setting';
|
||||
$fonts = oribi_get_available_fonts();
|
||||
|
||||
?>
|
||||
<div class="wrap oribi-settings-wrap">
|
||||
<h1><?php esc_html_e( 'Theme Design Settings', 'ots-theme' ); ?></h1>
|
||||
<p class="description" style="margin-bottom:24px;">
|
||||
<?php esc_html_e( 'Customise colours, typography, spacing, and border radii. Changes are applied site-wide via a generated CSS file.', 'ots-theme' ); ?>
|
||||
</p>
|
||||
|
||||
<form method="post" id="oribi-settings-form">
|
||||
<?php wp_nonce_field( 'oribi_save_settings', 'oribi_settings_nonce' ); ?>
|
||||
<input type="hidden" name="oribi_action" id="oribi-action-field" value="save" />
|
||||
|
||||
<!-- Tabs -->
|
||||
<nav class="oribi-tabs" role="tablist">
|
||||
<button type="button" class="oribi-tab active" data-tab="colors" role="tab"><?php esc_html_e( 'Colours', 'ots-theme' ); ?></button>
|
||||
<button type="button" class="oribi-tab" data-tab="dark" role="tab"><?php esc_html_e( 'Dark Mode', 'ots-theme' ); ?></button>
|
||||
<button type="button" class="oribi-tab" data-tab="typography" role="tab"><?php esc_html_e( 'Typography', 'ots-theme' ); ?></button>
|
||||
<button type="button" class="oribi-tab" data-tab="spacing" role="tab"><?php esc_html_e( 'Spacing & Layout', 'ots-theme' ); ?></button>
|
||||
<button type="button" class="oribi-tab" data-tab="radius" role="tab"><?php esc_html_e( 'Border Radius', 'ots-theme' ); ?></button>
|
||||
</nav>
|
||||
|
||||
<div class="oribi-panels-wrap">
|
||||
<div class="oribi-panels">
|
||||
|
||||
<!-- ═══ COLOURS ══════════════════════════════════════ -->
|
||||
<div class="oribi-panel active" data-panel="colors">
|
||||
<h2><?php esc_html_e( 'Light Mode Colour Palette', 'ots-theme' ); ?></h2>
|
||||
<p class="description"><?php esc_html_e( 'These colours apply when dark mode is off.', 'ots-theme' ); ?></p>
|
||||
|
||||
<div class="oribi-color-grid">
|
||||
<?php
|
||||
$light_colors = [
|
||||
'color_primary' => __( 'Primary', 'ots-theme' ),
|
||||
'color_primary_dk' => __( 'Primary Dark', 'ots-theme' ),
|
||||
'color_primary_lt' => __( 'Primary Light', 'ots-theme' ),
|
||||
'color_accent' => __( 'Accent', 'ots-theme' ),
|
||||
'color_accent_dk' => __( 'Accent Dark', 'ots-theme' ),
|
||||
'color_accent_lt' => __( 'Accent Light', 'ots-theme' ),
|
||||
'color_dark' => __( 'Dark', 'ots-theme' ),
|
||||
'color_dark_2' => __( 'Dark 2', 'ots-theme' ),
|
||||
'color_text' => __( 'Text', 'ots-theme' ),
|
||||
'color_text_muted' => __( 'Text Muted', 'ots-theme' ),
|
||||
'color_border' => __( 'Border', 'ots-theme' ),
|
||||
'color_bg' => __( 'Background', 'ots-theme' ),
|
||||
'color_bg_alt' => __( 'Background Alt', 'ots-theme' ),
|
||||
];
|
||||
foreach ( $light_colors as $key => $label ) :
|
||||
$val = $s( $key );
|
||||
?>
|
||||
<div class="oribi-color-field">
|
||||
<label for="oribi_<?php echo esc_attr( $key ); ?>">
|
||||
<?php echo esc_html( $label ); ?>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="oribi_<?php echo esc_attr( $key ); ?>"
|
||||
name="oribi_<?php echo esc_attr( $key ); ?>"
|
||||
value="<?php echo esc_attr( $val ); ?>"
|
||||
class="oribi-color-picker"
|
||||
data-default-color="<?php echo esc_attr( oribi_get_theme_defaults()[ $key ] ); ?>"
|
||||
/>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ DARK MODE ════════════════════════════════════ -->
|
||||
<div class="oribi-panel" data-panel="dark">
|
||||
<h2><?php esc_html_e( 'Dark Mode Colour Palette', 'ots-theme' ); ?></h2>
|
||||
<p class="description"><?php esc_html_e( 'These colours apply when dark mode is active. Some values support rgba() notation.', 'ots-theme' ); ?></p>
|
||||
|
||||
<div class="oribi-color-grid">
|
||||
<?php
|
||||
$dark_colors = [
|
||||
'dark_primary' => __( 'Primary', 'ots-theme' ),
|
||||
'dark_primary_dk' => __( 'Primary Dark', 'ots-theme' ),
|
||||
'dark_primary_lt' => __( 'Primary Light', 'ots-theme' ),
|
||||
'dark_accent' => __( 'Accent', 'ots-theme' ),
|
||||
'dark_accent_dk' => __( 'Accent Dark', 'ots-theme' ),
|
||||
'dark_accent_lt' => __( 'Accent Light', 'ots-theme' ),
|
||||
'dark_dark' => __( 'Dark (Text)', 'ots-theme' ),
|
||||
'dark_dark_2' => __( 'Dark 2 (Text)', 'ots-theme' ),
|
||||
'dark_text' => __( 'Body Text', 'ots-theme' ),
|
||||
'dark_text_muted' => __( 'Text Muted', 'ots-theme' ),
|
||||
'dark_border' => __( 'Border', 'ots-theme' ),
|
||||
'dark_bg' => __( 'Background', 'ots-theme' ),
|
||||
'dark_bg_alt' => __( 'Background Alt', 'ots-theme' ),
|
||||
'dark_bg_dark' => __( 'Background Darker', 'ots-theme' ),
|
||||
'dark_heading' => __( 'Heading', 'ots-theme' ),
|
||||
'dark_card_bg' => __( 'Card Background', 'ots-theme' ),
|
||||
];
|
||||
foreach ( $dark_colors as $key => $label ) :
|
||||
$val = $s( $key );
|
||||
$is_rgba = ( strpos( $val, 'rgba' ) !== false );
|
||||
?>
|
||||
<div class="oribi-color-field">
|
||||
<label for="oribi_<?php echo esc_attr( $key ); ?>">
|
||||
<?php echo esc_html( $label ); ?>
|
||||
</label>
|
||||
<?php if ( $is_rgba ) : ?>
|
||||
<input
|
||||
type="text"
|
||||
id="oribi_<?php echo esc_attr( $key ); ?>"
|
||||
name="oribi_<?php echo esc_attr( $key ); ?>"
|
||||
value="<?php echo esc_attr( $val ); ?>"
|
||||
class="regular-text oribi-rgba-input"
|
||||
placeholder="rgba(r,g,b,a)"
|
||||
/>
|
||||
<span class="oribi-color-swatch" style="background:<?php echo esc_attr( $val ); ?>;"></span>
|
||||
<?php else : ?>
|
||||
<input
|
||||
type="text"
|
||||
id="oribi_<?php echo esc_attr( $key ); ?>"
|
||||
name="oribi_<?php echo esc_attr( $key ); ?>"
|
||||
value="<?php echo esc_attr( $val ); ?>"
|
||||
class="oribi-color-picker"
|
||||
data-default-color="<?php echo esc_attr( oribi_get_theme_defaults()[ $key ] ); ?>"
|
||||
/>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ TYPOGRAPHY ═══════════════════════════════════ -->
|
||||
<div class="oribi-panel" data-panel="typography">
|
||||
<h2><?php esc_html_e( 'Typography', 'ots-theme' ); ?></h2>
|
||||
<p class="description">
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %s = link to Font Library */
|
||||
esc_html__( 'Select from available fonts below. To add more fonts, use the %s in the Site Editor.', 'ots-theme' ),
|
||||
'<a href="' . esc_url( admin_url( 'site-editor.php' ) ) . '">' . esc_html__( 'Font Library', 'ots-theme' ) . '</a>'
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="oribi_font_family"><?php esc_html_e( 'Body Font', 'ots-theme' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<select id="oribi_font_family" name="oribi_font_family" class="oribi-font-select">
|
||||
<?php foreach ( $fonts as $f ) : ?>
|
||||
<option
|
||||
value="<?php echo esc_attr( $f['slug'] ); ?>"
|
||||
data-font-family="<?php echo esc_attr( $f['fontFamily'] ); ?>"
|
||||
<?php selected( $s( 'font_family' ), $f['slug'] ); ?>
|
||||
>
|
||||
<?php echo esc_html( $f['name'] ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<p class="description"><?php esc_html_e( 'Applied to body text, paragraphs, and UI elements.', 'ots-theme' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="oribi_font_heading"><?php esc_html_e( 'Heading Font', 'ots-theme' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<select id="oribi_font_heading" name="oribi_font_heading" class="oribi-font-select">
|
||||
<option value=""><?php esc_html_e( '— Same as body font —', 'ots-theme' ); ?></option>
|
||||
<?php foreach ( $fonts as $f ) : ?>
|
||||
<option
|
||||
value="<?php echo esc_attr( $f['slug'] ); ?>"
|
||||
data-font-family="<?php echo esc_attr( $f['fontFamily'] ); ?>"
|
||||
<?php selected( $s( 'font_heading' ), $f['slug'] ); ?>
|
||||
>
|
||||
<?php echo esc_html( $f['name'] ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<p class="description"><?php esc_html_e( 'Applied to h1–h6 headings. Leave blank to use the body font.', 'ots-theme' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Font preview -->
|
||||
<div class="oribi-font-preview" id="oribi-font-preview">
|
||||
<h3 style="margin:0 0 8px;"><?php esc_html_e( 'Font Preview', 'ots-theme' ); ?></h3>
|
||||
<div class="oribi-font-preview-heading" id="oribi-preview-heading">
|
||||
The quick brown fox jumps over the lazy dog
|
||||
</div>
|
||||
<div class="oribi-font-preview-body" id="oribi-preview-body">
|
||||
The quick brown fox jumps over the lazy dog. 0123456789
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ SPACING & LAYOUT ═════════════════════════════ -->
|
||||
<div class="oribi-panel" data-panel="spacing">
|
||||
<h2><?php esc_html_e( 'Spacing & Layout', 'ots-theme' ); ?></h2>
|
||||
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="oribi_container_max"><?php esc_html_e( 'Container Max Width (px)', 'ots-theme' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" id="oribi_container_max" name="oribi_container_max"
|
||||
value="<?php echo esc_attr( $s( 'container_max' ) ); ?>"
|
||||
min="600" max="2400" step="10" class="small-text" />
|
||||
<p class="description"><?php esc_html_e( 'Maximum width of the main content area (default: 1200).', 'ots-theme' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="oribi_wide_size"><?php esc_html_e( 'Wide Block Width (px)', 'ots-theme' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" id="oribi_wide_size" name="oribi_wide_size"
|
||||
value="<?php echo esc_attr( $s( 'wide_size' ) ); ?>"
|
||||
min="800" max="3000" step="10" class="small-text" />
|
||||
<p class="description"><?php esc_html_e( 'Width of "wide" aligned blocks (default: 1536).', 'ots-theme' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Horizontal Padding', 'ots-theme' ); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<?php esc_html_e( 'Min:', 'ots-theme' ); ?>
|
||||
<input type="number" name="oribi_container_pad_min"
|
||||
value="<?php echo esc_attr( $s( 'container_pad_min' ) ); ?>"
|
||||
min="0" max="10" step="0.25" class="small-text" /> rem
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<?php esc_html_e( 'Max:', 'ots-theme' ); ?>
|
||||
<input type="number" name="oribi_container_pad_max"
|
||||
value="<?php echo esc_attr( $s( 'container_pad_max' ) ); ?>"
|
||||
min="0" max="10" step="0.25" class="small-text" /> rem
|
||||
</label>
|
||||
<p class="description"><?php esc_html_e( 'Responsive horizontal padding using clamp(min, 5vw, max). Default: 1rem – 2rem.', 'ots-theme' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- ═══ BORDER RADIUS ════════════════════════════════ -->
|
||||
<div class="oribi-panel" data-panel="radius">
|
||||
<h2><?php esc_html_e( 'Border Radius', 'ots-theme' ); ?></h2>
|
||||
<p class="description"><?php esc_html_e( 'Four preset sizes used across all blocks. Values in pixels.', 'ots-theme' ); ?></p>
|
||||
|
||||
<table class="form-table" role="presentation">
|
||||
<?php
|
||||
$radii = [
|
||||
'radius_sm' => [ 'SM', __( 'Buttons, badges, small elements', 'ots-theme' ) ],
|
||||
'radius_md' => [ 'MD', __( 'Cards, inputs, mid-size components', 'ots-theme' ) ],
|
||||
'radius_lg' => [ 'LG', __( 'Sections, larger surfaces', 'ots-theme' ) ],
|
||||
'radius_xl' => [ 'XL', __( 'Pills, fully rounded elements', 'ots-theme' ) ],
|
||||
];
|
||||
foreach ( $radii as $key => $info ) :
|
||||
?>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="oribi_<?php echo esc_attr( $key ); ?>">
|
||||
<?php echo esc_html( $info[0] ); ?>
|
||||
</label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" id="oribi_<?php echo esc_attr( $key ); ?>"
|
||||
name="oribi_<?php echo esc_attr( $key ); ?>"
|
||||
value="<?php echo esc_attr( $s( $key ) ); ?>"
|
||||
min="0" max="100" step="1" class="small-text" /> px
|
||||
<span class="oribi-radius-preview" style="border-radius:<?php echo esc_attr( $s( $key ) ); ?>px;"></span>
|
||||
<p class="description"><?php echo esc_html( $info[1] ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div><!-- .oribi-panels -->
|
||||
|
||||
<!-- ═══ LIVE PREVIEW SIDEBAR ═════════════════════════ -->
|
||||
<aside class="oribi-preview-sidebar" id="oribi-preview-sidebar">
|
||||
<h3><?php esc_html_e( 'Preview', 'ots-theme' ); ?></h3>
|
||||
<div class="oribi-preview-card" id="oribi-preview-card">
|
||||
<div class="oribi-preview-hero" id="preview-hero">
|
||||
<h2 id="preview-hero-title"><?php esc_html_e( 'Hero Heading', 'ots-theme' ); ?></h2>
|
||||
<p id="preview-hero-text"><?php esc_html_e( 'Body text preview. The quick brown fox jumps over the lazy dog.', 'ots-theme' ); ?></p>
|
||||
<button id="preview-btn-primary"><?php esc_html_e( 'Primary Button', 'ots-theme' ); ?></button>
|
||||
<button id="preview-btn-accent"><?php esc_html_e( 'Accent Button', 'ots-theme' ); ?></button>
|
||||
</div>
|
||||
<div class="oribi-preview-section" id="preview-card-section">
|
||||
<div class="oribi-prev-card" id="preview-card-1">
|
||||
<h4><?php esc_html_e( 'Card Title', 'ots-theme' ); ?></h4>
|
||||
<p><?php esc_html_e( 'Card body text with muted small text below.', 'ots-theme' ); ?></p>
|
||||
<small><?php esc_html_e( 'Muted text', 'ots-theme' ); ?></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div><!-- .oribi-panels-wrap -->
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="oribi-actions">
|
||||
<?php submit_button( __( 'Save Changes', 'ots-theme' ), 'primary', 'submit', false ); ?>
|
||||
<button type="button" id="oribi-reset-btn" class="button button-secondary">
|
||||
<?php esc_html_e( 'Reset to Defaults', 'ots-theme' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/* ── Inline CSS for the admin page ─────────────────────────────── */
|
||||
function oribi_admin_inline_css() {
|
||||
return <<<'CSS'
|
||||
|
||||
/* ── Settings page layout ── */
|
||||
.oribi-settings-wrap { max-width: 1400px; }
|
||||
|
||||
/* ── Tabs ── */
|
||||
.oribi-tabs {
|
||||
display: flex; gap: 0; border-bottom: 2px solid #c3c4c7;
|
||||
margin-bottom: 0; background: #f0f0f1;
|
||||
}
|
||||
.oribi-tab {
|
||||
padding: 12px 20px; border: none; background: transparent;
|
||||
cursor: pointer; font-size: 14px; font-weight: 500;
|
||||
border-bottom: 2px solid transparent; margin-bottom: -2px;
|
||||
color: #50575e; transition: all .15s ease;
|
||||
}
|
||||
.oribi-tab:hover { color: #1d2327; background: #fff; }
|
||||
.oribi-tab.active {
|
||||
color: #1d2327; background: #fff;
|
||||
border-bottom-color: #2271b1; font-weight: 600;
|
||||
}
|
||||
|
||||
/* ── Panels + Preview sidebar ── */
|
||||
.oribi-panels-wrap {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 320px;
|
||||
gap: 24px;
|
||||
margin-top: 0;
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
.oribi-panels-wrap { grid-template-columns: 1fr; }
|
||||
.oribi-preview-sidebar { order: -1; }
|
||||
}
|
||||
|
||||
.oribi-panel { display: none; padding: 24px; background: #fff; border: 1px solid #c3c4c7; border-top: none; }
|
||||
.oribi-panel.active { display: block; }
|
||||
.oribi-panel h2 { margin-top: 0; }
|
||||
|
||||
/* ── Colour grid ── */
|
||||
.oribi-color-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 16px; margin-top: 16px;
|
||||
}
|
||||
.oribi-color-field { display: flex; flex-direction: column; gap: 4px; }
|
||||
.oribi-color-field label { font-weight: 500; font-size: 13px; }
|
||||
.oribi-rgba-input { font-family: monospace; font-size: 13px; }
|
||||
.oribi-color-swatch {
|
||||
display: inline-block; width: 28px; height: 28px;
|
||||
border: 1px solid #ddd; border-radius: 4px; margin-top: 4px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* ── Font preview ── */
|
||||
.oribi-font-preview {
|
||||
margin-top: 24px; padding: 20px;
|
||||
background: #f9f9f9; border: 1px solid #ddd; border-radius: 8px;
|
||||
}
|
||||
.oribi-font-preview-heading {
|
||||
font-size: 28px; font-weight: 700; line-height: 1.3; margin-bottom: 8px;
|
||||
}
|
||||
.oribi-font-preview-body {
|
||||
font-size: 16px; line-height: 1.65; color: #555;
|
||||
}
|
||||
|
||||
/* ── Radius preview ── */
|
||||
.oribi-radius-preview {
|
||||
display: inline-block; width: 40px; height: 40px;
|
||||
background: #2271b1; vertical-align: middle; margin-left: 12px;
|
||||
}
|
||||
|
||||
/* ── Preview sidebar ── */
|
||||
.oribi-preview-sidebar {
|
||||
position: sticky; top: 32px; align-self: start;
|
||||
padding: 20px; background: #fff; border: 1px solid #c3c4c7;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.oribi-preview-sidebar h3 { margin-top: 0; }
|
||||
.oribi-preview-card { border-radius: 8px; overflow: hidden; }
|
||||
.oribi-preview-hero {
|
||||
padding: 24px 16px; text-align: center;
|
||||
transition: all .2s ease;
|
||||
}
|
||||
.oribi-preview-hero h2 { margin: 0 0 8px; font-size: 22px; }
|
||||
.oribi-preview-hero p { font-size: 14px; margin: 0 0 16px; }
|
||||
.oribi-preview-hero button {
|
||||
padding: 8px 16px; border: none; color: #fff;
|
||||
border-radius: 6px; cursor: default; margin: 4px;
|
||||
font-size: 13px; font-weight: 600;
|
||||
}
|
||||
.oribi-preview-section {
|
||||
padding: 16px; transition: all .2s ease;
|
||||
}
|
||||
.oribi-prev-card {
|
||||
padding: 16px; border: 1px solid #e2e8f0;
|
||||
transition: all .2s ease;
|
||||
}
|
||||
.oribi-prev-card h4 { margin: 0 0 6px; font-size: 15px; }
|
||||
.oribi-prev-card p { margin: 0 0 4px; font-size: 13px; }
|
||||
.oribi-prev-card small { font-size: 12px; }
|
||||
|
||||
/* ── Actions bar ── */
|
||||
.oribi-actions {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
margin-top: 24px; padding-top: 16px;
|
||||
border-top: 1px solid #c3c4c7;
|
||||
}
|
||||
.oribi-actions .button-primary { margin: 0 !important; }
|
||||
|
||||
CSS;
|
||||
}
|
||||
|
||||
/* ── Inline JS for the admin page ──────────────────────────────── */
|
||||
function oribi_admin_inline_js() {
|
||||
return <<<'JS'
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
/* ── Tabs ── */
|
||||
document.querySelectorAll('.oribi-tab').forEach(function(tab) {
|
||||
tab.addEventListener('click', function() {
|
||||
document.querySelectorAll('.oribi-tab').forEach(function(t) { t.classList.remove('active'); });
|
||||
document.querySelectorAll('.oribi-panel').forEach(function(p) { p.classList.remove('active'); });
|
||||
tab.classList.add('active');
|
||||
var panel = document.querySelector('[data-panel="' + tab.dataset.tab + '"]');
|
||||
if (panel) panel.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
/* ── Colour pickers ── */
|
||||
if (typeof jQuery !== 'undefined' && jQuery.fn.wpColorPicker) {
|
||||
jQuery('.oribi-color-picker').wpColorPicker({
|
||||
change: debounce(updatePreview, 150),
|
||||
clear: debounce(updatePreview, 150)
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Font preview ── */
|
||||
document.querySelectorAll('.oribi-font-select').forEach(function(sel) {
|
||||
sel.addEventListener('change', updateFontPreview);
|
||||
});
|
||||
updateFontPreview();
|
||||
|
||||
/* ── Reset button ── */
|
||||
document.getElementById('oribi-reset-btn').addEventListener('click', function() {
|
||||
if (confirm('Reset all settings to factory defaults? This cannot be undone.')) {
|
||||
document.getElementById('oribi-action-field').value = 'reset';
|
||||
document.getElementById('oribi-settings-form').submit();
|
||||
}
|
||||
});
|
||||
|
||||
/* ── Live preview updater ── */
|
||||
function updatePreview() {
|
||||
var get = function(id) {
|
||||
var el = document.getElementById(id);
|
||||
if (!el) return '';
|
||||
// wpColorPicker stores hex in the text input
|
||||
return el.value || '';
|
||||
};
|
||||
|
||||
var hero = document.getElementById('preview-hero');
|
||||
var card = document.getElementById('preview-card-1');
|
||||
var section = document.getElementById('preview-card-section');
|
||||
var btnP = document.getElementById('preview-btn-primary');
|
||||
var btnA = document.getElementById('preview-btn-accent');
|
||||
var heroTitle = document.getElementById('preview-hero-title');
|
||||
var heroText = document.getElementById('preview-hero-text');
|
||||
|
||||
if (hero) {
|
||||
hero.style.backgroundColor = get('oribi_color_dark') || '#0D1321';
|
||||
if (heroTitle) heroTitle.style.color = '#fff';
|
||||
if (heroText) heroText.style.color = 'rgba(255,255,255,.8)';
|
||||
}
|
||||
if (btnP) {
|
||||
btnP.style.backgroundColor = get('oribi_color_primary') || '#D83302';
|
||||
btnP.style.borderRadius = (get('oribi_radius_sm') || '6') + 'px';
|
||||
}
|
||||
if (btnA) {
|
||||
btnA.style.backgroundColor = get('oribi_color_accent') || '#00757c';
|
||||
btnA.style.borderRadius = (get('oribi_radius_sm') || '6') + 'px';
|
||||
}
|
||||
if (section) {
|
||||
section.style.backgroundColor = get('oribi_color_bg_alt') || '#FFF8F5';
|
||||
}
|
||||
if (card) {
|
||||
card.style.backgroundColor = get('oribi_color_bg') || '#fff';
|
||||
card.style.borderColor = get('oribi_color_border') || '#E2E8F0';
|
||||
card.style.borderRadius = (get('oribi_radius_md') || '12') + 'px';
|
||||
var h4 = card.querySelector('h4');
|
||||
if (h4) h4.style.color = get('oribi_color_dark') || '#0D1321';
|
||||
var p = card.querySelector('p');
|
||||
if (p) p.style.color = get('oribi_color_text') || '#2D3748';
|
||||
var sm = card.querySelector('small');
|
||||
if (sm) sm.style.color = get('oribi_color_text_muted') || '#718096';
|
||||
}
|
||||
|
||||
// Radius previews.
|
||||
document.querySelectorAll('.oribi-radius-preview').forEach(function(el) {
|
||||
var inp = el.parentElement.querySelector('input[type="number"]');
|
||||
if (inp) el.style.borderRadius = inp.value + 'px';
|
||||
});
|
||||
}
|
||||
|
||||
function updateFontPreview() {
|
||||
var bodySelect = document.getElementById('oribi_font_family');
|
||||
var headSelect = document.getElementById('oribi_font_heading');
|
||||
var previewHead = document.getElementById('oribi-preview-heading');
|
||||
var previewBody = document.getElementById('oribi-preview-body');
|
||||
|
||||
if (bodySelect && previewBody) {
|
||||
var opt = bodySelect.options[bodySelect.selectedIndex];
|
||||
previewBody.style.fontFamily = opt.dataset.fontFamily || 'system-ui';
|
||||
}
|
||||
if (headSelect && previewHead) {
|
||||
var hOpt = headSelect.options[headSelect.selectedIndex];
|
||||
if (hOpt.value) {
|
||||
previewHead.style.fontFamily = hOpt.dataset.fontFamily || 'system-ui';
|
||||
} else if (bodySelect) {
|
||||
var bOpt = bodySelect.options[bodySelect.selectedIndex];
|
||||
previewHead.style.fontFamily = bOpt.dataset.fontFamily || 'system-ui';
|
||||
}
|
||||
}
|
||||
|
||||
// Also update preview sidebar heading/body fonts.
|
||||
var heroTitle = document.getElementById('preview-hero-title');
|
||||
var heroText = document.getElementById('preview-hero-text');
|
||||
var cardH4 = document.querySelector('.oribi-prev-card h4');
|
||||
var cardP = document.querySelector('.oribi-prev-card p');
|
||||
|
||||
if (headSelect) {
|
||||
var hf = headSelect.options[headSelect.selectedIndex];
|
||||
var headingFont = hf.value
|
||||
? (hf.dataset.fontFamily || 'system-ui')
|
||||
: (bodySelect ? bodySelect.options[bodySelect.selectedIndex].dataset.fontFamily : 'system-ui');
|
||||
if (heroTitle) heroTitle.style.fontFamily = headingFont;
|
||||
if (cardH4) cardH4.style.fontFamily = headingFont;
|
||||
}
|
||||
if (bodySelect) {
|
||||
var bf = bodySelect.options[bodySelect.selectedIndex];
|
||||
var bodyFont = bf.dataset.fontFamily || 'system-ui';
|
||||
if (heroText) heroText.style.fontFamily = bodyFont;
|
||||
if (cardP) cardP.style.fontFamily = bodyFont;
|
||||
}
|
||||
}
|
||||
|
||||
function debounce(fn, ms) {
|
||||
var timer;
|
||||
return function() {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(fn, ms);
|
||||
};
|
||||
}
|
||||
|
||||
// Initial preview render.
|
||||
updatePreview();
|
||||
|
||||
// Attach change listeners to all inputs for live preview.
|
||||
document.querySelectorAll('input[type="number"]').forEach(function(inp) {
|
||||
inp.addEventListener('input', debounce(updatePreview, 100));
|
||||
});
|
||||
document.querySelectorAll('.oribi-rgba-input').forEach(function(inp) {
|
||||
inp.addEventListener('input', debounce(updatePreview, 200));
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
JS;
|
||||
}
|
||||
14
theme/index.php
Normal file
14
theme/index.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
/**
|
||||
* Fallback template — required by WordPress for all themes.
|
||||
*
|
||||
* In a Full Site Editing (block) theme the actual rendering is handled
|
||||
* by the HTML templates in /templates/ and /parts/. This file exists
|
||||
* solely to satisfy WordPress' theme validation requirement.
|
||||
*
|
||||
* @package OTS_Theme
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
1
theme/parts/footer.html
Normal file
1
theme/parts/footer.html
Normal file
@@ -0,0 +1 @@
|
||||
<!-- wp:oribi/site-footer /-->
|
||||
1
theme/parts/header.html
Normal file
1
theme/parts/header.html
Normal file
@@ -0,0 +1 @@
|
||||
<!-- wp:oribi/site-header /-->
|
||||
65
theme/readme.txt
Normal file
65
theme/readme.txt
Normal file
@@ -0,0 +1,65 @@
|
||||
=== Oribi Tech ===
|
||||
|
||||
Contributors: Oribi Technology Services
|
||||
Requires at least: 6.4
|
||||
Tested up to: 6.7
|
||||
Requires PHP: 7.4
|
||||
License: GPLv2 or later
|
||||
License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||
|
||||
== Description ==
|
||||
|
||||
Oribi Tech is a Full Site Editing (FSE) block theme built for Oribi Technology Services. It features 15+ custom Gutenberg blocks, dark/light mode toggle, a canvas-based datacenter hero animation, and complete page patterns for every service page.
|
||||
|
||||
**Key Features:**
|
||||
|
||||
* Full Site Editing with block templates and template parts
|
||||
* 15+ custom Gutenberg blocks (hero, pricing cards, comparison tables, FAQ accordions, etc.)
|
||||
* Dark / Light mode toggle with CSS custom properties
|
||||
* Responsive design optimised for all screen sizes
|
||||
* Canvas-based animated datacenter background
|
||||
* AJAX-powered contact form
|
||||
* Google Fonts (Inter) loaded for performance
|
||||
* Block patterns for all 10 service and marketing pages
|
||||
|
||||
== Installation ==
|
||||
|
||||
1. Upload the `theme` folder to `/wp-content/themes/oribi-tech` (rename the folder to `oribi-tech`).
|
||||
2. Activate the theme through **Appearance → Themes**.
|
||||
3. Go to **Appearance → Editor** to customise templates and template parts.
|
||||
4. Create pages and apply the matching block pattern from the **Oribi Pages** category.
|
||||
5. Set your home page under **Settings → Reading → A static page**.
|
||||
6. Configure the primary and footer navigation menus under **Appearance → Menus** or via the Site Editor.
|
||||
|
||||
== Page Setup ==
|
||||
|
||||
Create a WordPress page for each service, then insert the corresponding block pattern:
|
||||
|
||||
| Page Slug | Pattern Name |
|
||||
| -------------------- | ------------------------- |
|
||||
| / | Home Page |
|
||||
| /about | About Page |
|
||||
| /contact | Contact Page |
|
||||
| /faq | FAQ Page |
|
||||
| /managed-it | Managed IT Page |
|
||||
| /365care | 365Care Page |
|
||||
| /endpointcare | EndpointCare Page |
|
||||
| /netcare | NetCare Page |
|
||||
| /servercare | ServerCare Page |
|
||||
| /services-comparison | Services Comparison Page |
|
||||
|
||||
== Changelog ==
|
||||
|
||||
= 1.0.0 =
|
||||
* Initial release as a Full Site Editing block theme.
|
||||
* Custom blocks: hero, page-hero, cta-banner, intro-section, contact-section, feature-section/card, pricing-section/card, platform-section/row, trust-section/item, faq-section/item, comparison-table, site-header, site-footer.
|
||||
* FSE templates: front-page, page, single, index, archive, search, 404.
|
||||
* Template parts: header, footer.
|
||||
* 10 page patterns covering all marketing pages.
|
||||
* Dark/light mode system.
|
||||
* AJAX contact form.
|
||||
|
||||
== Credits ==
|
||||
|
||||
* Inter font by Rasmus Andersson — https://rsms.me/inter/ (SIL Open Font License)
|
||||
* Built with WordPress Full Site Editing and the Block API.
|
||||
14
theme/style.css
Normal file
14
theme/style.css
Normal file
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
Theme Name: Oribi Tech
|
||||
Theme URI: https://oribi-tech.com
|
||||
Author: Oribi Technology Services
|
||||
Author URI: https://oribi-tech.com
|
||||
Description: Custom marketing theme for Oribi Technology Services — Managed IT, 365Care, EndpointCare, NetCare, ServerCare.
|
||||
Version: 1.0.0
|
||||
Requires at least: 6.4
|
||||
Tested up to: 6.7
|
||||
Requires PHP: 7.4
|
||||
License: GNU General Public License v2 or later
|
||||
License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||
Text Domain: oribi-tech
|
||||
*/
|
||||
35
theme/templates/404.html
Normal file
35
theme/templates/404.html
Normal file
@@ -0,0 +1,35 @@
|
||||
<!-- wp:template-part {"slug":"header","area":"header"} /-->
|
||||
|
||||
<!-- wp:group {"tagName":"main","className":"page-header-light","layout":{"type":"default"}} -->
|
||||
<main class="wp-block-group page-header-light">
|
||||
|
||||
<!-- wp:oribi/page-hero {"title":"Page Not Found","description":"Sorry, the page you\u0027re looking for doesn\u0027t exist or has been moved."} /-->
|
||||
|
||||
<!-- wp:group {"className":"section","layout":{"type":"constrained"}} -->
|
||||
<div class="wp-block-group section">
|
||||
<!-- wp:group {"className":"container","style":{"spacing":{"padding":{"top":"2rem","bottom":"2rem"}}},"layout":{"type":"constrained"}} -->
|
||||
<div class="wp-block-group container" style="padding-top:2rem;padding-bottom:2rem">
|
||||
|
||||
<!-- wp:paragraph {"align":"center","fontSize":"md"} -->
|
||||
<p class="has-text-align-center has-md-font-size">The page you requested could not be found. It may have been moved or deleted.</p>
|
||||
<!-- /wp:paragraph -->
|
||||
|
||||
<!-- wp:buttons {"layout":{"type":"flex","justifyContent":"center"},"style":{"spacing":{"margin":{"top":"2rem"}}}} -->
|
||||
<div class="wp-block-buttons" style="margin-top:2rem">
|
||||
<!-- wp:button {"className":"btn btn-primary btn-lg"} -->
|
||||
<div class="wp-block-button btn btn-primary btn-lg"><a class="wp-block-button__link wp-element-button" href="/">Return to Home</a></div>
|
||||
<!-- /wp:button -->
|
||||
</div>
|
||||
<!-- /wp:buttons -->
|
||||
|
||||
<!-- wp:search {"label":"Search","showLabel":false,"placeholder":"Or try searching…","buttonText":"Search","buttonUseIcon":true,"style":{"spacing":{"margin":{"top":"2rem"}}}} /-->
|
||||
|
||||
</div>
|
||||
<!-- /wp:group -->
|
||||
</div>
|
||||
<!-- /wp:group -->
|
||||
|
||||
</main>
|
||||
<!-- /wp:group -->
|
||||
|
||||
<!-- wp:template-part {"slug":"footer","area":"footer"} /-->
|
||||
47
theme/templates/archive.html
Normal file
47
theme/templates/archive.html
Normal file
@@ -0,0 +1,47 @@
|
||||
<!-- wp:template-part {"slug":"header","area":"header"} /-->
|
||||
|
||||
<!-- wp:group {"tagName":"main","className":"page-header-light","layout":{"type":"default"}} -->
|
||||
<main class="wp-block-group page-header-light">
|
||||
|
||||
<!-- wp:group {"className":"section","layout":{"type":"constrained"}} -->
|
||||
<div class="wp-block-group section">
|
||||
<!-- wp:group {"className":"container","layout":{"type":"constrained"}} -->
|
||||
<div class="wp-block-group container">
|
||||
|
||||
<!-- wp:query-title {"type":"archive"} /-->
|
||||
|
||||
<!-- wp:query {"queryId":0,"query":{"perPage":10,"pages":0,"offset":0,"postType":"post","order":"desc","orderBy":"date","inherit":true}} -->
|
||||
|
||||
<!-- wp:post-template -->
|
||||
<!-- wp:group {"style":{"spacing":{"blockGap":"0.5rem","padding":{"bottom":"2rem"}}},"layout":{"type":"default"}} -->
|
||||
<div class="wp-block-group" style="padding-bottom:2rem">
|
||||
<!-- wp:post-title {"isLink":true,"fontSize":"xl"} /-->
|
||||
<!-- wp:post-date {"fontSize":"sm"} /-->
|
||||
<!-- wp:post-excerpt {"moreText":"Read More →"} /-->
|
||||
</div>
|
||||
<!-- /wp:group -->
|
||||
<!-- /wp:post-template -->
|
||||
|
||||
<!-- wp:query-pagination {"layout":{"type":"flex","justifyContent":"center"}} -->
|
||||
<!-- wp:query-pagination-previous /-->
|
||||
<!-- wp:query-pagination-numbers /-->
|
||||
<!-- wp:query-pagination-next /-->
|
||||
<!-- /wp:query-pagination -->
|
||||
|
||||
<!-- wp:query-no-results -->
|
||||
<!-- wp:paragraph {"align":"center"} -->
|
||||
<p class="has-text-align-center">No posts found in this archive.</p>
|
||||
<!-- /wp:paragraph -->
|
||||
<!-- /wp:query-no-results -->
|
||||
|
||||
<!-- /wp:query -->
|
||||
|
||||
</div>
|
||||
<!-- /wp:group -->
|
||||
</div>
|
||||
<!-- /wp:group -->
|
||||
|
||||
</main>
|
||||
<!-- /wp:group -->
|
||||
|
||||
<!-- wp:template-part {"slug":"footer","area":"footer"} /-->
|
||||
9
theme/templates/front-page.html
Normal file
9
theme/templates/front-page.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<!-- wp:template-part {"slug":"header","area":"header"} /-->
|
||||
|
||||
<!-- wp:group {"tagName":"main","layout":{"type":"default"}} -->
|
||||
<main class="wp-block-group">
|
||||
<!-- wp:post-content {"layout":{"type":"default"}} /-->
|
||||
</main>
|
||||
<!-- /wp:group -->
|
||||
|
||||
<!-- wp:template-part {"slug":"footer","area":"footer"} /-->
|
||||
45
theme/templates/index.html
Normal file
45
theme/templates/index.html
Normal file
@@ -0,0 +1,45 @@
|
||||
<!-- wp:template-part {"slug":"header","area":"header"} /-->
|
||||
|
||||
<!-- wp:group {"tagName":"main","className":"page-header-light","layout":{"type":"default"}} -->
|
||||
<main class="wp-block-group page-header-light">
|
||||
|
||||
<!-- wp:group {"className":"section","layout":{"type":"constrained"}} -->
|
||||
<div class="wp-block-group section">
|
||||
<!-- wp:group {"className":"container","layout":{"type":"constrained"}} -->
|
||||
<div class="wp-block-group container">
|
||||
|
||||
<!-- wp:query {"queryId":0,"query":{"perPage":10,"pages":0,"offset":0,"postType":"post","order":"desc","orderBy":"date","inherit":true}} -->
|
||||
|
||||
<!-- wp:post-template -->
|
||||
<!-- wp:group {"style":{"spacing":{"blockGap":"0.5rem","padding":{"bottom":"2rem"}}},"layout":{"type":"default"}} -->
|
||||
<div class="wp-block-group" style="padding-bottom:2rem">
|
||||
<!-- wp:post-title {"isLink":true,"fontSize":"xl"} /-->
|
||||
<!-- wp:post-date {"fontSize":"sm"} /-->
|
||||
<!-- wp:post-excerpt {"moreText":"Read More →"} /-->
|
||||
</div>
|
||||
<!-- /wp:group -->
|
||||
<!-- /wp:post-template -->
|
||||
|
||||
<!-- wp:query-pagination {"layout":{"type":"flex","justifyContent":"center"}} -->
|
||||
<!-- wp:query-pagination-previous /-->
|
||||
<!-- wp:query-pagination-numbers /-->
|
||||
<!-- wp:query-pagination-next /-->
|
||||
<!-- /wp:query-pagination -->
|
||||
|
||||
<!-- wp:query-no-results -->
|
||||
<!-- wp:paragraph {"align":"center"} -->
|
||||
<p class="has-text-align-center">No posts found.</p>
|
||||
<!-- /wp:paragraph -->
|
||||
<!-- /wp:query-no-results -->
|
||||
|
||||
<!-- /wp:query -->
|
||||
|
||||
</div>
|
||||
<!-- /wp:group -->
|
||||
</div>
|
||||
<!-- /wp:group -->
|
||||
|
||||
</main>
|
||||
<!-- /wp:group -->
|
||||
|
||||
<!-- wp:template-part {"slug":"footer","area":"footer"} /-->
|
||||
9
theme/templates/page.html
Normal file
9
theme/templates/page.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<!-- wp:template-part {"slug":"header","area":"header"} /-->
|
||||
|
||||
<!-- wp:group {"tagName":"main","className":"page-header-light","layout":{"type":"default"}} -->
|
||||
<main class="wp-block-group page-header-light">
|
||||
<!-- wp:post-content {"layout":{"type":"default"}} /-->
|
||||
</main>
|
||||
<!-- /wp:group -->
|
||||
|
||||
<!-- wp:template-part {"slug":"footer","area":"footer"} /-->
|
||||
51
theme/templates/search.html
Normal file
51
theme/templates/search.html
Normal file
@@ -0,0 +1,51 @@
|
||||
<!-- wp:template-part {"slug":"header","area":"header"} /-->
|
||||
|
||||
<!-- wp:group {"tagName":"main","className":"page-header-light","layout":{"type":"default"}} -->
|
||||
<main class="wp-block-group page-header-light">
|
||||
|
||||
<!-- wp:group {"className":"section","layout":{"type":"constrained"}} -->
|
||||
<div class="wp-block-group section">
|
||||
<!-- wp:group {"className":"container","layout":{"type":"constrained"}} -->
|
||||
<div class="wp-block-group container">
|
||||
|
||||
<!-- wp:heading {"level":1} -->
|
||||
<h1 class="wp-block-heading">Search Results</h1>
|
||||
<!-- /wp:heading -->
|
||||
|
||||
<!-- wp:search {"label":"Search","showLabel":false,"placeholder":"Search the site…","buttonText":"Search","buttonUseIcon":true} /-->
|
||||
|
||||
<!-- wp:query {"queryId":0,"query":{"perPage":10,"postType":"post","inherit":true}} -->
|
||||
|
||||
<!-- wp:post-template -->
|
||||
<!-- wp:group {"style":{"spacing":{"blockGap":"0.5rem","padding":{"bottom":"2rem"}}},"layout":{"type":"default"}} -->
|
||||
<div class="wp-block-group" style="padding-bottom:2rem">
|
||||
<!-- wp:post-title {"isLink":true,"fontSize":"xl"} /-->
|
||||
<!-- wp:post-date {"fontSize":"sm"} /-->
|
||||
<!-- wp:post-excerpt /-->
|
||||
</div>
|
||||
<!-- /wp:group -->
|
||||
<!-- /wp:post-template -->
|
||||
|
||||
<!-- wp:query-pagination {"layout":{"type":"flex","justifyContent":"center"}} -->
|
||||
<!-- wp:query-pagination-previous /-->
|
||||
<!-- wp:query-pagination-numbers /-->
|
||||
<!-- wp:query-pagination-next /-->
|
||||
<!-- /wp:query-pagination -->
|
||||
|
||||
<!-- wp:query-no-results -->
|
||||
<!-- wp:paragraph {"align":"center"} -->
|
||||
<p class="has-text-align-center">No results found. Please try a different search term.</p>
|
||||
<!-- /wp:paragraph -->
|
||||
<!-- /wp:query-no-results -->
|
||||
|
||||
<!-- /wp:query -->
|
||||
|
||||
</div>
|
||||
<!-- /wp:group -->
|
||||
</div>
|
||||
<!-- /wp:group -->
|
||||
|
||||
</main>
|
||||
<!-- /wp:group -->
|
||||
|
||||
<!-- wp:template-part {"slug":"footer","area":"footer"} /-->
|
||||
25
theme/templates/single.html
Normal file
25
theme/templates/single.html
Normal file
@@ -0,0 +1,25 @@
|
||||
<!-- wp:template-part {"slug":"header","area":"header"} /-->
|
||||
|
||||
<!-- wp:group {"tagName":"main","className":"page-header-light","layout":{"type":"default"}} -->
|
||||
<main class="wp-block-group page-header-light">
|
||||
|
||||
<!-- wp:group {"className":"section","layout":{"type":"constrained"}} -->
|
||||
<div class="wp-block-group section">
|
||||
<!-- wp:group {"className":"container","layout":{"type":"constrained"}} -->
|
||||
<div class="wp-block-group container">
|
||||
|
||||
<!-- wp:post-title {"level":1} /-->
|
||||
|
||||
<!-- wp:post-date {"fontSize":"sm"} /-->
|
||||
|
||||
<!-- wp:post-content {"layout":{"type":"default"}} /-->
|
||||
|
||||
</div>
|
||||
<!-- /wp:group -->
|
||||
</div>
|
||||
<!-- /wp:group -->
|
||||
|
||||
</main>
|
||||
<!-- /wp:group -->
|
||||
|
||||
<!-- wp:template-part {"slug":"footer","area":"footer"} /-->
|
||||
378
theme/theme.json
Normal file
378
theme/theme.json
Normal file
@@ -0,0 +1,378 @@
|
||||
{
|
||||
"$schema": "https://schemas.wp.org/trunk/theme.json",
|
||||
"version": 2,
|
||||
"settings": {
|
||||
"layout": {
|
||||
"contentSize": "1200px",
|
||||
"wideSize": "1536px"
|
||||
},
|
||||
"appearanceTools": false,
|
||||
"border": {
|
||||
"color": false,
|
||||
"radius": false,
|
||||
"style": false,
|
||||
"width": false
|
||||
},
|
||||
"shadow": {
|
||||
"defaultPresets": false,
|
||||
"presets": []
|
||||
},
|
||||
"spacing": {
|
||||
"padding": false,
|
||||
"margin": false,
|
||||
"blockGap": false,
|
||||
"customSpacingSize": false,
|
||||
"units": ["px", "%", "em", "rem", "vw", "vh"]
|
||||
},
|
||||
"color": {
|
||||
"defaultPalette": false,
|
||||
"defaultGradients": false,
|
||||
"defaultDuotone": false,
|
||||
"custom": true,
|
||||
"customDuotone": false,
|
||||
"customGradient": false,
|
||||
"palette": [
|
||||
{ "slug": "primary", "color": "#D83302", "name": "Primary" },
|
||||
{ "slug": "primary-dk", "color": "#B52B02", "name": "Primary Dark" },
|
||||
{ "slug": "primary-lt", "color": "#FEF0EB", "name": "Primary Light" },
|
||||
{ "slug": "accent", "color": "#00757c", "name": "Accent" },
|
||||
{ "slug": "accent-dk", "color": "#005a60", "name": "Accent Dark" },
|
||||
{ "slug": "accent-lt", "color": "#E6F4F5", "name": "Accent Light" },
|
||||
{ "slug": "dark", "color": "#0D1321", "name": "Dark" },
|
||||
{ "slug": "dark-2", "color": "#1A2236", "name": "Dark 2" },
|
||||
{ "slug": "text", "color": "#2D3748", "name": "Text" },
|
||||
{ "slug": "text-muted", "color": "#718096", "name": "Text Muted" },
|
||||
{ "slug": "border", "color": "#E2E8F0", "name": "Border" },
|
||||
{ "slug": "bg", "color": "#FFFFFF", "name": "Background" },
|
||||
{ "slug": "bg-alt", "color": "#FFF8F5", "name": "Background Alt" }
|
||||
]
|
||||
},
|
||||
"typography": {
|
||||
"defaultFontSizes": false,
|
||||
"customFontSize": true,
|
||||
"lineHeight": false,
|
||||
"dropCap": false,
|
||||
"textDecoration": false,
|
||||
"textTransform": false,
|
||||
"fontStyle": false,
|
||||
"fontWeight": false,
|
||||
"letterSpacing": false,
|
||||
"fontFamilies": [
|
||||
{
|
||||
"fontFamily": "'Inter', system-ui, -apple-system, sans-serif",
|
||||
"name": "Body",
|
||||
"slug": "sans"
|
||||
},
|
||||
{
|
||||
"fontFamily": "'Inter', system-ui, -apple-system, sans-serif",
|
||||
"name": "Heading",
|
||||
"slug": "heading"
|
||||
}
|
||||
],
|
||||
"fontSizes": [
|
||||
{ "slug": "xs", "size": "0.75rem", "name": "XS — 12px" },
|
||||
{ "slug": "sm", "size": "0.875rem", "name": "SM — 14px" },
|
||||
{ "slug": "base", "size": "1rem", "name": "Base — 16px" },
|
||||
{ "slug": "md", "size": "1.125rem", "name": "MD — 18px" },
|
||||
{ "slug": "lg", "size": "1.25rem", "name": "LG — 20px" },
|
||||
{ "slug": "xl", "size": "1.5rem", "name": "XL — 24px" },
|
||||
{ "slug": "2xl", "size": "1.875rem", "name": "2XL — 30px" },
|
||||
{ "slug": "3xl", "size": "2.25rem", "name": "3XL — 36px" },
|
||||
{ "slug": "4xl", "size": "3rem", "name": "4XL — 48px" },
|
||||
{ "slug": "5xl", "size": "3.75rem", "name": "5XL — 60px" },
|
||||
{ "slug": "6xl", "size": "4.5rem", "name": "6XL — 72px" }
|
||||
]
|
||||
},
|
||||
"custom": {
|
||||
"dark": {
|
||||
"primary": "#FF6B3D",
|
||||
"primary-dk": "#D83302",
|
||||
"primary-lt": "rgba(216,51,2,0.15)",
|
||||
"accent": "#00757c",
|
||||
"accent-dk": "#005a60",
|
||||
"accent-lt": "rgba(0,117,124,0.15)",
|
||||
"dark": "#E2E8F0",
|
||||
"dark-2": "#CBD5E0",
|
||||
"text": "#CBD5E0",
|
||||
"text-muted": "#A0AEC0",
|
||||
"border": "#2D3748",
|
||||
"bg": "#0F1724",
|
||||
"bg-alt": "#151F30",
|
||||
"bg-dark": "#0A0F1A",
|
||||
"heading": "#F7FAFC",
|
||||
"card-bg": "#151F30"
|
||||
},
|
||||
"radius": {
|
||||
"sm": "6px",
|
||||
"md": "12px",
|
||||
"lg": "20px",
|
||||
"xl": "32px"
|
||||
},
|
||||
"container": {
|
||||
"max": "1200px",
|
||||
"pad": "clamp(1rem, 5vw, 2rem)"
|
||||
}
|
||||
},
|
||||
"useRootPaddingAwareAlignments": false
|
||||
},
|
||||
"styles": {
|
||||
"color": {
|
||||
"text": "var:preset|color|text",
|
||||
"background": "var:preset|color|bg"
|
||||
},
|
||||
"typography": {
|
||||
"fontFamily": "var:preset|font-family|sans",
|
||||
"fontSize": "var:preset|font-size|base"
|
||||
},
|
||||
"spacing": {
|
||||
"blockGap": "0",
|
||||
"padding": "0",
|
||||
"margin": "0"
|
||||
},
|
||||
"elements": {
|
||||
"link": {
|
||||
"color": {
|
||||
"text": "var:preset|color|primary"
|
||||
},
|
||||
"typography": {
|
||||
"textDecoration": "none"
|
||||
},
|
||||
":hover": {
|
||||
"color": {
|
||||
"text": "var:preset|color|primary-dk"
|
||||
},
|
||||
"typography": {
|
||||
"textDecoration": "underline"
|
||||
}
|
||||
},
|
||||
":focus": {
|
||||
"color": {
|
||||
"text": "var:preset|color|primary-dk"
|
||||
}
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"color": {
|
||||
"text": "var:preset|color|bg",
|
||||
"background": "var:preset|color|primary"
|
||||
},
|
||||
"typography": {
|
||||
"fontFamily": "var:preset|font-family|sans",
|
||||
"fontSize": "var:preset|font-size|base"
|
||||
},
|
||||
":hover": {
|
||||
"color": {
|
||||
"background": "var:preset|color|primary-dk"
|
||||
}
|
||||
},
|
||||
":focus": {
|
||||
"color": {
|
||||
"background": "var:preset|color|primary-dk"
|
||||
}
|
||||
}
|
||||
},
|
||||
"h1": {
|
||||
"typography": {
|
||||
"fontFamily": "var:preset|font-family|heading"
|
||||
}
|
||||
},
|
||||
"h2": {
|
||||
"typography": {
|
||||
"fontFamily": "var:preset|font-family|heading"
|
||||
}
|
||||
},
|
||||
"h3": {
|
||||
"typography": {
|
||||
"fontFamily": "var:preset|font-family|heading"
|
||||
}
|
||||
},
|
||||
"h4": {
|
||||
"typography": {
|
||||
"fontFamily": "var:preset|font-family|heading"
|
||||
}
|
||||
},
|
||||
"h5": {
|
||||
"typography": {
|
||||
"fontFamily": "var:preset|font-family|heading"
|
||||
}
|
||||
},
|
||||
"h6": {
|
||||
"typography": {
|
||||
"fontFamily": "var:preset|font-family|heading"
|
||||
}
|
||||
}
|
||||
},
|
||||
"blocks": {
|
||||
"core/group": {
|
||||
"spacing": {
|
||||
"padding": "0",
|
||||
"margin": "0"
|
||||
}
|
||||
},
|
||||
"core/columns": {
|
||||
"spacing": {
|
||||
"padding": "0",
|
||||
"margin": "0",
|
||||
"blockGap": "0"
|
||||
}
|
||||
},
|
||||
"core/column": {
|
||||
"spacing": {
|
||||
"padding": "0",
|
||||
"margin": "0"
|
||||
}
|
||||
},
|
||||
"core/heading": {
|
||||
"spacing": {
|
||||
"padding": "0",
|
||||
"margin": "0"
|
||||
}
|
||||
},
|
||||
"core/paragraph": {
|
||||
"spacing": {
|
||||
"padding": "0",
|
||||
"margin": "0"
|
||||
}
|
||||
},
|
||||
"core/list": {
|
||||
"spacing": {
|
||||
"padding": "0",
|
||||
"margin": "0"
|
||||
}
|
||||
},
|
||||
"core/image": {
|
||||
"spacing": {
|
||||
"padding": "0",
|
||||
"margin": "0"
|
||||
}
|
||||
},
|
||||
"core/separator": {
|
||||
"color": {
|
||||
"text": "var:preset|color|border"
|
||||
}
|
||||
},
|
||||
"core/button": {
|
||||
"color": {
|
||||
"text": "var:preset|color|bg",
|
||||
"background": "var:preset|color|primary"
|
||||
},
|
||||
"typography": {
|
||||
"fontFamily": "var:preset|font-family|sans",
|
||||
"fontSize": "var:preset|font-size|base"
|
||||
}
|
||||
},
|
||||
"oribi/hero": {
|
||||
"spacing": {
|
||||
"padding": "0",
|
||||
"margin": "0"
|
||||
}
|
||||
},
|
||||
"oribi/page-hero": {
|
||||
"spacing": {
|
||||
"padding": "0",
|
||||
"margin": "0"
|
||||
}
|
||||
},
|
||||
"oribi/cta-banner": {
|
||||
"spacing": {
|
||||
"padding": "0",
|
||||
"margin": "0"
|
||||
}
|
||||
},
|
||||
"oribi/intro-section": {
|
||||
"spacing": {
|
||||
"padding": "0",
|
||||
"margin": "0"
|
||||
}
|
||||
},
|
||||
"oribi/contact-section": {
|
||||
"spacing": {
|
||||
"padding": "0",
|
||||
"margin": "0"
|
||||
}
|
||||
},
|
||||
"oribi/feature-section": {
|
||||
"spacing": {
|
||||
"padding": "0",
|
||||
"margin": "0"
|
||||
}
|
||||
},
|
||||
"oribi/feature-card": {
|
||||
"spacing": {
|
||||
"padding": "0",
|
||||
"margin": "0"
|
||||
}
|
||||
},
|
||||
"oribi/pricing-section": {
|
||||
"spacing": {
|
||||
"padding": "0",
|
||||
"margin": "0"
|
||||
}
|
||||
},
|
||||
"oribi/pricing-card": {
|
||||
"spacing": {
|
||||
"padding": "0",
|
||||
"margin": "0"
|
||||
}
|
||||
},
|
||||
"oribi/platform-section": {
|
||||
"spacing": {
|
||||
"padding": "0",
|
||||
"margin": "0"
|
||||
}
|
||||
},
|
||||
"oribi/platform-row": {
|
||||
"spacing": {
|
||||
"padding": "0",
|
||||
"margin": "0"
|
||||
}
|
||||
},
|
||||
"oribi/faq-section": {
|
||||
"spacing": {
|
||||
"padding": "0",
|
||||
"margin": "0"
|
||||
}
|
||||
},
|
||||
"oribi/faq-item": {
|
||||
"spacing": {
|
||||
"padding": "0",
|
||||
"margin": "0"
|
||||
}
|
||||
},
|
||||
"oribi/comparison-table": {
|
||||
"spacing": {
|
||||
"padding": "0",
|
||||
"margin": "0"
|
||||
}
|
||||
},
|
||||
"oribi/trust-section": {
|
||||
"spacing": {
|
||||
"padding": "0",
|
||||
"margin": "0"
|
||||
}
|
||||
},
|
||||
"oribi/trust-item": {
|
||||
"spacing": {
|
||||
"padding": "0",
|
||||
"margin": "0"
|
||||
}
|
||||
},
|
||||
"oribi/hero-animated": {
|
||||
"spacing": {
|
||||
"padding": "0",
|
||||
"margin": "0"
|
||||
}
|
||||
},
|
||||
"oribi/page-hero-animated": {
|
||||
"spacing": {
|
||||
"padding": "0",
|
||||
"margin": "0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"templateParts": [
|
||||
{ "name": "header", "title": "Header", "area": "header" },
|
||||
{ "name": "footer", "title": "Footer", "area": "footer" }
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user