Compare commits

...

15 Commits

Author SHA1 Message Date
e5479aacff Sync: update home from WordPress 2026-02-21 09:49:08 -05:00
Matt Batchelder
d68c2c1d31 Add TV stick animation and rendering support for platform rows 2026-02-21 09:43:58 -05:00
Matt Batchelder
4591578cb2 Update SVG dimensions and adjust layout for dashboard charts 2026-02-21 02:29:38 -05:00
Matt Batchelder
954c418556 Add dashboard TV frame styles and update rendering structure for animated charts 2026-02-21 02:23:06 -05:00
Matt Batchelder
b37bcfb72b Remove framed background and styles from dashboard visuals for a cleaner appearance 2026-02-21 02:20:45 -05:00
Matt Batchelder
0ec0e71d38 Remove background, border, border-radius, and box-shadow from card styles for a cleaner design 2026-02-21 02:18:35 -05:00
Matt Batchelder
e1d9b1a402 Refactor dashboard animator for improved performance and readability 2026-02-21 02:13:52 -05:00
Matt Batchelder
a33a6d62d2 Refactor dashboard SVG charts and animations
- Removed the old SVG file for the dashboard chart and replaced it with a new implementation directly in the PHP file.
- Updated the SVG structure to improve accessibility and styling, including the use of CSS classes for dynamic theming.
- Enhanced the bar charts, line graph, and pie chart with new gradients and animations.
- Adjusted the enqueue script for the dashboard animator to include versioning based on file modification time.
2026-02-21 02:08:54 -05:00
Matt Batchelder
38d585e071 Enhance dashboard chart animations with improved element selection, faster update speed, and robust error handling 2026-02-21 01:55:20 -05:00
Matt Batchelder
f8321568ce Add dashboard chart animations and SVG integration for dynamic data visualization 2026-02-21 01:45:51 -05:00
Matt Batchelder
be30e4d59f Enhance device animator styles by enforcing background and border properties, adjusting overflow, and resetting border-radius 2026-02-21 01:42:47 -05:00
Matt Batchelder
3e211c376f Refactor platform visual styles by removing background and border for device animator 2026-02-21 01:38:06 -05:00
Matt Batchelder
618ba6ded4 Add device animation functionality to platform row and enhance CSS for visual effects 2026-02-21 01:33:52 -05:00
3100a93d9a Sync: update pricing from WordPress 2026-02-21 01:22:19 -05:00
Matt Batchelder
8ceb81008c Update FAQ and Features sections for clarity and detail; enhance Pricing page descriptions and add comparison table 2026-02-21 01:20:03 -05:00
9 changed files with 1104 additions and 24 deletions

View File

@@ -10,8 +10,8 @@
<!-- wp:oribi/page-hero-animated {"label":"FAQ","title":"Your Questions, Answered","description":"Everything you need to know about our platform, pricing, setup, and support — in plain language."} /-->
<!-- wp:oribi/faq-section {"label":"Platform \u0026 Pricing","heading":"Plans, Pricing \u0026 What\u0027s Included","lead":"Straightforward answers about what you get and what it costs."} -->
<!-- wp:oribi/faq-item {"question":"What\u0027s included in the Essentials plan?","answer":"Essentials gives you up to 20 screens, a custom subdomain, content scheduling with day-parting, live data integration, unlimited users, and the ability to publish to your screens in seconds. Pricing is $7 per screen per month, or $70 per screen if you pay annually."} /-->
<!-- wp:oribi/faq-item {"question":"What extra do I get with Pro?","answer":"Pro is built for larger deployments: 500+ screens, a dedicated CMS server, custom domain, bespoke data integrations, advanced analytics, SSO with role-based access, an SLA guarantee, and a named account manager. Contact us for a tailored quote."} /-->
<!-- wp:oribi/faq-item {"question":"What\u0027s included in the Essentials plan?","answer":"Essentials gives you up to 50 screens on a shared CMS instance with a custom subdomain. You get full content scheduling with day-parting, DataSets, RSS feeds, social widgets, embedded HTML, menu boards, interactive layouts, Canva integration, offline playback, Proof of Play analytics with 30-day retention, unlimited users with standard roles, and two-factor authentication. Pricing is $7 per screen per month, or $70 per screen if you pay annually."} /-->
<!-- wp:oribi/faq-item {"question":"What extra do I get with Pro?","answer":"Pro gives you unlimited screens on a dedicated CMS instance with a custom domain. On top of everything in Essentials, you get geo-location and weather-triggered scheduling, video wall support, ad campaigns with SSP monetisation, Dashboard Connector and custom API integrations, Audience Reporting with scheduled PDF reports, Proof of Play with 12+ month retention, SSO via SAML or CAS, custom user roles, extended audit trails, display map view, shell commands, in-house creative services, white-glove onboarding, priority support with a 4-hour SLA, a dedicated account manager, and a contractual SLA guarantee. Contact us for a tailored quote."} /-->
<!-- wp:oribi/faq-item {"question":"Are there any hidden fees?","answer":"None. Your per-screen fee covers the CMS, cloud hosting, software updates, and standard support. Content creation services and hardware are quoted separately and always upfront."} /-->
<!-- wp:oribi/faq-item {"question":"Can I upgrade later?","answer":"Yes — at any time. Moving from Essentials to Pro is seamless. We handle the migration behind the scenes with no disruption to your live displays."} /-->
<!-- /wp:oribi/faq-section -->
@@ -19,7 +19,7 @@
<!-- wp:oribi/faq-section {"variant":"alt","label":"Setup \u0026 Integration","heading":"Getting Up and Running","lead":"What to expect when you set up your first screen and connect your systems."} -->
<!-- wp:oribi/faq-item {"question":"How quickly can I be up and running?","answer":"Most installations go live within a day. Plug in the player device, connect it to your network, and your content appears on screen. We configure your CMS in advance and can have your first content loaded and ready before the hardware arrives."} /-->
<!-- wp:oribi/faq-item {"question":"Do I need to buy new screens?","answer":"No. Our players work with any display that has an HDMI port — consumer TVs, commercial panels, or monitors you already own. If you do need screens, we offer bundled player-and-display packages built for commercial use."} /-->
<!-- wp:oribi/faq-item {"question":"How do live data integrations work?","answer":"We connect your existing web dashboards, APIs, RSS feeds, and social accounts directly to your signage layouts. Data updates appear on screen in real time. Standard integrations are included on Essentials; Pro adds custom integration development for bespoke data sources."} /-->
<!-- wp:oribi/faq-item {"question":"How do live data integrations work?","answer":"Every plan includes DataSets, RSS feeds, social widgets, and embedded HTML — data updates appear on screen in real time. Pro adds the Dashboard Connector for secure third-party service connections, custom API integrations for bespoke data sources, and the SSP Connector for ad monetisation."} /-->
<!-- wp:oribi/faq-item {"question":"What kind of internet connection do I need?","answer":"A standard business broadband connection is more than enough. Our players sync content incrementally and cache everything locally, so bandwidth usage is minimal. For locations with unreliable connectivity, offline playback ensures your displays never go dark."} /-->
<!-- /wp:oribi/faq-section -->

View File

