This commit is contained in:
Matt Batchelder
2026-02-20 21:28:00 -05:00
commit 19ee98c68d
37 changed files with 9405 additions and 0 deletions

27
pages/about.php Normal file
View File

@@ -0,0 +1,27 @@
<?php
/**
* Title: About
* Slug: ots-signs/page-about
* Categories: oribi-pages
* Keywords: about, company, mission, team, digital signage
* Post Types: page
*/
?>
<!-- wp:oribi/page-hero-animated {"label":"About Us","title":"The Complete Digital Signage Solution","description":"An Oribi Technology Services company, OTS Signs delivers enterprise-grade digital signage solutions that help businesses communicate, engage, and grow."} /-->
<!-- wp:oribi/intro-section {"variant":"normal","label":"Our Story","heading":"Built by People Who Understand Technology","description":"OTS Signs was born from a simple idea: digital signage should be powerful yet simple. As part of the Oribi Technology Services family, we bring years of IT infrastructure expertise to the signage industry. We understand networks, security, and reliability — and we\u0027ve built a platform that reflects that knowledge. Our team combines deep technical skills with creative vision to deliver signage solutions that truly work for your business.","visual":"📺"} /-->
<!-- wp:oribi/intro-section {"variant":"alt","label":"Our Mission","heading":"Empowering Businesses to Communicate Better","description":"We believe every business deserves access to professional-grade digital signage. Our mission is to make it affordable, reliable, and effortless — so you can focus on what matters most: your customers. Whether you\u0027re a single-location café or a multi-site enterprise, our platform scales to meet your needs without compromising on quality or support.","visual":"🎯","reversed":true} /-->
<!-- wp:oribi/value-section {"variant":"normal","label":"What Sets Us Apart","heading":"Why Businesses Choose OTS Signs","lead":"We\u0027re not just a software company — we\u0027re a full-service digital signage partner.","columns":3} -->
<!-- wp:oribi/value-card {"iconType":"fontawesome","faIcon":"fas fa-camera","title":"In-House Content Production","description":"Professional photography, video production, and graphic design services to ensure your signage always looks its best."} /-->
<!-- wp:oribi/value-card {"iconType":"fontawesome","faIcon":"fas fa-server","title":"Infrastructure Expertise","description":"Built on enterprise-grade cloud infrastructure by the same team that manages IT systems for businesses across the region."} /-->
<!-- wp:oribi/value-card {"iconType":"fontawesome","faIcon":"fas fa-handshake","title":"Partnership Approach","description":"We work as an extension of your team. Dedicated support, personalised onboarding, and ongoing optimisation."} /-->
<!-- /wp:oribi/value-section -->
<!-- wp:oribi/trust-section {"label":"Our Commitment","heading":"We Stand Behind Every Screen","lead":"Your success is our success. From initial concept to ongoing management, we\u0027re with you every step of the way.","btnText":"Get in Touch","btnUrl":"/contact","btnSub":"Let\u0027s discuss your digital signage needs"} -->
<!-- wp:oribi/trust-item {"heading":"Reliable Technology","description":"Enterprise-grade security, 99.9% uptime, and intelligent offline playback ensure your message is always on screen."} /-->
<!-- wp:oribi/trust-item {"heading":"Continuous Innovation","description":"We\u0027re constantly improving our platform with new features, integrations, and content tools to keep you ahead of the curve."} /-->
<!-- /wp:oribi/trust-section -->
<!-- wp:oribi/cta-banner {"heading":"Let\u0027s Build Something Together","text":"Whether you\u0027re starting fresh or upgrading an existing setup, we\u0027re ready to help.","btnText":"Contact Us","btnUrl":"/contact"} /-->

20
pages/contact.php Normal file
View File

@@ -0,0 +1,20 @@
<?php
/**
* Title: Contact
* Slug: ots-signs/page-contact
* Categories: oribi-pages
* Keywords: contact, sales, support, inquiry, get in touch
* Post Types: page
*/
?>
<!-- wp:oribi/page-hero-animated {"label":"Contact Us","title":"Get in Touch","description":"Ready to transform your screens? Whether you\u0027re exploring options or ready to get started, we\u0027re here to help."} /-->
<!-- wp:oribi/contact-section {"heading":"Let\u0027s Talk","lead":"Tell us about your digital signage needs and we\u0027ll get back to you within one business day.","email":"hello@ots-signs.com","supportUrl":"https://ots-signs.com/support","portalUrl":"https://ots-signs.com/portal","location":"An Oribi Technology Services Company","formHeading":"Send Us a Message"} /-->
<!-- wp:oribi/value-section {"variant":"alt","label":"How We Can Help","heading":"Reach Out For","lead":"Whatever stage you\u0027re at, we have the expertise to guide you.","columns":3} -->
<!-- wp:oribi/value-card {"iconType":"fontawesome","faIcon":"fas fa-comments","title":"Sales Inquiries","description":"Explore our plans, get a custom quote, or learn how digital signage can benefit your business."} /-->
<!-- wp:oribi/value-card {"iconType":"fontawesome","faIcon":"fas fa-headset","title":"Technical Support","description":"Need help with your existing setup? Our support team is ready to troubleshoot and resolve any issues."} /-->
<!-- wp:oribi/value-card {"iconType":"fontawesome","faIcon":"fas fa-handshake","title":"Partnerships","description":"Interested in reselling or integrating our platform? Let\u0027s discuss partnership opportunities."} /-->
<!-- /wp:oribi/value-section -->
<!-- wp:oribi/cta-banner {"heading":"Prefer a Live Demo?","text":"See our platform in action before you commit. Request access to our live demo instance.","btnText":"Request Demo","btnUrl":"/demo"} /-->

