From 5c2458dfcd3efcf9d752f0920f0ee8ac9b2facf7 Mon Sep 17 00:00:00 2001 From: Anton Afanasyeu Date: Sat, 6 Jun 2026 12:09:34 +0200 Subject: [PATCH] landing sync, multipage --- examples/app_hub/DEPLOY.md | 2 + examples/app_hub/assets/hub-i18n.js | 104 ++++++ examples/app_hub/assets/hub-landing.js | 166 +++++++++ examples/app_hub/assets/i18n/hub-en.json | 44 +++ examples/app_hub/assets/i18n/hub-ru.json | 44 +++ examples/app_hub/hub-landing.css | 444 +++++++++++++++++++++++ examples/app_hub/index.php | 181 +++++---- examples/app_hub/landing-pages.inc.php | 130 +++++++ 8 files changed, 1022 insertions(+), 93 deletions(-) create mode 100644 examples/app_hub/assets/hub-i18n.js create mode 100644 examples/app_hub/assets/hub-landing.js create mode 100644 examples/app_hub/assets/i18n/hub-en.json create mode 100644 examples/app_hub/assets/i18n/hub-ru.json create mode 100644 examples/app_hub/hub-landing.css create mode 100644 examples/app_hub/landing-pages.inc.php diff --git a/examples/app_hub/DEPLOY.md b/examples/app_hub/DEPLOY.md index d13766b..4573576 100644 --- a/examples/app_hub/DEPLOY.md +++ b/examples/app_hub/DEPLOY.md @@ -20,6 +20,8 @@ On FE, hub and (if you fix it) crashes should both target **`http://artc0.intra. Landing uses **`index.php`** (shared footer from `examples/platform/footer.config.php`). Copy **`index.html`** only as a redirect fallback. +**Multipage landing (2026-06):** sync `hub-landing.css`, `landing-pages.inc.php`, `assets/hub-landing.js`, `assets/hub-i18n.js`, `assets/i18n/hub-en.json`, `assets/i18n/hub-ru.json` with `index.php`. + ```bash cp .../android_cast/examples/app_hub/index.php \ .../htdocs/apps/app/androidcast_project/index.php diff --git a/examples/app_hub/assets/hub-i18n.js b/examples/app_hub/assets/hub-i18n.js new file mode 100644 index 0000000..a58a821 --- /dev/null +++ b/examples/app_hub/assets/hub-i18n.js @@ -0,0 +1,104 @@ +(function (global) { + 'use strict'; + var STORAGE_KEY = 'crash_console_lang'; + var SUPPORTED = ['en', 'ru']; + var DEFAULT_LANG = 'en'; + var LANG_DISPLAY = { en: { flag: '\uD83C\uDDEC\uD83C\uDDE7', code: 'EN' }, ru: { flag: '\uD83C\uDDF7\uD83C\uDDFA', code: 'RU' } }; + var lang = DEFAULT_LANG; + var catalog = {}; + var fallback = {}; + + function hubAsset(rel) { + var link = document.querySelector('link[href*="hub.css"]'); + var base = link ? (link.getAttribute('href') || '').replace(/[^/]*$/, '') : ''; + return base + rel; + } + + function storedLang() { + try { + var v = localStorage.getItem(STORAGE_KEY); + return SUPPORTED.indexOf(v) >= 0 ? v : DEFAULT_LANG; + } catch (e) { + return DEFAULT_LANG; + } + } + + function t(key, params) { + var s = catalog[key] || fallback[key] || key; + if (params) { + Object.keys(params).forEach(function (k) { + s = s.split('{' + k + '}').join(String(params[k])); + }); + } + return s; + } + + function apply(root) { + var scope = root || document; + scope.querySelectorAll('[data-i18n]').forEach(function (el) { + var key = el.getAttribute('data-i18n'); + if (!key) return; + el.textContent = t(key); + }); + scope.querySelectorAll('[data-i18n-placeholder]').forEach(function (el) { + var key = el.getAttribute('data-i18n-placeholder'); + if (key) el.setAttribute('placeholder', t(key)); + }); + scope.querySelectorAll('[data-i18n-aria]').forEach(function (el) { + var key = el.getAttribute('data-i18n-aria'); + if (key) el.setAttribute('aria-label', t(key)); + }); + scope.querySelectorAll('[data-i18n-title]').forEach(function (el) { + var key = el.getAttribute('data-i18n-title'); + if (key) el.setAttribute('title', t(key)); + }); + var flag = document.getElementById('locale-flag'); + var code = document.getElementById('locale-code'); + var disp = LANG_DISPLAY[lang] || LANG_DISPLAY.en; + if (flag) flag.textContent = disp.flag; + if (code) code.textContent = disp.code; + document.documentElement.setAttribute('lang', lang); + var footerLeft = document.querySelector('.platform-footer__left[data-i18n]'); + if (footerLeft) { + var holder = footerLeft.getAttribute('data-holder') || 'Anton Afanaasyeu'; + var year = footerLeft.getAttribute('data-year') || String(new Date().getFullYear()); + footerLeft.textContent = t('footer.copyright', { holder: holder, year: year }); + } + } + + function load(code) { + lang = SUPPORTED.indexOf(code) >= 0 ? code : DEFAULT_LANG; + try { localStorage.setItem(STORAGE_KEY, lang); } catch (e) {} + return fetch(hubAsset('assets/i18n/hub-' + lang + '.json'), { credentials: 'same-origin' }) + .then(function (r) { return r.json(); }) + .then(function (json) { + catalog = json || {}; + if (lang !== DEFAULT_LANG) { + return fetch(hubAsset('assets/i18n/hub-en.json'), { credentials: 'same-origin' }) + .then(function (r) { return r.json(); }) + .then(function (fb) { fallback = fb || {}; }); + } + fallback = catalog; + }) + .then(function () { apply(document); }); + } + + function init() { + var sel = document.getElementById('lang-select'); + lang = storedLang(); + if (sel) sel.value = lang; + load(lang).then(function () { + if (sel) { + sel.addEventListener('change', function () { load(sel.value); }); + } + }); + } + + global.HubI18n = { t: t, apply: apply, load: load, init: init }; + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})(window); diff --git a/examples/app_hub/assets/hub-landing.js b/examples/app_hub/assets/hub-landing.js new file mode 100644 index 0000000..912b2a1 --- /dev/null +++ b/examples/app_hub/assets/hub-landing.js @@ -0,0 +1,166 @@ +(function () { + 'use strict'; + + var PAGE_COUNT = 10; + var scrollEl = null; + var railEl = null; + var currentPage = 1; + + function pageEl(n) { + return document.getElementById('landing-page-' + n); + } + + function setActivePage(n, opts) { + n = Math.max(1, Math.min(PAGE_COUNT, n | 0)); + currentPage = n; + var page = pageEl(n); + if (page && scrollEl) { + if (!opts || !opts.skipScroll) { + page.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + } + document.querySelectorAll('.landing-rail-dot').forEach(function (dot) { + var p = Number(dot.getAttribute('data-page')); + dot.classList.toggle('is-active', p === n); + dot.setAttribute('aria-current', p === n ? 'true' : 'false'); + }); + document.querySelectorAll('[data-goto-page]').forEach(function (el) { + var p = Number(el.getAttribute('data-goto-page')); + el.classList.toggle('is-active', p === n); + }); + if (window.history && window.history.replaceState) { + var url = new URL(window.location.href); + if (n <= 1) { + url.searchParams.delete('page'); + } else { + url.searchParams.set('page', String(n)); + } + window.history.replaceState({ page: n }, '', url.toString()); + } + } + + function pageFromScroll() { + if (!scrollEl) return 1; + var mid = scrollEl.scrollTop + scrollEl.clientHeight * 0.4; + var best = 1; + var bestDist = Infinity; + for (var i = 1; i <= PAGE_COUNT; i++) { + var el = pageEl(i); + if (!el) continue; + var dist = Math.abs(el.offsetTop - mid); + if (dist < bestDist) { + bestDist = dist; + best = i; + } + } + return best; + } + + function bindScrollSync() { + if (!scrollEl) return; + var ticking = false; + scrollEl.addEventListener('scroll', function () { + if (ticking) return; + ticking = true; + requestAnimationFrame(function () { + ticking = false; + var p = pageFromScroll(); + if (p !== currentPage) { + setActivePage(p, { skipScroll: true }); + } + }); + }, { passive: true }); + } + + function bindRail() { + railEl = document.getElementById('landing-rail'); + var handle = document.getElementById('landing-rail-handle'); + if (handle && railEl) { + handle.addEventListener('mouseenter', function () { + railEl.classList.add('is-visible'); + }); + handle.addEventListener('focus', function () { + railEl.classList.add('is-visible'); + }); + var dragY = 0; + handle.addEventListener('pointerdown', function (ev) { + dragY = ev.clientY; + handle.setPointerCapture(ev.pointerId); + railEl.classList.add('is-visible'); + }); + handle.addEventListener('pointermove', function (ev) { + if (!handle.hasPointerCapture(ev.pointerId)) return; + var dy = ev.clientY - dragY; + if (Math.abs(dy) > 40) { + setActivePage(currentPage + (dy > 0 ? 1 : -1)); + dragY = ev.clientY; + } + }); + } + document.querySelectorAll('.landing-rail-dot').forEach(function (dot) { + dot.addEventListener('click', function () { + setActivePage(Number(dot.getAttribute('data-page'))); + }); + }); + } + + function bindGotoLinks() { + document.body.addEventListener('click', function (ev) { + var t = ev.target.closest('[data-goto-page]'); + if (!t) return; + if (t.tagName === 'A' && t.getAttribute('href') === '#') { + ev.preventDefault(); + } + setActivePage(Number(t.getAttribute('data-goto-page'))); + }); + } + + function bindLeftNav() { + var pane = document.getElementById('landing-nav-pane'); + var handle = document.getElementById('landing-nav-handle'); + if (handle && pane) { + handle.addEventListener('click', function () { + pane.classList.toggle('open'); + }); + } + } + + function initFromUrl() { + var params = new URLSearchParams(window.location.search); + var p = parseInt(params.get('page') || '1', 10); + if (p >= 1 && p <= PAGE_COUNT) { + setTimeout(function () { + setActivePage(p); + }, 80); + } else { + setActivePage(1, { skipScroll: true }); + } + } + + function init() { + scrollEl = document.getElementById('landing-scroll'); + bindScrollSync(); + bindRail(); + bindGotoLinks(); + bindLeftNav(); + initFromUrl(); + window.addEventListener('keydown', function (ev) { + if (ev.target && /input|textarea|select/i.test(ev.target.tagName)) return; + if (ev.key === 'PageDown' || ev.key === 'ArrowDown') { + ev.preventDefault(); + setActivePage(currentPage + 1); + } else if (ev.key === 'PageUp' || ev.key === 'ArrowUp') { + ev.preventDefault(); + setActivePage(currentPage - 1); + } + }); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } + + window.LandingNav = { goTo: setActivePage }; +})(); diff --git a/examples/app_hub/assets/i18n/hub-en.json b/examples/app_hub/assets/i18n/hub-en.json new file mode 100644 index 0000000..895dddf --- /dev/null +++ b/examples/app_hub/assets/i18n/hub-en.json @@ -0,0 +1,44 @@ +{ + "nav.androidcast": "AndroidCast", + "nav.about": "About", + "nav.why": "Why", + "nav.tech": "Technologies", + "nav.resources": "Resources", + "nav.people": "People", + "nav.culture": "Culture", + "nav.pricing": "Pricing", + "nav.contact": "Contact us", + "nav.git": "Git", + "nav.tickets": "Tickets", + "nav.crashes": "Crashes", + "nav.graphs": "Graphs", + "nav.remote": "Remote access", + "nav.builder": "Builder", + "nav.toggle": "Navigation", + "lang.label": "Language", + "theme.label": "Theme", + "rail.label": "Page navigation", + "rail.handle": "Show page navigation", + "page1.title": "Android Cast", + "page1.lead": "Screen casting, crash intelligence, and CI — one ecosystem.", + "page1.cta": "Explore AndroidCast", + "page2.title": "AndroidCast", + "page2.lead": "Mirror Android screens with low latency, native codecs, and operator-grade tooling.", + "page3.title": "About", + "page3.lead": "Android Cast connects devices, backends, and operators across your stack.", + "page4.title": "Why AndroidCast", + "page4.lead": "Built for teams who need reliability, observability, and control — not another black box.", + "page5.title": "Technologies", + "page5.lead": "WebRTC, Opus/Speex, WireGuard remote access, PHP consoles, and Docker CI pipelines.", + "page6.title": "Resources", + "page6.lead": "Documentation, deployment guides, and integration patterns for your environment.", + "page7.title": "People", + "page7.lead": "Engineers and operators shipping cast, crash, and build workflows every day.", + "page8.title": "Culture", + "page8.lead": "Open tooling, green master branches, and honest postmortems.", + "page9.title": "Pricing", + "page9.lead": "Self-hosted core; enterprise options for SLA, SSO, and dedicated builders.", + "page10.title": "Contact us", + "page10.lead": "Reach the maintainer for partnerships, support, or deployment help.", + "footer.copyright": "© {holder}, {year}" +} diff --git a/examples/app_hub/assets/i18n/hub-ru.json b/examples/app_hub/assets/i18n/hub-ru.json new file mode 100644 index 0000000..209e1d5 --- /dev/null +++ b/examples/app_hub/assets/i18n/hub-ru.json @@ -0,0 +1,44 @@ +{ + "nav.androidcast": "AndroidCast", + "nav.about": "О проекте", + "nav.why": "Зачем", + "nav.tech": "Технологии", + "nav.resources": "Ресурсы", + "nav.people": "Люди", + "nav.culture": "Культура", + "nav.pricing": "Цены", + "nav.contact": "Контакты", + "nav.git": "Git", + "nav.tickets": "Тикеты", + "nav.crashes": "Крэши", + "nav.graphs": "Графики", + "nav.remote": "Удалённый доступ", + "nav.builder": "Сборщик", + "nav.toggle": "Навигация", + "lang.label": "Язык", + "theme.label": "Тема", + "rail.label": "Навигация по страницам", + "rail.handle": "Показать навигацию", + "page1.title": "Android Cast", + "page1.lead": "Трансляция экрана, аналитика сбоев и CI — одна экосистема.", + "page1.cta": "Узнать об AndroidCast", + "page2.title": "AndroidCast", + "page2.lead": "Зеркалирование Android с низкой задержкой, нативными кодеками и инструментами оператора.", + "page3.title": "О проекте", + "page3.lead": "Android Cast связывает устройства, бэкенды и операторов в едином стеке.", + "page4.title": "Зачем AndroidCast", + "page4.lead": "Для команд, которым нужны надёжность, наблюдаемость и контроль — не чёрный ящик.", + "page5.title": "Технологии", + "page5.lead": "WebRTC, Opus/Speex, WireGuard, PHP-консоли и Docker CI.", + "page6.title": "Ресурсы", + "page6.lead": "Документация, деплой и паттерны интеграции для вашей среды.", + "page7.title": "Люди", + "page7.lead": "Инженеры и операторы, которые каждый день развивают cast, crash и сборки.", + "page8.title": "Культура", + "page8.lead": "Открытые инструменты, зелёный master и честные postmortem.", + "page9.title": "Цены", + "page9.lead": "Self-hosted ядро; enterprise — SLA, SSO и выделенные builder-ы.", + "page10.title": "Контакты", + "page10.lead": "Связь с maintainer: партнёрство, поддержка, помощь с деплоем.", + "footer.copyright": "© {holder}, {year}" +} diff --git a/examples/app_hub/hub-landing.css b/examples/app_hub/hub-landing.css new file mode 100644 index 0000000..b84f121 --- /dev/null +++ b/examples/app_hub/hub-landing.css @@ -0,0 +1,444 @@ +/* Multipage landing shell — f0xx.org / lovable-style layout on hub tokens */ + +.hub-landing { + --landing-header-h: 56px; + --landing-footer-h: 40px; + --landing-left-w: 56px; + --landing-rail-w: 44px; + overflow: hidden; +} + +/* (1) Top header */ +.landing-top { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 20; + height: var(--landing-header-h); + display: flex; + align-items: center; + gap: 16px; + padding: 0 16px 0 12px; + background: rgba(15, 20, 25, 0.72); + backdrop-filter: blur(12px); + border-bottom: 1px solid rgba(45, 58, 79, 0.65); + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.25); +} +[data-theme="light"] .landing-top { + background: rgba(255, 255, 255, 0.82); + border-bottom-color: rgba(197, 208, 224, 0.9); +} + +.landing-top-logo { + display: flex; + align-items: center; + gap: 10px; + flex-shrink: 0; + text-decoration: none; + color: var(--text); +} +.landing-top-logo:hover { color: var(--text); text-decoration: none; } +.landing-top-logo img { + width: 36px; + height: 27px; +} +.landing-top-logo strong { + font-size: 1.05rem; + font-weight: 800; + letter-spacing: -0.02em; +} + +.landing-top-nav { + flex: 1; + display: flex; + justify-content: center; + align-items: center; + gap: 4px; + min-width: 0; +} + +.landing-menu-item { + position: relative; +} +.landing-menu-btn, +.landing-menu-link { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 14px; + border: none; + border-radius: 8px; + background: transparent; + color: var(--text); + font: inherit; + font-size: 0.92rem; + cursor: pointer; + text-decoration: none; + transition: background 0.15s ease, box-shadow 0.15s ease; +} +.landing-menu-link:hover, +.landing-menu-btn:hover, +.landing-menu-item:focus-within > .landing-menu-btn { + background: rgba(61, 139, 253, 0.14); + text-decoration: none; +} +.landing-menu-btn::after { + content: ''; + width: 0; + height: 0; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 5px solid currentColor; + opacity: 0.7; +} +.landing-menu-link::after { display: none; } + +.landing-dropdown { + position: absolute; + top: calc(100% + 4px); + left: 50%; + transform: translateX(-50%) translateY(6px); + min-width: 168px; + padding: 6px; + border-radius: 10px; + background: var(--surface); + border: 1px solid var(--border); + box-shadow: 0 12px 32px rgba(0, 0, 0, 0.35), 0 0 20px rgba(61, 139, 253, 0.12); + opacity: 0; + visibility: hidden; + pointer-events: none; + transition: opacity 0.16s ease, transform 0.16s ease, visibility 0.16s; + z-index: 30; +} +.landing-menu-item:hover .landing-dropdown, +.landing-menu-item:focus-within .landing-dropdown, +.landing-menu-item.is-open .landing-dropdown { + opacity: 1; + visibility: visible; + pointer-events: auto; + transform: translateX(-50%) translateY(0); +} +.landing-dropdown a { + display: block; + padding: 8px 12px; + border-radius: 6px; + color: var(--text); + text-decoration: none; + font-size: 0.9rem; +} +.landing-dropdown a:hover, +.landing-dropdown a.is-active { + background: rgba(61, 139, 253, 0.16); + color: var(--accent); +} + +.landing-top-tools { + display: flex; + align-items: center; + gap: 10px; + flex-shrink: 0; +} +.landing-top-tools .toolbar-select { + font-size: 13px; +} + +/* Frame: (2) left + (3) body + space for (4) rail */ +.landing-frame { + position: fixed; + top: var(--landing-header-h); + left: 0; + right: 0; + bottom: var(--landing-footer-h); + display: flex; + z-index: 2; + pointer-events: none; +} + +/* (2) Left shell — console nav-pane */ +.landing-left.nav-pane { + position: relative; + flex: 0 0 var(--landing-left-w); + width: var(--landing-left-w); + height: 100%; + z-index: 3; + pointer-events: auto; + border-right: 1px solid var(--border); +} +.landing-left.nav-pane.open { + width: 200px; + flex-basis: 200px; +} + +/* (3) Body — full viewport pages, scroll snap */ +.landing-scroll { + flex: 1; + min-width: 0; + height: 100%; + overflow-y: auto; + overflow-x: hidden; + scroll-snap-type: y mandatory; + scroll-behavior: smooth; + pointer-events: auto; + scrollbar-width: thin; + scrollbar-color: var(--border) transparent; +} + +.landing-page { + scroll-snap-align: start; + scroll-snap-stop: always; + min-height: 100%; + height: 100%; + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: center; + padding: 32px 48px 32px 24px; + position: relative; +} + +.landing-page-inner { + width: min(920px, 100%); + max-height: 100%; + overflow-y: auto; + padding: 8px 4px; +} + +.landing-page-inner.card.card--lift { + padding: 28px 32px; + background: rgba(26, 35, 50, 0.78); + backdrop-filter: blur(8px); + border: 1px solid rgba(45, 58, 79, 0.85); + box-shadow: 0 16px 48px rgba(0, 0, 0, 0.35), 0 0 32px rgba(61, 139, 253, 0.08); +} +[data-theme="light"] .landing-page-inner.card.card--lift { + background: rgba(255, 255, 255, 0.88); +} + +.landing-page h2 { + margin: 0 0 12px; + font-size: clamp(1.75rem, 4vw, 2.5rem); + font-weight: 800; + letter-spacing: -0.02em; +} +.landing-page .landing-lead { + margin: 0 0 20px; + font-size: 1.08rem; + line-height: 1.55; + color: var(--muted); + max-width: 52ch; +} +.landing-page .btn { margin-left: 0; } + +.landing-gallery { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + gap: 12px; + margin-top: 20px; +} +.landing-gallery img { + width: 100%; + aspect-ratio: 4 / 3; + object-fit: cover; + border-radius: 10px; + border: 1px solid var(--border); +} + +.landing-feature-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 14px; + margin-top: 20px; +} +.landing-feature-grid .card { + padding: 16px; + margin: 0; +} + +.landing-price-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 16px; + margin-top: 20px; +} +.landing-price-card { + padding: 20px; + border-radius: 12px; + border: 1px solid var(--border); + background: var(--surface2); +} +.landing-price-card h3 { margin: 0 0 8px; } +.landing-price-card .price { + font-size: 1.75rem; + font-weight: 800; + color: var(--accent); + margin-bottom: 12px; +} + +.landing-contact-form { + display: grid; + gap: 12px; + max-width: 420px; + margin-top: 16px; +} +.landing-contact-form label { + display: grid; + gap: 4px; + font-size: 0.9rem; +} +.landing-contact-form input, +.landing-contact-form textarea { + padding: 10px 12px; + border-radius: 8px; + border: 1px solid var(--border); + background: var(--surface2); + color: var(--text); + font: inherit; +} + +/* Page 1 hero — lighter card, more globe visible */ +.landing-page--hero .landing-page-inner { + background: transparent; + border: none; + box-shadow: none; + backdrop-filter: none; + text-align: center; + max-width: 640px; +} +.landing-page--hero h2 { + font-size: clamp(2rem, 5vw, 3rem); +} + +/* (4) Right page rail — hidden until handle hover/drag */ +.landing-rail { + position: fixed; + top: var(--landing-header-h); + right: 0; + bottom: var(--landing-footer-h); + width: var(--landing-rail-w); + z-index: 15; + pointer-events: none; + transform: translateX(calc(var(--landing-rail-w) - 6px)); + transition: transform 0.22s ease; +} +.landing-rail.is-visible, +.landing-rail:hover, +.landing-rail:focus-within { + transform: translateX(0); + pointer-events: auto; +} + +.landing-rail-handle { + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + width: 14px; + height: 72px; + padding: 0; + border: 1px solid var(--border); + border-right: none; + border-radius: 10px 0 0 10px; + background: rgba(26, 35, 50, 0.92); + cursor: grab; + pointer-events: auto; + box-shadow: -4px 0 16px rgba(0, 0, 0, 0.25); +} +.landing-rail-handle:active { cursor: grabbing; } +.landing-rail-handle::before { + content: ''; + display: block; + width: 4px; + height: 28px; + margin: 0 auto; + border-radius: 2px; + background: repeating-linear-gradient( + to bottom, + var(--muted) 0, + var(--muted) 4px, + transparent 4px, + transparent 7px + ); +} + +.landing-rail-dots { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + display: flex; + flex-direction: column; + gap: 10px; + padding: 12px 6px; + border-radius: 12px; + background: rgba(15, 20, 25, 0.82); + border: 1px solid var(--border); + backdrop-filter: blur(8px); +} +.landing-rail-dot { + width: 10px; + height: 10px; + padding: 0; + border: 2px solid var(--muted); + border-radius: 50%; + background: transparent; + cursor: pointer; + transition: border-color 0.15s, background 0.15s, transform 0.15s; +} +.landing-rail-dot:hover { + border-color: var(--accent); + transform: scale(1.15); +} +.landing-rail-dot.is-active { + background: var(--accent); + border-color: var(--accent); + box-shadow: 0 0 10px rgba(61, 139, 253, 0.55); +} + +/* (5) Footer stays platform-footer; lift above globe */ +.hub-landing .platform-footer { + position: fixed; + bottom: 0; + left: 0; + right: 0; + z-index: 12; +} + +/* Clock widget below header */ +.hub-landing .hub-clock-widget { + top: calc(var(--landing-header-h) + 12px); + z-index: 18; +} + +@media (max-width: 900px) { + .landing-top-nav { display: none; } + .landing-page { padding: 24px 16px 24px 12px; } + .landing-rail { --landing-rail-w: 36px; } +} + +.landing-top-tools .locale-picker { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 8px; + border-radius: 8px; + border: 1px solid var(--border); + background: rgba(26, 35, 50, 0.5); +} +.landing-top-tools .locale-picker__select { + border: none; + background: transparent; + color: var(--text); + font: inherit; + cursor: pointer; +} + +/* Globe stays fixed behind all landing chrome */ +.hub-landing #hub-logo-stage { + z-index: 0; +} + +@media (prefers-reduced-motion: reduce) { + .landing-scroll { scroll-behavior: auto; } + .landing-rail { transition: none; } +} diff --git a/examples/app_hub/index.php b/examples/app_hub/index.php index dd639e9..7c8f9a0 100644 --- a/examples/app_hub/index.php +++ b/examples/app_hub/index.php @@ -11,16 +11,19 @@ unset($__platformDir); - Android Cast — Project hub + Android Cast — Landing - - + + + + + - + @@ -69,100 +74,90 @@ unset($__platformDir); -
-
-
-
- -
-

Android Cast

- -
+
+ +
+
+
+ + +
+ Pricing + Contact us + +
+ +
+ + + +
+
+
+ +
+ + +
+
- - false]); ?> + + + + false, + 'extra_class' => 'bottom-bar', + ]); ?>