@@ -12,7 +12,7 @@
<!-- wp:oribi/platform-section {"label":"Core Features","heading":"Everything You Need, Nothing You Don\u0027t","lead":"Create, schedule, and manage digital signage content from a single dashboard — whether you have one screen or one thousand."} -->
<!-- wp:oribi/platform-row {"heading":"One Dashboard for Every Display","description":"Manage your entire signage network from a single cloud-based console. Organise screens by location, group, or purpose. Push content updates across your whole estate in one click — no matter how many sites you operate.","btnText":"Get Started","btnUrl":"/contact"} /-->
<!-- wp:oribi/platform-row {"heading":"Scheduling That Runs Itself","description":"Set content to appear at the right time, in the right place, automatically. Day-parting, date ranges, and event-triggered playback let you plan weeks ahead while the platform handles the execution.","btnText":"See Pricing","btnUrl":"/pricing","reversed":true} /-->
<!-- wp:oribi/platform-row {"heading":"Works With Your Existing Screens","description":"Our player devices connect to any screen with an HDMI port — no proprietary hardware required. Already have displays? Plug in and go. Need a full setup? We offer bundled player-and-display packages too.","btnText":"View Devices","btnUrl":"/devices"} /-->
<!-- wp:oribi/platform-row {"heading":"Works With Your Existing Screens","description":"Our player devices connect to any screen with an HDMI port — no proprietary hardware required. Already have displays? Plug in and go. Need a full setup? We offer bundled player-and-display packages too.","btnText":"View Devices","btnUrl":"/devices","tvStick":true} /-->
<!-- wp:oribi/platform-row {"heading":"Live Data, Straight to Screen","description":"Pull in web dashboards, social feeds, KPIs, and real-time APIs directly to your displays. Content updates automatically — no manual refreshing, no extra steps. Turn any screen into a live information hub.","btnText":"Learn More","btnUrl":"/solutions","reversed":true} /-->
<!-- /wp:oribi/platform-section -->
@@ -21,8 +21,8 @@
<!-- wp:oribi/feature-card {"iconType":"fontawesome","faIcon":"fas fa-clock","title":"Intelligent Scheduling","description":"Day-parting, date-based playlists, and event triggers let you automate content rotation down to the minute."} /-->
<!-- wp:oribi/feature-card {"iconType":"fontawesome","faIcon":"fas fa-wifi","title":"Offline Playback","description":"Content is cached on the player device. If your connection drops, your displays keep running seamlessly until it returns."} /-->
<!-- wp:oribi/feature-card {"iconType":"fontawesome","faIcon":"fas fa-users","title":"Unlimited Users","description":"Invite your entire team at no extra cost. No per-seat charges, no access restrictions — on any plan."} /-->
<!-- wp:oribi/feature-card {"iconType":"fontawesome","faIcon":"fas fa-chart-pie","title":"Playback Analytics","description":"Track what's playing, where, and when. Screen health monitoring and content logs give you full visibility across your network."} /-->
<!-- wp:oribi/feature-card {"iconType":"fontawesome","faIcon":"fas fa-shield-halved","title":"Enterprise Security","description":"End-to-end encryption, role-based access control, secure boot hardware, and SOC 2-aligned cloud infrastructure keep your content and network protected."} /-->
<!-- wp:oribi/feature-card {"iconType":"fontawesome","faIcon":"fas fa-chart-pie","title":"Playback Analytics","description":"Track what\u0027s playing, where, and when. Proof of Play reporting, screen health monitoring, and content logs give you full visibility — with retention depth that scales with your plan."} /-->
<!-- wp:oribi/feature-card {"iconType":"fontawesome","faIcon":"fas fa-shield-halved","title":"Enterprise Security","description":"End-to-end encryption, two-factor authentication, secure boot hardware, and predefined user roles on every plan. Pro adds SSO via SAML or CAS, custom role definitions, and extended audit trails."} /-->
<!-- /wp:oribi/feature-section -->
<!-- wp:oribi/value-section {"variant":"normal","label":"Why Choose Us","heading":"Beyond the Software","lead":"Great signage takes more than a CMS. Here\u0027s what you get when you work with OTS Signs.","columns":3} -->

View File

@@ -11,9 +11,9 @@ return <<<'ORIBI_SYNC_CONTENT'
<!-- wp:oribi/platform-section {"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."} -->
<!-- wp:oribi/platform-row {"heading":"Professional Content Creation","description":"Our in-house photography and video production services showcase your products, services, and environment with polished, engaging visuals. From digital menu boards to branded promotions, we create content that captures attention.","btnText":"See Features","btnUrl":"/features"} /-->
<!-- wp:oribi/platform-row {"heading":"Live Data \u0026amp; Web Dashboards","description":"Integrate your existing web dashboards, social feeds, and real-time data sources directly to your displays. Bring your most important information to life on screen, automatically and effortlessly.","btnUrl":"/features","reversed":true} /-->
<!-- wp:oribi/platform-row {"heading":"Live Data u0026amp; Web Dashboards","description":"Integrate your existing web dashboards, social feeds, and real-time data sources directly to your displays. Bring your most important information to life on screen, automatically and effortlessly.","btnUrl":"/features","reversed":true,"isDashboard":true} /-->
<!-- wp:oribi/platform-row {"heading":"Reliable on Any Screen","description":"Our intelligent player devices work on any screen with HDMI, and keep your message running even when the internet goes down. Enterprise-grade hardware designed for uninterrupted, always-on signage.","btnText":"View Devices","btnUrl":"/devices"} /-->
<!-- wp:oribi/platform-row {"heading":"Reliable on Any Screen","description":"Our intelligent player devices work on any screen with HDMI, and keep your message running even when the internet goes down. Enterprise-grade hardware designed for uninterrupted, always-on signage.","btnText":"View Devices","btnUrl":"/devices","deviceAnim":true} /-->
<!-- /wp:oribi/platform-section -->
<!-- wp:oribi/feature-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} -->

View File