25
pages/demo.php Normal file
View File

@@ -0,0 +1,25 @@
<?php
/**
* Title: Demo
* Slug: ots-signs/page-demo
* Categories: oribi-pages
* Keywords: demo, trial, try, preview, platform
* Post Types: page
*/
?>
<!-- wp:oribi/page-hero-animated {"label":"Try It","title":"See It in Action","description":"Experience our digital signage platform firsthand. Request access to our live demo instance and explore every feature — no commitment required."} /-->
<!-- wp:oribi/intro-section {"variant":"normal","label":"Live Demo","heading":"Explore Our Demo Instance","description":"Our demo environment gives you full access to the OTS Signs CMS platform. Create content, schedule displays, explore integrations, and see exactly how your digital signage will look and feel in production. No credit card needed — just fill in the form below and we\u0027ll send you access within 24 hours.","visual":"🎬"} /-->
<!-- wp:oribi/feature-section {"variant":"alt","label":"What You\u0027ll See","heading":"Full Platform Access","lead":"The demo instance includes all the features available on our Pro plan.","columns":3} -->
<!-- wp:oribi/feature-card {"iconType":"fontawesome","faIcon":"fas fa-desktop","title":"Content Management","description":"Upload images, videos, and HTML content. Create playlists and manage your media library."} /-->
<!-- wp:oribi/feature-card {"iconType":"fontawesome","faIcon":"fas fa-clock","title":"Scheduling","description":"Set up content schedules with day-parting and time-based triggers to see automated playback in action."} /-->
<!-- wp:oribi/feature-card {"iconType":"fontawesome","faIcon":"fas fa-chart-line","title":"Live Data Feeds","description":"Connect sample data sources and see how real-time information appears on your digital signage displays."} /-->
<!-- wp:oribi/feature-card {"iconType":"fontawesome","faIcon":"fas fa-th-large","title":"Template Library","description":"Browse our collection of professionally designed templates for menus, promotions, and informational displays."} /-->
<!-- wp:oribi/feature-card {"iconType":"fontawesome","faIcon":"fas fa-users-cog","title":"User Management","description":"See how roles and permissions work to manage team access across your signage network."} /-->
<!-- wp:oribi/feature-card {"iconType":"fontawesome","faIcon":"fas fa-chart-pie","title":"Analytics Dashboard","description":"View playback analytics and screen health metrics to understand how your content is performing."} /-->
<!-- /wp:oribi/feature-section -->
<!-- wp:oribi/contact-section {"heading":"Request Demo Access","lead":"Fill in your details and we\u0027ll send you credentials for our demo instance within 24 hours.","email":"hello@ots-signs.com","supportUrl":"https://demo.ots-signs.com/","portalUrl":"https://demo.ots-signs.com/","location":"Online Demo Available 24/7","formHeading":"Get Your Demo Access"} /-->
<!-- wp:oribi/cta-banner {"heading":"Ready to Skip the Demo?","text":"If you already know what you need, let\u0027s get started right away.","btnText":"View Pricing","btnUrl":"/pricing"} /-->

29
pages/devices.php Normal file
View File

