Skip to content

Commit d720773

Browse files
simonhampclaude
andauthored
Homepage: Mobile/Desktop path chooser with SuperNative-first messaging (#438)
* Homepage: Mobile/Desktop path chooser, SuperNative-first messaging for v4 The hero CTAs become a platform toggle (persisted, Mobile by default) and the Under the hood, tools, Fast/Tiny apps and install-steps blocks adapt to the chosen track. Mobile leads with SuperNative: Blade rendering to real SwiftUI/Jetpack Compose views over shared memory, web view demoted to an optional component. Desktop keeps the accurate Electron story: static PHP binary, authenticated HTTP, Chromium window. Also: Bifrost diagram redrawn with a two-arm fan (Windows track removed) and a centred bubble, announcement cards scoped to the Mobile track, and the invalid flux-pro carousel stub class excluded from Tailwind's sources to silence a Lightning CSS warning on every build. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Fix CI: fake Torchlight for the SuperNative docs-page test The test renders a docs page with fenced code blocks; Torchlight throws outside production when no token is configured (as in CI). Worse, the thrown request skipped the middleware's clearBlocks(), so the static block registry leaked into every later HTML response in the process, 500ing 12 unrelated tests. Fake the token and API (the DocumentationRenderingTest pattern) and clear the registry in tearDown so a failure here can never cascade. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Mobile v4 changelog: pull release candidates from GitHub release notes releasesAfter('4.0.0') excluded every 4.0.0-rc.* tag because version_compare ranks prereleases below the release, so the automated feed rendered nothing while only RCs exist. Add releasesFrom(), which filters on a "-dev" floor (dev ranks below every prerelease marker), admitting alphas, betas, RCs and the release itself, and switch the v4 changelog to it. Covered by unit tests and a page test against a faked GitHub API. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Promote Mobile v4 docs to stable; SuperNative banner, changelog and homepage updates - Docs: v4 is now the latest version — beta notice retired (machinery still tested via pinned config), unversioned URLs and hardcoded v3 links resolve to the latest version, renamed pages 301 in one hop - Changelog: pull v4 release candidates from GitHub release notes, fix double-encoded links in hand-written release bodies, cap the v3 feed below 4.0.0, replace the static v4.0 section with the automated feed - New SuperNative header banner replacing The Vibes event banner - Homepage: announcement row moved above Plugins with Jump leading, Jump/Bifrost card copy and pricing ($10/mo), matched card padding, and hover transitions scoped to shadow/colors so the card fade-ins no longer fight gsap frame-by-frame * Homepage: move the platform chooser above Under the hood, reshape the Desktop track The chooser (heading, buttons and docs link) moves out of the hero to sit directly above the section it controls, and selecting a track eases the page down to it. One chooser on the page now, pinned by a test that counts them. Desktop track: text left with the diagram right across the full width, the diagram redrawn as a ~4:3 app window with a vertical internal flow, and Fast apps / One file stacked beside the tools card. Mobile track: reworded How does it work? around Blade rendering to real SwiftUI and Compose, the 240fps headroom, and the SuperNative name. * Keep the "New" nav badge on Learn only Ultra, Consulting, Build and Support no longer read as new, so drop their badges from the mobile menu and the footer. Learn keeps its badge in both. The header's nav pills never carried one. Prettier reflowed both files while it was in there, so the diff is larger than the eight removed spans; the rendered link set is unchanged. * Show coming-soon course content to users who haven't purchased The pre-purchase curriculum hid anything unreleased: modules without a published lesson were filtered out entirely, and unpublished lessons were dropped from the modules that survived. Free users couldn't see what was still to come. Published modules now always appear, badged "Coming Soon" when nothing in them has been released yet, and every lesson is listed. Unreleased lessons carry their own "Coming Soon" badge and deliberately no lock icon — they aren't gated by purchase, they just aren't out yet. Two tests asserted the old hiding behaviour and are inverted to match. Also reconciles TheVibesBannerTest, which arrived with main and expected the Vibes countdown on the homepage; this branch replaced that banner with the SuperNative one, so its page-level assertions now check for absence while main's countdown component tests stay as they were. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent e6b4db8 commit d720773

45 files changed

Lines changed: 1897 additions & 666 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎app/Livewire/Customer/Course/Index.php‎

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,13 @@ public function completedCount(): int
122122
}
123123

124124
/**
125-
* Published modules with at least one published lesson.
125+
* Every published module, including those still awaiting their first
126+
* released lesson.
126127
*
127128
* Drives the pre-purchase curriculum outline, which lists every lesson —
128-
* locked or not — so non-purchasers can see the whole course.
129+
* locked or not — so non-purchasers can see the whole course. Modules with
130+
* nothing released yet are listed as coming soon rather than hidden, so the
131+
* outline reflects the full shape of the course.
129132
*
130133
* Keys are preserved so the view can number each module by its real position
131134
* in the course rather than its position within this filtered subset.
@@ -140,8 +143,17 @@ public function outlineModules(): Collection
140143
}
141144

142145
return $this->course->modules
143-
->filter(fn (CourseModule $module): bool => $module->is_published
144-
&& $module->lessons->contains(fn (CourseLesson $lesson): bool => $lesson->is_published));
146+
->filter(fn (CourseModule $module): bool => $module->is_published);
147+
}
148+
149+
/**
150+
* A published module whose lessons are all still unreleased.
151+
*/
152+
public function isComingSoonModule(CourseModule $module): bool
153+
{
154+
return ! $module->lessons->contains(
155+
fn (CourseLesson $lesson): bool => $lesson->is_published
156+
);
145157
}
146158