@@ -1,26 +1,32 @@
<?php
/**
/*
* Title: Pricing
* Slug: ots-signs/page-pricing
* Categories: oribi-pages
* Keywords: pricing, plans, affordable, scalable, essentials, pro
* Post Types: page
* Slug: pricing
* Post Type: page
*/
?>
<!-- wp:oribi/page-hero-animated {"label":"Pricing","title":"Straightforward Pricing, No Surprises","description":"Every plan includes the full platform. Pick the tier that matches your scale, and start displaying. No hidden fees, no feature gates on the essentials."} /-->
<!-- wp:oribi/value-section {"variant":"normal","label":"Included on Every Plan","heading":"The Full Platform, From Day One","lead":"Whether you choose Essentials or Pro, you get the same powerful core. No artificial restrictions.","columns":4} -->
<!-- wp:oribi/value-card {"iconType":"fontawesome","faIcon":"fas fa-clock","title":"Automated Scheduling","description":"Day-parting, date ranges, and event-based triggers — your content plays at exactly the right time, automatically."} /-->
<!-- wp:oribi/value-card {"iconType":"fontawesome","faIcon":"fas fa-chart-line","title":"Live Data Feeds","description":"Display web dashboards, social feeds, and real-time metrics directly on your screens — updated automatically."} /-->
return <<<'ORIBI_SYNC_CONTENT'
<!-- wp:oribi/page-hero-animated {"label":"Pricing","title":"Straightforward Pricing, No Surprises","description":"Every plan includes the full content engine. Scale your infrastructure, integrations, and support as you grow. No hidden fees, no per-user charges."} /-->
<!-- wp:oribi/value-section {"label":"Included on Every Plan","heading":"The Full Content Engine, From Day One","lead":"Whether you choose Essentials or Pro, your team gets the same powerful tools to create, schedule, and publish.","columns":4} -->
<!-- wp:oribi/value-card {"iconType":"fontawesome","faIcon":"fas fa-clock","title":"Automated Scheduling","description":"Day-parting, date ranges, and recurring schedules — your content plays at exactly the right time, automatically."} /-->
<!-- wp:oribi/value-card {"iconType":"fontawesome","faIcon":"fas fa-rss","title":"Live Data to Screen","description":"Pull DataSets, RSS feeds, social widgets, and embedded HTML directly to your displays — updated in real time."} /-->
<!-- wp:oribi/value-card {"iconType":"fontawesome","faIcon":"fas fa-users","title":"Unlimited Team Access","description":"Invite everyone who needs access. No per-user fees, no seat limits, no gatekeeping."} /-->
<!-- wp:oribi/value-card {"iconType":"fontawesome","faIcon":"fas fa-rocket","title":"Instant Publishing","description":"Upload content and push it live across your network in seconds — not hours."} /-->
<!-- /wp:oribi/value-section -->
<!-- wp:oribi/pricing-section {"variant":"alt","label":"Choose Your Plan","heading":"Scale When You\u0027re Ready","lead":"Start with Essentials and upgrade seamlessly as your network grows. No disruption, no data loss."} -->
<!-- wp:oribi/pricing-card {"name":"Essentials","tagline":"Everything you need to get started","price":"$7","pricePer":"per screen / month · or $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":"For large-scale and enterprise deployments","price":"Custom","pricePer":"tailored to your network size","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 {"variant":"alt","label":"Choose Your Plan","heading":"Scale When Youu0027re Ready","lead":"Start with Essentials and upgrade seamlessly as your network grows. No disruption, no data loss."} -->
<!-- wp:oribi/pricing-card {"name":"Essentials","tagline":"The full content engine for growing networks","price":"$7","pricePer":"per screen / month · or $70/screen annually","features":["Up to 50 screens","Custom subdomain","Shared CMS instance","Content scheduling \u0026 day-parting","DataSets, RSS, social \u0026 embedded widgets","Menu boards \u0026 interactive layouts","Unlimited users with standard roles","Canva integration","Offline playback","Proof of Play analytics (30-day retention)","Email support (next-business-day)"]} /-->
<!-- wp:oribi/pricing-card {"name":"Pro","tagline":"Dedicated infrastructure, enterprise integrations \u0026 white-glove service","price":"Custom","pricePer":"tailored to your network size","features":["Unlimited screens","Custom domain","Dedicated CMS instance","Geo-location \u0026 weather-triggered scheduling","Dashboard Connector \u0026 custom API integrations","Video wall support","Ad campaigns \u0026 SSP monetisation","SSO (SAML/CAS) \u0026 custom user roles","Proof of Play analytics (12+ month retention)","Audience Reporting \u0026 scheduled PDF reports","Priority support (4-hour SLA) \u0026 account manager","Contractual SLA guarantee"],"btnText":"Contact Sales","featured":true,"badge":"Enterprise"} /-->
<!-- /wp:oribi/pricing-section -->
<!-- wp:oribi/intro-section {"variant":"normal","label":"Try Before You Commit","heading":"Want to Explore the Platform First?","description":"Request access to our live demo instance and take the full CMS for a spin — create content, set up schedules, and see exactly how it works. No credit card, no obligation.","visual":""} /-->
<!-- wp:oribi/comparison-table {"label":"Plan Comparison","heading":"See Exactly Whatu0027s Included","lead":"A full breakdown of what you get on each plan — so there are no surprises.","columns":["Essentials","Pro"],"rows":[{"group":"Scale \u0026 Infrastructure"},{"feature":"Screen limit","values":["Up to 50","Unlimited"]},{"feature":"CMS instance","values":["Shared","Dedicated"]},{"feature":"Custom subdomain","values":[true,true]},{"feature":"Custom domain","values":[false,true]},{"group":"Content \u0026 Scheduling"},{"feature":"Day-parting \u0026 date scheduling","values":[true,true]},{"feature":"Playlists \u0026 campaigns","values":[true,true]},{"feature":"Menu boards","values":[true,true]},{"feature":"Interactive touchscreen actions","values":[true,true]},{"feature":"Overlay layouts","values":[true,true]},{"feature":"Geo-location scheduling","values":[false,true]},{"feature":"Weather-triggered scheduling","values":[false,true]},{"feature":"Video wall","values":[false,true]},{"feature":"Ad campaigns \u0026 plays-per-hour control","values":[false,true]},{"group":"Data \u0026 Integrations"},{"feature":"DataSets, RSS \u0026 tickers","values":[true,true]},{"feature":"Embedded HTML \u0026 web pages","values":[true,true]},{"feature":"Social feeds","values":[true,true]},{"feature":"Canva integration","values":[true,true]},{"feature":"Dashboard Connector","values":[false,true]},{"feature":"Custom API integrations","values":[false,true]},{"feature":"SSP Connector (ad monetisation)","values":[false,true]},{"group":"Analytics \u0026 Reporting"},{"feature":"Proof of Play reporting","values":["30-day retention","12+ month retention"]},{"feature":"Scheduled PDF reports","values":[false,true]},{"feature":"Audience Reporting","values":[false,true]},{"feature":"Display health monitoring","values":[true,true]},{"group":"Users \u0026 Security"},{"feature":"Unlimited user seats","values":[true,true]},{"feature":"Predefined roles (admin/editor/viewer)","values":[true,true]},{"feature":"Custom user roles \u0026 feature access","values":[false,true]},{"feature":"Two-factor authentication","values":[true,true]},{"feature":"SSO (SAML / CAS)","values":[false,true]},{"feature":"Audit trail","values":["7-day retention","Extended retention"]},{"group":"Display Management"},{"feature":"Screen power on/off control","values":[true,true]},{"feature":"Offline playback","values":[true,true]},{"feature":"Portrait / landscape","values":[true,true]},{"feature":"Email alerts (player offline)","values":[true,true]},{"feature":"Periodic screenshots","values":[false,true]},{"feature":"Display map view","values":[false,true]},{"feature":"Shell commands \u0026 RS232","values":[false,true]},{"group":"Support \u0026 Services"},{"feature":"Email support","values":["Next-business-day",true]},{"feature":"Priority support","values":[false,"4-hour SLA"]},{"feature":"Dedicated account manager","values":[false,true]},{"feature":"In-house creative services","values":[false,"Included hours"]},{"feature":"White-glove onboarding","values":[false,true]}]} /-->
<!-- wp:oribi/intro-section {"label":"Try Before You Commit","heading":"Want to Explore the Platform First?","description":"Request access to our live demo instance and take the full CMS for a spin — create content, set up schedules, and see exactly how it works. No credit card, no obligation."} /-->
<!-- wp:oribi/cta-banner {"heading":"Questions About Pricing?","text":"We're happy to walk you through the plans, build a custom quote, or set up a demo so you can see the value firsthand.","btnText":"Get in Touch","btnUrl":"/contact"} /-->
ORIBI_SYNC_CONTENT;

View File

@@ -1984,6 +1984,648 @@ p:last-child { margin-bottom: 0; }
.platform-row.reverse .platform-visual { order: unset; }
}
/* Dashboard visual - remove framed background */
.platform-visual.has-dashboard {
background: none !important;
border: none !important;
border-radius: 0;
aspect-ratio: unset;
padding: 0;
overflow: visible;
box-shadow: none;
}
/* ── Dashboard TV frame ────────────────────────── */
.dashboard-tv {
display: flex;
flex-direction: column;
align-items: center;
}
.dashboard-tv__body {
width: 100%;
max-width: 520px;
background: var(--color-bg-alt);
border: 4px solid var(--color-bg-alt);
border-radius: 6px 6px 4px 4px;
outline: 1px solid var(--color-border);
padding: 3px;
position: relative;
box-shadow: 0 14px 48px rgba(0,0,0,0.55);
}
.dashboard-tv__body::after {
content: '\25B6';
position: absolute;
bottom: -13px;
left: 50%;
transform: translateX(-50%);
font-size: 8px;
color: rgba(74,222,128,0.7);
}
.dashboard-tv__screen {
width: 100%;
aspect-ratio: 16/9;
background: #111;
border-radius: 2px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.dashboard-tv__screen .dashboard-chart {
width: 100%;
height: 100%;
display: block;
}
.dashboard-tv__feet {
display: flex;
justify-content: space-between;
width: 60%;
max-width: 300px;
}
.dashboard-tv__foot {
width: 12px;
height: 8px;
background: var(--color-bg-alt);
border: 1px solid var(--color-border);
border-radius: 0 0 4px 4px;
}
/* ── 10b. Device Animator ───────────────────────────────────── */
.platform-visual.has-anim {
background: none !important;
border: none !important;
border-radius: 0;
aspect-ratio: unset;
padding: 0;
overflow: visible;
position: relative;
font-size: inherit;
}
.da-stage {
position: absolute;
inset: 0;
}
/* Each device panel hidden by default, centred in stage */
.da-device {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.88);
opacity: 0;
display: flex;
flex-direction: column;
align-items: center;
transition: opacity 0.55s cubic-bezier(0.4,0,0.2,1),
transform 0.55s cubic-bezier(0.4,0,0.2,1);
will-change: opacity, transform;
}
.da-device.is-active {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
.da-device.is-leaving {
opacity: 0;
transform: translate(-50%, -50%) scale(1.07);
}
/* Screen surface */
.da-screen {
width: 100%;
height: 100%;
border-radius: 2px;
position: relative;
overflow: hidden;
background:
repeating-linear-gradient(
180deg,
transparent,
transparent 3px,
rgba(0,0,0,0.10) 3px,
rgba(0,0,0,0.10) 4px
),
linear-gradient(135deg, #0c1016 0%, #151c28 60%, #0c1016 100%);
}
.da-screen::before {
content: '';
position: absolute;
top: -20%;
left: -10%;
width: 60%;
height: 60%;
background: radial-gradient(ellipse, rgba(74,222,128,0.12) 0%, transparent 70%);
pointer-events: none;
}
@keyframes da-scan {
0% { top: -6%; opacity: 0; }
5% { opacity: 1; }
95% { opacity: 1; }
100% { top: 106%; opacity: 0; }
}
.da-screen::after {
content: '';
position: absolute;
left: 0;
width: 100%;
height: 3px;
background: linear-gradient(90deg, transparent, rgba(74,222,128,0.28), transparent);
animation: da-scan 3s linear infinite;
pointer-events: none;
}
/* Device label */
.da-label {
display: block;
margin-top: 11px;
font-size: 0.68rem;
font-weight: 600;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--color-text-muted);
text-align: center;
}
/* ── Tablet ────────────────────────────────────── */
.da-tablet .da-body {
width: 128px;
height: 194px;
background: var(--color-bg-alt);
border: 2px solid var(--color-border);
border-radius: 14px;
padding: 10px 8px 14px;
display: flex;
align-items: stretch;
position: relative;
box-shadow: 0 16px 48px rgba(0,0,0,0.50);
}
.da-tablet .da-body::before {
content: '';
position: absolute;
top: 5px;
left: 50%;
transform: translateX(-50%);
width: 6px;
height: 6px;
background: var(--color-border);
border-radius: 50%;
}
.da-tablet .da-body::after {
content: '';
position: absolute;
bottom: 5px;
left: 50%;
transform: translateX(-50%);
width: 36px;
height: 3px;
background: var(--color-border);
border-radius: 2px;
}
/* ── Small Monitor ─────────────────────────────── */
.da-monitor-sm .da-body {
width: 236px;
height: 146px;
background: var(--color-bg-alt);
border: 5px solid var(--color-bg-alt);
border-radius: 6px;
outline: 1px solid var(--color-border);
padding: 3px;
display: flex;
align-items: stretch;
position: relative;
box-shadow: 0 10px 36px rgba(0,0,0,0.50);
}
.da-monitor-sm .da-body::after {
content: '';
position: absolute;
bottom: -9px;
right: 8px;
width: 5px;
height: 5px;
background: var(--color-primary);
border-radius: 50%;
box-shadow: 0 0 5px var(--color-primary);
}
.da-monitor-sm .da-stand,
.da-monitor-lg .da-stand { display: flex; flex-direction: column; align-items: center; }
.da-monitor-sm .da-stem {
width: 14px;
height: 20px;
background: var(--color-bg-alt);
border-left: 1px solid var(--color-border);
border-right: 1px solid var(--color-border);
}
.da-monitor-sm .da-base {
width: 68px;
height: 5px;
background: var(--color-bg-alt);
border: 1px solid var(--color-border);
border-radius: 3px;
}
/* ── Large Monitor ─────────────────────────────── */
.da-monitor-lg .da-body {
width: 298px;
height: 177px;
background: var(--color-bg-alt);
border: 4px solid var(--color-bg-alt);
border-radius: 6px;
outline: 1px solid var(--color-border);
padding: 3px;
display: flex;
align-items: stretch;
position: relative;
box-shadow: 0 12px 40px rgba(0,0,0,0.50);
}
.da-monitor-lg .da-stem {
width: 16px;
height: 26px;
background: var(--color-bg-alt);
border-left: 1px solid var(--color-border);
border-right: 1px solid var(--color-border);
}
.da-monitor-lg .da-base {
width: 88px;
height: 5px;
background: var(--color-bg-alt);
border: 1px solid var(--color-border);
border-radius: 3px;
}
/* ── TV ────────────────────────────────────────── */
.da-tv .da-body {
width: 320px;
height: 188px;
background: var(--color-bg-alt);
border: 4px solid var(--color-bg-alt);
border-radius: 6px 6px 4px 4px;
outline: 1px solid var(--color-border);
padding: 3px;
display: flex;
align-items: stretch;
position: relative;
box-shadow: 0 14px 48px rgba(0,0,0,0.55);
}
.da-tv .da-body::after {
content: '\25B6';
position: absolute;
bottom: -13px;
left: 50%;
transform: translateX(-50%);
font-size: 8px;
color: rgba(74,222,128,0.7);
}
.da-tv .da-feet { display: flex; justify-content: space-between; width: 180px; }
.da-tv .da-foot {
width: 12px;
height: 8px;
background: var(--color-bg-alt);
border: 1px solid var(--color-border);
border-radius: 0 0 4px 4px;
}
/* ── Projector ─────────────────────────────────── */
.da-projector .da-proj-layout { display: flex; flex-direction: column; align-items: center; }
.da-projector .da-proj-body {
width: 156px;
height: 62px;
background: var(--color-bg-alt);
border: 1px solid var(--color-border);
border-radius: 10px 10px 8px 8px;
display: flex;
align-items: center;
padding: 0 14px;
gap: 10px;
box-shadow: 0 6px 20px rgba(0,0,0,0.45);
position: relative;
}
.da-projector .da-proj-body::after {
content: '';
position: absolute;
top: 8px;
right: 10px;
width: 7px;
height: 7px;
background: var(--color-primary);
border-radius: 50%;
box-shadow: 0 0 6px var(--color-primary);
}
.da-projector .da-proj-body::before {
content: '';
position: absolute;
right: 10px;
bottom: 8px;
width: 28px;
height: 8px;
background: repeating-linear-gradient(
90deg,
var(--color-border) 0px,
var(--color-border) 2px,
transparent 2px,
transparent 5px
);
border-radius: 1px;
}
.da-projector .da-lens {
width: 38px;
height: 38px;
background: #080c12;
border: 2px solid var(--color-border);
border-radius: 50%;
flex-shrink: 0;
position: relative;
box-shadow: inset 0 0 8px rgba(0,0,0,0.8);
}
.da-projector .da-lens::after {
content: '';
position: absolute;
inset: 5px;
background: radial-gradient(circle at 35% 35%, rgba(74,222,128,0.30) 0%, #080c12 65%);
border-radius: 50%;
}
.da-projector .da-beam {
width: 240px;
height: 50px;
clip-path: polygon(31% 0%, 69% 0%, 100% 100%, 0% 100%);
background: linear-gradient(
180deg,
rgba(74,222,128,0.07) 0%,
rgba(74,222,128,0.02) 100%
);
}
.da-projector .da-proj-screen {
width: 240px;
height: 72px;
background: var(--color-bg-alt);
border: 1px solid var(--color-border);
border-radius: 3px;
overflow: hidden;
padding: 3px;
box-shadow: 0 4px 14px rgba(0,0,0,0.40);
}
/* ── Video Wall (2×2) ──────────────────────────── */
.da-vwall .da-vwall-grid {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
gap: 5px;
background: #0a0d12;
padding: 5px;
border: 1px solid var(--color-border);
border-radius: 4px;
box-shadow: 0 14px 48px rgba(0,0,0,0.60);
}
.da-vwall .da-panel {
width: 148px;
height: 90px;
background: var(--color-bg-alt);
border: 2px solid var(--color-bg-alt);
padding: 2px;
display: flex;
align-items: stretch;
overflow: hidden;
}
.da-vwall .da-panel:nth-child(2) .da-screen::after { animation-delay: -0.75s; }
.da-vwall .da-panel:nth-child(3) .da-screen::after { animation-delay: -1.5s; }
.da-vwall .da-panel:nth-child(4) .da-screen::after { animation-delay: -2.25s; }
/* ── Responsive scale-down ─────────────────────── */
@media (max-width: 900px) {
.da-device { transform: translate(-50%,-50%) scale(0.76); }
.da-device.is-active { transform: translate(-50%,-50%) scale(0.84); }
.da-device.is-leaving { transform: translate(-50%,-50%) scale(0.91); }
}
@media (max-width: 640px) {
.da-device { transform: translate(-50%,-50%) scale(0.56); }
.da-device.is-active { transform: translate(-50%,-50%) scale(0.64); }
.da-device.is-leaving { transform: translate(-50%,-50%) scale(0.70); }
}
@media (prefers-reduced-motion: reduce) {
.da-device { transition: none; }
.da-screen::after { animation: none; }
}
/* ── 10c. TV Stick Plug Animation ──────────────────────────── */
.platform-visual.has-tv-stick {
background: none !important;
border: none !important;
border-radius: 0;
aspect-ratio: unset;
padding: 0;
overflow: visible;
position: relative;
font-size: inherit;
}
.ts-stage {
position: relative;
width: 100%;
aspect-ratio: 4/3;
display: flex;
align-items: center;
justify-content: center;
}
/* ── TV ── */
.ts-tv {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
}
.ts-tv__body {
width: 320px;
height: 188px;
background: var(--color-bg-alt);
border: 4px solid var(--color-bg-alt);
border-radius: 6px 6px 4px 4px;
outline: 1px solid var(--color-border);
padding: 3px;
display: flex;
align-items: stretch;
position: relative;
box-shadow: 0 14px 48px rgba(0,0,0,0.55);
}
.ts-tv__screen {
width: 100%;
height: 100%;
border-radius: 2px;
position: relative;
overflow: hidden;
background:
repeating-linear-gradient(
180deg,
transparent,
transparent 3px,
rgba(0,0,0,0.10) 3px,
rgba(0,0,0,0.10) 4px
),
linear-gradient(135deg, #0c1016 0%, #151c28 60%, #0c1016 100%);
}
/* Subtle green ambient glow on screen */
.ts-tv__screen::before {
content: '';
position: absolute;
top: -20%;
left: -10%;
width: 60%;
height: 60%;
background: radial-gradient(ellipse, rgba(74,222,128,0.12) 0%, transparent 70%);
pointer-events: none;
}
/* Scan line on screen */
.ts-tv__screen::after {
content: '';
position: absolute;
left: 0;
width: 100%;
height: 3px;
background: linear-gradient(90deg, transparent, rgba(74,222,128,0.28), transparent);
animation: da-scan 3s linear infinite;
pointer-events: none;
}
/* Screen glow when stick plugs in */
.ts-stage.is-plugged .ts-tv__screen {
animation: ts-screen-glow 0.8s ease 0.1s both;
}
@keyframes ts-screen-glow {
0% { filter: brightness(1); }
40% { filter: brightness(1.25); }
100% { filter: brightness(1); }
}
/* HDMI port on back-right of TV */
.ts-tv__port {
position: absolute;
right: -6px;
top: 50%;
transform: translateY(-50%);
width: 6px;
height: 14px;
background: #1a1a1a;
border: 1px solid var(--color-border);
border-left: none;
border-radius: 0 2px 2px 0;
z-index: 1;
}
.ts-tv__port::before {
content: '';
position: absolute;
left: 0;
top: 2px;
width: 3px;
height: 8px;
background: #333;
border-radius: 0 1px 1px 0;
}
/* Feet */
.ts-tv__feet {
display: flex;
justify-content: space-between;
width: 180px;
}
.ts-tv__foot {
width: 12px;
height: 8px;
background: var(--color-bg-alt);
border: 1px solid var(--color-border);
border-radius: 0 0 4px 4px;
}
/* ── Stick device ── */
.ts-stick {
position: absolute;
right: -20px;
top: 50%;
transform: translateY(-50%) translateX(80px);
display: flex;
align-items: center;
opacity: 0;
z-index: 2;
}
.ts-stick__body {
width: 68px;
height: 26px;
background: linear-gradient(180deg, #f5f5f5, #e0e0e0);
border: 1px solid #ccc;
border-radius: 5px;
position: relative;
box-shadow:
0 2px 8px rgba(0,0,0,0.25),
inset 0 1px 0 rgba(255,255,255,0.6);
}
/* Brand logo area subtle inset */
.ts-stick__body::before {
content: '';
position: absolute;
top: 6px;
left: 10px;
width: 28px;
height: 12px;
background: rgba(0,0,0,0.04);
border-radius: 2px;
}
/* LED indicator */
.ts-stick__led {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 4px;
background: #999;
border-radius: 50%;
transition: background 0.4s ease, box-shadow 0.4s ease;
}
.ts-stage.is-plugged .ts-stick__led {
background: #4CAF50;
box-shadow: 0 0 6px rgba(76,175,80,0.8);
}
/* HDMI connector */
.ts-stick__connector {
width: 14px;
height: 10px;
background: linear-gradient(180deg, #888, #666);
border-radius: 0 2px 2px 0;
margin-left: -1px;
position: relative;
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
}
.ts-stick__connector::before {
content: '';
position: absolute;
left: 2px;
top: 2px;
width: 6px;
height: 6px;
background: #555;
border-radius: 1px;
}
/* ── Plug-in animation ── */
.ts-stage.is-animating .ts-stick {
animation: ts-slide-in 1.4s cubic-bezier(0.22, 0.61, 0.36, 1) forwards;
}
@keyframes ts-slide-in {
0% { opacity: 0; transform: translateY(-50%) translateX(80px); }
20% { opacity: 1; transform: translateY(-50%) translateX(60px); }
70% { transform: translateY(-50%) translateX(8px); }
85% { transform: translateY(-50%) translateX(12px); }
100% { opacity: 1; transform: translateY(-50%) translateX(6px); }
}
/* Responsive scale-down */
@media (max-width: 900px) {
.ts-tv__body { width: 260px; height: 152px; }
.ts-tv__feet { width: 140px; }
.ts-stick__body { width: 56px; height: 22px; }
}
@media (max-width: 640px) {
.ts-tv__body { width: 200px; height: 118px; }
.ts-tv__feet { width: 110px; }
.ts-stick__body { width: 46px; height: 18px; }
.ts-stick__connector { width: 10px; height: 8px; }
.ts-stick__body::before { top: 4px; left: 6px; width: 20px; height: 8px; }
.ts-stick__led { width: 3px; height: 3px; right: 5px; }
}
@media (prefers-reduced-motion: reduce) {
.ts-stage .ts-stick {
opacity: 1;
transform: translateY(-50%) translateX(6px);
}
.ts-stage.is-animating .ts-stick { animation: none; }
.ts-stage.is-plugged .ts-tv__screen { animation: none; }
.ts-stage.is-plugged .ts-stick__led {
background: #4CAF50;
box-shadow: 0 0 6px rgba(76,175,80,0.8);
}
}
/* ── 11. Page Hero (inner pages) ───────────────────────────── */
.page-hero {
background: #111111;
@@ -2255,6 +2897,24 @@ p:last-child { margin-bottom: 0; }
0% { opacity: 1; }
100% { opacity: 1; }
}
/* ── Dashboard Chart ───────────────────────────────────── */
.dashboard-chart {
width: 100%;
height: auto;
display: block;
user-select: none;
overflow: visible;
}
/* Reduced-motion: JS already checks the media query, belt-and-suspenders */
@media (prefers-reduced-motion: reduce) {
.dashboard-chart .bar,
.dashboard-chart .pie-segment { transition: none !important; }
}
@media (max-width: 768px) {
.dashboard-chart-container { padding: 1rem; border-radius: var(--radius-md, 12px); }
}
.cta-banner {
position: relative;
overflow: hidden;
@@ -2606,7 +3266,7 @@ p:last-child { margin-bottom: 0; }
border-color: rgba(255,255,255,.25);
}
[data-theme="dark"] .platform-visual:not(.has-img) {
[data-theme="dark"] .platform-visual:not(.has-img):not(.has-dashboard) {
background: var(--color-bg-alt);
border-color: var(--color-border);
}

View File

@@ -0,0 +1,157 @@
/**
* Dashboard Chart Animator
* Gently animates SVG bar charts, line graph, and pie chart.
* Pauses off-screen via IntersectionObserver for performance.
*/
(function () {
'use strict';
var SPEED = 0.002; // phase increment per frame
var BAR_H = 120; // max bar height (SVG units)
var BAR_MIN = 0.15; // min bar ratio
var LINE_PTS = 8;
var LINE_W = 340; // line graph width in SVG units
var PIE_R = 55; // pie chart radius
var DARK = { text: '#E0E0E0', muted: '#9E9E9E', border: '#333', center: '#222' };
var LIGHT = { text: '#333333', muted: '#666666', border: '#E0E0E0', center: '#fff' };
function isDark() { return document.documentElement.getAttribute('data-theme') === 'dark'; }
function pal() { return isDark() ? DARK : LIGHT; }
function wave(t, off) {
return Math.max(0, Math.min(1,
0.55 +
Math.sin(t + off) * 0.25 +
Math.sin(t * 1.8 + off * 1.3) * 0.15
));
}
function makeState(svg) {
return {
svg: svg,
bars1: svg.querySelectorAll('#bars-group-1 .bar'),
bars2: svg.querySelectorAll('#bars-group-2 .bar'),
vals1: svg.querySelectorAll('#values-group-1 text'),
vals2: svg.querySelectorAll('#values-group-2 text'),
linePath: svg.querySelector('#line-path'),
lineFill: svg.querySelector('#line-fill'),
pieSegs: svg.querySelectorAll('.pie-segment'),
phase: Math.random() * Math.PI * 2,
paused: false,
themeN: 0
};
}
function updateBars(bars, vals, st, pct) {
for (var i = 0; i < bars.length; i++) {
var v = Math.max(BAR_MIN, wave(st.phase, i * 1.1));
var h = v * BAR_H;
bars[i].setAttribute('height', h);
bars[i].setAttribute('y', BAR_H - h);
}
for (var j = 0; j < Math.min(bars.length, vals.length); j++) {
var val = Math.max(BAR_MIN, wave(st.phase, j * 1.1));
vals[j].textContent = pct ? Math.round(val * 100) + '%' : Math.round(val * 5000);
}
}
function updateLine(st) {
if (!st.linePath) return;
var d = 'M';
for (var i = 0; i < LINE_PTS; i++) {
var x = (i / (LINE_PTS - 1)) * LINE_W;
var y = 25 + (1 - wave(st.phase * 0.8, i * 0.9)) * 110;
d += (i ? ' L' : '') + x.toFixed(1) + ',' + y.toFixed(1);
}
st.linePath.setAttribute('d', d);
if (st.lineFill) st.lineFill.setAttribute('d', d + ' L' + LINE_W + ',145 L0,145 Z');
}
/* Pie: each segment gently shifts its slice size, no spinning */
function updatePie(st) {
if (!st.pieSegs.length) return;
var n = st.pieSegs.length;
// Generate proportional weights that shift over time
var weights = [], total = 0;
for (var i = 0; i < n; i++) {
var w = 0.5 + wave(st.phase * 0.4, i * 2.0) * 0.5;
weights.push(w);
total += w;
}
// Convert to cumulative angles
var angle = 0;
for (var j = 0; j < n; j++) {
var sweep = (weights[j] / total) * 360;
var startA = angle * Math.PI / 180;
var endA = (angle + sweep) * Math.PI / 180;
// Large-arc flag needed when sweep > 180
var large = sweep > 180 ? 1 : 0;
var r = PIE_R;
var x1 = Math.sin(startA) * r, y1 = -Math.cos(startA) * r;
var x2 = Math.sin(endA) * r, y2 = -Math.cos(endA) * r;
var path = st.pieSegs[j].querySelector('path');
if (path) {
path.setAttribute('d',
'M0,0 L' + x1.toFixed(2) + ',' + y1.toFixed(2) +
' A' + r + ',' + r + ' 0 ' + large + ',1 ' +
x2.toFixed(2) + ',' + y2.toFixed(2) + ' Z'
);
}
// Remove any rotation transform — segments are positioned by path geometry
st.pieSegs[j].removeAttribute('transform');
angle += sweep;
}
}
function applyTheme(st) {
var c = pal(), q = st.svg.querySelectorAll.bind(st.svg), all, k;
all = q('.ct'); for (k = 0; k < all.length; k++) all[k].setAttribute('fill', c.text);
all = q('.cl'); for (k = 0; k < all.length; k++) all[k].setAttribute('fill', c.muted);
all = q('.cv'); for (k = 0; k < all.length; k++) all[k].setAttribute('fill', c.text);
all = q('.grid-line'); for (k = 0; k < all.length; k++) all[k].setAttribute('stroke', c.border);
var cc = st.svg.querySelector('#pie-center');
if (cc) { cc.setAttribute('fill', c.center); cc.setAttribute('stroke', c.border); }
var pt = st.svg.querySelector('#pie-center-text');
if (pt) pt.setAttribute('fill', c.text);
}
function tick(st) {
if (!st.paused) {
st.phase += SPEED * 16;
updateBars(st.bars1, st.vals1, st, true);
updateBars(st.bars2, st.vals2, st, false);
updateLine(st);
updatePie(st);
if (++st.themeN > 30) { st.themeN = 0; applyTheme(st); }
}
requestAnimationFrame(function () { tick(st); });
}
function observe(st) {
if (!('IntersectionObserver' in window)) return;
new IntersectionObserver(function (entries) {
for (var i = 0; i < entries.length; i++) st.paused = !entries[i].isIntersecting;
}, { rootMargin: '200px', threshold: 0.05 }).observe(st.svg);
}
function boot() {
var svgs = document.querySelectorAll('.dashboard-chart');
if (!svgs.length) return;
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
for (var i = 0; i < svgs.length; i++) {
if (svgs[i]._dbAnim) continue;
var st = makeState(svgs[i]);
svgs[i]._dbAnim = st;
applyTheme(st);
tick(st);
observe(st);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', boot);
} else {
boot();
}
})();

View File

@@ -610,3 +610,99 @@ document.addEventListener('DOMContentLoaded', () => {
paintBg();
start();
})();
/* ── Device Animator ──────────────────────────────────────────────────────── */
(function () {
const DEVICES = [
'da-tablet', 'da-monitor-sm', 'da-monitor-lg',
'da-tv', 'da-projector', 'da-vwall'
];
const DWELL = 2500; // ms each device is shown
document.querySelectorAll('.da-stage').forEach(function (stage) {
let current = 0;
let timer = null;
// Collect the 6 device panels
const panels = DEVICES.map(function (cls) {
return stage.querySelector('.' + cls);
});
function show(idx) {
panels.forEach(function (el, i) {
if (!el) return;
el.classList.toggle('is-active', i === idx);
el.classList.remove('is-leaving');
});
}
function advance() {
const leaving = current;
current = (current + 1) % DEVICES.length;
if (panels[leaving]) panels[leaving].classList.add('is-leaving');
show(current);
setTimeout(function () {
if (panels[leaving]) panels[leaving].classList.remove('is-leaving');
}, 600);
}
function startCycle() {
if (timer) return;
timer = setInterval(advance, DWELL);
}
function stopCycle() {
clearInterval(timer);
timer = null;
}
// Honour reduced-motion preference: show first device statically
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
show(0);
return;
}
show(0);
startCycle();
// Pause when scrolled out of view to save resources
if ('IntersectionObserver' in window) {
new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
e.isIntersecting ? startCycle() : stopCycle();
});
}, { threshold: 0.2 }).observe(stage);
}
});
})();
/* ── TV Stick Plug Animation ─────────────────────────────────────────────── */
(function () {
var stages = document.querySelectorAll('[data-tv-stick-anim]');
if (!stages.length) return;
// Honour reduced-motion: show plugged-in state immediately
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
stages.forEach(function (stage) {
stage.classList.add('is-plugged');
});
return;
}
if ('IntersectionObserver' in window) {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (e.isIntersecting) {
var stage = e.target;
stage.classList.add('is-animating');
// Add plugged state after slide-in completes (1.4s)
setTimeout(function () {
stage.classList.add('is-plugged');
}, 1400);
io.unobserve(stage);
}
});
}, { threshold: 0.3 });
stages.forEach(function (stage) { io.observe(stage); });
}
})();