@@ -0,0 +1,29 @@
<?php
/**
* Title: Player Devices
* Slug: ots-signs/page-devices
* Categories: oribi-pages
* Keywords: devices, hardware, player, screens, HDMI, commercial display
* Post Types: page
*/
?>
<!-- wp:oribi/page-hero-animated {"label":"Hardware","title":"Player Devices Built for Reliability","description":"Our digital signage players are engineered for performance, security, and simplicity. Plug in, connect, and start displaying — it\u0027s that easy."} /-->
<!-- wp:oribi/platform-section {"label":"Our Devices","heading":"Hardware That Just Works","lead":"Purpose-built player devices designed for 24/7 commercial use with zero maintenance."} -->
<!-- wp:oribi/platform-row {"heading":"Use Existing Screens","description":"Our digital signage players work on any screen with HDMI, so you can easily integrate them into your current setup. No need to replace your existing displays — just plug in our player and start broadcasting. If you need a complete solution, we also offer bundled packages that include both the player and a high-quality commercial-grade display.","btnText":"Get a Quote","btnUrl":"/contact"} /-->
<!-- wp:oribi/platform-row {"heading":"Intelligent Offline Playback","description":"Our player devices are engineered to keep your message on screen, even when the internet isn\u0027t available. Content is cached locally so your displays continue running seamlessly. When connectivity returns, new content syncs automatically — no manual intervention needed.","btnText":"See Features","btnUrl":"/features","reversed":true} /-->
<!-- wp:oribi/platform-row {"heading":"Enterprise-Grade Security","description":"Every device is hardened with enterprise-grade security protocols. Encrypted communications, secure boot, and remote management capabilities ensure your signage network stays protected. Automatic firmware updates keep your devices current without any downtime.","btnText":"Learn More","btnUrl":"/about"} /-->
<!-- /wp:oribi/platform-section -->
<!-- wp:oribi/feature-section {"variant":"alt","label":"Device Specifications","heading":"What\u0027s Inside Every Player","lead":"Commercial-grade components designed for continuous operation in any environment.","columns":3} -->
<!-- wp:oribi/feature-card {"iconType":"fontawesome","faIcon":"fas fa-display","title":"4K Output","description":"Crystal-clear 4K resolution output via HDMI for stunning visual quality on any display."} /-->
<!-- wp:oribi/feature-card {"iconType":"fontawesome","faIcon":"fas fa-wifi","title":"Dual Connectivity","description":"Built-in Wi-Fi and Ethernet for flexible network connectivity options in any installation."} /-->
<!-- wp:oribi/feature-card {"iconType":"fontawesome","faIcon":"fas fa-hard-drive","title":"Local Storage","description":"On-device storage caches your content for seamless offline playback and instant startup."} /-->
<!-- wp:oribi/feature-card {"iconType":"fontawesome","faIcon":"fas fa-shield-halved","title":"Secure Boot","description":"Hardware-level security with encrypted storage and secure boot ensures tamper-proof operation."} /-->
<!-- wp:oribi/feature-card {"iconType":"fontawesome","faIcon":"fas fa-rotate","title":"Auto Updates","description":"Over-the-air firmware updates keep your devices current without manual intervention or downtime."} /-->
<!-- wp:oribi/feature-card {"iconType":"fontawesome","faIcon":"fas fa-temperature-low","title":"Silent Operation","description":"Fanless design means zero noise, making our players perfect for quiet environments like lobbies and restaurants."} /-->
<!-- /wp:oribi/feature-section -->
<!-- wp:oribi/intro-section {"variant":"normal","label":"Bundles Available","heading":"Complete Display Solutions","description":"Need screens too? We offer bundled packages that include our player device paired with a commercial-grade display — rated for 24/7 operation with enhanced brightness and durability. Available in a range of sizes from 32\" to 75\". Contact us for custom configurations and volume pricing.","visual":"🖥️"} /-->
<!-- wp:oribi/cta-banner {"heading":"Need Help Choosing?","text":"Our team can recommend the right device and display combination for your specific environment and use case.","btnText":"Request a Quote","btnUrl":"/contact"} /-->

33
pages/faq.php Normal file
View File