147159
#[Computed]

‎app/Notifications/PluginReviewChecksIncomplete.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class PluginReviewChecksIncomplete extends Notification implements ShouldQueue
1212
{
1313
use Queueable;
1414

15-
private const DOCS_BASE = 'https://nativephp.com/docs/mobile/3/plugins';
15+
private const DOCS_BASE = 'https://nativephp.com/docs/mobile/plugins';
1616

1717
/**
1818
* @var array<string, array{label: string, passing_label: string, docs_url: string, docs_label: string}>

‎app/Support/GitHub.php‎

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,38 @@ public function releases(): Collection
7070
) ?? collect();
7171
}
7272

73-
public function releasesAfter(string $version): Collection
73+
/**
74+
* Releases strictly after the given version, optionally capped below
75+
* another version and its prereleases — without a cap, a versioned
76+
* changelog would list the next major's releases too (4.0.0-rc.1
77+
* compares greater than any 3.x tag).
78+
*/
79+
public function releasesAfter(string $version, ?string $before = null): Collection
7480
{
7581
$version = ltrim($version, 'v');
82+
$ceiling = $before === null ? null : ltrim($before, 'v').'-dev';
83+
84+
return $this->releases()->filter(function (Release $release) use ($version, $ceiling) {
85+
$tag = ltrim((string) $release->tag_name, 'v');
86+
87+
return version_compare($tag, $version, '>')
88+
&& ($ceiling === null || version_compare($tag, $ceiling, '<'));
89+
})->values();
90+
}
91+
92+
/**
93+
* Releases for the given version and everything later, including that
94+
* prefix version itself and its prereleases. releasesAfter("4.0.0")
95+
* would exclude 4.0.0-rc.1 because version_compare ranks prereleases
96+
* below the release; "dev" ranks below every other prerelease marker,
97+
* so a "-dev" floor admits alphas, betas, RCs and the release itself.
98+
*/
99+
public function releasesFrom(string $version): Collection
100+
{
101+
$floor = ltrim($version, 'v').'-dev';
76102

77103
return $this->releases()->filter(
78-
fn (Release $release) => version_compare(ltrim((string) $release->tag_name, 'v'), $version, '>')
104+
fn (Release $release) => version_compare(ltrim((string) $release->tag_name, 'v'), $floor, '>=')
79105
)->values();
80106
}
81107

‎app/Support/GitHub/Release.php‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,25 +49,31 @@ public function getBodyForMarkdown(): string
4949
{
5050
$body = $this->body;
5151

52-
// Convert any URLs to Markdown links
52+
// Convert bare URLs to Markdown links. Release bodies can already
53+
// contain Markdown links, so URLs preceded by "(" or "<" are left
54+
// alone — wrapping those again nests link syntax and the page ends
55+
// up rendering both the text and the URL. Closing ")" and "]" are
56+
// excluded from the URL so a link's own delimiters are never
57+
// swallowed into it.
5358
if ($this->convertLinks) {
5459
$body = preg_replace(
55-
'/https?:\/\/[^\s]+\/pull\/(\d+)/',
60+
'/(?<![(<])https?:\/\/[^\s)\]]+\/pull\/(\d+)/',
5661
'[#$1]($0)',
5762
$body
5863
);
5964

6065
$body = preg_replace(
61-
'/(https?:\/\/(?![^\s]+\/pull\/\d+)[^\s]+)/',
66+
'/(?<![(<])(https?:\/\/(?![^\s]+\/pull\/\d+)[^\s)\]]+)/',
6267
'[$1]($1)',
6368
$body
6469
);
6570
}
6671

67-
// Change any @ tags to markdown links to GitHub
72+
// Change any @ tags to markdown links to GitHub, unless they are
73+
// already the text of a Markdown link or part of an email address.
6874
if ($this->withUserLinks) {
6975
$body = preg_replace(
70-
'/@([a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?)/',
76+
'/(?<![\w\[])@([a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?)/',
7177
'[@$1](https://github.com/$1)',
7278
$body
7379
);

‎config/docs.php‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
'latest_versions' => [
1818
'desktop' => 2,
19-
'mobile' => 3,
19+
'mobile' => 4,
2020
],
2121

2222
/*
@@ -32,7 +32,7 @@
3232

3333
'prerelease_versions' => [
3434
'desktop' => [],
35-
'mobile' => [4],
35+
'mobile' => [],
3636
],
3737

3838
/*
@@ -53,6 +53,7 @@
5353
4 => [
5454
'the-basics/native-components' => 'the-basics/native-ui',
5555
'the-basics/dialog' => 'the-basics/dialogs',
56+
'the-basics/web-view' => 'edge-components/web-view',
5657
'getting-started/deployment' => 'publishing/introduction',
5758
'plugins/vibe' => 'digging-deeper/websockets',
5859

‎resources/css/app.css‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@
88
@source '../../storage/framework/views/*.php';
99
@source '../../vendor/filament/**/*.blade.php';
1010

11+
/*
12+
* flux.css pulls in the flux-pro stubs, and the carousel slide stub declares
13+
* `[snap="mandatory"_&]:snap-always` — one bracket short, so it compiles to the
14+
* invalid selector `snap="mandatory" &` and Lightning CSS warns on every build.
15+
* We don't use <flux:carousel>, so skip that stub. Drop this once Flux fixes it
16+
* upstream; remove it first if we ever adopt the carousel.
17+
*/
18+
@source not '../../vendor/livewire/flux-pro/stubs/resources/views/flux/carousel';
19+
1120
/* Safelist classes used in dynamic content (blog posts from database) */
1221
@source inline("grid-cols-1 grid-cols-2 grid-cols-3 grid-cols-4 grid-cols-5 grid-cols-6");
1322

‎resources/js/app.js‎

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,58 @@ Alpine.data('countdown', (iso) => ({
115115
}, // tidy up
116116
}))
117117

118+
// Which platform path the homepage is showing: 'mobile' | 'desktop'.
119+
// Shared across sections (hero, explainer) and remembered between visits.
120+
// Registered on alpine:init because $persist only exists once Livewire has
121+
// registered Alpine's plugins.
122+
document.addEventListener('alpine:init', () => {
123+
Alpine.store('platform', {
124+
current: Alpine.$persist('mobile').as('nativephpPlatform'),
125+
is(name) {
126+
return this.current === name
127+
},
128+
select(name) {
129+
if (this.current === name) return
130+
131+
this.current = name
132+
this.revealExplainer()
133+
},
134+
// Switching tracks rewrites the explainer, so bring it into view.
135+
// Two frames: one for Alpine to apply the change, one for the
136+
// browser to lay it out, so the target offset is the final one.
137+
revealExplainer() {
138+
requestAnimationFrame(() =>
139+
requestAnimationFrame(() => {
140+
const target = document.getElementById('platform-explainer')
141+
if (!target) return
142+
143+
const nav = document.querySelector('[data-site-nav]')
144+
const offset = (nav?.offsetHeight ?? 0) + 16
145+
const top =
146+
target.getBoundingClientRect().top +
147+
window.scrollY -
148+
offset
149+
150+
window.scrollTo({
151+
top: Math.max(0, top),
152+
behavior: window.matchMedia(
153+
'(prefers-reduced-motion: reduce)',
154+
).matches
155+
? 'auto'
156+
: 'smooth',
157+
})
158+
}),
159+
)
160+
},
161+
})
162+
})
163+
118164
Livewire.start()
119165

120166
// Docsearch
121-
const docsPathMatch = window.location.pathname.match(/^\/docs\/(desktop|mobile)\/(\d+)/)
167+
const docsPathMatch = window.location.pathname.match(
168+
/^\/docs\/(desktop|mobile)\/(\d+)/,
169+
)
122170
const docsearchOptions = {
123171
appId: 'ZNII9QZ8WI',
124172
apiKey: '9be495a1aaf367b47c873d30a8e7ccf5',
@@ -148,7 +196,9 @@ docsearch({
148196
// pressing Cmd+K only registers one handler (avoiding duplicate modals).
149197
const mobileContainer = document.getElementById('docsearch-mobile')
150198
if (mobileContainer) {
151-
const desktopButton = document.querySelector('#docsearch-desktop .DocSearch-Button')
199+
const desktopButton = document.querySelector(
200+
'#docsearch-desktop .DocSearch-Button',
201+
)
152202
if (desktopButton) {
153203
const mobileButton = desktopButton.cloneNode(true)
154204
mobileContainer.appendChild(mobileButton)

‎resources/views/components/blog/ad-rotation.blade.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ class="group relative z-0 grid place-items-center overflow-hidden rounded-2xl bg
180180

181181
{{-- Price hint --}}
182182
<div class="mt-2 text-xs text-sky-100">
183-
Plans from $19/mo
183+
Plans from $10/mo
184184
</div>
185185

186186
{{-- Decorative stars --}}

‎resources/views/components/docs/platform-switcher.blade.php‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
@php
22
$isMobile = request()->is('docs/mobile/*');
3-
$mobileHref = '/docs/mobile/3';
4-
$desktopHref = '/docs/desktop/2';
3+
$mobileHref = '/docs/mobile/'.config('docs.latest_versions.mobile');
4+
$desktopHref = '/docs/desktop/'.config('docs.latest_versions.desktop');
55
@endphp
66

77
<a

0 commit comments

Comments
 (0)