diff --git a/config/docs.php b/config/docs.php
index fa3fabd0..c1e7695d 100644
--- a/config/docs.php
+++ b/config/docs.php
@@ -59,7 +59,7 @@
1 => ['1.0', '1.1'],
2 => ['2.0'],
3 => ['3.0', '3.1', '3.2', '3.3'],
- 4 => ['4.0', '4.1', '4.2', '4.3', '4.4', '4.5', '4.6'],
+ 4 => ['4.0', '4.1', '4.2', '4.3', '4.4', '4.5', '4.6', '4.7'],
],
],
@@ -86,7 +86,7 @@
'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'],
+ 'released_versions' => ['0.1', '0.2', '0.3', '0.4', '0.5', '0.6', '0.7', '0.8', '0.9'],
],
],
diff --git a/resources/views/docs/mobile/4/edge-components/bottom-nav.md b/resources/views/docs/mobile/4/edge-components/bottom-nav.md
index cee4c63e..da63f651 100644
--- a/resources/views/docs/mobile/4/edge-components/bottom-nav.md
+++ b/resources/views/docs/mobile/4/edge-components/bottom-nav.md
@@ -141,6 +141,9 @@ shorthand) override it per platform. Each override accepts a generated enum case
```
@endverbatim
+[Custom icons](icon#custom-icons) work here too: put an SVG in `resources/icons`
+and pass `custom:` as `icon` or as either override.
+
### Search tab
Mark one item with the boolean `search` attribute to make it present a native search field instead of navigating.
diff --git a/resources/views/docs/mobile/4/edge-components/icon.md b/resources/views/docs/mobile/4/edge-components/icon.md
index 0e45e309..61867102 100644
--- a/resources/views/docs/mobile/4/edge-components/icon.md
+++ b/resources/views/docs/mobile/4/edge-components/icon.md
@@ -8,7 +8,7 @@ order: 230
Displays a platform-native icon. On iOS, icons render as [SF Symbols](https://developer.apple.com/sf-symbols/);
on Android, as [Material Icons](https://fonts.google.com/icons?icon.set=Material+Icons). You don't need to worry about
the differences — use one consistent icon name and EDGE's smart mapping system translates it to the right platform
-symbol automatically.
+symbol automatically. When no system icon fits, you can ship [your own SVG icons](#custom-icons).
@verbatim
```blade
@@ -151,6 +151,8 @@ through a four-tier resolution strategy:
This approach means you can use intuitive icon names for common cases, leverage direct platform icons for advanced use
cases, and get consistent results across iOS and Android.
+A name that starts with `custom:` points at one of your [custom icons](#custom-icons) instead.
+
### iOS (SF Symbols)
On iOS, icons render as SF Symbols. Manual mappings convert common icon names to their SF Symbol equivalents.
@@ -219,6 +221,144 @@ For Blade components whose tag only takes a single `icon` string, pick the platf
```
@endverbatim
+## Custom icons
+
+
+
+Ship your own icons as SVG files. When NativePHP builds your app, it turns each one into a native icon: a template image
+in the iOS asset catalog, with its vector data kept, and a VectorDrawable on Android.
+
+Custom icons are tinted in one color, the same way system icons are. They take the color you give ``, and
+a tab follows the bar's active and inactive colors. You can use them in tab bars, top-bar actions and their menus,
+``, and the icon props of other EDGE components such as buttons and list items.
+
+Tab bars, top-bar actions and their menus need `nativephp/mobile` 4.7. `` and icons in other EDGE
+components also need `nativephp/mobile-ui` 0.9.
+
+### Adding an icon
+
+Save each icon as `resources/icons/.svg`, directly in that folder rather than in a subfolder. The file name is the
+icon's name. It must be lowercase kebab-case: letters, numbers and single hyphens, such as `git-commit-vertical`. A
+`.svg` file with any other name fails the build. Files that aren't `.svg` are ignored.
+
+Here's `resources/icons/git-commit-vertical.svg`, downloaded from [Lucide](https://lucide.dev):
+
+```xml
+
+```
+
+
+
+### Using an icon
+
+Pass `custom:` anywhere an icon name goes:
+
+@verbatim
+```blade static
+
+```
+@endverbatim
+
+The same string works in any `icon` attribute, such as ``.
+
+In PHP, `Icon::custom()` builds that string for you. It throws an `InvalidArgumentException` if the name isn't
+lowercase kebab-case:
+
+```php
+use Native\Mobile\Edge\Layouts\Builders\NavAction;
+use Native\Mobile\Edge\Layouts\Builders\Tab;
+use Native\Mobile\Icon\Icon;
+
+// A tab in your layout's tabBar()
+Tab::link('Log', '/log', Icon::custom('git-commit-vertical'));
+
+// A top-bar action, or an item in its menu
+NavAction::make('star')->icon(Icon::custom('star'))->press('toggleStar');
+```
+
+`Icon::custom('star')` returns the string `'custom:star'`, so you can pass the string itself if you prefer. This
+`Icon` class shares its short name with the [`Icon` element](#element). If a file needs both, alias one of them:
+`use Native\Mobile\Icon\Icon as CustomIcon;`.
+
+### Per-platform custom icons
+
+The `ios` and `android` overrides take custom icons too, so you can use your icon on one platform and keep the system
+icon on the other:
+
+```php
+use App\Icons\AndroidOutlined;
+use Native\Mobile\Edge\Layouts\Builders\NavAction;
+use Native\Mobile\Icon\Icon;
+
+// Your bell on iOS, the outlined Material bell on Android
+NavAction::make('alerts')->icon(Icon::custom('bell'), android: AndroidOutlined::Notifications);
+
+// The system bell on Android, your bell on iOS
+NavAction::make('alerts')->icon('bell', ios: Icon::custom('bell'));
+```
+
+In Blade, pass the string:
+
+@verbatim
+```blade static
+
+```
+@endverbatim
+
+### Which SVGs work
+
+SVGs downloaded from Lucide, [Tabler](https://tabler.io/icons) and similar stroked sets, or from
+[Material Symbols](https://fonts.google.com/icons), work as they are. NativePHP supports:
+
+- `path`, `circle`, `ellipse`, `line`, `rect` (including rounded corners), `polyline` and `polygon`, with or without
+ `g` groups around them
+- Strokes as well as fills. Lucide's style of `stroke-width="2"`, round caps and joins, `fill="none"` and
+ `currentColor` is kept
+- The presentation attributes `fill`, `stroke`, `stroke-width`, `stroke-linecap`, `stroke-linejoin`,
+ `stroke-miterlimit`, `fill-rule`, `opacity`, `fill-opacity` and `stroke-opacity`, set on the shape or inherited from
+ a parent `svg` or `g`. They also work inside a `style` attribute
+- Any `viewBox`. Icons are scaled so their longer side is 24pt on iOS and 24dp on Android, so a 24×24 Lucide icon and
+ a 960×960 Material Symbols icon come out the same size. A file without a `viewBox` needs numeric `width` and `height`
+
+`title`, `desc`, comments and an empty `defs` are ignored. Because icons are tinted in one color, the color in the file
+doesn't matter as long as there's only one: `currentColor`, `black` and any single hex or `rgb()` color all become the
+tint.
+
+Anything else fails the build with an error that names the file, the element and the attribute. That includes:
+
+- `transform` attributes
+- Gradients, patterns, masks, clip paths and filters
+- `use`, `symbol` and `marker` elements, and anything inside `defs`
+- `text`, `image` and `foreignObject` elements
+- `