@@ -0,0 +1,33 @@
<?php
/**
* Title: FAQ
* Slug: ots-signs/page-faq
* Categories: oribi-pages
* Keywords: faq, questions, answers, support, help
* Post Types: page
*/
?>
<!-- wp:oribi/page-hero-animated {"label":"FAQ","title":"Frequently Asked Questions","description":"Got questions? We\u0027ve got answers. Find everything you need to know about our digital signage platform, pricing, and support."} /-->
<!-- wp:oribi/faq-section {"label":"Platform \u0026 Pricing","heading":"Plans, Pricing \u0026 Platform","lead":"Common questions about our plans, what\u0027s included, and how pricing works."} -->
<!-- wp:oribi/faq-item {"question":"What\u0027s included in the Essentials plan?","answer":"The Essentials plan includes up to 20 screens, a custom subdomain, shared CMS server, content scheduling with day-parting, integration with live data sources, unlimited user seats, and the ability to publish content to your signs in minutes. It\u0027s $7 per screen per month, or $70 per screen annually."} /-->
<!-- wp:oribi/faq-item {"question":"What does the Pro plan include?","answer":"The Pro plan supports 500+ screens with a custom domain, dedicated CMS server, custom live data integrations, priority support, advanced analytics, SSO and role-based access, SLA guarantee, and a dedicated account manager. Contact us for pricing tailored to your needs."} /-->
<!-- wp:oribi/faq-item {"question":"Are there any hidden fees?","answer":"No. Our pricing is fully transparent. The per-screen monthly or annual fee covers everything — CMS access, software updates, cloud hosting, and standard support. Content creation services and hardware are quoted separately."} /-->
<!-- wp:oribi/faq-item {"question":"Can I switch plans later?","answer":"Absolutely. You can upgrade from Essentials to Pro at any time as your business grows. We\u0027ll handle the migration seamlessly with no disruption to your displays."} /-->
<!-- /wp:oribi/faq-section -->
<!-- wp:oribi/faq-section {"variant":"alt","label":"Setup \u0026 Integration","heading":"Getting Started","lead":"Everything about setup, installation, and integrating with your existing systems."} -->
<!-- wp:oribi/faq-item {"question":"How long does setup take?","answer":"Most installations are up and running within a day. Plug in our player device, connect to your network, and your content appears on screen. Our team handles the CMS configuration and can have your first content ready to display immediately."} /-->
<!-- wp:oribi/faq-item {"question":"Can I use my existing screens?","answer":"Yes! Our players work with any screen that has an HDMI input. TVs, commercial displays, or monitors — if it has HDMI, our player works with it. No need to replace your existing hardware."} /-->
<!-- wp:oribi/faq-item {"question":"How do live data integrations work?","answer":"We can connect your existing web dashboards, APIs, RSS feeds, social media accounts, and other data sources to your digital signage. Data updates are reflected in real-time on your displays. The Essentials plan supports standard integrations, while Pro includes custom integration development."} /-->
<!-- wp:oribi/faq-item {"question":"Do I need special internet bandwidth?","answer":"Our players are designed to be bandwidth-efficient. Content is cached locally and synced incrementally. A standard business internet connection is more than sufficient. For locations with limited connectivity, our offline playback ensures your content keeps running."} /-->
<!-- /wp:oribi/faq-section -->
<!-- wp:oribi/faq-section {"label":"Support \u0026 Security","heading":"Support, Security \u0026 Reliability","lead":"How we keep your signage network secure, reliable, and fully supported."} -->
<!-- wp:oribi/faq-item {"question":"What happens if my internet goes down?","answer":"Our intelligent player devices continue playing cached content even when offline. When connectivity returns, new content syncs automatically. Your displays never go dark."} /-->
<!-- wp:oribi/faq-item {"question":"How secure is the platform?","answer":"We use enterprise-grade security throughout our stack: end-to-end encryption for all communications, secure boot on player devices, role-based access control in the CMS, and SOC 2-aligned cloud infrastructure. Your content and network are always protected."} /-->
<!-- wp:oribi/faq-item {"question":"What kind of support do you offer?","answer":"Essentials plans include email support with a 24-hour response time. Pro plans include priority support with a dedicated account manager, phone support, and guaranteed SLA response times."} /-->
<!-- wp:oribi/faq-item {"question":"Can I create content myself, or do you do it for me?","answer":"Both! Our CMS is designed for self-service content creation with drag-and-drop simplicity. We also offer professional content creation services — photography, video production, and graphic design — to make your signage look its absolute best."} /-->
<!-- /wp:oribi/faq-section -->
<!-- wp:oribi/cta-banner {"heading":"Still Have Questions?","text":"Our team is happy to answer anything not covered here. Get in touch and we\u0027ll help.","btnText":"Contact Us","btnUrl":"/contact"} /-->

34
pages/features.php Normal file
View File

