diff --git a/app/Support/DocsLabels.php b/app/Support/DocsLabels.php
index 97adc0996..ed1eab0d6 100644
--- a/app/Support/DocsLabels.php
+++ b/app/Support/DocsLabels.php
@@ -22,9 +22,9 @@ public static function productName(): string
* Null when that version's tree has no versioning page — an unlinked
* label beats one that 404s.
*/
- public static function versioningPolicyUrl(): ?string
+ public static function versioningPolicyUrl(string $fragment = 'version-labels'): ?string
{
- return self::pageUrl('getting-started/versioning', 'version-labels');
+ return self::pageUrl('getting-started/versioning', $fragment);
}
public static function jumpUrl(): ?string
diff --git a/config/docs.php b/config/docs.php
index 022b2365c..5468e4447 100644
--- a/config/docs.php
+++ b/config/docs.php
@@ -63,6 +63,33 @@
],
],
+ /*
+ |--------------------------------------------------------------------------
+ | Separately Versioned Packages
+ |--------------------------------------------------------------------------
+ |
+ | Packages documented inside a platform's docs that ship on their own
+ | release line, keyed by the slug a label passes as `package`. Their labels
+ | name the package and are checked against the package's own released
+ | versions in the test suite, rather than the platform's.
+ |
+ | Labels at or below the baseline don't render. The baseline is whatever
+ | was current when the platform major shipped (mobile-ui 0.3.0 was current
+ | when nativephp/mobile 4.0.0 shipped), the same way x.0 never renders for
+ | the platform itself.
+ |
+ | Add the new entry here as part of shipping a release.
+ |
+ */
+
+ 'packages' => [
+ 'mobile-ui' => [
+ 'name' => 'nativephp/mobile-ui',
+ 'baseline' => '0.3',
+ 'released_versions' => ['0.1', '0.2', '0.3', '0.4', '0.5', '0.6', '0.7'],
+ ],
+ ],
+
/*
|--------------------------------------------------------------------------
| Jump
diff --git a/resources/views/components/docs/version-badge.blade.php b/resources/views/components/docs/version-badge.blade.php
index fd67a3af2..c00e433de 100644
--- a/resources/views/components/docs/version-badge.blade.php
+++ b/resources/views/components/docs/version-badge.blade.php
@@ -7,6 +7,7 @@
'changed' => null,
'deprecated' => null,
'removed' => null,
+ 'package' => null,
])
@php
@@ -22,9 +23,22 @@
// x.0 never renders — everything in a major's tree was there at x.0
// unless stated otherwise.
$minor = (int) (explode('.', (string) ($state['version'] ?? ''))[1] ?? 0);
+
+ // A package on its own release line (config('docs.packages')) is named in
+ // its labels. Its baseline was current when the platform major shipped, so
+ // labels at or below it never render, just like x.0. Neither does a
+ // package missing from the config.
+ $release = filled($package) ? (config('docs.packages')[$package] ?? null) : null;
@endphp
-@if ($state && $minor > 0)
+@if ($release && $state && version_compare($state['version'], $release['baseline']) > 0)
+
+@elseif (blank($package) && $state && $minor > 0)
- Per-platform overrides for the leading icon: an [SF Symbol](icon#ios-sf-symbols)
name on iOS, a [Material Icon](icon#android-material-icons) name on Android. `icon` is the fallback on whichever
platform has no override — or skip it and declare only per-platform icons. `iosIcon` / `androidIcon` and the
``-style `ios` / `android` are accepted as aliases
- `icon-trailing` - A trailing [icon](icon#icon-name-reference) name (optional)
-- `ios-icon-trailing` / `android-icon-trailing` - Per-platform overrides for the trailing icon, mirroring
+- `ios-icon-trailing` / `android-icon-trailing` - Per-platform overrides for the trailing icon, mirroring
`ios-icon` / `android-icon` (camelCase aliases accepted; no `ios` / `android` shorthand for this slot)
- `font` - Custom font for the label: a `resources/fonts/` file token or a config alias like `accent` (optional, string) — see [Text › Custom fonts](text#custom-fonts)
- `line-height` - Label line height as a multiplier of the font size (optional, float)
@@ -104,6 +104,8 @@ attributes are intentionally dropped before reaching the renderer.
### Platform icons
+
+
When one shared name doesn't map well on both platforms, give each platform its own symbol — an
[SF Symbol](icon#ios-sf-symbols) name on iOS, a [Material Icon](icon#android-material-icons) name on Android:
diff --git a/resources/views/docs/mobile/4/edge-components/chip.md b/resources/views/docs/mobile/4/edge-components/chip.md
index 56a3f80e5..36a4ea920 100644
--- a/resources/views/docs/mobile/4/edge-components/chip.md
+++ b/resources/views/docs/mobile/4/edge-components/chip.md
@@ -27,7 +27,7 @@ fully rounded and can be adjusted with `rounded-*` classes.
- `label` - Chip text (optional, string). Can also be passed as the first argument to `make()`
- `selected` / `value` - Whether the chip is active (optional, boolean, default: `false`)
- `icon` - Leading [icon](icon#icon-name-reference) name (optional, string)
-- `ios-icon` / `android-icon` - Per-platform overrides for the leading icon: an [SF Symbol](icon#ios-sf-symbols)
+- `ios-icon` / `android-icon` - Per-platform overrides for the leading icon: an [SF Symbol](icon#ios-sf-symbols)
name on iOS, a [Material Icon](icon#android-material-icons) name on Android. `icon` is the fallback on whichever
platform has no override. Bound `:ios-icon` / `:android-icon` also accept
[enum cases](icon#typed-icon-enums); `iosIcon` / `androidIcon` and `ios` / `android` are accepted as aliases
diff --git a/resources/views/docs/mobile/4/edge-components/pager.md b/resources/views/docs/mobile/4/edge-components/pager.md
index 348093f5e..149230bfd 100644
--- a/resources/views/docs/mobile/4/edge-components/pager.md
+++ b/resources/views/docs/mobile/4/edge-components/pager.md
@@ -1,6 +1,8 @@
---
title: Pager
order: 312
+package: mobile-ui
+since: "0.5"
---
## Overview
diff --git a/resources/views/docs/mobile/4/edge-components/tab-row.md b/resources/views/docs/mobile/4/edge-components/tab-row.md
index 99c3f7506..911590ec4 100644
--- a/resources/views/docs/mobile/4/edge-components/tab-row.md
+++ b/resources/views/docs/mobile/4/edge-components/tab-row.md
@@ -68,7 +68,7 @@ Here `currentTab` stands in for `public int $currentTab = 0;` on your component.
- `label` - Tab label (required, string). Can also be passed as the first argument to `make()`
- `icon` - Optional [icon](icon#icon-name-reference) name rendered above the label
-- `ios-icon` / `android-icon` - Per-platform overrides for the tab icon: an [SF Symbol](icon#ios-sf-symbols) name
+- `ios-icon` / `android-icon` - Per-platform overrides for the tab icon: an [SF Symbol](icon#ios-sf-symbols) name
on iOS, a [Material Icon](icon#android-material-icons) name on Android. `icon` is the fallback on whichever
platform has no override. Bound `:ios-icon` / `:android-icon` also accept
[enum cases](icon#typed-icon-enums); `iosIcon` / `androidIcon` and `ios` / `android` are accepted as aliases
diff --git a/resources/views/docs/mobile/4/edge-components/text-input.md b/resources/views/docs/mobile/4/edge-components/text-input.md
index 59cb2130f..53613458d 100644
--- a/resources/views/docs/mobile/4/edge-components/text-input.md
+++ b/resources/views/docs/mobile/4/edge-components/text-input.md
@@ -61,7 +61,7 @@ All three variants accept the same shared prop set. The bare variant adds a `col
- `keyboard` - Keyboard hint string: `text` (default), `number`, `email`, `phone`, `url`, `decimal`, `password`,
`numberPassword`. On iOS `password` uses the standard keyboard; `secure` is the masking mechanism. The keyboard
type also decides capitalization and autocorrect — see [Capitalization](#capitalization)
-- `autocapitalize` - Override that capitalization: `none`, `sentences`, `words`, or `characters` (HTML's
+- `autocapitalize` - Override that capitalization: `none`, `sentences`, `words`, or `characters` (HTML's
vocabulary). Leave it unset to let `keyboard` decide (optional, string)
- `secure` - Mask input for passwords (optional, boolean, default: `false`)
- `multiline` - Allow multiple lines (optional, boolean, default: `false`)
@@ -73,7 +73,7 @@ All three variants accept the same shared prop set. The bare variant adds a `col
- `sync-mode` - How change events dispatch back to your component: `live` (default), `blur`, or `debounce`. Usually
set via the `native:model` modifiers below, but accepted directly too
- `debounce-ms` - Milliseconds of inactivity before a `debounce` sync fires (optional, int, default: `300`)
-- `selection-debounce-ms` - Coalescing window for `@selectionChange` events (optional, int, default: `150`). `0` or
+- `selection-debounce-ms` - Coalescing window for `@selectionChange` events (optional, int, default: `150`). `0` or
less means the default; positive values are floored at one frame (16ms). See
[Caret and selection reporting](#caret-and-selection-reporting)
@@ -83,9 +83,9 @@ All three variants accept the same shared prop set. The bare variant adds a `col
- `suffix` - Text rendered after the input (optional, string)
- `leading-icon` - Icon name rendered at the start (optional, string)
- `trailing-icon` - Icon name rendered at the end (optional, string)
-- `ios-leading-icon` / `android-leading-icon` - Per-platform overrides for `leading-icon` (optional). See
+- `ios-leading-icon` / `android-leading-icon` - Per-platform overrides for `leading-icon` (optional). See
[Per-platform icons](#per-platform-icons)
-- `ios-trailing-icon` / `android-trailing-icon` - Per-platform overrides for `trailing-icon` (optional)
+- `ios-trailing-icon` / `android-trailing-icon` - Per-platform overrides for `trailing-icon` (optional)
### Typography
@@ -101,6 +101,8 @@ All three variants accept the same shared prop set. The bare variant adds a `col
## Capitalization
+
+
Declaring a `keyboard` type carries its typing behaviour with it, not just the key layout. Fields whose content is
case-sensitive or non-alphabetic never capitalize, and never autocorrect:
@@ -140,7 +142,7 @@ own default, left as-is. Set `autocapitalize` explicitly when you need the two t
- `@change` - Component method called when the text changes. Receives the new value
- `@submit` - Component method called when the user submits (e.g. presses return). Receives the current value
-- `@selectionChange` - Component method called when the caret moves or the selection changes. Receives the full
+- `@selectionChange` - Component method called when the caret moves or the selection changes. Receives the full
current text plus the selection start and end offsets — see
[Caret and selection reporting](#caret-and-selection-reporting)
@@ -186,6 +188,8 @@ automatically, so the `@{{ $name }}` echo updates as you type.
## Caret and selection reporting
+
+
`@selectionChange` reports caret position and text selection back to your component — for the cases where `@change`
alone can't tell you *where* the user is typing. The handler receives the full current text plus the selection range:
@@ -266,6 +270,8 @@ Caret and selection reporting requires `nativephp/mobile` 4.0+, which ships the
## Per-platform icons
+
+
The shared `leading-icon` / `trailing-icon` names render the same icon on both platforms. When each platform should
show its own symbol, prefix the attribute with the platform — the same convention as
[``](button)'s `ios-icon`:
@@ -471,9 +477,9 @@ All three elements share the same fluent API (defined on `BaseTextInput`):
- `font(string $name)` - Custom font (file token or config alias)
- `a11yLabel(string $value)`, `a11yHint(string $value)`
- `syncMode(string $mode)`, `debounceMs(int $ms)`
-- `selectionDebounceMs(int $ms)` - Coalescing window for `@selectionChange` events (150ms when unset; `0` or less
+- `selectionDebounceMs(int $ms)` - Coalescing window for `@selectionChange` events (150ms when unset; `0` or less
means the default, positive values floored at 16ms)
-- `onChange(string $method)`, `onSubmit(string $method)`, `onSelectionChange(string $method)`
+- `onChange(string $method)`, `onSubmit(string $method)`, `onSelectionChange(string $method)`
`BareTextInput` adds one method on top of the shared API:
diff --git a/resources/views/docs/mobile/4/edge-components/text.md b/resources/views/docs/mobile/4/edge-components/text.md
index 1211bc0b7..134c81a3b 100644
--- a/resources/views/docs/mobile/4/edge-components/text.md
+++ b/resources/views/docs/mobile/4/edge-components/text.md
@@ -74,6 +74,8 @@ and is available fluently as `->font('Inter-Bold')`.
### Downloading from Google Fonts
+
+
The `native:font` command downloads a [Google Fonts](https://fonts.google.com)
family into `resources/fonts/` and registers it in your config — no API key
needed:
@@ -99,9 +101,9 @@ whether one style should become the app-wide `default`. Google Fonts are
libre-licensed (OFL / Apache), so bundling them in your app is permitted.
- `--default` — make the downloaded font the app-wide `default` alias without asking
-- `--force` — overwrite files that already exist in `resources/fonts/` (and skip the `--reset` confirmation)
-- `--reset` — delete every font file in `resources/fonts/` and reset the config to `'default' => 'System'` (asks first)
-- `--clear-cache` — clear the cached catalog (the family list is cached for 24 hours)
+- `--force` — overwrite files that already exist in `resources/fonts/` (and skip the `--reset` confirmation)
+- `--reset` — delete every font file in `resources/fonts/` and reset the config to `'default' => 'System'` (asks first)
+- `--clear-cache` — clear the cached catalog (the family list is cached for 24 hours)
Non-interactive runs (`--no-interaction`, CI) require the family argument,
download only Regular (400) — or the first listed style for families without
@@ -140,7 +142,7 @@ explicit `font-serif` / `font-mono` classes still win over it. Swapping a font
app-wide becomes a one-line config change; blades keep their semantic names.
Each alias must point directly at a file token (no alias-to-alias chaining).
-`native:font` maintains this array for you — every downloaded style gets an
+`native:font` maintains this array for you — every downloaded style gets an
entry, and the `--default` flag (or the prompt) sets `default`. Prefer aliases
over the older `font-family` theme token (which `fonts.default` supersedes when
both are set); `native:font --default` only falls back to writing `font-family`
diff --git a/resources/views/docs/mobile/4/getting-started/versioning.md b/resources/views/docs/mobile/4/getting-started/versioning.md
index 7c4cea919..ec9c87b16 100644
--- a/resources/views/docs/mobile/4/getting-started/versioning.md
+++ b/resources/views/docs/mobile/4/getting-started/versioning.md
@@ -85,6 +85,30 @@ render on your phone when you scan a QR code. Where that's the case, you'll see:
These disappear on their own as Jump catches up. Pages carrying one don't offer the "Preview in Jump" QR code, since
scanning it wouldn't show you the component.
+### mobile-ui labels
+
+The [EDGE components](../edge-components/introduction) ship in `nativephp/mobile-ui`, which has its own version
+numbers. Anything on those pages without a label has been there since mobile-ui 0.3, the release that shipped
+alongside 4.0. Later additions and changes carry one of these labels:
+
+| Label | Meaning |
+|-------|---------|
+| | Added in that mobile-ui release. Upgrade mobile-ui to at least that version to use it |
+| | Behaviour or signature changed in that release. Check it against what your app relies on before upgrading |
+| | Still works, but slated for removal. Move off it when convenient |
+| | Gone as of that release. Documented only so you know what replaced it |
+
+Because mobile-ui is still on 0.x, Composer treats each minor release like a major one, so a `^0.5` constraint never
+installs 0.6. Raise the constraint to pick up a newer release:
+
+```shell
+composer require nativephp/mobile-ui:^0.6
+```
+
+Then [rebuild your app](../plugins/using-plugins#rebuild-your-app), because the components' renderers are native code
+compiled in at build time. Newer mobile-ui releases can need a newer `nativephp/mobile` too (0.5 and later need 4.5),
+and Composer will say so.
+
## Your application versioning
Just because we're using semantic versioning for the `nativephp/mobile` package, doesn't mean your app must follow that
diff --git a/tests/Feature/Docs/VersionBadgeTest.php b/tests/Feature/Docs/VersionBadgeTest.php
index d77710fe8..11b6e8513 100644
--- a/tests/Feature/Docs/VersionBadgeTest.php
+++ b/tests/Feature/Docs/VersionBadgeTest.php
@@ -58,6 +58,49 @@ public function test_removed_renders_with_prefix(): void
->assertSee('Removed 4.1');
}
+ public function test_package_since_renders_the_package_and_version(): void
+ {
+ $this->blade('')
+ ->assertSee('>mobile-ui 0.6<', false)
+ ->assertSee('title="Added in nativephp/mobile-ui 0.6"', false);
+ }
+
+ public function test_package_changed_renders_with_prefix(): void
+ {
+ $this->blade('')
+ ->assertSee('>Changed mobile-ui 0.6<', false)
+ ->assertSee('title="Changed in nativephp/mobile-ui 0.6"', false);
+ }
+
+ public function test_package_deprecated_renders_with_prefix(): void
+ {
+ $this->blade('')
+ ->assertSee('>Deprecated mobile-ui 0.6<', false);
+ }
+
+ public function test_package_removed_renders_with_prefix(): void
+ {
+ $this->blade('')
+ ->assertSee('>Removed mobile-ui 0.6<', false);
+ }
+
+ public function test_package_labels_at_or_below_the_baseline_render_nothing(): void
+ {
+ // mobile-ui 0.3 was current when Mobile 4.0 shipped, so like x.0 it's
+ // the starting point rather than something to label.
+ $this->blade('')
+ ->assertDontSee('0.3');
+
+ $this->blade('')
+ ->assertDontSee('0.1');
+ }
+
+ public function test_an_unknown_package_renders_nothing(): void
+ {
+ $this->blade('')
+ ->assertDontSee('0.6');
+ }
+
public function test_a_badge_renders_on_one_line(): void
{
// Labels sit inline in markdown, including inside table cells, where a
@@ -65,8 +108,9 @@ public function test_a_badge_renders_on_one_line(): void
// rest of the table into paragraph text.
$linked = (string) $this->blade('');
$bare = (string) $this->blade('');
+ $package = (string) $this->blade('');
- foreach ([$linked, $bare] as $badge) {
+ foreach ([$linked, $bare, $package] as $badge) {
$this->assertStringNotContainsString("\n", $badge);
$this->assertSame(trim($badge), $badge);
}
@@ -94,6 +138,25 @@ public function test_layout_page_contains_the_4_2_pill(): void
->assertSee('4.2');
}
+ public function test_a_package_label_links_to_its_section_of_the_versioning_page(): void
+ {
+ $response = $this->get('/docs/mobile/4/getting-started/versioning')
+ ->assertStatus(200)
+ ->assertSee('id="mobile-ui-labels"', false);
+
+ $this->assertMatchesRegularExpression(
+ '/]*>mobile-ui 0\.6<\/a>/',
+ $response->getContent()
+ );
+ }
+
+ public function test_accordion_page_carries_a_page_level_mobile_ui_label(): void
+ {
+ $this->get('/docs/mobile/4/edge-components/accordion')
+ ->assertStatus(200)
+ ->assertSee('>mobile-ui 0.4<', false);
+ }
+
public function test_section_label_does_not_change_the_heading_anchor_id(): void
{
$html = CommonMark::convertToHtml(
@@ -122,6 +185,7 @@ public function test_search_index_content_contains_no_badge_markup(): void
public function test_every_version_label_points_at_a_released_version(): void
{
$releasedVersions = config('docs.released_versions');
+ $packages = config('docs.packages');
$finder = (new Finder)->files()->name('*.md')->in(resource_path('views/docs'));
$violations = [];
@@ -135,25 +199,62 @@ public function test_every_version_label_points_at_a_released_version(): void
}
[$platform, $major] = [$parts[0], (int) $parts[1]];
- $allowed = $releasedVersions[$platform][$major] ?? [];
+
+ // A package label is checked against the package's own releases.
+ // Null means the package isn't in config('docs.packages').
+ $releasedFor = fn (?string $package): ?array => $package === null
+ ? ($releasedVersions[$platform][$major] ?? [])
+ : ($packages[$package]['released_versions'] ?? null);
$content = $file->getContents();
$document = YamlFrontMatter::parse($content);
+ $package = $document->matter('package');
+ $packageNote = $package === null ? '' : " `package: {$package}`";
+ $allowed = $releasedFor($package);
+
+ if ($allowed === null) {
+ $violations[] = "{$relative} front matter `package: {$package}` isn't in config('docs.packages')";
+ }
+
foreach (['since', 'changed', 'deprecated', 'removed'] as $key) {
$value = $document->matter($key);
- if ($value !== null && ! in_array((string) $value, $allowed, true)) {
- $violations[] = "{$relative} front matter `{$key}: {$value}`";
+ if ($value === null) {
+ continue;
}
+
+ // YAML reads an unquoted `since: 0.10` as the float 0.1, so
+ // the label would quietly show the wrong version.
+ if (! is_string($value)) {
+ $violations[] = "{$relative} front matter `{$key}: ".json_encode($value)."` must be quoted, e.g. `{$key}: \"0.10\"`, or YAML reads it as a number";
+ } elseif ($allowed !== null && ! in_array($value, $allowed, true)) {
+ $violations[] = "{$relative} front matter{$packageNote} `{$key}: {$value}`";
+ }
+ }
+
+ $jump = $document->matter('jump');
+
+ if ($jump !== null && $jump !== false && ! is_string($jump)) {
+ $violations[] = "{$relative} front matter `jump: ".json_encode($jump).'` must be a quoted version, e.g. `jump: "3.10"`, or false';
}
if (preg_match_all('/]*)\/>/s', $content, $tagMatches)) {
foreach ($tagMatches[1] as $attrs) {
+ $package = preg_match('/package="([^"]+)"/', $attrs, $m) ? $m[1] : null;
+ $packageAttribute = $package === null ? '' : " package=\"{$package}\"";
+ $allowed = $releasedFor($package);
+
+ if ($allowed === null) {
+ $violations[] = "{$relative} isn't in config('docs.packages')";
+
+ continue;
+ }
+
foreach (['since', 'changed', 'deprecated', 'removed'] as $key) {
if (preg_match('/'.$key.'="([^"]+)"/', $attrs, $m)) {
if (! in_array($m[1], $allowed, true)) {
- $violations[] = "{$relative} ";
+ $violations[] = "{$relative} ";
}
break;
}
@@ -164,7 +265,22 @@ public function test_every_version_label_points_at_a_released_version(): void
$this->assertEmpty(
$violations,
- "Version labels pointing at an unreleased or mismatched version:\n".implode("\n", $violations)
+ "Version labels that are unquoted, name an unknown package, or point at an unreleased or mismatched version:\n".implode("\n", $violations)
);
}
+
+ public function test_every_package_baseline_is_one_of_its_released_versions(): void
+ {
+ $packages = config('docs.packages');
+
+ $this->assertNotEmpty($packages);
+
+ foreach ($packages as $slug => $package) {
+ $this->assertContains(
+ $package['baseline'],
+ $package['released_versions'],
+ "config('docs.packages.{$slug}.baseline') isn't one of its released_versions"
+ );
+ }
+ }
}