View File

@@ -553,6 +553,8 @@ add_action( 'init', function () {
'imgUrl' => [ 'type' => 'string', 'default' => '' ],
'imgAlt' => [ 'type' => 'string', 'default' => '' ],
'imgWidth' => [ 'type' => 'number', 'default' => 300 ],
'deviceAnim' => [ 'type' => 'boolean', 'default' => false ],
'tvStick' => [ 'type' => 'boolean', 'default' => false ],
],
'supports' => $block_supports,
'render_callback' => 'oribi_render_platform_row',
@@ -1338,7 +1340,128 @@ function oribi_render_platform_row( $a ) {
$img_alt = ! empty( $a['imgAlt'] ) ? $a['imgAlt'] : '';
$img_w = ! empty( $a['imgWidth'] ) ? intval( $a['imgWidth'] ) : 300;
if ( $img_url ) {
// Only render animated dashboard when explicitly flagged
$is_dashboard = ! empty( $a['isDashboard'] );
if ( $is_dashboard ) {
// Render animated dashboard chart SVG
// Text uses class hooks: .ct = title, .cl = label, .cv = value
// JS will dynamically set fill colours based on data-theme
$visual_html = '<div class="dashboard-tv" data-dashboard-container="true">
<div class="dashboard-tv__body">
<div class="dashboard-tv__screen">
<svg viewBox="0 0 800 450" xmlns="http://www.w3.org/2000/svg" class="dashboard-chart" role="img" aria-label="Animated dashboard charts">
<defs>
<linearGradient id="barGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#004225" stop-opacity="1"/>
<stop offset="100%" stop-color="#4CAF50" stop-opacity=".8"/>
</linearGradient>
<linearGradient id="lineGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#4CAF50" stop-opacity=".3"/>
<stop offset="100%" stop-color="#4CAF50" stop-opacity="0"/>
</linearGradient>
</defs>
<!-- ── Top-left: Performance bars ── -->
<g transform="translate(35,20)">
<text class="ct" x="0" y="0" font-size="14" font-weight="600" fill="#333">Performance</text>
<g id="bars-group-1" transform="translate(0,25)">
<rect class="bar" x="0" y="120" width="28" height="0" fill="url(#barGradient)"/>
<rect class="bar" x="40" y="120" width="28" height="0" fill="url(#barGradient)"/>
<rect class="bar" x="80" y="120" width="28" height="0" fill="url(#barGradient)"/>
<rect class="bar" x="120" y="120" width="28" height="0" fill="url(#barGradient)"/>
<rect class="bar" x="160" y="120" width="28" height="0" fill="url(#barGradient)"/>
</g>
<g transform="translate(0,152)">
<text class="cl" x="14" y="0" font-size="10" text-anchor="middle" fill="#666">API</text>
<text class="cl" x="54" y="0" font-size="10" text-anchor="middle" fill="#666">Cache</text>
<text class="cl" x="94" y="0" font-size="10" text-anchor="middle" fill="#666">DB</text>
<text class="cl" x="134" y="0" font-size="10" text-anchor="middle" fill="#666">Queue</text>
<text class="cl" x="174" y="0" font-size="10" text-anchor="middle" fill="#666">Worker</text>
</g>
<g id="values-group-1" transform="translate(0,168)">
<text class="cv" x="14" y="0" font-size="11" font-weight="600" text-anchor="middle" fill="#333">0%</text>
<text class="cv" x="54" y="0" font-size="11" font-weight="600" text-anchor="middle" fill="#333">0%</text>
<text class="cv" x="94" y="0" font-size="11" font-weight="600" text-anchor="middle" fill="#333">0%</text>
<text class="cv" x="134" y="0" font-size="11" font-weight="600" text-anchor="middle" fill="#333">0%</text>
<text class="cv" x="174" y="0" font-size="11" font-weight="600" text-anchor="middle" fill="#333">0%</text>
</g>
</g>
<!-- ── Top-right: Requests/sec bars ── -->
<g transform="translate(430,20)">
<text class="ct" x="0" y="0" font-size="14" font-weight="600" fill="#333">Requests/sec</text>
<g id="bars-group-2" transform="translate(0,25)">
<rect class="bar" x="0" y="120" width="28" height="0" fill="url(#barGradient)"/>
<rect class="bar" x="40" y="120" width="28" height="0" fill="url(#barGradient)"/>
<rect class="bar" x="80" y="120" width="28" height="0" fill="url(#barGradient)"/>
<rect class="bar" x="120" y="120" width="28" height="0" fill="url(#barGradient)"/>
</g>
<g transform="translate(0,152)">
<text class="cl" x="14" y="0" font-size="10" text-anchor="middle" fill="#666">Read</text>
<text class="cl" x="54" y="0" font-size="10" text-anchor="middle" fill="#666">Write</text>
<text class="cl" x="94" y="0" font-size="10" text-anchor="middle" fill="#666">Update</text>
<text class="cl" x="134" y="0" font-size="10" text-anchor="middle" fill="#666">Delete</text>
</g>
<g id="values-group-2" transform="translate(0,168)">
<text class="cv" x="14" y="0" font-size="11" font-weight="600" text-anchor="middle" fill="#333">0</text>
<text class="cv" x="54" y="0" font-size="11" font-weight="600" text-anchor="middle" fill="#333">0</text>
<text class="cv" x="94" y="0" font-size="11" font-weight="600" text-anchor="middle" fill="#333">0</text>
<text class="cv" x="134" y="0" font-size="11" font-weight="600" text-anchor="middle" fill="#333">0</text>
</g>
</g>
<!-- ── Bottom-left: Traffic Trend line ── -->
<g id="line-graph" transform="translate(35,245)">
<text class="ct" x="0" y="0" font-size="14" font-weight="600" fill="#333">Traffic Trend</text>
<g transform="translate(0,25)">
<line class="grid-line" x1="0" y1="0" x2="340" y2="0" stroke="#E0E0E0" stroke-width=".5"/>
<line class="grid-line" x1="0" y1="40" x2="340" y2="40" stroke="#E0E0E0" stroke-width=".5"/>
<line class="grid-line" x1="0" y1="80" x2="340" y2="80" stroke="#E0E0E0" stroke-width=".5"/>
<line class="grid-line" x1="0" y1="120" x2="340" y2="120" stroke="#E0E0E0" stroke-width=".5"/>
</g>
<g transform="translate(0,25)">
<path id="line-fill" d="M0,80 L340,80 L340,145 L0,145 Z" fill="url(#lineGradient)"/>
<path id="line-path" d="M0,80 L340,80" stroke="#4CAF50" stroke-width="2.5" fill="none" stroke-linecap="round"/>
</g>
</g>
<!-- ── Bottom-right: Distribution pie ── -->
<g transform="translate(490,245)">
<text class="ct" x="100" y="0" font-size="14" font-weight="600" text-anchor="middle" fill="#333">Distribution</text>
<g transform="translate(100,90)">
<g class="pie-segment" transform="rotate(0)">
<path d="M0,0 L0,-55 A55,55 0 0,1 38.89,-38.89 Z" fill="#004225" opacity=".9"/>
</g>
<g class="pie-segment" transform="rotate(90)">
<path d="M0,0 L0,-55 A55,55 0 0,1 38.89,-38.89 Z" fill="#4CAF50" opacity=".8"/>
</g>
<g class="pie-segment" transform="rotate(180)">
<path d="M0,0 L0,-55 A55,55 0 0,1 38.89,-38.89 Z" fill="#f59e0b" opacity=".7"/>
</g>
<g class="pie-segment" transform="rotate(270)">
<path d="M0,0 L0,-55 A55,55 0 0,1 38.89,-38.89 Z" fill="#ef4444" opacity=".7"/>
</g>
<circle id="pie-center" cx="0" cy="0" r="22" fill="#fff" stroke="#E0E0E0" stroke-width="1"/>
<text id="pie-center-text" x="0" y="5" text-anchor="middle" font-size="13" font-weight="600" fill="#333">100%</text>
</g>
<g transform="translate(30,170)">
<rect x="0" y="0" width="8" height="8" fill="#004225"/>
<text class="cl" x="12" y="8" font-size="11" fill="#666">Service A</text>
<rect x="0" y="15" width="8" height="8" fill="#4CAF50"/>
<text class="cl" x="12" y="23" font-size="11" fill="#666">Service B</text>
<rect x="100" y="0" width="8" height="8" fill="#f59e0b"/>
<text class="cl" x="112" y="8" font-size="11" fill="#666">Service C</text>
<rect x="100" y="15" width="8" height="8" fill="#ef4444"/>
<text class="cl" x="112" y="23" font-size="11" fill="#666">Service D</text>
</g>
</g>
</svg>
</div></div>
<div class="dashboard-tv__feet"><div class="dashboard-tv__foot"></div><div class="dashboard-tv__foot"></div></div>
</div>';
$visual_cls = 'platform-visual has-dashboard';
} elseif ( $img_url ) {
$img_style = 'width:' . $img_w . 'px;max-width:100%;height:auto;border-radius:var(--radius-sm);object-fit:contain;display:block;margin-inline:auto;';
if ( $img_id ) {
$visual_html = wp_get_attachment_image( $img_id, 'full', false, [ 'style' => $img_style, 'alt' => $img_alt ] );
@@ -1346,6 +1469,35 @@ function oribi_render_platform_row( $a ) {
$visual_html = '<img src="' . esc_url( $img_url ) . '" alt="' . esc_attr( $img_alt ) . '" style="' . esc_attr( $img_style ) . '">';
}
$visual_cls = 'platform-visual has-img';
} elseif ( ! empty( $a['deviceAnim'] ) ) {
$da = '<div class="da-stage" aria-hidden="true">';
$da .= '<div class="da-device da-tablet"><div class="da-body"><div class="da-screen"></div></div><span class="da-label">Tablet</span></div>';
$da .= '<div class="da-device da-monitor-sm"><div class="da-body"><div class="da-screen"></div></div><div class="da-stand"><div class="da-stem"></div><div class="da-base"></div></div><span class="da-label">Small Monitor</span></div>';
$da .= '<div class="da-device da-monitor-lg"><div class="da-body"><div class="da-screen"></div></div><div class="da-stand"><div class="da-stem"></div><div class="da-base"></div></div><span class="da-label">Large Monitor</span></div>';
$da .= '<div class="da-device da-tv"><div class="da-body"><div class="da-screen"></div></div><div class="da-feet"><div class="da-foot"></div><div class="da-foot"></div></div><span class="da-label">TV</span></div>';
$da .= '<div class="da-device da-projector"><div class="da-proj-layout"><div class="da-proj-body"><div class="da-lens"></div></div><div class="da-beam"></div><div class="da-proj-screen"><div class="da-screen"></div></div></div><span class="da-label">Projector</span></div>';
$da .= '<div class="da-device da-vwall"><div class="da-vwall-grid"><div class="da-panel"><div class="da-screen"></div></div><div class="da-panel"><div class="da-screen"></div></div><div class="da-panel"><div class="da-screen"></div></div><div class="da-panel"><div class="da-screen"></div></div></div><span class="da-label">Video Wall</span></div>';
$da .= '</div>';
$visual_html = $da;
$visual_cls = 'platform-visual has-anim';
} elseif ( ! empty( $a['tvStick'] ) ) {
$ts = '<div class="ts-stage" data-tv-stick-anim aria-hidden="true">';
$ts .= '<div class="ts-tv">';
$ts .= '<div class="ts-tv__body">';
$ts .= '<div class="ts-tv__screen"></div>';
$ts .= '<div class="ts-tv__port"></div>';
$ts .= '</div>';
$ts .= '<div class="ts-tv__feet"><div class="ts-tv__foot"></div><div class="ts-tv__foot"></div></div>';
$ts .= '</div>';
$ts .= '<div class="ts-stick">';
$ts .= '<div class="ts-stick__body">';
$ts .= '<div class="ts-stick__led"></div>';
$ts .= '</div>';
$ts .= '<div class="ts-stick__connector"></div>';
$ts .= '</div>';
$ts .= '</div>';
$visual_html = $ts;
$visual_cls = 'platform-visual has-tv-stick';
} else {
$visual_html = oribi_render_icon( $a['visual'] ?? '' );
$visual_cls = 'platform-visual';

View File

@@ -29,6 +29,15 @@ add_action( 'wp_enqueue_scripts', function () {
true
);
// Dashboard chart animator - smooth continuous animations for dashboard cards
wp_enqueue_script(
'oribi-dashboard-animator',
ORIBI_URI . '/assets/js/dashboard-animator.js',
[],
ORIBI_VERSION . '.' . filemtime( ORIBI_DIR . '/assets/js/dashboard-animator.js' ),
true
);
// Localize AJAX endpoint for the contact form
wp_localize_script( 'oribi-main', 'oribiAjax', [
'url' => admin_url( 'admin-ajax.php' ),