@@ -0,0 +1,34 @@
<?php
/**
* Title: Features
* Slug: ots-signs/page-features
* Categories: oribi-pages
* Keywords: features, platform, content scheduling, cloud CMS, real-time data
* Post Types: page
*/
?>
<!-- wp:oribi/page-hero-animated {"label":"Platform","title":"Technology Designed to Work for You","description":"Our application platform lets you publish content instantly across any network, with built-in real-time analytics. Its secure, cloud-native architecture scales effortlessly while keeping deployments fast and maintenance minimal."} /-->
<!-- wp:oribi/platform-section {"label":"Core Features","heading":"Powerful Tools, Simple Experience","lead":"Everything you need to create, schedule, and manage stunning digital signage content — from a single dashboard."} -->
<!-- wp:oribi/platform-row {"heading":"Manage All Your Screens in One Place","description":"Our cloud CMS offers the power and flexibility to manage digital displays for businesses of all sizes. Whether you\u0027re a single location or a multi-site enterprise, our digital signage network is built to grow with you.","btnText":"Get Started","btnUrl":"/contact"} /-->
<!-- wp:oribi/platform-row {"heading":"Dynamic Content Scheduling","description":"Effortlessly schedule your content for maximum impact. Plan ahead by setting content to play at specific times, daypart, or even trigger based on real-world events. Automate your messaging to ensure the right information is displayed at the right time.","btnText":"See Pricing","btnUrl":"/pricing","reversed":true} /-->
<!-- wp:oribi/platform-row {"heading":"Use Existing Screens","description":"Our digital signage players work on any screen with HDMI, so you can easily integrate them into your current setup. If you need a complete solution, we also offer bundled packages that include both the player and a high-quality commercial-grade display.","btnText":"View Devices","btnUrl":"/devices"} /-->
<!-- wp:oribi/platform-row {"heading":"Secure \u0026 Reliable","description":"Enterprise-grade security safeguards your content and network, while our advanced hosting platform guarantees rock-solid reliability. Plus, our intelligent player devices are designed to continue playing content even when offline.","btnText":"Learn More","btnUrl":"/about","reversed":true} /-->
<!-- /wp:oribi/platform-section -->
<!-- wp:oribi/feature-section {"variant":"alt","label":"Capabilities","heading":"Built for Modern Business","lead":"From content creation to analytics, our platform covers every step of your digital signage journey.","columns":3} -->
<!-- wp:oribi/feature-card {"iconType":"fontawesome","faIcon":"fas fa-cloud","title":"Cloud-Native CMS","description":"Access your content management system from anywhere. No software to install — just log in and start publishing."} /-->
<!-- wp:oribi/feature-card {"iconType":"fontawesome","faIcon":"fas fa-clock","title":"Content Scheduling","description":"Set content to play at specific times with day-parting. Automate your messaging for maximum impact."} /-->
<!-- wp:oribi/feature-card {"iconType":"fontawesome","faIcon":"fas fa-chart-line","title":"Live Data Integration","description":"Connect your existing web dashboards, social feeds, and real-time data sources directly to your displays."} /-->
<!-- wp:oribi/feature-card {"iconType":"fontawesome","faIcon":"fas fa-wifi","title":"Offline Playback","description":"Content keeps playing even when the internet goes down. Our players cache content locally for uninterrupted display."} /-->
<!-- wp:oribi/feature-card {"iconType":"fontawesome","faIcon":"fas fa-users","title":"Unlimited User Seats","description":"Add as many team members as you need. No per-user fees or access restrictions on any plan."} /-->
<!-- wp:oribi/feature-card {"iconType":"fontawesome","faIcon":"fas fa-shield-halved","title":"Enterprise Security","description":"End-to-end encryption, role-based access control, and secure cloud infrastructure protect your content and network."} /-->
<!-- /wp:oribi/feature-section -->
<!-- wp:oribi/value-section {"variant":"normal","label":"Why Choose Us","heading":"The OTS Signs Advantage","lead":"We don\u0027t just provide software — we deliver a complete digital signage experience.","columns":3} -->
<!-- wp:oribi/value-card {"iconType":"fontawesome","faIcon":"fas fa-camera","title":"Professional Content","description":"Our in-house photography and video production team creates stunning visuals that make your signage stand out."} /-->
<!-- wp:oribi/value-card {"iconType":"fontawesome","faIcon":"fas fa-rocket","title":"Publish in Minutes","description":"Upload content and push it to your screens instantly. No waiting, no complex workflows — just fast deployment."} /-->
<!-- wp:oribi/value-card {"iconType":"fontawesome","faIcon":"fas fa-headset","title":"Dedicated Support","description":"Our team is always available to help with setup, content creation, troubleshooting, and ongoing optimisation."} /-->
<!-- /wp:oribi/value-section -->
<!-- wp:oribi/cta-banner {"heading":"See It in Action","text":"Request access to our live demo instance and experience the platform for yourself.","btnText":"Request Demo","btnUrl":"/demo"} /-->

32
pages/home.php Normal file
View File

