diff --git a/app/NativeComponents/Concerns/InteractsWithDiscovery.php b/app/NativeComponents/Concerns/InteractsWithDiscovery.php index d0131a4..9b1ff32 100644 --- a/app/NativeComponents/Concerns/InteractsWithDiscovery.php +++ b/app/NativeComponents/Concerns/InteractsWithDiscovery.php @@ -26,6 +26,22 @@ trait InteractsWithDiscovery /** Whether the discovered-servers bottom sheet is open on this screen. */ public bool $showServers = false; + /** + * Whether the full-screen "connecting…" cover is up. + * + * It exists for more than politeness: the remote app pushes its OWN theme + * (colors, typography) as it boots, which lands while Jump's UI is still on + * screen. Its font aliases name files that don't exist in Jump's bundle, so + * home visibly re-renders in fallback fonts for the second or two before + * the remote tree takes over. Covering that window hides the shift — and + * the cover itself is styled with fixed colors and no font tokens, so the + * incoming theme can't move it either. + */ + public bool $connecting = false; + + /** Advertised name of the server being connected to, for the cover's copy. */ + public string $connectingTo = ''; + /** * Start browsing the LAN. Call from each tab's `mount()` — NOT from a * service provider: the native browser reports already-running servers in @@ -53,6 +69,12 @@ public function startDiscovery(): void public function jumpSessionResumed(): void { $this->showServers = false; + // Covers both exits: a clean 3-finger swipe, and a connection that + // died or never produced a tree (the relay routes an abandoned + // reconnect through the same escape-hatch path). Either way home is + // about to repaint, so the cover must not survive into it. + $this->connecting = false; + $this->connectingTo = ''; app(DiscoveredServers::class)->flush(); Discovery::stop(); Discovery::start(); @@ -126,9 +148,22 @@ public function connect(string $host, string $port): void return; } + $this->raiseConnectingCover($host, $port); + Discovery::connect($host, $port); } + /** + * Raise the cover BEFORE the bridge call. Discovery::connect() returns + * immediately (the native side dials on the main queue), so this render + * lands first and the remote tree replaces it whenever it's ready. + */ + private function raiseConnectingCover(string $host, string $port): void + { + $this->connectingTo = app(DiscoveredServers::class)->nameFor($host, $port); + $this->connecting = true; + } + /** The coaching sheet's "don't show this again" checkbox. */ public function setDontShowExitHintAgain(bool $value): void { @@ -147,6 +182,8 @@ public function connectPending(): void $this->showExitHint = false; if ($this->pendingHost !== '') { + $this->raiseConnectingCover($this->pendingHost, $this->pendingPort); + Discovery::connect($this->pendingHost, $this->pendingPort); } } diff --git a/app/NativeComponents/Docs.php b/app/NativeComponents/Docs.php index 306d311..453d62f 100644 --- a/app/NativeComponents/Docs.php +++ b/app/NativeComponents/Docs.php @@ -8,6 +8,8 @@ use Illuminate\Support\Facades\Blade; use Illuminate\Support\Fluent; use Illuminate\View\View; +use Native\Mobile\Attributes\Lazy; +use Native\Mobile\Edge\Element; use Native\Mobile\Edge\Layouts\Builders\NavAction; use Native\Mobile\Edge\NativeComponent; use Native\Mobile\Edge\NativeElementCollector; @@ -17,7 +19,10 @@ /** * Docs tab — the NativePHP mobile docs, fetched from the public MCP navigation * API (full page content inline), cached 24h. Collapsible section TOC → page. + * #[Lazy] paints the skeleton while a cold/stale cache is fetched; a fresh + * cache paints the real TOC straight away (see publishPlaceholder()). */ +#[Lazy] class Docs extends NativeComponent { use InteractsWithDiscovery; @@ -41,8 +46,6 @@ class Docs extends NativeComponent public ?array $page = null; - public bool $loading = true; - public bool $failed = false; /** @@ -60,17 +63,38 @@ public function navTitle(): string return 'Docs'; } + protected function placeholder(): Element|View + { + return view('native.docs-skeleton'); + } + + /** + * Skip the skeleton when the corpus is already cached and fresh — mount() + * then paints the real TOC in the same frame, so a skeleton would just be + * a one-frame flash. Only a cold or stale cache pays the network-first + * fetch that the skeleton exists to cover. + */ + public function publishPlaceholder(): void + { + if (DocsIndex::fresh() !== null) { + return; + } + + parent::publishPlaceholder(); + } + public function mount(): void { $this->startDiscovery(); + // Fresh cache → paint from it (publishPlaceholder() skipped the + // skeleton on the same condition); otherwise fetch network-first. try { - $this->sections = DocsIndex::sections(); - $this->failed = empty($this->sections); + $this->sections = DocsIndex::fresh() ?? DocsIndex::sections(); } catch (\Throwable) { - $this->failed = true; + // Leave sections empty — the failed flag below covers it. } - $this->loading = false; + $this->failed = empty($this->sections); // Deep link (https://nativephp.com/docs/mobile/{v}/{section}/{page}) — // land directly on the linked page. API page ids mirror the website diff --git a/app/NativeComponents/Home.php b/app/NativeComponents/Home.php index d587797..9136fad 100644 --- a/app/NativeComponents/Home.php +++ b/app/NativeComponents/Home.php @@ -10,6 +10,7 @@ use Native\Mobile\Attributes\On; use Native\Mobile\Edge\NativeComponent; use Native\Mobile\Events\Scanner\CodeScanned; +use Native\Mobile\Events\System\AppearanceChanged; use Native\Mobile\Facades\Browser; use Native\Mobile\Facades\Scanner; @@ -101,36 +102,56 @@ public function openPartner(string $url): void /** * Vetted NativePHP consulting partners, mirrored from - * nativephp.com/consulting. `logo` is a remote raster URL where the partner - * publishes one (the native renderer needs http(s) raster, not the - * site's bundled SVG); null falls back to a lettered monogram tile. + * nativephp.com/consulting. Logos are transparent PNGs bundled under + * public/img/partners (rasterised from the site's SVGs — the native + * renderer can't draw SVG), with a `-dark` variant swapped in via + * isDark(). width/height preserve each PNG's aspect ratio at a display + * size balanced across the set. * - * @return list + * @return list */ protected function partners(): array { return [ [ 'name' => 'Nexcalia', - 'tagline' => 'Smart tools for scheduling & visitor management.', 'url' => 'https://www.nexcalia.com/?ref=nativephp', - 'logo' => null, + 'logo' => $this->partnerLogo('nexcalia'), + 'width' => 128, + 'height' => 40, ], [ 'name' => 'Web Mavens', - 'tagline' => 'Laravel Partners crafting secure, SOC 2-ready apps.', 'url' => 'https://www.webmavens.com/?ref=nativephp', - 'logo' => null, + 'logo' => $this->partnerLogo('webmavens'), + 'width' => 208, + 'height' => 28, ], [ 'name' => 'Synergi Tech', - 'tagline' => 'Bespoke software for complex infrastructure.', 'url' => 'https://synergitech.co.uk/partners/nativephp/', - 'logo' => 'https://synergitech.co.uk/logo.png', + 'logo' => $this->partnerLogo('synergi'), + 'width' => 104, + 'height' => 48, ], ]; } + protected function partnerLogo(string $slug): string + { + return public_path('img/partners/'.$slug.(isDark() ? '-dark' : '').'.png'); + } + + /** + * The partner logos are appearance-specific rasters picked server-side, + * so a light/dark flip must re-render to swap them. + */ + #[On(AppearanceChanged::class)] + public function appearanceChanged(string $mode): void + { + // Re-render only. + } + public function render(): View { return view('native.home', [ diff --git a/app/NativeComponents/Layouts/JumpTabsLayout.php b/app/NativeComponents/Layouts/JumpTabsLayout.php index 0f4444a..b72f17b 100644 --- a/app/NativeComponents/Layouts/JumpTabsLayout.php +++ b/app/NativeComponents/Layouts/JumpTabsLayout.php @@ -120,7 +120,14 @@ public function floatingOverlay(NativeComponent $screen): ?FloatingOverlay // must render (pill hidden) whenever the active screen has it open. $exitHintOpen = (bool) ($screen->showExitHint ?? false); - if ($store->isEmpty() && ! $exitHintOpen) { + // Same for the connecting cover, and it matters more: the server can + // drop out of the store mid-connect (its ServerLost fires while we're + // dialling), which would empty the store and take the cover down with + // the overlay — leaving Jump's UI exposed for exactly the window the + // cover exists to hide. + $connecting = (bool) ($screen->connecting ?? false); + + if ($store->isEmpty() && ! $exitHintOpen && ! $connecting) { return null; } diff --git a/app/NativeComponents/Videos.php b/app/NativeComponents/Videos.php index 8a5dc85..e2f55b3 100644 --- a/app/NativeComponents/Videos.php +++ b/app/NativeComponents/Videos.php @@ -8,13 +8,18 @@ use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Http; use Illuminate\View\View; +use Native\Mobile\Attributes\Lazy; +use Native\Mobile\Edge\Element; use Native\Mobile\Edge\NativeComponent; use Native\Mobile\Facades\Browser; /** * Videos tab — the NativePHP YouTube channel feed (RSS), fetched + parsed * server-side and cached 6h. Featured card + list, matching the Jump app. + * #[Lazy] paints the skeleton while a cold cache is fetched; a warm cache + * paints the real feed straight away (see publishPlaceholder()). */ +#[Lazy] class Videos extends NativeComponent { use InteractsWithDiscovery; @@ -22,13 +27,32 @@ class Videos extends NativeComponent private const FEED = 'https://www.youtube.com/feeds/videos.xml?channel_id=UCbkAE6vLlR6lOy_nxd--22g'; + private const CACHE_KEY = 'jump.videos'; + /** @var array> */ public array $videos = []; - public bool $loading = true; - public bool $failed = false; + protected function placeholder(): Element|View + { + return view('native.videos-skeleton'); + } + + /** + * Skip the skeleton when the feed is already cached — load() returns from + * cache within the frame, so a skeleton would just be a one-frame flash. + * Only a cold cache pays the fetch that the skeleton exists to cover. + */ + public function publishPlaceholder(): void + { + if (Cache::has(self::CACHE_KEY)) { + return; + } + + parent::publishPlaceholder(); + } + public function navTitle(): string { return 'Videos'; @@ -42,8 +66,7 @@ public function mount(): void public function reload(): void { - Cache::forget('jump.videos'); - $this->loading = true; + Cache::forget(self::CACHE_KEY); $this->failed = false; $this->load(); } @@ -51,12 +74,11 @@ public function reload(): void private function load(): void { try { - $this->videos = Cache::remember('jump.videos', now()->addHours(6), fn () => $this->fetch()); + $this->videos = Cache::remember(self::CACHE_KEY, now()->addHours(6), fn () => $this->fetch()); $this->failed = empty($this->videos); } catch (\Throwable) { $this->failed = true; } - $this->loading = false; } /** @return array> */ diff --git a/app/Support/DiscoveredServers.php b/app/Support/DiscoveredServers.php index c6a36bd..34d89ab 100644 --- a/app/Support/DiscoveredServers.php +++ b/app/Support/DiscoveredServers.php @@ -48,6 +48,15 @@ public function all(): array return array_values($this->servers); } + /** + * Advertised name for a connection, or '' when it isn't (or is no longer) + * in the store — a QR scan connects to a host:port that was never browsed. + */ + public function nameFor(string $host, string $port): string + { + return $this->servers[$host.':'.$port]['name'] ?? ''; + } + public function count(): int { return count($this->servers); diff --git a/app/Support/DocsIndex.php b/app/Support/DocsIndex.php index 502a5c8..6710b6c 100644 --- a/app/Support/DocsIndex.php +++ b/app/Support/DocsIndex.php @@ -15,6 +15,19 @@ */ class DocsIndex { + private const CACHE_KEY = 'jump.docs'; + + /** + * Marker key whose mere presence means the cached corpus was fetched + * recently enough to trust without going to the network. Separate from + * the corpus key so the corpus itself survives as an offline fallback + * long after it stops counting as fresh. + */ + private const FRESH_KEY = 'jump.docs.fresh'; + + /** Minutes a fetched corpus is served straight from cache. */ + private const FRESH_MINUTES = 30; + private const SECTION_NAMES = [ 'getting-started' => 'Getting Started', 'the-basics' => 'The Basics', @@ -41,7 +54,8 @@ public static function sections(): array try { $fresh = static::fetch(); if (! empty($fresh)) { - Cache::put('jump.docs', $fresh, now()->addHours(24)); + Cache::put(self::CACHE_KEY, $fresh, now()->addHours(24)); + Cache::put(self::FRESH_KEY, true, now()->addMinutes(self::FRESH_MINUTES)); return $fresh; } @@ -49,7 +63,30 @@ public static function sections(): array // fall through to the cached copy } - return Cache::get('jump.docs', []); + return Cache::get(self::CACHE_KEY, []); + } + + /** + * The cached corpus when it's recent enough to paint immediately, else + * null. Lets a screen skip both the network-first fetch above AND its + * loading skeleton — with a warm cache there's nothing to wait for, so + * the skeleton would only flash for a frame. + * + * The window is deliberately short: past it, sections() refetches (and + * the screen shows its skeleton) so a docs update lands without waiting + * out the 24h fallback TTL. Pull-to-refresh still forces a fetch anytime. + * + * @return ?array>}> + */ + public static function fresh(): ?array + { + if (! Cache::has(self::FRESH_KEY)) { + return null; + } + + $cached = Cache::get(self::CACHE_KEY, []); + + return empty($cached) ? null : $cached; } /** @@ -72,7 +109,7 @@ public static function search(string $query, int $limit = 15): array // outright when it's unreachable, e.g. a nativephp.test URL from the // Android emulator). Warm the cache once via sections() only if the // Docs tab hasn't already populated it this session. - $sections = Cache::get('jump.docs'); + $sections = Cache::get(self::CACHE_KEY); if (empty($sections)) { try { $sections = static::sections(); diff --git a/composer.lock b/composer.lock index 0703acd..b1a0dfc 100644 --- a/composer.lock +++ b/composer.lock @@ -2365,12 +2365,12 @@ "source": { "type": "git", "url": "https://github.com/NativePHP/mobile-air.git", - "reference": "c53cae83eaa0a73d45f351f4931dc86487869a27" + "reference": "1b6131924f8e127b141f95bf2b03501bfb0119a6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/NativePHP/mobile-air/zipball/c53cae83eaa0a73d45f351f4931dc86487869a27", - "reference": "c53cae83eaa0a73d45f351f4931dc86487869a27", + "url": "https://api.github.com/repos/NativePHP/mobile-air/zipball/1b6131924f8e127b141f95bf2b03501bfb0119a6", + "reference": "1b6131924f8e127b141f95bf2b03501bfb0119a6", "shasum": "" }, "require": { @@ -2452,7 +2452,7 @@ "issues": "https://github.com/NativePHP/mobile-air/issues", "source": "https://github.com/NativePHP/mobile-air/tree/main" }, - "time": "2026-07-23T03:13:17+00:00" + "time": "2026-07-25T02:04:36+00:00" }, { "name": "nativephp/mobile-biometrics", diff --git a/plugins/nativephp/discovery/resources/android/JumpBridgeRelay.kt b/plugins/nativephp/discovery/resources/android/JumpBridgeRelay.kt index e3e79eb..46ae6b9 100644 --- a/plugins/nativephp/discovery/resources/android/JumpBridgeRelay.kt +++ b/plugins/nativephp/discovery/resources/android/JumpBridgeRelay.kt @@ -143,15 +143,45 @@ object JumpBridgeRelay { Log.i(TAG, "Escape hatch — exiting remote app back to Jump") + // Drop any fonts the remote app pushed for the session, so its faces + // stop answering to tokens Jump also uses. `__jumpResume` re-pushes + // Jump's own theme right after. + // + // Resolved by NAME through the bridge registry rather than calling the + // plugin's resolver directly: this file is built against whatever + // native-ui version the host bundles, and older ones have no runtime + // font store at all. Absent function → nothing was ever pushed → + // nothing to clean up. + com.nativephp.mobile.bridge.BridgeFunctionRegistry.shared + .get("NativeUI.Fonts.Clear") + ?.let { runCatching { it.execute(emptyMap()) } } + // Stop forwarding first so any in-flight WebView request falls back // to the local runtime instead of a dead dev server. JumpWebViewSession.stop() if (elementLive) { - JumpElementRuntime.endSession() // clears NativeUIBridge tree + isActive + // Keep the remote app's final frame on screen so AnimatedContent has + // something to animate OUT when home's republish bumps screenKey. + // Clearing the tree here is what made the exit snap: the remote UI + // vanished a frame or more before home arrived. + JumpElementRuntime.endSession(keepingLastFrame = true) } disconnect() mainHandler.post { + if (elementLive) { + // Stage the swap BEFORE waking home, so the publish that + // repaints home is consumed as a navigation and bumps + // screenKey — driving AnimatedContent. `slide_from_left` sends + // the remote app out to the trailing edge while home enters + // from the leading one: the "back" idiom, matching the + // 3-finger swipe-RIGHT that got us here. + // + // Degrades quietly: a remote app whose root sentinel matches + // home's (tabs → tabs) counts as a native-chrome continuation, + // which skips the screenKey bump — today's instant repaint. + NativeUIBridge.setNavigationPending("slide_from_left") + } if (webviewLive && !elementLive) { // The served app's nav chrome (top bar / bottom nav / side // nav / FAB) arrived via its response headers into diff --git a/plugins/nativephp/discovery/resources/android/JumpElementRuntime.kt b/plugins/nativephp/discovery/resources/android/JumpElementRuntime.kt index acb01ca..18a54e8 100644 --- a/plugins/nativephp/discovery/resources/android/JumpElementRuntime.kt +++ b/plugins/nativephp/discovery/resources/android/JumpElementRuntime.kt @@ -80,24 +80,55 @@ object JumpElementRuntime { // Element lifecycle // ───────────────────────────────────────────────────────────────────── - /** Element.Init — register renderers (idempotent) and mark the session live. */ + /** + * Set once the renderer registrations have run for this process. + * + * They are NOT idempotent, despite reading that way. Element renderers go + * into a map keyed by type, so re-registering those is harmless — but + * plugin ROOT HOSTS (the floating overlay, the drawer) append to the root + * host registry, which folds every entry around the content. Registering + * on each Element.Init therefore stacked one extra copy of the overlay per + * session: two "servers nearby" pills after one Jump, three after two, + * each with its own copy of the bottom sheets inside (which is what made a + * dismissed sheet look like it reopened — there was a second one behind + * it). Nothing clears the registries, so once per process is enough. + */ + @Volatile + private var renderersRegistered = false + + /** Element.Init — register renderers once, and mark the session live. */ fun initialize() { Log.i(TAG, "Element.Init") mainHandler.post { - registerNativeChromeRenderers() - registerPluginRenderers() + if (!renderersRegistered) { + registerNativeChromeRenderers() + registerPluginRenderers() + renderersRegistered = true + } suppressed = false isActive = true } } - /** Tear down the current session's UI and stop routing events remotely. */ - fun endSession() { + /** + * Tear down the current session's UI and stop routing events remotely. + * + * [keepingLastFrame] stops the session WITHOUT clearing the bridge's tree, + * leaving the remote app's final frame on screen for `AnimatedContent` to + * animate OUT as Jump's home animates in (see `JumpBridgeRelay.exitToJump`). + * The frame is inert either way — `suppressed`/`isActive` are already false, + * so no further remote publishes land and taps stop routing remotely. + * Callers that keep the frame MUST get a local publish on screen afterwards + * (waking home does this), or the dead frame just sits there. Mirrors iOS. + */ + fun endSession(keepingLastFrame: Boolean = false) { mainHandler.post { suppressed = true isActive = false - NativeUIBridge.isActive.value = false - NativeUIBridge.currentTree.value = null + if (!keepingLastFrame) { + NativeUIBridge.isActive.value = false + NativeUIBridge.currentTree.value = null + } } reset() } diff --git a/plugins/nativephp/discovery/resources/ios/JumpBridgeRelay.swift b/plugins/nativephp/discovery/resources/ios/JumpBridgeRelay.swift index 1082f28..fb23ed4 100644 --- a/plugins/nativephp/discovery/resources/ios/JumpBridgeRelay.swift +++ b/plugins/nativephp/discovery/resources/ios/JumpBridgeRelay.swift @@ -59,10 +59,29 @@ class JumpBridgeRelay: NSObject, ObservableObject { logger.info("Escape hatch — exiting remote app back to Jump") + // Drop any fonts the remote app pushed for the session. Font + // registration is process-wide, so leaving them installed would let the + // remote app's faces keep answering to tokens Jump also uses. + // `__jumpResume` re-pushes Jump's own theme right after. + // + // Resolved by NAME through the bridge registry rather than calling the + // plugin's resolver directly: this file is built against whatever + // native-ui version the host bundles, and older ones have no runtime + // font store at all. Absent function → nothing was ever pushed → + // nothing to clean up. + if let clearFonts = BridgeFunctionRegistry.shared.get("NativeUI.Fonts.Clear") { + _ = try? clearFonts.execute(parameters: [:]) + } + // Stop forwarding / streaming and drop the WS. JumpWebViewSession.shared.stop() if elementLive { - JumpElementRuntime.shared.endSession() // clears currentTree + isActive + // Keep the remote app's final frame on screen. It's inert (no + // further remote publishes, taps no longer route remotely), but it + // gives home's republish something to animate in OVER. Clearing the + // tree here is what made the exit snap: the remote UI vanished a + // frame or more before home arrived. + JumpElementRuntime.shared.endSession(keepingLastFrame: true) } disconnect() @@ -72,11 +91,25 @@ class JumpBridgeRelay: NSObject, ObservableObject { // local publishes were only suppressed while forwarding), so // flipping isActive shows it immediately and interactive. NativeUIBridge.shared.isActive = true + } else { + // Stage the swap BEFORE waking home, so the publish that + // repaints home is consumed as a navigation: the held remote + // frame is reclassified as `outgoingScreen` and home gets a + // fresh screenKey whose insertion transition animates. + // `slide_from_left` brings home in from the leading edge — the + // iOS "back" idiom, matching the 3-finger swipe-RIGHT that got + // us here. + // + // Degrades quietly: a remote app whose root sentinel matches + // home's (tabs → tabs) counts as a native-chrome continuation, + // which skips the two-layer swap — you get today's instant + // repaint, not a broken frame. + NativeUIBridge.shared.setNavigationPending(transition: "slide_from_left") } // Wake the LOCAL Jump home runloop parked in wait_event. For a - // native-ui exit this is what repaints home (endSession cleared - // currentTree; the re-render's publish restores it — the - // "3-finger swipe → white screen" fix). For BOTH exit kinds the + // native-ui exit this is what repaints home (and, with the + // transition staged above, what drives the exit animation) — the + // "3-finger swipe → white screen" fix. For BOTH exit kinds the // __jumpResume listener also resyncs the server list and // re-pushes Jump's theme (the remote app's Theme.Set clobbered // the native theme store). Mirrors Android. diff --git a/plugins/nativephp/discovery/resources/ios/JumpElementRuntime.swift b/plugins/nativephp/discovery/resources/ios/JumpElementRuntime.swift index d11ebfe..7e0f165 100644 --- a/plugins/nativephp/discovery/resources/ios/JumpElementRuntime.swift +++ b/plugins/nativephp/discovery/resources/ios/JumpElementRuntime.swift @@ -47,22 +47,50 @@ final class JumpElementRuntime: ObservableObject, @unchecked Sendable { // MARK: - Element lifecycle + /// Set once `registerPluginRenderers()` has run for this process. + /// + /// It is NOT idempotent, despite reading that way. Element renderers go + /// into a dictionary keyed by type, so re-registering those is harmless — + /// but plugin ROOT HOSTS (the floating overlay, the drawer) append to + /// `NativeRootHostRegistry`, whose `wrap()` folds every entry around the + /// content. Registering on each Element.Init therefore stacked one extra + /// copy of the overlay per session: two "servers nearby" pills after one + /// Jump, three after two, each with its own copy of the bottom sheets + /// inside (which is what made a dismissed sheet look like it reopened — + /// there was a second one behind it). + /// + /// Nothing clears either registry, so once per process is enough. + private var pluginRenderersRegistered = false + func initialize() { logger.info("Element.Init") DispatchQueue.main.async { - registerPluginRenderers() // shell's renderer registration (idempotent, main-thread) + if !self.pluginRenderersRegistered { + registerPluginRenderers() // main-thread; see the flag's note + self.pluginRenderersRegistered = true + } self.suppressed = false self.isActive = true } } /// Tear down the current session's UI and stop routing events remotely. - func endSession() { + /// + /// `keepingLastFrame` stops the session WITHOUT clearing the shell's tree, + /// leaving the remote app's final frame on screen as something for Jump's + /// home to animate in over (see `JumpBridgeRelay.exitToJump`). The frame is + /// inert either way — `suppressed`/`isActive` are already false, so no + /// further remote publishes land and taps no longer route to the remote + /// queue. Callers that keep the frame MUST get a local publish on screen + /// afterwards (waking home does this), or the dead frame just sits there. + func endSession(keepingLastFrame: Bool = false) { DispatchQueue.main.async { self.suppressed = true self.isActive = false - NativeUIBridge.shared.isActive = false - NativeUIBridge.shared.currentTree = nil + if !keepingLastFrame { + NativeUIBridge.shared.isActive = false + NativeUIBridge.shared.currentTree = nil + } } reset() } diff --git a/public/img/partners/nexcalia-dark.png b/public/img/partners/nexcalia-dark.png new file mode 100644 index 0000000..fe60a53 Binary files /dev/null and b/public/img/partners/nexcalia-dark.png differ diff --git a/public/img/partners/nexcalia.png b/public/img/partners/nexcalia.png new file mode 100644 index 0000000..b2929e0 Binary files /dev/null and b/public/img/partners/nexcalia.png differ diff --git a/public/img/partners/nexcalia.svg b/public/img/partners/nexcalia.svg new file mode 100644 index 0000000..b19d420 --- /dev/null +++ b/public/img/partners/nexcalia.svg @@ -0,0 +1,164 @@ + \ No newline at end of file diff --git a/public/img/partners/synergi-dark.png b/public/img/partners/synergi-dark.png index c635a59..0065cea 100644 Binary files a/public/img/partners/synergi-dark.png and b/public/img/partners/synergi-dark.png differ diff --git a/public/img/partners/synergi.png b/public/img/partners/synergi.png index 1f2d5cf..265c01f 100644 Binary files a/public/img/partners/synergi.png and b/public/img/partners/synergi.png differ diff --git a/public/img/partners/webmavens-dark.png b/public/img/partners/webmavens-dark.png new file mode 100644 index 0000000..5760ef9 Binary files /dev/null and b/public/img/partners/webmavens-dark.png differ diff --git a/public/img/partners/webmavens.png b/public/img/partners/webmavens.png new file mode 100644 index 0000000..6f7b57e Binary files /dev/null and b/public/img/partners/webmavens.png differ diff --git a/public/img/partners/webmavens.svg b/public/img/partners/webmavens.svg new file mode 100644 index 0000000..7219927 --- /dev/null +++ b/public/img/partners/webmavens.svg @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/resources/views/native/discovery-pill.blade.php b/resources/views/native/discovery-pill.blade.php index 2d6d3c0..0fcc2ff 100644 --- a/resources/views/native/discovery-pill.blade.php +++ b/resources/views/native/discovery-pill.blade.php @@ -33,6 +33,38 @@ + {{-- + Connecting cover. Full-screen (iOS presents as a + fullScreenCover) so Jump's UI is hidden for the second or two between + tapping a server and the remote app's first frame. + + Not just polish: the remote app pushes its own theme as it boots, and + its font aliases name files Jump doesn't bundle — so home would + visibly re-render in fallback fonts while it waits. Every colour here + is a literal and there are no `font=` tokens, so the incoming theme + can't move this screen either. + + Not dismissible: the escape-hatch gesture is the way out, which is + exactly what the sheet below has just taught. + --}} + + + + + + @if ($connectingTo !== '') + Connecting to {{ $connectingTo }} + @else + Connecting + @endif + + Starting the app on your + phone — swipe right with three fingers any time to come back. + + + + + {{-- Escape-hatch coaching sheet. Interjected by InteractsWithDiscovery::connect() before every Jump until the user diff --git a/resources/views/native/docs-skeleton.blade.php b/resources/views/native/docs-skeleton.blade.php new file mode 100644 index 0000000..8c21e65 --- /dev/null +++ b/resources/views/native/docs-skeleton.blade.php @@ -0,0 +1,29 @@ +{{-- #[Lazy] placeholder — static skeleton mirroring the docs TOC (first + section expanded with page rows, the rest collapsed) so the frame + doesn't shift when the corpus lands. Varied bar widths read as text. --}} + + + + {{-- Expanded first section: header + page rows --}} + + + + + + @foreach ([210, 150, 240, 180, 120] as $w) + + + + @endforeach + + {{-- Collapsed section headers --}} + @foreach ([120, 170, 100, 140, 190, 110] as $w) + + + + + + @endforeach + + + diff --git a/resources/views/native/docs.blade.php b/resources/views/native/docs.blade.php index 68f1089..80a6b1b 100644 --- a/resources/views/native/docs.blade.php +++ b/resources/views/native/docs.blade.php @@ -1,9 +1,4 @@ -@if ($loading) - - Loading documentation… - - -@elseif ($failed) +@if ($failed) Unable to load documentation. Check your connection. diff --git a/resources/views/native/home.blade.php b/resources/views/native/home.blade.php index 71d0230..97b8e18 100644 --- a/resources/views/native/home.blade.php +++ b/resources/views/native/home.blade.php @@ -4,7 +4,11 @@ layout space when hidden). Making the scroll-view a sibling of the sheet without this wrapper collapses its height and cuts off the bottom. --}} - +{{-- The root carries bg-theme-background, not just the scroll-view: the tree + reaches the physical screen edge, but the scroll-view's content is inset + by the top safe area, so an unpainted root leaves the status-bar strip + showing the host's systemBackground (white) against our off-white bg. --}} + @@ -139,32 +143,14 @@ class="text-red-600"/> - + {{-- Bare tappable logos (dark-variant rasters swap in via the + component) — sized per-logo to keep aspect ratio, so no card + chrome or text to fight platform layout quirks. --}} + @foreach ($partners as $partner) - - - @if ($partner['logo']) - {{-- Logo on a white chip so a dark wordmark stays legible on the dark card too. --}} - - {{ $partner['name'] }} logo - - @else - - {{ Str::substr($partner['name'], 0, 1) }} - - @endif - - {{ $partner['name'] }} - {{ $partner['tagline'] }} - - - + + {{ $partner['name'] }} logo @endforeach diff --git a/resources/views/native/videos-skeleton.blade.php b/resources/views/native/videos-skeleton.blade.php new file mode 100644 index 0000000..6de06fd --- /dev/null +++ b/resources/views/native/videos-skeleton.blade.php @@ -0,0 +1,40 @@ +{{-- #[Lazy] placeholder — static skeleton mirroring videos.blade.php + (featured card, section header, list rows) so the frame doesn't shift + when the feed lands. Bones are theme-surface-variant blocks; the + renderer has no pulse animation, so they sit still. --}} + + + + {{-- Featured --}} + + + + + + + + + + + {{-- Section header --}} + + + + + + {{-- List rows --}} + + @foreach ([230, 150, 200, 170] as $w) + + + + + + + + + @endforeach + + + + diff --git a/resources/views/native/videos.blade.php b/resources/views/native/videos.blade.php index 82026da..c6ad129 100644 --- a/resources/views/native/videos.blade.php +++ b/resources/views/native/videos.blade.php @@ -1,11 +1,7 @@ - @if ($loading) - - Loading videos… - - @elseif ($failed) + @if ($failed) Unable to load videos. Check your connection. @@ -22,7 +18,7 @@ - + {{ $featured['category'] }} @@ -45,7 +41,7 @@ - + {{ $video['title'] }}