@@ -0,0 +1,32 @@
<?php
/**
* Title: Home Page
* Slug: ots-signs/page-home
* Categories: oribi-pages
* Keywords: home, digital signage, landing, hero
* Post Types: page
*/
?>
<!-- wp:oribi/hero-animated {"label":"● Digital Signage Solutions","title":"Turn any screen into a dynamic communication tool.","highlightWord":"dynamic","description":"Digital signage is the modern way to connect with your audience. From eye-catching retail displays to dynamic informational screens, we craft tailored solutions that capture attention and deliver your message.","primaryBtnText":"Get Started","primaryBtnUrl":"/contact","secondaryBtnText":"Request Demo","secondaryBtnUrl":"/demo","stat1Value":"6+","stat1Label":"Industries Served","stat2Value":"500+","stat2Label":"Screens Supported","stat3Value":"99.9%","stat3Label":"Uptime"} /-->
<!-- wp:oribi/feature-section {"variant":"normal","label":"The Complete Package","heading":"Everything You Need for Engaging Digital Signage","lead":"High-quality visuals, real-time data, and reliable playback — all managed from one powerful platform.","columns":4} -->
<!-- wp:oribi/feature-card {"iconType":"fontawesome","faIcon":"fas fa-camera","title":"Showcase Your Products","description":"Professional photography and video production services to showcase your products, services, or environment with polished, engaging content.","url":"/features"} /-->
<!-- wp:oribi/feature-card {"iconType":"fontawesome","faIcon":"fas fa-chart-line","title":"Live Data Integrations","description":"Integrate your existing web dashboards and real-time data with our platform to bring your most important information to life on digital signage.","url":"/features"} /-->
<!-- wp:oribi/feature-card {"iconType":"fontawesome","faIcon":"fas fa-utensils","title":"Digital Menu Boards","description":"Make your menu as appealing as your product. We incorporate your branding and include high-quality photography to showcase your offerings.","url":"/solutions"} /-->
<!-- wp:oribi/feature-card {"iconType":"fontawesome","faIcon":"fas fa-wifi","title":"Offline Playback Support","description":"Our intelligent player devices keep your message on screen even when the internet isn\u0027t available, ensuring reliability in any location.","url":"/devices"} /-->
<!-- /wp:oribi/feature-section -->
<!-- wp:oribi/image-section {"variant":"alt","label":"Who It's For","heading":"Solutions for Every Industry","lead":"Modern businesses need real-time communication. Digital signage helps you connect, inform, and engage.","columns":4} -->
<!-- wp:oribi/image-card {"iconType":"fontawesome","faIcon":"fas fa-hotel","title":"Hospitality","description":"Showcase menus, promotions, and special events while guiding guests through lobbies, restaurants, and bars.","url":"/solutions"} /-->
<!-- wp:oribi/image-card {"iconType":"fontawesome","faIcon":"fas fa-store","title":"Retail","description":"Drive product upsells, announce flash-sales, and offer in-store navigation with fresh, eye-catching displays.","url":"/solutions"} /-->
<!-- wp:oribi/image-card {"iconType":"fontawesome","faIcon":"fas fa-building","title":"Corporate","description":"Enhance meeting experiences with Teams integration. Communicate schedules, company news, and employee alerts.","url":"/solutions"} /-->
<!-- wp:oribi/image-card {"iconType":"fontawesome","faIcon":"fas fa-graduation-cap","title":"Education","description":"Broadcast class schedules, announcements, and interactive learning content in campuses and auditoriums.","url":"/solutions"} /-->
<!-- /wp:oribi/image-section -->
<!-- wp:oribi/link-section {"label":"Explore","heading":"Take the Next Step","lead":"See our plans, explore devices, or request a demo.","columns":3} -->
<!-- wp:oribi/link-card {"iconType":"fontawesome","faIcon":"fas fa-tags","title":"Pricing Plans","description":"Affordable, scalable options for businesses of all sizes. See what\u0027s included at every level.","linkText":"View Pricing","linkUrl":"/pricing"} /-->
<!-- wp:oribi/link-card {"iconType":"fontawesome","faIcon":"fas fa-microchip","title":"Player Devices","description":"Enterprise-grade hardware designed for reliability, with offline playback and HDMI compatibility.","linkText":"See Devices","linkUrl":"/devices"} /-->
<!-- wp:oribi/link-card {"iconType":"fontawesome","faIcon":"fas fa-play-circle","title":"Request a Demo","description":"See our platform in action. Request access to our live demo instance.","linkText":"Try It Out","linkUrl":"/demo"} /-->
<!-- /wp:oribi/link-section -->
<!-- wp:oribi/cta-banner {"heading":"Ready to Transform Your Screens?","text":"Get in touch to discuss your digital signage needs. No pressure, no jargon — just a conversation about how we can help.","btnText":"Get Started Today","btnUrl":"/contact"} /-->

26
pages/pricing.php Normal file
View File

@@ -0,0 +1,26 @@
<?php
/**
* Title: Pricing
* Slug: ots-signs/page-pricing
* Categories: oribi-pages
* Keywords: pricing, plans, affordable, scalable, essentials, pro
* Post Types: page
*/
?>
<!-- wp:oribi/page-hero-animated {"label":"Pricing","title":"Affordable Solutions, Scalable Options","description":"Simple, transparent pricing designed to grow with your business. Every plan includes our core platform features — no hidden fees, no surprises."} /-->
<!-- wp:oribi/feature-section {"variant":"normal","label":"All Plans Include","heading":"Core Features on Every Plan","lead":"No matter which plan you choose, you get the full power of our digital signage platform.","columns":4} -->
<!-- wp:oribi/feature-card {"iconType":"fontawesome","faIcon":"fas fa-clock","title":"Content Scheduling","description":"Day-parting, time-based triggers, and automated content rotation included on every plan."} /-->
<!-- wp:oribi/feature-card {"iconType":"fontawesome","faIcon":"fas fa-chart-line","title":"Live Data Integration","description":"Connect your web dashboards and real-time data sources directly to your digital signage."} /-->
<!-- wp:oribi/feature-card {"iconType":"fontawesome","faIcon":"fas fa-users","title":"Unlimited User Seats","description":"Add your entire team — no per-user charges or access limitations."} /-->
<!-- wp:oribi/feature-card {"iconType":"fontawesome","faIcon":"fas fa-rocket","title":"Publish in Minutes","description":"Upload content and push it to your screens instantly across your entire network."} /-->
<!-- /wp:oribi/feature-section -->
<!-- wp:oribi/pricing-section {"variant":"alt","label":"Choose Your Plan","heading":"Plans That Fit Your Business","lead":"Start small and scale as you grow. All plans come with our full-featured cloud CMS."} -->
<!-- wp:oribi/pricing-card {"name":"Essentials","tagline":"$7/Screen Monthly · $70/Screen Annually","features":["Up to 20 screens","Custom subdomain","Shared CMS server","Content scheduling & day-parting","Integration with live data sources","Unlimited user seats","Publish content in minutes","Offline playback support","Email support"],"btnText":"Get Started","btnUrl":"/contact"} /-->
<!-- wp:oribi/pricing-card {"name":"Pro","tagline":"Contact us for more information","features":["500+ screens","Custom domain","Dedicated CMS server","Custom live data integrations","Priority support","Advanced analytics","SSO & role-based access","SLA guarantee","Dedicated account manager"],"btnText":"Contact Sales","btnUrl":"/contact","featured":true,"badge":"Enterprise"} /-->
<!-- /wp:oribi/pricing-section -->
<!-- wp:oribi/intro-section {"variant":"normal","label":"Try Before You Buy","heading":"Want to See How Our Platform Works?","description":"Request access to our demo instance and explore the full platform — no commitment required. See how easy it is to create, schedule, and publish digital signage content.","visual":"🖥️"} /-->
<!-- wp:oribi/cta-banner {"heading":"Ready to Get Started?","text":"Contact us to find the right plan for your business, or request a demo to see the platform in action.","btnText":"Get in Touch","btnUrl":"/contact"} /-->

30
pages/resources.php Normal file
View File

@@ -0,0 +1,30 @@
<?php
/**
* Title: Resources
* Slug: ots-signs/page-resources
* Categories: oribi-pages
* Keywords: resources, documentation, guides, knowledge base, support
* Post Types: page
*/
?>
<!-- wp:oribi/page-hero-animated {"label":"Resources","title":"Knowledge \u0026 Support Resources","description":"Guides, documentation, and tools to help you get the most out of your digital signage platform."} /-->
<!-- wp:oribi/link-section {"variant":"normal","label":"Getting Started","heading":"Essential Resources","lead":"Everything you need to set up, manage, and optimise your digital signage network.","columns":3} -->
<!-- wp:oribi/link-card {"iconType":"fontawesome","faIcon":"fas fa-book","title":"Getting Started Guide","description":"Step-by-step instructions to set up your first screen, configure your CMS, and publish your first content.","linkText":"Read the Guide","linkUrl":"/contact"} /-->
<!-- wp:oribi/link-card {"iconType":"fontawesome","faIcon":"fas fa-code","title":"API Documentation","description":"Developer resources for integrating your systems with our digital signage platform via REST API.","linkText":"View API Docs","linkUrl":"/contact"} /-->
<!-- wp:oribi/link-card {"iconType":"fontawesome","faIcon":"fas fa-plug","title":"Integration Guides","description":"How to connect live data sources, social media feeds, POS systems, and web dashboards to your displays.","linkText":"See Integrations","linkUrl":"/contact"} /-->
<!-- /wp:oribi/link-section -->
<!-- wp:oribi/link-section {"variant":"alt","label":"Support","heading":"Help \u0026 Support","lead":"Get assistance with setup, troubleshooting, or any questions about the platform.","columns":3} -->
<!-- wp:oribi/link-card {"iconType":"fontawesome","faIcon":"fas fa-headset","title":"Support Portal","description":"Submit support tickets, track issue resolution, and access our knowledge base for common troubleshooting steps.","linkText":"Open Support Portal","linkUrl":"/contact"} /-->
<!-- wp:oribi/link-card {"iconType":"fontawesome","faIcon":"fas fa-video","title":"Video Tutorials","description":"Watch step-by-step video guides on content creation, scheduling, device management, and platform features.","linkText":"Watch Tutorials","linkUrl":"/contact"} /-->
<!-- wp:oribi/link-card {"iconType":"fontawesome","faIcon":"fas fa-question-circle","title":"FAQ","description":"Find answers to the most common questions about our platform, pricing, setup, and support options.","linkText":"Browse FAQ","linkUrl":"/faq"} /-->
<!-- /wp:oribi/link-section -->
<!-- wp:oribi/feature-section {"variant":"normal","label":"Content Tools","heading":"Create Better Content","lead":"Tools and templates to help you create engaging digital signage content.","columns":3} -->
<!-- wp:oribi/feature-card {"iconType":"fontawesome","faIcon":"fas fa-palette","title":"Template Library","description":"Browse our collection of professionally designed templates for menus, promotions, wayfinding, and informational displays.","url":"/contact"} /-->
<!-- wp:oribi/feature-card {"iconType":"fontawesome","faIcon":"fas fa-image","title":"Media Best Practices","description":"Guidelines for image resolution, video formats, file sizes, and aspect ratios optimised for digital signage.","url":"/contact"} /-->
<!-- wp:oribi/feature-card {"iconType":"fontawesome","faIcon":"fas fa-lightbulb","title":"Content Strategy Tips","description":"Learn how to structure your content rotation, day-parting schedules, and messaging for maximum audience impact.","url":"/contact"} /-->
<!-- /wp:oribi/feature-section -->
<!-- wp:oribi/cta-banner {"heading":"Need Personalised Help?","text":"Our team is available to walk you through any aspect of the platform or help with content creation.","btnText":"Contact Support","btnUrl":"/contact"} /-->

28
pages/solutions.php Normal file
View File

@@ -0,0 +1,28 @@
<?php
/**
* Title: Industry Solutions
* Slug: ots-signs/page-solutions
* Categories: oribi-pages
* Keywords: solutions, industries, hospitality, retail, corporate, education, marketplace
* Post Types: page
*/
?>
<!-- wp:oribi/page-hero-animated {"label":"Industry Solutions","title":"Solutions for Every Industry","description":"From hospitality to education, our digital signage platform adapts to the unique needs of your industry. Discover how we can transform your customer experience."} /-->
<!-- wp:oribi/platform-section {"label":"Industries We Serve","heading":"Tailored Solutions for Your Sector","lead":"Every industry has unique communication challenges. Our platform is built to address them all."} -->
<!-- wp:oribi/platform-row {"heading":"Hospitality","description":"Showcase menus, promotions, and special events while guiding guests through lobbies, restaurants, and bars. Digital signage creates a premium guest experience, drives upsells, and keeps your visitors informed — from check-in to checkout. Integrate with your POS for real-time menu updates and pricing changes.","btnText":"Get Started","btnUrl":"/contact"} /-->
<!-- wp:oribi/platform-row {"heading":"Retail","description":"Drive product upsells, announce flash-sales, and offer in-store navigation with fresh, eye-catching displays. Digital signage in retail has been shown to increase sales by up to 30%. Our platform makes it easy to update promotions across all locations instantly with targeted, data-driven content.","btnText":"See Pricing","btnUrl":"/pricing","reversed":true} /-->
<!-- wp:oribi/platform-row {"heading":"Corporate Office","description":"Enhance your meeting experience with Microsoft Teams room integration. Communicate meeting schedules, company news, KPIs, and employee alerts with interactive dashboards. Transform your lobby and common areas into dynamic communication hubs that keep your workforce informed and engaged.","btnText":"Learn More","btnUrl":"/features"} /-->
<!-- wp:oribi/platform-row {"heading":"Education","description":"Broadcast class schedules, announcements, campus wayfinding, and interactive learning content in campuses and auditoriums. Digital signage helps educational institutions communicate effectively with students, faculty, and visitors across multiple buildings and campuses.","btnText":"Contact Us","btnUrl":"/contact","reversed":true} /-->
<!-- wp:oribi/platform-row {"heading":"Outdoor Marketplace","description":"Whether it\u0027s a weekly farmers market or seasonal fairs, digital signage adds a modern, professional touch without losing the charm of the market experience. Weather-resistant display options and offline playback ensure your signage works reliably in any outdoor environment.","btnText":"Get a Quote","btnUrl":"/contact"} /-->
<!-- wp:oribi/platform-row {"heading":"Live Data Displays","description":"Connect your existing web dashboards and real-time data to our platform for customised digital signage displays that bring your key metrics and insights to life. Perfect for operations centres, trading floors, and management dashboards that need to be visible to entire teams.","btnText":"See Features","btnUrl":"/features","reversed":true} /-->
<!-- /wp:oribi/platform-section -->
<!-- wp:oribi/stat-section {"variant":"alt","label":"By the Numbers","heading":"Impact Across Industries","lead":"Digital signage delivers measurable results for businesses of all sizes.","columns":4} -->
<!-- wp:oribi/stat-card {"value":"400%","label":"Increase in Views","description":"Digital displays capture 400% more views than static signs."} /-->
<!-- wp:oribi/stat-card {"value":"30%","label":"Sales Uplift","description":"Retail digital signage drives up to 30% increase in sales."} /-->
<!-- wp:oribi/stat-card {"value":"80%","label":"Brand Awareness","description":"Customers recall digital signage content 80% of the time."} /-->
<!-- wp:oribi/stat-card {"value":"50%","label":"Wait Time Perception","description":"Perceived wait times drop by 50% with engaging displays."} /-->
<!-- /wp:oribi/stat-section -->
<!-- wp:oribi/cta-banner {"heading":"Not Sure Which Solution Fits?","text":"Tell us about your industry and use case, and we\u0027ll recommend the perfect setup.","btnText":"Talk to an Expert","btnUrl":"/contact"} /-->

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

View 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
View 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

File diff suppressed because it is too large Load Diff

1548
theme/blocks/index.php Normal file

File diff suppressed because it is too large Load Diff

29
theme/functions.php Normal file
View 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
View 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
View 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
View 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
View 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' );
} );

View 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 );
}

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

View 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 h1h6 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>
&nbsp;&nbsp;
<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
View 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
View File

@@ -0,0 +1 @@
<!-- wp:oribi/site-footer /-->

1
theme/parts/header.html Normal file
View File

@@ -0,0 +1 @@
<!-- wp:oribi/site-header /-->

65
theme/readme.txt Normal file
View 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
View 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
View 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"} /-->

View 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"} /-->

View 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"} /-->

View 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"} /-->

View 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"} /-->

View 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"} /-->

View 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
View 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" }
]
}