@@ -95,23 +106,23 @@ Any `url` that doesn't match a registered native route will exit to the web view
## Builder API
When a `
` is supplied by a [layout](../the-basics/layouts), you build it fluently with the `TabBar`
-and `Tab` builders rather than writing it in Blade.
+and `Tab` builders rather than writing it in Blade. This is the same bar as the [Overview](#overview) example:
```php
use Native\Mobile\Edge\Layouts\Builders\Tab;
use Native\Mobile\Edge\Layouts\Builders\TabBar;
TabBar::make()
- ->dark()
- ->activeColor('#0891b2')
->labelVisibility('labeled')
- ->backgroundColor('#0F172A')
- ->textColor('#94A3B8')
- ->add(Tab::link('Chats', '/syncup', icon: 'chat_bubble')->badge('2'))
- ->add(Tab::link('Friends', '/syncup/friends', icon: 'person.3.fill')->news())
- ->add(Tab::link('Profile', '/syncup/profile', icon: 'person')->active());
+ ->activeColor('#0891b2')
+ ->add(Tab::link('Home', '/home', icon: 'home')->active())
+ ->add(Tab::link('Friends', '/friends', icon: 'person.3.fill')->news())
+ ->add(Tab::link('Profile', '/profile', icon: 'person')->badge('3'));
```
+To force a dark bar regardless of the system theme, chain `->dark()` — or take full control with
+`->backgroundColor('#0F172A')->textColor('#94A3B8')`, which wins over `dark()`'s default.
+
### `TabBar` methods
- `make()` - Create a new builder
@@ -126,7 +137,41 @@ TabBar::make()
- `link(string $label, string $url, ?string $icon = null)` - Build a tab. The id defaults to the label slugified
- `id(string $id)` - Override the auto-generated id
-- `icon(string $icon)` - A named [icon](icons)
+- `icon(string $icon)` - A named [icon](icon#icon-name-reference)
- `badge(string $badge, ?string $color = null)` - Show a numeric/text badge
- `news(bool $news = true)` - Show a red dot indicator
- `active(bool $active = true)` - Mark this tab as active
+
+## Per-screen tab bar
+
+Screens can adjust their layout's tab bar for the current screen by overriding `tabBarOptions()`. Non-null fields
+override the layout's defaults; null fields fall through. This is the tab-bar parallel to the top bar's
+[`navigationOptions()`](top-bar#per-screen-overrides). Per-screen tab content edits (inserting or removing tabs) are
+out of scope — define your tabs once at the layout level.
+
+```php
+use Native\Mobile\Edge\Layouts\Builders\TabBarOptions;
+use Native\Mobile\Edge\NativeComponent;
+
+class ChatThread extends NativeComponent
+{
+ public function tabBarOptions(): ?TabBarOptions
+ {
+ return TabBarOptions::make()
+ ->hidden() // hide the tab bar on this pushed detail screen
+ ->highlight('chats'); // keep the "Chats" tab lit while you're inside it
+ }
+}
+```
+
+### `TabBarOptions` methods
+
+- `make()` - Create a new builder
+- `hidden(bool $hidden = true)` - Hide the tab bar on this screen — the pushed-detail pattern
+- `highlight(string $tabId)` - Force a tab id to render as active, even when the screen's URL doesn't match any tab (e.g. a search-results screen reached from the Search tab)
+- `activeColor(string $color)` - Color of the active tab's icon and label on this screen
+- `backgroundColor(string $color)` - Bar background color on this screen
+
+For the common "hide the tab bar on this detail screen" case, the shorter `protected bool $hidesTabBar = true;`
+property on the screen is equivalent to `TabBarOptions::make()->hidden()`. Use either; if both are set, the explicit
+builder wins.
diff --git a/resources/views/docs/mobile/4/edge-components/bottom-sheet.md b/resources/views/docs/mobile/4/edge-components/bottom-sheet.md
index 128bf064..4bbdd035 100644
--- a/resources/views/docs/mobile/4/edge-components/bottom-sheet.md
+++ b/resources/views/docs/mobile/4/edge-components/bottom-sheet.md
@@ -9,7 +9,7 @@ A modal panel that slides up from the bottom of the screen. Use it for contextua
that overlay the main content. Renders as SwiftUI's `.sheet` with `presentationDetents` on iOS and a Material3
`ModalBottomSheet` on Android.
-Per Model 3, the container color resolves from `theme.surface`. For a custom surface wrap content in a
+Per Material 3, the container color resolves from `theme.surface`. For a custom surface wrap content in a
``.
@verbatim
@@ -21,7 +21,7 @@ Per Model 3, the container color resolves from `theme.surface`. For a custom sur
- Sheet Title
+ Sheet Title
Sheet content goes here.
@@ -40,6 +40,7 @@ Per Model 3, the container color resolves from `theme.surface`. For a custom sur
- `full` (100% of screen)
- A numeric fraction `0.0`–`1.0` for a custom height (e.g. `"0.4"` for 40%)
- `a11y-label` - Accessibility label (optional)
+- `a11y-hint` - Accessibility hint (optional)
## Events
@@ -63,21 +64,21 @@ Accepts any EDGE elements as children. The children are rendered inside the shee
-
-
- Edit
+
+
+ Edit
-
-
- Share
+
+
+ Share
-
+
Delete
@@ -103,10 +104,10 @@ Accepts any EDGE elements as children. The children are rendered inside the shee
- Add Item
+ Add Item
-
+
@@ -157,4 +158,5 @@ BottomSheet::make()
- `visible(bool $value = true)` - Toggle visibility
- `detents(string $detents)` - Allowed heights
- `a11yLabel(string $value)` - Accessibility label
+- `a11yHint(string $value)` - Accessibility hint
- `onDismiss(string $method)` - Component method invoked on dismissal
diff --git a/resources/views/docs/mobile/4/edge-components/button-group.md b/resources/views/docs/mobile/4/edge-components/button-group.md
index 3ac9a8eb..f71a5d32 100644
--- a/resources/views/docs/mobile/4/edge-components/button-group.md
+++ b/resources/views/docs/mobile/4/edge-components/button-group.md
@@ -13,16 +13,24 @@ Use this for short, mutually-exclusive choices that fit on one row. For more opt
@verbatim
```blade
+@php $period = 0; @endphp
+
+
+Showing {{ ['Daily', 'Weekly', 'Monthly'][$period] }} stats
```
@endverbatim
+`period` is a public int property on your component — the `@php` line stands in for `public int $period = 0;`. Tapping a segment syncs the new index back automatically, so anything echoing `$period` re-renders.
+
## Props
- `options` - Array of option labels (required, array of strings)
- `value` / `selected-index` - Currently selected index (optional, int, default: `0`)
- `disabled` - Disable the group (optional, boolean, default: `false`)
+- `sync-mode` - How selection changes sync back to your component: `live | blur | debounce` (optional, usually set by `native:model` modifiers)
- `a11y-label` - Accessibility label (optional)
+- `a11y-hint` - Accessibility hint (optional)
## Events
@@ -30,11 +38,16 @@ Use this for short, mutually-exclusive choices that fit on one row. For more opt
## Two-way Binding
-`native:model` binds the selected index to an integer property on your component:
+`native:model` binds the selected index to an integer property on your component — declare it as
+`public int $planTier = 1;` and the group keeps it in sync:
@verbatim
```blade
+@php $planTier = 1; @endphp
+
+
+Selected plan: {{ $tiers[$planTier] }}
```
@endverbatim
@@ -42,24 +55,38 @@ Use this for short, mutually-exclusive choices that fit on one row. For more opt
### Period picker
+`reportRange` is a public int property on your component (`public int $reportRange = 2;`):
+
@verbatim
```blade
+@php $reportRange = 2; @endphp
+
+
+Report range: {{ ['Day', 'Week', 'Month', 'Year'][$reportRange] }}
```
@endverbatim
### With manual change handler
+Instead of `native:model`, pass the current index with `:value` and handle changes yourself.
+`$difficulty` is a public int property on your component, and `setDifficulty(int $index)` is the
+method that receives the new index — assign it to `$difficulty` there:
+
@verbatim
```blade
+@php $difficulty = 1; @endphp
+
+
+Difficulty: {{ ['Easy', 'Medium', 'Hard'][$difficulty] }}
```
@endverbatim
@@ -79,5 +106,6 @@ ButtonGroup::make()
- `selectedIndex(int $index)` - Currently selected index
- `disabled(bool $value = true)` - Disable the group
- `a11yLabel(string $value)` - Accessibility label
+- `a11yHint(string $value)` - Accessibility hint
- `syncMode(string $mode)` - `live | blur | debounce` (set by `native:model` modifiers)
- `onChange(string $method)` - Component method invoked on change
diff --git a/resources/views/docs/mobile/4/edge-components/button.md b/resources/views/docs/mobile/4/edge-components/button.md
index 5bf9adfa..f9bf3bff 100644
--- a/resources/views/docs/mobile/4/edge-components/button.md
+++ b/resources/views/docs/mobile/4/edge-components/button.md
@@ -17,20 +17,32 @@ control drop to a [``](pressable) wrapping your own content.
```
@endverbatim
+`@press` names a public method on your component — tapping this button calls `handleStart()`.
+
## Props
The label can be passed as the `label` attribute or as slot content between the tags. If both are set, `label` wins.
+Slot content is treated as plain text — nested tags are stripped and whitespace is collapsed. Use the `icon` /
+`icon-trailing` props to add icons rather than nesting elements in the slot.
- `label` - Button text (optional if using slot content)
-- `variant` - Semantic style: `primary` (default), `secondary`, `destructive`, `ghost`
+- `variant` - Semantic style: `primary` (default), `secondary`, `destructive`, `ghost`. Each fills its theme
+ token solid; for a softer tonal fill, set opacity on the token itself (e.g. `'secondary' => 'fuchsia-500/70'`
+ in `config/native-ui.php`) — see [Theming](../digging-deeper/theming)
- `size` - `sm`, `md` (default), `lg`
-- `icon` - A leading [icon](icons) name (optional)
-- `icon-trailing` - A trailing [icon](icons) name (optional)
-- `disabled` - Disable the button (optional, boolean, default: `false`)
-- `loading` - Show a spinner in place of the leading icon and prevent presses (optional, boolean, default: `false`)
+- `icon` - A leading [icon](icon#icon-name-reference) name (optional)
+- `icon-trailing` - A trailing [icon](icon#icon-name-reference) name (optional)
+- `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)
+- `line-height-px` - Label line height as an absolute value in pixels (optional, float)
+- `disabled` - Disable the button (optional, boolean, default: `false`). Disabled buttons render with the theme's
+ `surface-variant` fill and `on-surface-variant` label on both platforms
+- `loading` - Show a spinner in place of the leading icon and prevent presses (optional, boolean, default: `false`).
+ Styled like `disabled` while the spinner runs
- `a11y-label` - Accessibility label override (optional)
- `a11y-hint` - Accessibility hint (optional)
-- `menu` - Attach a tap-to-open dropdown; opening the menu shadows `@press`. See [Menus](menus)
+- `menu` - Attach a tap-to-open dropdown menu — an array of [`NavAction`](menus) items. Tapping opens the menu
+ instead of firing `@press`. See [Menus](menus)
## Events
@@ -63,7 +75,7 @@ attributes are intentionally dropped before reaching the renderer.
@verbatim
```blade
-
+
@@ -120,8 +132,11 @@ Button::make('Save')
- `make(string $label = '')` - Create a button with an optional label
- `variant(string $value)` - `primary | secondary | destructive | ghost`
- `size(string $value)` - `sm | md | lg`
-- `icon(string $name)` - Leading icon
-- `iconTrailing(string $name)` - Trailing icon
+- `font(string $name)` - Custom label font (file token or config alias)
+- `icon(?string $name = null, IosSymbol|string|null $ios = null, AndroidSymbol|string|null $android = null)` -
+ Leading icon; pass `ios:` / `android:` for per-platform symbols
+- `iconTrailing(?string $name = null, IosSymbol|string|null $ios = null, AndroidSymbol|string|null $android = null)` -
+ Trailing icon; pass `ios:` / `android:` for per-platform symbols
- `disabled(bool $value = true)` - Disable the button
- `loading(bool $value = true)` - Show a spinner and prevent presses
- `a11yLabel(string $value)` - Accessibility label override
diff --git a/resources/views/docs/mobile/4/edge-components/canvas.md b/resources/views/docs/mobile/4/edge-components/canvas.md
index 63c56d63..427ed517 100644
--- a/resources/views/docs/mobile/4/edge-components/canvas.md
+++ b/resources/views/docs/mobile/4/edge-components/canvas.md
@@ -10,13 +10,16 @@ children stack vertically by default. Use it as a semantic wrapper when grouping
@verbatim
```blade
-
-
+
+
```
@endverbatim
+Shapes take their fill from a `bg` attribute or any `bg-*` class (including `bg-theme-*` tokens), and corner
+rounding from `rounded-*` classes.
+
## Props
All [shared layout and style attributes](layout) are supported. There are no canvas-specific props.
@@ -29,6 +32,66 @@ Accepts any EDGE elements as children. Typically used with [shape primitives](sh
For overlay-style layering of shapes use a [``](stack) instead — `` arranges children
along the column main axis, which is rarely what you want for free-form drawing.
+## Examples
+
+### Mini bar chart
+
+Shapes plus flex layout are enough for lightweight data graphics. A bottom-aligned row of rects with varying
+heights makes a bar chart — vary the `opacity-*` class to get a tonal ramp from a single theme color:
+
+@verbatim
+```blade
+
+
+
+
+
+
+
+
+```
+@endverbatim
+
+In a real app you'd generate the rects with `@@foreach` over a public array property on your component and
+compute each `:height` from the data point.
+
+### Pulsing beacon
+
+Transform attributes (`:scale`, `:rotate`, `:translate-x`, `:translate-y`) combine with `animate-loop` and
+`:animate-duration` to produce continuous, auto-reversing animations. Layer an animated circle behind a static
+one in a stack to get a pulsing status beacon:
+
+@verbatim
+```blade
+
+
+
+
+
+
+```
+@endverbatim
+
+The loop oscillates the outer circle between its resting state and the declared `:scale` (and `opacity-*`),
+reversing each cycle. `:animate-duration` is in milliseconds; add `animate-easing` to change the curve.
+
+### Concentric rings
+
+Layering same-center circles of decreasing size in a stack gives a bullseye — again using opacity steps of one
+theme color so it works in both light and dark mode:
+
+@verbatim
+```blade
+
+
+
+
+
+
+
+```
+@endverbatim
+
## Element
```php
diff --git a/resources/views/docs/mobile/4/edge-components/carousel.md b/resources/views/docs/mobile/4/edge-components/carousel.md
index 033b0639..16f6eb60 100644
--- a/resources/views/docs/mobile/4/edge-components/carousel.md
+++ b/resources/views/docs/mobile/4/edge-components/carousel.md
@@ -12,8 +12,8 @@ stack with `item-spacing` between items.
```blade
@foreach($posts as $post)
-
- {{ $post->title }}
+
+ {{ $post->title }}
{{ $post->excerpt }}
@endforeach
@@ -26,11 +26,13 @@ stack with `item-spacing` between items.
- `item-width` - Width of each child in dp (optional, float, default: `200`)
- `item-spacing` - Spacing between items in dp (optional, float, default: `8`)
- `variant` - Reserved for future variants (optional, string)
+- `a11y-label` - Accessibility label (optional)
+- `a11y-hint` - Accessibility hint (optional)
## Children
-Accepts any EDGE elements as children. Each child is clipped to a 16dp rounded rectangle by the renderer, so
-border-radius styling on the child itself is optional.
+Accepts any EDGE elements as children. Each child is clipped to a rounded rectangle by the renderer — 16pt on iOS,
+and the Material `extraLarge` shape (~28dp) on Android — so border-radius styling on the child itself is optional.
@@ -64,14 +66,23 @@ wrapping a [``](row).
@foreach($contacts as $contact)
-
- {{ $contact->name }}
+
+
+ {{ $contact->initials }}
+
+
+
+ {{ $contact->name }}
@endforeach
```
@endverbatim
+Use `rounded-full` (a class) to make the avatar circular — the `border-radius` *attribute* is not
+read on images. The initials layer underneath the image acts as a fallback: it shows while the
+avatar is loading, or whenever `$contact->avatar` is empty.
+
## Element
```php
@@ -86,3 +97,5 @@ Carousel::make($child1, $child2, $child3)
- `itemWidth(float $width)` - Width per item
- `itemSpacing(float $spacing)` - Spacing between items
- `variant(string $variant)` - Variant identifier (reserved)
+- `a11yLabel(string $value)` - Accessibility label
+- `a11yHint(string $value)` - Accessibility hint
diff --git a/resources/views/docs/mobile/4/edge-components/checkbox.md b/resources/views/docs/mobile/4/edge-components/checkbox.md
index e5671fa2..e1254dd2 100644
--- a/resources/views/docs/mobile/4/edge-components/checkbox.md
+++ b/resources/views/docs/mobile/4/edge-components/checkbox.md
@@ -9,19 +9,28 @@ A binary tick/untick control with an optional inline label. On iOS, renders as a
(`checkmark.square.fill` / `square`) — SwiftUI has no native checkbox primitive. On Android, renders as a Material3
`Checkbox`.
-Per Model 3, check/border/label colors come from the theme — no per-instance overrides.
+Per Material 3, check/border/label colors come from the theme — no per-instance overrides.
@verbatim
```blade
+@php $agreed = false; @endphp
+
+
+{{ $agreed ? 'Thanks for agreeing!' : 'Tap the box to agree' }}
```
@endverbatim
+Here `agreed` is a public boolean property on your component — the `@php` line stands in for
+`public bool $agreed = false;`. Toggling the box syncs the new state back automatically.
+
## Props
- `value` - Current checked state (optional, boolean, default: `false`)
- `label` - Inline label rendered to the right of the box (optional, string)
- `disabled` - Disable the checkbox (optional, boolean, default: `false`)
+- `sync-mode` - `live | blur | debounce` (optional, string; set by `native:model` modifiers)
+- `debounce-ms` - Debounce interval when `sync-mode` is `debounce` (optional, int)
- `a11y-label` - Accessibility label (optional)
- `a11y-hint` - Accessibility hint (optional)
@@ -29,30 +38,56 @@ Per Model 3, check/border/label colors come from the theme — no per-instance o
- `@change` - Component method called when toggled. Receives the new boolean value as a parameter
+
+
+Margin classes position the checkbox; the check, border, and label colors come from the theme.
+
+
+
## Two-way Binding
-Use `native:model` for automatic two-way binding with a boolean property on your component.
+Use `native:model` for automatic two-way binding with a public boolean property on your component. The `live`,
+`blur`, and `debounce` modifiers set `sync-mode` (and `debounce-ms`) for you, though for a discrete tap every
+toggle is a single event.
@verbatim
```blade
-
+@php $subscribed = true; @endphp
+
+
+
+{{ $subscribed ? 'You are subscribed' : 'Not subscribed' }}
```
@endverbatim
+`subscribed` is a public boolean property on your component (the `@php` line stands in for
+`public bool $subscribed = true;`). Every toggle syncs the new value back, so the `@{{ $subscribed }}` echo
+updates as soon as you tap.
+
## Examples
### Multiple options
@verbatim
```blade
+@php
+ $emailNotifications = true;
+ $smsNotifications = false;
+ $pushNotifications = true;
+@endphp
+
+
+ Enabled: {{ ($emailNotifications ? 1 : 0) + ($smsNotifications ? 1 : 0) + ($pushNotifications ? 1 : 0) }} of 3
```
@endverbatim
+Each checkbox binds its own public boolean property; the summary line re-renders on every toggle.
+
### Disabled
@verbatim
diff --git a/resources/views/docs/mobile/4/edge-components/chip.md b/resources/views/docs/mobile/4/edge-components/chip.md
index 9166ce76..0e3d88e0 100644
--- a/resources/views/docs/mobile/4/edge-components/chip.md
+++ b/resources/views/docs/mobile/4/edge-components/chip.md
@@ -8,19 +8,25 @@ order: 200
A compact selectable tag with a boolean active state and an optional leading icon. Renders as a capsule.
When selected, the chip fills with `theme.primary` and uses `theme.onPrimary` for content. When unselected, it uses
-`theme.surfaceVariant` with a `theme.outline` 1pt stroke. Per Model 3 — no per-instance color overrides.
+`theme.surfaceVariant` with a `theme.outline` 1pt stroke. Colors come from the theme; the capsule radius defaults to
+fully rounded and can be adjusted with `rounded-*` classes.
@verbatim
```blade
+@php $filterVerified = false; @endphp
+
```
@endverbatim
+`filterVerified` is a public boolean property on your component — the `@php` line stands in for
+`public bool $filterVerified = false;`.
+
## Props
-- `label` - Chip text (required, string). Can also be passed as the first argument to `make()`
+- `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](icons) name (optional, string)
+- `icon` - Leading [icon](icon#icon-name-reference) name (optional, string)
- `disabled` - Disable the chip (optional, boolean, default: `false`)
- `a11y-label` - Accessibility label (optional)
- `a11y-hint` - Accessibility hint (optional)
@@ -35,17 +41,28 @@ When selected, the chip fills with `theme.primary` and uses `theme.onPrimary` fo
@verbatim
```blade
+@php $filterOnSale = false; @endphp
+
+
+
+ {{ $filterOnSale ? 'Showing sale items only' : 'Showing everything' }}
+
```
@endverbatim
+Toggling the chip syncs the new boolean back to `filterOnSale` automatically, and anything that reads the
+property — like the `@{{ $filterOnSale ? ... }}` echo above — re-renders with the new value.
+
## Examples
### Filter chip row
@verbatim
```blade
-
+@php $filter = 'all'; @endphp
+
+
@@ -53,10 +70,15 @@ When selected, the chip fills with `theme.primary` and uses `theme.onPrimary` fo
```
@endverbatim
+Here `$filter` is a public string property and `setFilter()` is a public method on your component that assigns it —
+driving `selected` from one property keeps the row single-select.
+
### With icon
@verbatim
```blade
+@php $onlyVerified = false; @endphp
+
```
@endverbatim
@@ -75,7 +97,8 @@ Chip::make('Verified')
- `make(string $label = '')` - Create a chip with an optional label
- `label(string $label)` - Set the chip text
- `selected(bool $selected = true)` - Active state
-- `icon(string $icon)` - Leading icon
+- `icon(?string $name = null, IosSymbol|string|null $ios = null, AndroidSymbol|string|null $android = null)` -
+ Leading icon; pass `ios:` / `android:` for per-platform symbols
- `disabled(bool $value = true)` - Disable the chip
- `a11yLabel(string $value)`, `a11yHint(string $value)` - Accessibility
- `syncMode(string $mode)` - Set by `native:model` modifiers
diff --git a/resources/views/docs/mobile/4/edge-components/column.md b/resources/views/docs/mobile/4/edge-components/column.md
index 67ad083d..b4667d7d 100644
--- a/resources/views/docs/mobile/4/edge-components/column.md
+++ b/resources/views/docs/mobile/4/edge-components/column.md
@@ -11,10 +11,10 @@ and serves as the foundation for most screen layouts — think of it as the mobi
@verbatim
```blade
- First item
- Second item
- Third item
-
+ First item
+ Second item
+ Third item
+
```
@endverbatim
@@ -42,8 +42,23 @@ Everything else from the shared list applies the same as on any element (`w-*`,
### Full-screen layout with safe area
+A column at the page root typically fills the screen and pushes actions to the bottom with a spacer:
+
@verbatim
```blade
+
+ My App
+
+
+
+```
+@endverbatim
+
+The full-screen version below adds `safe-area` at the page root so content clears the notch and home
+indicator — run it in your app to see it edge-to-edge:
+
+@verbatim
+```blade static
My App
@@ -58,7 +73,7 @@ Everything else from the shared list applies the same as on any element (`w-*`,
```blade
- Loading...
+ Loading...
```
@endverbatim
@@ -70,7 +85,7 @@ Everything else from the shared list applies the same as on any element (`w-*`,
Section Title
Surface description goes here.
-
+
@@ -80,16 +95,21 @@ Everything else from the shared list applies the same as on any element (`w-*`,
### Space-between distribution
+`justify-between` spreads children across the column's height, placing the leftover space between them:
+
@verbatim
```blade
-
- Top
- Middle
- Bottom
+
+ Top
+ Middle
+ Bottom
```
@endverbatim
+Distribution needs a bounded height to work with — on a real screen you would typically use `h-full` at the
+page root; the fixed `h-[220]` here just gives the preview a bounded height to distribute.
+
## Element
```php
diff --git a/resources/views/docs/mobile/4/edge-components/divider.md b/resources/views/docs/mobile/4/edge-components/divider.md
index f8f94149..bbe359f4 100644
--- a/resources/views/docs/mobile/4/edge-components/divider.md
+++ b/resources/views/docs/mobile/4/edge-components/divider.md
@@ -14,7 +14,8 @@ the platform separator color (`UIColor.separator` on iOS, Material `outlineVaria
```
@endverbatim
-` ` is an alias of ` ` exposed for use inside [side navigation](side-nav).
+` ` is an equivalent divider component exposed for use inside [side navigation](side-nav). It
+emits its own `horizontal_divider` element but renders the same visual rule as ` `.
@@ -44,11 +45,11 @@ The classes that affect how a divider renders:
@verbatim
```blade
- Section One
- Some content here.
+ Section One
+ Some content here.
- Section Two
- More content here.
+ Section Two
+ More content here.
```
@endverbatim
@@ -68,7 +69,7 @@ The classes that affect how a divider renders:
@foreach($items as $item)
- {{ $item->name }}
+ {{ $item->name }}
@unless($loop->last)
@@ -91,7 +92,8 @@ The classes that affect how a divider renders:
```php
use Native\Mobile\Edge\Elements\Divider;
-Divider::make()->borderColor('#E2E8F0');
+Divider::make()->border(1, '#E2E8F0');
```
- `make()` - Create a divider
+- `border(float $width, string $color)` - Set the line width and color
diff --git a/resources/views/docs/mobile/4/edge-components/gesture-area.md b/resources/views/docs/mobile/4/edge-components/gesture-area.md
index 37edc864..63da331e 100644
--- a/resources/views/docs/mobile/4/edge-components/gesture-area.md
+++ b/resources/views/docs/mobile/4/edge-components/gesture-area.md
@@ -10,10 +10,10 @@ Captures a vertical pan/drag gesture over its content and writes the translation
Children render normally — gesture detection wraps the whole content frame.
@verbatim
-```blade
+```blade static
@php $drag = \Native\Mobile\Edge\SharedValue::make(); @endphp
-
+
Drag me
@@ -21,29 +21,20 @@ Children render normally — gesture detection wraps the whole content frame.
```
@endverbatim
+This example needs a real app to try out: the drag runs entirely on the UI thread against a live
+`SharedValue` bound from your component, so there's no inline preview here — drop the snippet into a
+screen in your app and drag the card.
+
## Props
- `pan-y` - A [`SharedValue`](../digging-deeper/gestures) that receives the vertical drag translation (required for
the gesture to do anything). Bind it, then read it from animatable props (`translate-y`, `opacity`, `scale`, …)
on the children.
-## Events
-
-- `@drag-end` - Fired when the user lifts their finger, with the final value as `{value: float}`. Use it to decide
- commit-vs-revert in PHP:
-
-```php
-public function onRelease(float $value): void
-{
- if ($value > 150) {
- $this->dismiss();
- }
-}
-```
-
-Per-frame drag values stay on the native side and never round-trip through PHP — only `@drag-end` calls back. See
-[Gestures & Animation](../digging-deeper/gestures) for shared values and interpolation formulas.
+Per-frame drag values stay on the native side and drive the bound props on the UI thread — nothing round-trips
+through PHP during the gesture. See [Gestures & Animation](../digging-deeper/gestures) for shared values and
+interpolation formulas.
diff --git a/resources/views/docs/mobile/4/edge-components/icon.md b/resources/views/docs/mobile/4/edge-components/icon.md
index 47e40a70..91e72f92 100644
--- a/resources/views/docs/mobile/4/edge-components/icon.md
+++ b/resources/views/docs/mobile/4/edge-components/icon.md
@@ -5,12 +5,14 @@ order: 230
## Overview
-Displays a platform-native icon. On iOS, icons render as SF Symbols. On Android, icons render as Material Icons.
-A smart mapping system translates common icon names across platforms automatically.
+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.
@verbatim
```blade
-
+
```
@endverbatim
@@ -18,19 +20,21 @@ A smart mapping system translates common icon names across platforms automatical
All [shared layout and style attributes](layout) are supported, plus:
-- `name` - Icon name (required unless `ios`/`android` are given, string). See the [Icons](icons) reference
-- `ios` / `android` - Per-platform overrides: an [SF Symbol](icons) name for iOS and a [Material Icon](icons) name
+- `name` - Icon name (required unless `ios`/`android` are given, string). See the [Icon name reference](#icon-name-reference)
+- `ios` / `android` - Per-platform overrides: an [SF Symbol](#ios-sf-symbols) name for iOS and a [Material Icon](#android-material-icons) name
for Android, so one tag renders the right symbol on each platform. Use in place of `name` when the platforms
- need different icons (` `)
+ need different icons (` `). When bound with `:ios` / `:android`,
+ these also accept enum cases directly — see [Typed icon enums](#typed-icon-enums)
- `size` - Icon size in dp (optional, float, default: `24`)
- `color` - Icon color as hex string (optional, default: platform default)
+- `dark-color` - Icon color when the device is in dark mode, as a hex string (optional). Overrides `color` in dark mode
- `a11y-label` - Accessibility label (optional). Icons are decorative by default — hidden from screen readers
unless this is set. Label any icon that conveys meaning on its own. See [Accessibility](../digging-deeper/accessibility)
-` ` is a self-closing element. It does not accept children. For a complete list of available icon names
-and platform-specific usage, see the [Icons](icons) reference page.
+` ` is a self-closing element. It does not accept children. For a complete list of icon names guaranteed
+to work on both platforms, see the [Icon name reference](#icon-name-reference) below.
@@ -41,10 +45,10 @@ and platform-specific usage, see the [Icons](icons) reference page.
@verbatim
```blade
-
-
-
-
+
+
+
+
```
@endverbatim
@@ -65,32 +69,342 @@ and platform-specific usage, see the [Icons](icons) reference page.
@verbatim
```blade
-
+
No messages
```
@endverbatim
-### Platform-specific icon
+### Platform-specific icons
+
+Give each platform its own symbol with the `ios` / `android` attributes — resolution happens per platform, so one
+tag renders the right icon on each:
+
+@verbatim
+```blade
+
+```
+@endverbatim
+
+### Typed icon enums
+
+Bind `:ios` / `:android` to pass enum cases instead of strings for typed, autocompletable symbol names. Import
+the enums into your Blade view with `@@use` first — compiled views have no namespace, so a bare `Ios::Gearshape`
+won't resolve:
+
+@verbatim
+```blade
+@use('App\Icons\Ios')
+@use('App\Icons\Android')
+
+
+```
+@endverbatim
+
+The `AndroidOutlined` enum renders the outlined Material style instead of the filled one:
+
+@verbatim
+```blade
+@use('App\Icons\Ios')
+@use('App\Icons\AndroidOutlined')
+
+
+```
+@endverbatim
+
+You can also combine a shared `name` with a per-platform enum override:
+
+@verbatim
+```blade
+@use('App\Icons\Ios')
+
+
+```
+@endverbatim
+
+If you'd rather skip the `@@use` import, fully-qualified cases work anywhere:
+`:ios="\App\Icons\Ios::Gearshape"`.
+
+
+
+Three icon enums are generated into your app by the [native-ui](https://github.com/nativephp/native-ui) plugin:
+`App\Icons\Ios` (SF Symbols), `App\Icons\Android` (filled Material Icons), and `App\Icons\AndroidOutlined`
+(outlined Material Icons — its cases tell the renderer to use the outlined Material font). Run the command below
+once to create them, then reference any symbol as a typed, autocompletable case:
+
+```shell
+php artisan native-ui:generate-icons
+```
+
+
+
+## How names resolve
+
+Every icon name — whether passed to `` or to the `icon` attribute of any other EDGE component — goes
+through a four-tier resolution strategy:
+
+1. **Direct platform icons** - On iOS, if the name contains a `.` it's used as a direct SF Symbol path (e.g., `car.side.fill`). On Android, any Material Icon ligature name works directly (e.g., `shopping_cart`)
+2. **Manual mapping** - Explicit mappings for common icons and aliases (e.g., `home`, `settings`, `user`)
+3. **Smart fallback** - Normalizes unmapped icon names to a platform equivalent
+4. **Default fallback** - Uses a circle icon if no match is found
+
+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.
+
+### iOS (SF Symbols)
+
+On iOS, icons render as SF Symbols. Manual mappings convert common icon names to their SF Symbol equivalents.
+For example:
+
+- `home` → `house.fill`
+- `settings` → `gearshape.fill`
+- `check` → `checkmark.circle.fill`
+
+Any name containing a `.` bypasses the mapping and is used as a direct SF Symbol path. Dotted paths are iOS-only,
+so pair them with an `android` override:
@verbatim
```blade
-
+```
+@endverbatim
+
+If a name isn't manually mapped and isn't a dotted path, the smart fallback lower-cases it and strips dashes and
+underscores (`archive-box` → `archivebox`). It deliberately does **not** guess filled or circled variations — many
+SF Symbols ship their plain glyph at the bare name, so when you want the filled variant, ask for it explicitly
+(`archivebox.fill`, or the `Ios::ArchiveboxFill` enum case).
+
+### Android (Material Icons)
+
+On Android, icons render using a lightweight font-based approach that supports the entire Material Icons library. You
+can use any Material Icon by its ligature name directly (e.g., `shopping_cart`, `qr_code_2`) — no mapping required.
+
+Manual mappings provide convenient aliases for common icon names. For example:
+
+- `home` → `home`
+- `settings` → `settings`
+- `check` → `check`
+- `cart` → `shopping_cart`
+
+## Icons in other components
+
+Every EDGE component with an `icon` attribute resolves names through this same system — pass it the same names you
+would give ``:
+
+@verbatim
+```blade static
+
```
@endverbatim
+The PHP Element builders take per-platform overrides everywhere too: any builder with an icon accepts
+`icon(?string $name = null, IosSymbol|string|null $ios = null, AndroidSymbol|string|null $android = null)`, so you can
+pass a shared name, `ios:` / `android:` named arguments, or both.
+
+For Blade components whose tag only takes a single `icon` string, pick the platform variant with the `System` facade:
+
+@verbatim
+```blade static
+
+```
+@endverbatim
+
+## Icon name reference
+
+All icons listed here are manually mapped and guaranteed to work consistently across iOS and Android.
+
+### Navigation
+
+| Icon | Description |
+|------|-------------|
+| `dashboard` | Grid-style dashboard view |
+| `home` | House/home screen |
+| `menu` | Three-line hamburger menu |
+| `settings` | Gear/settings |
+| `account`, `profile`, `user` | User account or profile |
+| `person` | Single person |
+| `people`, `connections`, `contacts` | Multiple people |
+| `group`, `groups` | Group of people |
+
+### Business & Commerce
+
+| Icon | Description |
+|------|-------------|
+| `orders`, `receipt` | Receipt or order |
+| `cart`, `shopping` | Shopping cart |
+| `shop`, `store` | Store or storefront |
+| `products`, `inventory` | Products or inventory |
+
+### Charts & Data
+
+| Icon | Description |
+|------|-------------|
+| `chart`, `barchart` | Bar chart |
+| `analytics` | Analytics/analysis |
+| `summary`, `report`, `assessment` | Summary or report |
+
+### Time & Scheduling
+
+| Icon | Description |
+|------|-------------|
+| `clock`, `schedule`, `time` | Clock or time |
+| `calendar` | Calendar |
+| `history` | History or recent |
+
+### Actions
+
+| Icon | Description |
+|------|-------------|
+| `add`, `plus` | Add or create new |
+| `edit` | Edit or modify |
+| `delete` | Delete or remove |
+| `save` | Save |
+| `search` | Search |
+| `filter` | Filter |
+| `refresh` | Refresh or reload |
+| `share` | Share |
+| `download` | Download |
+| `upload` | Upload |
+
+### Communication
+
+| Icon | Description |
+|------|-------------|
+| `notifications` | Notifications or alerts |
+| `message` | Message or SMS |
+| `email`, `mail` | Email |
+| `chat` | Chat or conversation |
+| `phone` | Phone or call |
+
+### Navigation Arrows
+
+| Icon | Description |
+|------|-------------|
+| `back` | Back or previous |
+| `forward` | Forward or next |
+| `up` | Up arrow |
+| `down` | Down arrow |
+
+### Status
+
+| Icon | Description |
+|------|-------------|
+| `check`, `done` | Check or complete |
+| `close` | Close or dismiss |
+| `warning` | Warning |
+| `error` | Error |
+| `info` | Information |
+
+### Authentication
+
+| Icon | Description |
+|------|-------------|
+| `login` | Login |
+| `logout`, `exit` | Logout or exit |
+| `lock` | Locked |
+| `unlock` | Unlocked |
+
+### Content
+
+| Icon | Description |
+|------|-------------|
+| `favorite`, `heart` | Favorite or like |
+| `star` | Star or rating |
+| `bookmark` | Bookmark |
+| `image`, `photo` | Image or photo |
+| `image-plus` | Add photo |
+| `video` | Video |
+| `folder` | Folder |
+| `folder-lock` | Locked folder |
+| `file`, `description` | Document or file |
+| `book-open` | Book |
+| `newspaper`, `news`, `article` | News or article |
+
+### Device & Hardware
+
+| Icon | Description |
+|------|-------------|
+| `camera` | Camera |
+| `qr`, `qrcode`, `qr-code` | QR code scanner |
+| `device-phone-mobile`, `smartphone` | Mobile phone |
+| `vibrate` | Vibration |
+| `bell` | Bell or notification |
+| `finger-print`, `fingerprint` | Fingerprint or biometric |
+| `light-bulb`, `lightbulb`, `flashlight` | Light bulb or flashlight |
+| `map`, `location` | Map or location |
+| `globe-alt`, `globe`, `web` | Globe or web |
+| `bolt`, `flash` | Lightning bolt or flash |
+
+### Audio & Volume
+
+| Icon | Description |
+|------|-------------|
+| `speaker`, `speaker-wave` | Speaker with sound |
+| `volume-up` | Volume up |
+| `volume-down` | Volume down |
+| `volume-mute`, `mute` | Muted |
+| `volume-off` | Volume off |
+| `music`, `audio`, `music-note` | Music or audio |
+| `microphone`, `mic` | Microphone |
+
+### Miscellaneous
+
+| Icon | Description |
+|------|-------------|
+| `help` | Help or question |
+| `about`, `information-circle` | Information or about |
+| `more` | More options |
+| `list` | List view |
+| `visibility` | Visible |
+| `visibility_off` | Hidden |
+
+## Finding icons
+
+Browse the complete Material Icons library at [Google Fonts Icons](https://fonts.google.com/icons). Use the icon name
+exactly as shown (with underscores, e.g., `shopping_cart`, `qr_code_2`).
+
+For the complete SF Symbols library, download the [SF Symbols app](https://developer.apple.com/sf-symbols/) for macOS.
+This [community Figma file](https://www.figma.com/community/file/1549047589273604548) is another great starting point,
+though not comprehensive.
+
+
+
+SF Symbol names use dots (e.g., `house.fill`), while Material Icon names use underscores (e.g., `shopping_cart`).
+
+
+
+Icons carry meaning that users recognize across apps, so stay consistent: use the same icon name for the same action
+throughout your app. And if you rely on auto-converted names, test that they appear correctly on both platforms.
+
## Element
```php
-use Nativephp\NativeUi\Elements\Icon;
+use App\Icons\Android;
+use App\Icons\Ios;
+use Native\Mobile\Edge\Elements\Icon;
Icon::make('home')->size(24)->color('#1E293B');
+
+// Per-platform symbols — a shared name, enum overrides, or both:
+Icon::make(ios: Ios::Gearshape, android: Android::Settings);
+Icon::make('share', ios: Ios::SquareAndArrowUp);
```
-- `make(string $name = '')` - Create an icon
+- `make(?string $name = null, IosSymbol|string|null $ios = null, AndroidSymbol|string|null $android = null)` -
+ Create an icon from a shared name, per-platform overrides, or both
+- `name(?string $name = null, IosSymbol|string|null $ios = null, AndroidSymbol|string|null $android = null)` -
+ Set the icon; pass `ios:` / `android:` named args for per-platform overrides
- `size(float $size)` - Icon size in dp
- `color(string $hex)` - Icon color
+- `darkColor(string $hex)` - Icon color in dark mode (overrides `color`)
- `a11yLabel(string $label)` - Accessibility label (icons are hidden from screen readers without one)
diff --git a/resources/views/docs/mobile/4/edge-components/icons.md b/resources/views/docs/mobile/4/edge-components/icons.md
deleted file mode 100644
index 7d22fe57..00000000
--- a/resources/views/docs/mobile/4/edge-components/icons.md
+++ /dev/null
@@ -1,288 +0,0 @@
----
-title: Icons
-order: 240
----
-
-## Overview
-
-NativePHP EDGE components use a smart icon mapping system that automatically converts icon names to platform-specific
-icons. On iOS, icons render as [SF Symbols](https://developer.apple.com/sf-symbols/), while Android uses
-[Material Icons](https://fonts.google.com/icons?icon.set=Material+Icons).
-
-You don't need to worry about the differences! Just use a single, consistent icon name in your components, and the EDGE
-handles the platform translation automatically.
-
-## How It Works
-
-The icon system uses a four-tier resolution strategy:
-
-1. **Direct Platform Icons** - On iOS, if the name contains a `.` it's used as a direct SF Symbol path (e.g., `car.side.fill`). On Android, any Material Icon ligature name works directly (e.g., `shopping_cart`).
-2. **Manual Mapping** - Explicit mappings for common icons and aliases (e.g., `home`, `settings`, `user`)
-3. **Smart Fallback** - Attempts to auto-convert unmapped icon names to platform equivalents
-4. **Default Fallback** - Uses a circle icon if no match is found
-
-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.
-
-## Platform Differences
-
-### iOS (SF Symbols)
-
-On iOS, icons render as SF Symbols. Manual mappings convert common icon names to their SF Symbol equivalents.
-For example:
-
-- `home` → `house.fill`
-- `settings` → `gearshape.fill`
-- `check` → `checkmark.circle.fill`
-
-If an icon name isn't manually mapped, the system attempts to find a matching SF Symbol by trying variations like
-`.fill`, `.circle.fill`, and `.square.fill`.
-
-### Android (Material Icons)
-
-On Android, icons render using a lightweight font-based approach that supports the entire Material Icons library. You
-can use any Material Icon by its ligature name directly (e.g., `shopping_cart`, `qr_code_2`).
-
-Manual mappings provide convenient aliases for common icon names. For example:
-
-- `home` → `home`
-- `settings` → `settings`
-- `check` → `check`
-- `cart` → `shopping_cart`
-
-## Direct Platform Icons
-
-For advanced use cases, you can use platform-specific icon names directly.
-
-### iOS SF Symbols
-
-On iOS, include a `.` in the icon name to use an SF Symbol path directly:
-
-@verbatim
-```blade
-
-
-
-```
-@endverbatim
-
-### Android Material Icons
-
-On Android, use any Material Icon ligature name (with underscores):
-
-@verbatim
-```blade
-
-
-
-```
-@endverbatim
-
-## Platform-Specific Icons
-
-When you need different icons on each platform, use the `System` facade:
-
-@verbatim
-```blade
-
-```
-@endverbatim
-
-This is useful when the mapped icon doesn't match your needs or you want to use platform-specific variants.
-
-## Basic Usage
-
-Use the `icon` attribute in any EDGE component that supports icons, simply passing the name of the icon you wish to use:
-
-@verbatim
-```blade
-
-```
-@endverbatim
-
-## Icon Reference
-
-All icons listed here are manually mapped and guaranteed to work consistently across iOS and Android.
-
-### Navigation
-
-| Icon | Description |
-|------|-------------|
-| `dashboard` | Grid-style dashboard view |
-| `home` | House/home screen |
-| `menu` | Three-line hamburger menu |
-| `settings` | Gear/settings |
-| `account`, `profile`, `user` | User account or profile |
-| `person` | Single person |
-| `people`, `connections`, `contacts` | Multiple people |
-| `group`, `groups` | Group of people |
-
-### Business & Commerce
-
-| Icon | Description |
-|------|-------------|
-| `orders`, `receipt` | Receipt or order |
-| `cart`, `shopping` | Shopping cart |
-| `shop`, `store` | Store or storefront |
-| `products`, `inventory` | Products or inventory |
-
-### Charts & Data
-
-| Icon | Description |
-|------|-------------|
-| `chart`, `barchart` | Bar chart |
-| `analytics` | Analytics/analysis |
-| `summary`, `report`, `assessment` | Summary or report |
-
-### Time & Scheduling
-
-| Icon | Description |
-|------|-------------|
-| `clock`, `schedule`, `time` | Clock or time |
-| `calendar` | Calendar |
-| `history` | History or recent |
-
-### Actions
-
-| Icon | Description |
-|------|-------------|
-| `add`, `plus` | Add or create new |
-| `edit` | Edit or modify |
-| `delete` | Delete or remove |
-| `save` | Save |
-| `search` | Search |
-| `filter` | Filter |
-| `refresh` | Refresh or reload |
-| `share` | Share |
-| `download` | Download |
-| `upload` | Upload |
-
-### Communication
-
-| Icon | Description |
-|------|-------------|
-| `notifications` | Notifications or alerts |
-| `message` | Message or SMS |
-| `email`, `mail` | Email |
-| `chat` | Chat or conversation |
-| `phone` | Phone or call |
-
-### Navigation Arrows
-
-| Icon | Description |
-|------|-------------|
-| `back` | Back or previous |
-| `forward` | Forward or next |
-| `up` | Up arrow |
-| `down` | Down arrow |
-
-### Status
-
-| Icon | Description |
-|------|-------------|
-| `check`, `done` | Check or complete |
-| `close` | Close or dismiss |
-| `warning` | Warning |
-| `error` | Error |
-| `info` | Information |
-
-### Authentication
-
-| Icon | Description |
-|------|-------------|
-| `login` | Login |
-| `logout`, `exit` | Logout or exit |
-| `lock` | Locked |
-| `unlock` | Unlocked |
-
-### Content
-
-| Icon | Description |
-|------|-------------|
-| `favorite`, `heart` | Favorite or like |
-| `star` | Star or rating |
-| `bookmark` | Bookmark |
-| `image`, `photo` | Image or photo |
-| `image-plus` | Add photo |
-| `video` | Video |
-| `folder` | Folder |
-| `folder-lock` | Locked folder |
-| `file`, `description` | Document or file |
-| `book-open` | Book |
-| `newspaper`, `news`, `article` | News or article |
-
-### Device & Hardware
-
-| Icon | Description |
-|------|-------------|
-| `camera` | Camera |
-| `qr`, `qrcode`, `qr-code` | QR code scanner |
-| `device-phone-mobile`, `smartphone` | Mobile phone |
-| `vibrate` | Vibration |
-| `bell` | Bell or notification |
-| `finger-print`, `fingerprint` | Fingerprint or biometric |
-| `light-bulb`, `lightbulb`, `flashlight` | Light bulb or flashlight |
-| `map`, `location` | Map or location |
-| `globe-alt`, `globe`, `web` | Globe or web |
-| `bolt`, `flash` | Lightning bolt or flash |
-
-### Audio & Volume
-
-| Icon | Description |
-|------|-------------|
-| `speaker`, `speaker-wave` | Speaker with sound |
-| `volume-up` | Volume up |
-| `volume-down` | Volume down |
-| `volume-mute`, `mute` | Muted |
-| `volume-off` | Volume off |
-| `music`, `audio`, `music-note` | Music or audio |
-| `microphone`, `mic` | Microphone |
-
-### Miscellaneous
-
-| Icon | Description |
-|------|-------------|
-| `help` | Help or question |
-| `about`, `information-circle` | Information or about |
-| `more` | More options |
-| `list` | List view |
-| `visibility` | Visible |
-| `visibility_off` | Hidden |
-
-## Best Practices
-
-Icons have meaning and most users will associate the visual cues of icons and the underlying behavior or section of an
-application across apps. So try to maintain consistent use of icons to help guide users through your app.
-
-- **Stay consistent** - Use the same icon name throughout your app for the same action
-- **Test on both platforms** - If you use auto-converted icons, verify they appear correctly on iOS and Android
-
-## Finding Icons
-
-### Android Material Icons
-
-Browse the complete Material Icons library at [Google Fonts Icons](https://fonts.google.com/icons). Use the icon name
-exactly as shown (with underscores, e.g., `shopping_cart`, `qr_code_2`).
-
-### iOS SF Symbols
-
-Browse SF Symbols using this [community Figma file](https://www.figma.com/community/file/1549047589273604548). While not
-comprehensive, it's a great starting point for discovering available symbols.
-
-For the complete library, download the [SF Symbols app](https://developer.apple.com/sf-symbols/) for macOS.
-
-
-
-SF Symbol names use dots (e.g., `house.fill`), while Material Icon names use underscores (e.g., `shopping_cart`).
-
-
diff --git a/resources/views/docs/mobile/4/edge-components/image.md b/resources/views/docs/mobile/4/edge-components/image.md
index 7f1484cc..86271d97 100644
--- a/resources/views/docs/mobile/4/edge-components/image.md
+++ b/resources/views/docs/mobile/4/edge-components/image.md
@@ -9,7 +9,13 @@ Displays an image from a URL. Loaded asynchronously by the native platform — `
@verbatim
```blade
-
+
```
@endverbatim
@@ -40,7 +46,7 @@ The renderer collapses fit modes to two effective behaviors: `fit` and `fill`. M
@verbatim
```blade
-
+
```
@endverbatim
@@ -49,11 +55,11 @@ The renderer collapses fit modes to two effective behaviors: `fit` and `fill`. M
@verbatim
```blade
```
@endverbatim
@@ -61,24 +67,33 @@ The renderer collapses fit modes to two effective behaviors: `fit` and `fill`. M
### Tinted icon image
@verbatim
-```blade
+```blade static
```
@endverbatim
+
+
+Use an image with an alpha channel (a monochrome logo or template asset) for tinting — tinting an opaque photo
+just fills the frame with a solid block of color. Tint rendering is still being stabilized across platforms, so
+this example is shown as code only.
+
+
+
### Image in a card
@verbatim
```blade
-
-
-
- Article Title
+
+
+
+ Article Title
A brief description of the article.
@@ -90,7 +105,7 @@ The renderer collapses fit modes to two effective behaviors: `fit` and `fill`. M
```php
use Native\Mobile\Edge\Elements\Image;
-Image::make('https://example.com/photo.jpg')
+Image::make('https://picsum.photos/seed/nativephp/400/300')
->fit(2)
->tintColor('#7C3AED');
```
diff --git a/resources/views/docs/mobile/4/edge-components/introduction.md b/resources/views/docs/mobile/4/edge-components/introduction.md
index 13b5275e..46deb0af 100644
--- a/resources/views/docs/mobile/4/edge-components/introduction.md
+++ b/resources/views/docs/mobile/4/edge-components/introduction.md
@@ -8,79 +8,16 @@ order: 1
EDGE (Element Definition and Generation Engine) is NativePHP for Mobile's component system that transforms Blade
template syntax into platform-native UI elements that look beautiful whichever device your users are using.
-
-
Instead of rendering in the web view, EDGE components are compiled into truly native elements and live apart from the
web view's lifecycle. This means they are persistent and offer truly native performance.
There's no custom rendering engine and complex ahead-of-time compilation process, just a lightweight transformation
step that happens at runtime. You end up with pure, fast and flexible native components — all configured by PHP!
-## Available Components
-
-EDGE provides a full suite of native UI components for building your app, from layout containers and typography to
-interactive forms and navigation chrome.
-
-### Layout
-
-- **[Layout & Styling](layout)** - Shared sizing, spacing, flex, style, and event attributes available on all elements
-- **[Column](column)** - Vertical flex container
-- **[Row](row)** - Horizontal flex container
-- **[Scroll View](scroll-view)** - Scrollable container with virtualization
-- **[Stack](stack)** - Overlay container (ZStack) for layering elements
-- **[Spacer](spacer)** - Flexible space element
-- **[Pressable](pressable)** - Touch-sensitive container wrapper
-- **[Web View](web-view)** - Embed web content as a native element and surround it with native UI
-
-### Content
-
-- **[Text](text)** - Text display with font sizing, weight, color, and alignment
-- **[Image](image)** - Image display with fit modes and tinting
-- **[Icon](icon)** - Platform-native icons (SF Symbols on iOS, Material Icons on Android)
-- **[Divider](divider)** - Horizontal line separator
-- **[Activity Indicator](activity-indicator)** - Loading spinner
-- **[Progress Bar](progress-bar)** - Linear progress indicator
-- **[Badge](badge)** - Count or label pill marker
-
-### Forms
-
-- **[Button](button)** - Tappable button with variants, sizes, and disabled / loading state
-- **[Button Group](button-group)** - Segmented single-choice selector
-- **[Text Input](text-input)** - Outlined and filled text input variants
-- **[Toggle](toggle)** - On/off switch control
-- **[Checkbox](checkbox)** - Tick/untick control with optional inline label
-- **[Radio Group](radio-group)** - Single-choice radio selector
-- **[Select](select)** - Dropdown picker
-- **[Slider](slider)** - Continuous (or stepped) value selector
-- **[Chip](chip)** - Compact selectable tag
-
-### Navigation
-
-- **[Bottom Navigation](bottom-nav)** - The always-accessible bottom navigation bar
-- **[Top Bar](top-bar)** - A title bar with action buttons
-- **[Side Navigation](side-nav)** - A slide-out navigation drawer
-- **[Tab Row](tab-row)** - Horizontal tab strip for in-screen sectioning
-
-### Lists & data
-
-- **[List](list)** - Virtualized list with pull-to-refresh, end-reached, and swipe actions
-- **[List Item](list)** - Material3 row with leading + trailing slot system
-- **[Carousel](carousel)** - Horizontal paging carousel
-
-### Overlays
-
-- **[Bottom Sheet](bottom-sheet)** - Modal bottom sheet for contextual actions and forms
-- **[Modal](modal)** - Full-screen modal overlay
-
-### Drawing
-
-- **[Canvas](canvas)** - Drawing surface for shape primitives
-- **[Shapes](shapes)** - Rect, circle, and line elements
-
## How It Works
@verbatim
-```blade
+```blade static
@@ -147,19 +84,3 @@ The error message will list all missing required props and show you exactly whic
validation happens at render time, making it easy to catch configuration issues during development.
Each component's documentation page indicates which props are required vs optional.
-
-## Using Inertia?
-
-Each link in an EDGE component will do a full post back to PHP, which may not be what you want if you are using Inertia. To transform these requests into Inertia ` `, add `router` to your `window` object:
-
-```typescript
-import { router } from '@inertiajs/vue3';
-
-declare global {
- interface Window {
- router: typeof router;
- }
-}
-
-window.router = router;
-```
diff --git a/resources/views/docs/mobile/4/edge-components/layout.md b/resources/views/docs/mobile/4/edge-components/layout.md
index 885c9fef..2c22151d 100644
--- a/resources/views/docs/mobile/4/edge-components/layout.md
+++ b/resources/views/docs/mobile/4/edge-components/layout.md
@@ -15,7 +15,7 @@ This page documents the shared attribute system that powers the layout engine ac
Control element dimensions with width, height, and fill attributes.
@verbatim
-```blade
+```blade static
{{-- Fixed dimensions (in dp) --}}
...
@@ -55,7 +55,7 @@ Padding and margin follow CSS shorthand conventions. Pass a single value for uni
per-side control.
@verbatim
-```blade
+```blade static
{{-- Uniform padding --}}
...
@@ -93,7 +93,7 @@ The layout engine uses a Flexbox-based system. Containers (column, row) arrange
properties control how children grow, shrink, and align.
@verbatim
-```blade
+```blade static
{{-- Grow to fill remaining space --}}
...
@@ -112,8 +112,8 @@ properties control how children grow, shrink, and align.
-The Tailwind `flex-1` class is shorthand for `flex-grow: 1; flex-basis: 0` — the most common pattern for "fill the
-remaining space along the parent's main axis."
+The Tailwind `flex-1` class is shorthand for `flex-grow: 1; flex-shrink: 1; flex-basis: 0` — the most common pattern
+for "fill the remaining space along the parent's main axis."
@@ -130,7 +130,7 @@ Alignment values are integers that map to standard flex alignment:
| `4` | baseline |
@verbatim
-```blade
+```blade static
{{-- Center children on both axes --}}
...
@@ -166,7 +166,7 @@ A child with `w-full` (or `h-full`) overrides its parent's `items-center` along
Visual styling attributes that apply to any element.
@verbatim
-```blade
+```blade static
- Tap or long press me
+```blade static
+
+ Tap, double tap, or long press me
```
@endverbatim
- `@press` - PHP method to call on tap
+- `@doubleTap` - PHP method to call on double tap
- `@longPress` - PHP method to call on long press
## Safe Area
@@ -209,7 +210,7 @@ Respect the device's safe area insets (notch, home indicator, status bar) by add
typically applied to your outermost column.
@verbatim
-```blade
+```blade static
{{-- Content will not overlap the notch or home indicator --}}
@@ -228,7 +229,7 @@ chrome already handles safe-area insets for you.
Hide elements without removing them from the tree.
@verbatim
-```blade
+```blade static
{{-- This element is not displayed --}}
@@ -242,7 +243,7 @@ Hide elements without removing them from the tree.
Override styles for dark mode using the `dark:` prefix with Tailwind classes, or pass a `dark` attribute array.
@verbatim
-```blade
+```blade static
{{-- Tailwind dark mode --}}
@@ -260,7 +261,7 @@ EDGE includes a built-in Tailwind CSS parser that converts familiar utility clas
the `class` attribute on any element.
@verbatim
-```blade
+```blade static
Styled with Tailwind
@@ -277,11 +278,13 @@ The parser recognizes the classes listed below.
|----------|---------|
| Width | `w-full`, `w-N`, fractional (`w-1/2`, `w-1/3`, `w-2/3`, `w-1/4`, `w-3/4`, `w-1/5`…), arbitrary `w-[N]` |
| Height | `h-full`, `h-N`, arbitrary `h-[N]` |
+| Aspect ratio | `aspect-square`, `aspect-video`, arbitrary `aspect-[N]` |
+| Object fit (images) | `object-contain`, `object-cover`, `object-fill`, `object-none`, `object-scale-down` |
| Padding | `p-N`, `px-N`, `py-N`, `pt-N`, `pr-N`, `pb-N`, `pl-N`, arbitrary `p-[N]` etc. |
| Margin | `m-N`, `mx-N`, `my-N`, `mt-N`, `mr-N`, `mb-N`, `ml-N`, arbitrary `m-[N]` etc. |
| Gap | `gap-N`, `gap-[N]` (uniform — no `gap-x-*` or `gap-y-*`) |
| Position | `absolute`, `relative`, `top-N`, `right-N`, `bottom-N`, `left-N`, arbitrary `top-[N]` etc. |
-| Flex | `flex-1`, `flex-grow`, `flex-grow-0`, `flex-shrink`, `flex-shrink-0` |
+| Flex | `flex-1`, `flex-grow`, `flex-grow-0`, `flex-shrink`, `flex-shrink-0`, `flex-wrap`, `flex-nowrap`, `flex-wrap-reverse` |
| Items (cross-axis) | `items-start`, `items-center`, `items-end`, `items-stretch` |
| Justify (main-axis) | `justify-start`, `justify-center`, `justify-end`, `justify-between`, `justify-around`, `justify-evenly` |
| Self | `self-start`, `self-center`, `self-end`, `self-stretch` |
@@ -294,6 +297,12 @@ The parser recognizes the classes listed below.
| Opacity | `opacity-{0..100}`, arbitrary `opacity-[0.5]` |
| Text size | `text-xs`, `text-sm`, `text-base`, `text-lg`, `text-xl`, `text-2xl`, `text-3xl`, `text-4xl`, `text-5xl`, `text-6xl`, arbitrary `text-[N]` |
| Font weight | `font-thin`, `font-extralight`, `font-light`, `font-normal`, `font-medium`, `font-semibold`, `font-bold`, `font-extrabold`, `font-black` |
+| Font family | `font-sans`, `font-serif`, `font-mono` |
+| Font style | `italic`, `not-italic` |
+| Text decoration | `underline`, `line-through`, `no-underline` |
+| Text transform | `uppercase`, `lowercase`, `capitalize`, `normal-case` |
+| Letter spacing | `tracking-tighter`, `tracking-tight`, `tracking-normal`, `tracking-wide`, `tracking-wider`, `tracking-widest` |
+| Line height | `leading-none`, `leading-tight`, `leading-snug`, `leading-normal`, `leading-relaxed`, `leading-loose`, arbitrary `leading-[1.4]` / `leading-[24px]` |
| Text align | `text-left`, `text-center`, `text-right` |
| Text selection | `select-text`, `select-none` (container-scoped; descendants inherit) |
| Safe area | `safe-area` (top + bottom), `safe-area-top`, `safe-area-bottom` |
@@ -317,7 +326,8 @@ border-theme-outline/50
```
**Arbitrary values** — `prefix-[value]` for the prefixes shown above: `w`, `h`, `p`/`px`/`py`/`pt`/`pr`/`pb`/`pl`,
-`m`/`mx`/`my`/`mt`/`mr`/`mb`/`ml`, `gap`, `bg`, `text`, `border`, `rounded`, `opacity`, `top`, `right`, `bottom`, `left`.
+`m`/`mx`/`my`/`mt`/`mr`/`mb`/`ml`, `gap`, `bg`, `text`, `border`, `rounded`, `opacity`, `leading`, `aspect`, `top`,
+`right`, `bottom`, `left`.
diff --git a/resources/views/docs/mobile/4/edge-components/lazy-grid.md b/resources/views/docs/mobile/4/edge-components/lazy-grid.md
new file mode 100644
index 00000000..9d923af4
--- /dev/null
+++ b/resources/views/docs/mobile/4/edge-components/lazy-grid.md
@@ -0,0 +1,105 @@
+---
+title: Lazy Grid
+order: 255
+---
+
+## Overview
+
+A self-scrolling grid that only materializes the cells currently in — or about to enter — the viewport. On iOS it
+maps to SwiftUI `ScrollView { LazyVGrid }`; on Android to Compose `LazyVerticalGrid`. Both platforms build and lay
+out just the visible rows, so a grid of thousands of cells paints instantly and stays smooth.
+
+Reach for it in place of a [``](scroll-view) wrapping a manually-chunked
+[``](row) grid whenever the cell count is large enough to be felt at parse or layout time — a good rule
+of thumb is around 50 or more cells.
+
+@verbatim
+```blade
+
+ @foreach (['star', 'heart', 'bell', 'bookmark', 'camera', 'paperplane', 'flag', 'gearshape'] as $icon)
+
+
+
+ @endforeach
+
+```
+@endverbatim
+
+In a real app you would typically loop a collection property on your component (`$icons`) rather than a literal list.
+
+## Props
+
+- `columns` - Number of equal-width tracks the cells flow across. When `horizontal` is set, this becomes the number
+ of fixed rows instead (optional, int, default: `2`, minimum `1`)
+- `gap` - Spacing in dp applied to **both** axes — between rows and between columns. Match the surrounding column's
+ `gap-N` if you want flush alignment with neighbouring content (optional, float, default: `0`)
+- `horizontal` - Flip the scroll orientation. Rows become the cross axis, `columns` is the number of fixed-height
+ rows, and the grid scrolls horizontally (SwiftUI `LazyHGrid` / Compose `LazyHorizontalGrid`) (optional, boolean,
+ default: `false`)
+
+## Children
+
+Each child becomes one grid cell. Cells fill their column width by default and size to their intrinsic height — wrap
+a child in a [``](column) with explicit sizing if you need uniform cell heights.
+
+
+
+On Android, when a lazy grid sits inside another scrolling container (a scroll view or scrollable column), its main
+axis is unbounded and Compose's lazy grids can't measure. In that case the renderer falls back to a non-lazy chunked
+grid that wraps its content — the same visual result, without virtualization. Give the grid a bounded height (or let
+it be the screen's scroll container) to keep lazy composition.
+
+
+
+## Examples
+
+### Photo grid
+
+@verbatim
+```blade
+
+ @foreach (range(1, 9) as $i)
+
+
+
+ @endforeach
+
+```
+@endverbatim
+
+Swap `range(1, 9)` for your own `$photos` collection and bind `src` from each item (e.g. `$photo->url`).
+
+### Horizontal category shelves
+
+@verbatim
+```blade static
+
+ @foreach($products as $product)
+
+
+ {{ $product->name }}
+
+ @endforeach
+
+```
+@endverbatim
+
+## Element
+
+```php
+use Native\Mobile\Edge\Elements\LazyGrid;
+use Native\Mobile\Edge\Elements\Column;
+use Native\Mobile\Edge\Elements\Icon;
+
+LazyGrid::make(
+ Column::make(Icon::make(ios: 'star')),
+ Column::make(Icon::make(ios: 'heart')),
+)
+ ->columns(4)
+ ->gap(12);
+```
+
+- `make(Element ...$children)` - Create a lazy grid with children
+- `columns(int $count)` - Number of tracks (clamped to a minimum of `1`)
+- `gap(float $gap)` - Spacing applied to both axes
+- `horizontal(bool $value = true)` - Scroll horizontally with fixed rows
diff --git a/resources/views/docs/mobile/4/edge-components/list.md b/resources/views/docs/mobile/4/edge-components/list.md
index 40179306..87bbabd1 100644
--- a/resources/views/docs/mobile/4/edge-components/list.md
+++ b/resources/views/docs/mobile/4/edge-components/list.md
@@ -30,7 +30,10 @@ Pair with [``](#list-item) for Material3 list rows, or use any
- `horizontal` - Lay out children horizontally instead of vertically (optional, boolean, default: `false`)
- `shows-indicators` - Show scroll indicators (optional, boolean, default: `false`) [iOS]
-- `separator` - Render dividers between rows (optional, boolean, default: `false`) [iOS]
+- `separator` - Render dividers between rows (optional, boolean, default: `false`)
+- `plain` - Force a flat, ungrouped list. By default a list containing `` children adopts the
+ inset-grouped style (rounded cards — iOS `.insetGrouped`, grouped cards on Android); `plain` keeps flat rows with
+ plain section headers instead (optional, boolean, default: `false`)
- `on-refresh` - Component method called on pull-to-refresh (optional, string) [iOS]
- `on-end-reached` - Component method called when the user nears the end of the list (optional, string)
@@ -63,33 +66,45 @@ content slots.
### Leading slot (mutually exclusive)
-- `leadingIcon` - Icon name rendered as a leading icon
+- `leadingIcon` - Icon name rendered as a leading icon. Pair with `leadingIconIos` / `leadingIconAndroid` to
+ override the [icon](icon) per platform
- `leadingAvatar` - URL of a circular avatar image
- `leadingMonogram` - 1-2 character monogram (combine with `leadingMonogramColor`)
- `leadingMonogramColor` - Hex color for monogram background
- `leadingImage` - URL of a square image with a small radius
-- `leadingCheckbox` - Boolean value for a leading checkbox
-- `leadingRadio` - Boolean value for a leading radio button
+- `leadingCheckbox` - Boolean value for a leading checkbox. Interactive when `on-leading-change` is set —
+ tapping the box fires your handler with the new value (the row's own `@press` still handles taps elsewhere
+ on the row); without a handler it renders as a static state glyph
+- `leadingRadio` - Boolean value for a leading radio button. Interactive when `on-leading-change` is set;
+ static glyph otherwise
### Trailing slot (mutually exclusive)
-- `trailingIcon` - Icon name rendered as a trailing icon
+- `trailingIcon` - Icon name rendered as a trailing icon. Pair with `trailingIconIos` / `trailingIconAndroid` to
+ override the [icon](icon) per platform
- `trailingText` - Trailing text label
-- `trailingCheckbox` - Boolean value for a trailing checkbox
+- `trailingCheckbox` - Boolean value for a trailing checkbox. Interactive when `on-trailing-change` is set;
+ static glyph otherwise
- `trailingSwitch` - Boolean value for a trailing switch [Android]
- `trailingIconButton` - Icon name for a tappable trailing button
- `trailing-a11y-label` - Accessibility label for the trailing icon button (recommended whenever
`trailingIconButton` is set). See [Accessibility](../digging-deeper/accessibility)
-- `trailing-menu` - Attach a tap-to-open dropdown to the row's trailing edge. See [Menus](menus)
+- `trailing-menu` - Attach a tap-to-open dropdown to the row's trailing edge. When set without an explicit trailing
+ slot, an `ellipsis` icon button is auto-created as the anchor. See [Menus](menus)
Independent of the mutually-exclusive slot above, a row can also show a stack of small status icons:
- `trailing-badges` - An array of small status badges drawn right-aligned, so several can show at once (e.g. a
- flag and a pin). Each badge is `['ios' => ..., 'android' => ..., 'color' => '#hex']`.
+ flag and a pin). Each badge is `['icon' => ..., 'ios' => ..., 'android' => ..., 'color' => 'red-500']`, where
+ `icon` is a shared [icon](icon) name, `ios` / `android` override it per platform, and `color` takes any
+ [color value](../digging-deeper/theming#color-values).
### Color overrides
-- `headlineColor`, `supportingColor`, `overlineColor` - Hex colors for the text styles
+All color props accept the full [color grammar](../digging-deeper/theming#color-values) — hex (including
+`#RRGGBBAA` alpha), Tailwind palette names, and `/N` opacity modifiers (`red-300/20`).
+
+- `headlineColor`, `supportingColor`, `overlineColor` - Colors for the text styles
- `containerColor` - Row background color
- `leadingIconColor`, `trailingIconColor`, `trailingTextColor` - Colors for the slot content
- `leadingIconBgColor` - Background color of the leading icon's circle
@@ -103,12 +118,15 @@ Independent of the mutually-exclusive slot above, a row can also show a stack of
### Events
- `@press` / `@longPress` - Standard press handlers on the row
-- `@leading-change` - Fired when a leading checkbox or radio toggles; receives the new value
-- `@trailing-change` - Fired when a trailing checkbox or switch toggles; receives the new value
-- `@trailing-press` - Fired when the trailing icon button is tapped
- `on-swipe-delete` - Shortcut for a single destructive trailing swipe. For anything richer, use
`trailing-actions` below.
+- `on-leading-change` / `on-trailing-change` - Component method called when the leading/trailing checkbox or
+ radio is toggled, receiving the new value. Without a handler the control renders as a static state glyph.
+
+`onTrailingPress()` fires on both platforms when the trailing icon button is tapped. The trailing switch is
+interactive on [Android] only.
+
### Swipe actions
Configure swipe actions on either edge with `leading-actions` and `trailing-actions` — each an array of action
@@ -120,18 +138,25 @@ definitions the user reveals by swiping the row. Each action is an array:
- `tint` - Background color as a hex string
- `role` - Set to `destructive` to render in the platform's delete style (trailing only)
+Swipe actions only work on rows that are direct children of `` (or a ``) —
+they are attached by the list renderer, so a standalone `` silently ignores them.
+
@verbatim
```blade
-
+
+ @foreach ($emails as $email)
+
+ @endforeach
+
```
@endverbatim
@@ -140,6 +165,9 @@ definitions the user reveals by swiping the row. Each action is an array:
Group rows under a header (and optional footer) with `` — a SwiftUI `Section` on iOS, a
sticky-header group on Android. Place `` children inside; a section on its own renders nothing.
+A list that contains sections automatically adopts the inset-grouped style (rounded cards); pass `plain` to the
+list to keep flat rows with plain section headers instead.
+
@verbatim
```blade
@@ -163,6 +191,12 @@ use Nativephp\NativeUi\Elements\ListSection;
ListSection::make('Fruits', ListItem::make('Apple'))->footer('1 item');
```
+### `ListSection` methods
+
+- `make(string $header = '', Element ...$children)` - Create a section with a header and rows
+- `header(string $text)` - Set the section header text
+- `footer(string $text)` - Set the optional footer text
+
## Examples
### Settings menu
@@ -180,6 +214,9 @@ ListSection::make('Fruits', ListItem::make('Apple'))->footer('1 item');
### Swipe-to-delete with pull-to-refresh
+The checkbox, swipe, and row press are three independent targets on one row: tapping the box fires
+`on-leading-change`, swiping left fires `on-swipe-delete`, tapping anywhere else fires `@press`.
+
@verbatim
```blade
@@ -188,6 +225,7 @@ ListSection::make('Fruits', ListItem::make('Apple'))->footer('1 item');
headline="{{ $task->title }}"
supporting="{{ $task->due }}"
leadingCheckbox="{{ $task->done }}"
+ on-leading-change="toggleTask({{ $task->id }})"
trailingIcon="forward"
on-swipe-delete="deleteTask({{ $task->id }})"
@press="openTask({{ $task->id }})"
@@ -197,13 +235,22 @@ ListSection::make('Fruits', ListItem::make('Apple'))->footer('1 item');
```
@endverbatim
+> [!NOTE]
+> Pull-to-refresh needs to own the pull gesture, so it only fires when the list is the screen's scrolling
+> container — inside another scroll view (like this docs page) the outer container wins the pull. Swipe and
+> checkbox work inline; run the refresh on a dedicated screen.
+
### Infinite scroll
+`loadMore()` is a method on your component that fetches the next page and appends it to the collection the loop
+renders — in a real app the loop is `@@foreach ($posts as $post)` over your paginated results. The fixed `range()`
+here just gives the demo enough rows to scroll before the end-reached trigger fires.
+
@verbatim
```blade
- @foreach($posts as $post)
-
+ @foreach (range(1, 15) as $i)
+
@endforeach
```
@@ -241,7 +288,7 @@ Text:
Leading slot:
-- `leadingIcon(string $icon)`
+- `leadingIcon(?string $name = null, IosSymbol|string|null $ios = null, AndroidSymbol|string|null $android = null)`
- `leadingAvatar(string $url)`
- `leadingMonogram(string $initials, ?string $color = null)`
- `leadingImage(string $url)`
@@ -250,23 +297,24 @@ Leading slot:
Trailing slot:
-- `trailingIcon(string $icon)`
+- `trailingIcon(?string $name = null, IosSymbol|string|null $ios = null, AndroidSymbol|string|null $android = null)`
- `trailingText(string $text)`
- `trailingCheckbox(bool $checked = false)`
- `trailingSwitch(bool $checked = false)`
-- `trailingIconButton(string $icon)`
+- `trailingIconButton(?string $name = null, IosSymbol|string|null $ios = null, AndroidSymbol|string|null $android = null)`
- `trailingA11yLabel(string $label)` - Accessibility label for the trailing icon button
Swipe actions & badges:
- `leadingActions(array $actions)`, `trailingActions(array $actions)` - Arrays of swipe-action definitions
(`method`, `label`, `ios`/`android`, `tint`, and `role` for trailing)
-- `trailingBadges(array $badges)` - Stacked right-aligned status icons
+- `trailingBadges(array $badges)` - Stacked right-aligned status icons; each badge is
+ `['icon' => ..., 'ios' => ..., 'android' => ..., 'color' => '#hex']`
Styling:
- `headlineColor`, `supportingColor`, `overlineColor`, `containerColor`,
- `leadingIconColor`, `trailingIconColor`, `trailingTextColor` (all `(string $color)`)
+ `leadingIconColor`, `leadingIconBackgroundColor`, `trailingIconColor`, `trailingTextColor` (all `(string $color)`)
- `tonalElevation(float $dp)`, `shadowElevation(float $dp)`
Callbacks:
diff --git a/resources/views/docs/mobile/4/edge-components/menus.md b/resources/views/docs/mobile/4/edge-components/menus.md
index f5f3bf5c..86df571f 100644
--- a/resources/views/docs/mobile/4/edge-components/menus.md
+++ b/resources/views/docs/mobile/4/edge-components/menus.md
@@ -10,9 +10,10 @@ standalone element — you build a list of actions and attach it with an attribu
- `:menu` on a [button](button) or [pressable](pressable) — tapping opens the menu instead of firing `@press`.
- `:trailing-menu` on a [list item](list) — opens from the row's trailing edge.
+- `items()` on a nav-bar action — see [Menus in the nav bar](#menus-in-the-nav-bar) below.
-All three share one item model: an array of `NavAction`, the same builder used for
-[nav-bar menus](../the-basics/layouts#builder-reference).
+All of them share one item model: an array of `NavAction`, the same builder used
+[throughout layouts](../the-basics/layouts#builder-reference).
## Building the items
@@ -64,6 +65,53 @@ Use `:trailing-menu` to open a menu from a row's trailing edge — a common patt
```
@endverbatim
+## Menus in the nav bar
+
+The same `NavAction` builder powers pull-down menus on nav-bar (top bar) actions. Give an action sub-items
+with `items()` and it renders as a menu that drops from the bar button instead of firing a plain press:
+
+```php
+use Native\Mobile\Edge\Layouts\Builders\NavAction;
+use Native\Mobile\Edge\Layouts\Builders\NavBarOptions;
+
+public function navigationOptions(): ?NavBarOptions
+{
+ return NavBarOptions::make()
+ ->action(
+ NavAction::make('share')
+ ->icon('share')
+ ->a11yLabel('Share')
+ ->press('share')
+ )
+ ->action(
+ NavAction::make('more')
+ ->icon('ellipsis')
+ ->a11yLabel('More options')
+ ->items([
+ NavAction::make('mute')
+ ->label('Mute')
+ ->icon(ios: 'bell.slash', android: 'notifications_off')
+ ->press('mute'),
+ NavAction::make('pin')->label('Pin')->icon('pin')->press('pin'),
+ NavAction::divider(),
+ NavAction::make('delete')
+ ->label('Delete')
+ ->icon('trash')
+ ->press('delete')
+ ->destructive(),
+ ])
+ );
+}
+```
+
+Here `share` stays a plain press, while `more` opens a native pull-down (SwiftUI `Menu` / Compose `DropdownMenu`)
+with dividers and destructive tinting, exactly like the inline menus above. This works in both stack and tab
+layouts — you can also attach it directly in a layout definition via `NavBar::make()->action(...)` in your
+layout's `navBar()`.
+
+> **Note** Menus are only supported on nav-bar actions. Bottom-nav / tab-bar items and the inline
+> `` element render their actions as plain buttons — `items()` is ignored there.
+
## Item reference
The menu-relevant `NavAction` methods:
@@ -73,6 +121,8 @@ The menu-relevant `NavAction` methods:
- `label(string)` — the row text
- `press(string $method)` — the component method to call when chosen
- `url(string)` — navigate to a URL instead of calling a method
+- `items(array $items)` — sub-items that turn a nav-bar action into a pull-down menu (nav-bar actions only)
+- `a11yLabel(string)` — screen-reader label for icon-only actions
- `destructive(bool = true)` — tint the item as destructive
- `NavAction::divider()` — a separator row
diff --git a/resources/views/docs/mobile/4/edge-components/modal.md b/resources/views/docs/mobile/4/edge-components/modal.md
index ff2cfd7d..80d900b3 100644
--- a/resources/views/docs/mobile/4/edge-components/modal.md
+++ b/resources/views/docs/mobile/4/edge-components/modal.md
@@ -9,7 +9,7 @@ A full-screen modal overlay. Visibility is driven by the `visible` prop. Use a [
contextual actions; reach for `` when you want the entire screen covered (e.g. an onboarding flow,
image preview, or detail view).
-Per Model 3, backdrop and surface colors come from `theme.background`. The close icon uses `theme.onSurfaceVariant`.
+Per Material 3, backdrop and surface colors come from `theme.background`. The close icon uses `theme.onSurfaceVariant`.
@verbatim
```blade
@@ -23,8 +23,8 @@ Per Model 3, backdrop and surface colors come from `theme.background`. The close
- Details
- {{ $description }}
+ Details
+ {{ $description }}
@@ -36,6 +36,7 @@ Per Model 3, backdrop and surface colors come from `theme.background`. The close
- `visible` - Whether the modal is shown (required, boolean)
- `dismissible` - Render a close icon and allow swipe-to-dismiss (optional, boolean, default: `true`)
- `a11y-label` - Accessibility label (optional)
+- `a11y-hint` - Accessibility hint (optional)
## Events
@@ -89,7 +90,7 @@ Programmatically setting `visible` to `false` from PHP does not fire the callbac
- Processing...
+ Processing...
{{-- A real app closes this from PHP when the work finishes;
the button stands in for that here since the modal can't
be dismissed by the user. --}}
@@ -115,4 +116,5 @@ Modal::make()
- `visible(bool $value = true)` - Toggle visibility
- `dismissible(bool $value = true)` - Allow user dismissal
- `a11yLabel(string $value)` - Accessibility label
+- `a11yHint(string $value)` - Accessibility hint
- `onDismiss(string $method)` - Component method invoked on dismissal
diff --git a/resources/views/docs/mobile/4/edge-components/pressable.md b/resources/views/docs/mobile/4/edge-components/pressable.md
index d7f9d5bf..5183bf03 100644
--- a/resources/views/docs/mobile/4/edge-components/pressable.md
+++ b/resources/views/docs/mobile/4/edge-components/pressable.md
@@ -13,23 +13,28 @@ provides a clear tap target that wraps multiple children.
@verbatim
```blade
-id }})" class="w-full p-4 rounded-xl" bg="#FFFFFF">
- {{ $item->name }}
- {{ $item->description }}
+
+ Account
+ Signed in as {{ $name }} — tap anywhere on the card
```
@endverbatim
+`@press` names a method on your component — declare `public function openAccount(): void` and it runs when any
+part of the card is tapped.
+
## Props
All [shared layout and style attributes](layout) are supported, plus:
- `menu` - Attach a tap-to-open dropdown; opening the menu shadows `@press`. See [Menus](menus)
+- `@navigate` - Navigate directly from the view, with no component method. See [Routing](../the-basics/routing#navigating-from-blade)
## Events
- `@press` - Component method to call on tap
- `@longPress` - Component method to call on long press
+- `@doubleTap` - Component method to call on double tap
## Children
@@ -46,10 +51,10 @@ Accepts any EDGE elements as children. Children are arranged vertically (like a
@press="selectItem({{ $item->id }})"
class="w-full px-4 py-3"
>
-
+
-
- {{ $item->name }}
+
+ {{ $item->name }}
{{ $item->subtitle }}
@@ -62,33 +67,68 @@ Accepts any EDGE elements as children. Children are arranged vertically (like a
```
@endverbatim
+Interpolate arguments straight into the handler — each row calls `selectItem()` on your component with its own id.
+
### Card with tap and long press
@verbatim
```blade
-id }})"
- @longPress="showOptions({{ $post->id }})"
- class="w-full p-4 rounded-2xl gap-2"
- bg="#FFFFFF"
- :elevation="2"
->
- {{ $post->title }}
- {{ $post->excerpt }}
-
+@foreach ($posts as $post)
+ id }})"
+ @longPress="showOptions({{ $post->id }})"
+ class="w-full p-4 mb-3 rounded-2xl gap-2 bg-theme-surface"
+ :elevation="2"
+ >
+ {{ $post->title }}
+ {{ $post->excerpt }}
+
+@endforeach
```
@endverbatim
+A tap calls `openDetail()`; holding the card calls `showOptions()` instead — one pressable can route each gesture to
+a different component method.
+
### Navigation with @navigate
@verbatim
```blade
-id }}" class="w-full p-4">
- {{ $item->name }}
+@foreach ($items as $item)
+ id }}" class="w-full px-4 py-3">
+ {{ $item->name }}
+
+@endforeach
+```
+@endverbatim
+
+## Press feedback
+
+Give the pressable a tactile response while it's held down. These run on the native thread with no PHP round-trip, so
+the animation stays smooth even while a handler is dispatching:
+
+- `press-scale` - Scale factor while pressed (e.g. `0.92` to shrink slightly)
+- `press-opacity` - Opacity while pressed (e.g. `0.85` to dim)
+- `press-translate-y` - Vertical offset in points while pressed (e.g. `3` to nudge down)
+
+@verbatim
+```blade
+
+ Press and hold
+ The card scales down, dims, and nudges while pressed.
```
@endverbatim
+For the full animation system — shared values, gesture-driven animation, and transitions — see
+[Gestures & Animation](../digging-deeper/gestures).
+
## Element
```php
diff --git a/resources/views/docs/mobile/4/edge-components/progress-bar.md b/resources/views/docs/mobile/4/edge-components/progress-bar.md
index 32345254..7867373d 100644
--- a/resources/views/docs/mobile/4/edge-components/progress-bar.md
+++ b/resources/views/docs/mobile/4/edge-components/progress-bar.md
@@ -10,7 +10,7 @@ value (or with `indeterminate`), renders an animated wave.
For a circular spinner use [``](activity-indicator) instead.
-Per Model 3, the progress fill uses `theme.primary` and the track uses `theme.surfaceVariant`. The optional `color`
+Per Material 3, the progress fill uses `theme.primary` and the track uses `theme.surfaceVariant`. The optional `color`
prop is an escape hatch for non-theme containers.
@verbatim
@@ -26,6 +26,7 @@ prop is an escape hatch for non-theme containers.
- `color` - Override the fill color as hex string (optional)
- `track-color` - Override the track color as hex string (optional)
- `a11y-label` - Accessibility label (optional)
+- `a11y-hint` - Accessibility hint (optional)
@@ -39,16 +40,20 @@ prop is an escape hatch for non-theme containers.
@verbatim
```blade
+@php $progress = 0.65; @endphp
+
-
- Uploading
- {{ round($progress * 100) }}%
+
+ Uploading
+ {{ round($progress * 100) }}%
```
@endverbatim
+Here `$progress` stands in for a public property on your component — declare `public float $progress = 0.65;` and update it as the work advances; the bar and the percentage label track it automatically.
+
### Indeterminate
@verbatim
@@ -65,6 +70,12 @@ prop is an escape hatch for non-theme containers.
```
@endverbatim
+
+
+`track-color` currently applies on Android only — iOS keeps the default theme track. The `color` fill override works on both platforms. Prefer the theme defaults (fill: `theme.primary`, track: `theme.surfaceVariant`) unless a fixed color is the point, since hex overrides don't adapt to dark mode.
+
+
+
## Element
```php
@@ -81,3 +92,4 @@ ProgressBar::make()->indeterminate();
- `color(string $hex)` - Override the fill tint
- `trackColor(string $hex)` - Override the track color
- `a11yLabel(string $value)` - Accessibility label
+- `a11yHint(string $value)` - Accessibility hint
diff --git a/resources/views/docs/mobile/4/edge-components/radio-group.md b/resources/views/docs/mobile/4/edge-components/radio-group.md
index 16bc48a0..b9232030 100644
--- a/resources/views/docs/mobile/4/edge-components/radio-group.md
+++ b/resources/views/docs/mobile/4/edge-components/radio-group.md
@@ -8,10 +8,12 @@ order: 310
A single-choice container holding `` children. The group owns the selection; each child declares its
own `value` and label.
-Per Model 3, all colors come from theme tokens.
+Per Material 3, all colors come from theme tokens.
@verbatim
```blade
+@php $plan = 'pro'; @endphp
+
@@ -20,6 +22,9 @@ Per Model 3, all colors come from theme tokens.
```
@endverbatim
+Here `plan` is a public string property on your component — the `@php` line stands in for
+`public string $plan = 'pro';`.
+
## Props (Group)
- `value` - Currently selected `value` string (optional). Use `native:model` for two-way binding
@@ -34,17 +39,25 @@ Per Model 3, all colors come from theme tokens.
## Two-way Binding
-`native:model` binds the group's selected value to a string property on your component:
+`native:model` binds the group's selected value to a public string property on your component:
@verbatim
```blade
-
-
-
+@php $billing = 'monthly'; @endphp
+
+
+
+
+
+Billed: {{ $billing }}
```
@endverbatim
+`billing` is a public string property on your component (the `@php` line stands in for
+`public string $billing = 'monthly';`). Picking an option syncs the new value back automatically,
+so the `@{{ $billing }}` echo updates as soon as you tap a different option.
+
## Children
`` declares a single option:
@@ -52,6 +65,8 @@ Per Model 3, all colors come from theme tokens.
- `value` - The option's value (required, string). Must be unique within the group
- `label` - Inline label (optional, string)
- `disabled` - Disable just this option (optional, boolean, default: `false`)
+- `a11y-label` - Accessibility label (optional)
+- `a11y-hint` - Accessibility hint (optional)
## Examples
@@ -59,17 +74,25 @@ Per Model 3, all colors come from theme tokens.
@verbatim
```blade
-
+@php $selectedPlan = 'free'; @endphp
+
+
+
+Current plan: {{ $selectedPlan }}
```
@endverbatim
### Manual handler
+When you need side effects beyond a simple property write, bind `:value` yourself and handle
+`@change` — `$shippingMethod` is a public string property and `setShipping()` a public method on
+your component, receiving the new value as its parameter:
+
@verbatim
```blade
@@ -111,3 +134,4 @@ RadioGroup::make(
- `make(string $value = '')` - Create a radio with a value
- `label(string $label)` - Inline label
- `disabled(bool $value = true)` - Disable the option
+- `a11yLabel(string $value)`, `a11yHint(string $value)` - Accessibility
diff --git a/resources/views/docs/mobile/4/edge-components/refreshable.md b/resources/views/docs/mobile/4/edge-components/refreshable.md
new file mode 100644
index 00000000..f78ee07a
--- /dev/null
+++ b/resources/views/docs/mobile/4/edge-components/refreshable.md
@@ -0,0 +1,93 @@
+---
+title: Refreshable
+order: 315
+---
+
+## Overview
+
+A standalone scrolling container with native pull-to-refresh. Wrap any content in it, point `@refresh` at a
+component method, and the platform handles the gesture, spinner, and physics for you. On iOS it uses SwiftUI
+`ScrollView { ... }.refreshable { }`; on Android, Compose `PullToRefreshBox` wrapping a `LazyColumn`. Both show
+their native pull-to-refresh spinner, with system haptics on iOS.
+
+@verbatim
+```blade static
+
+ @foreach($items as $item)
+
+ {{ $item->name }}
+
+ @endforeach
+
+```
+@endverbatim
+
+The refreshable container wants the full height of the screen — run it in your app to feel the pull gesture.
+
+## Events
+
+- `@refresh` - Component method called when the user pulls down past the release threshold (optional, string)
+
+## Children
+
+Children are the scrollable content — the refreshable element **is** the scrolling container, so don't nest a
+[``](scroll-view) (or another refreshable) inside, or you'll get nested scrolling.
+
+
+
+The refresh spinner stays visible for a short minimum window (around 800ms) after `@refresh` fires, so a fast PHP
+handler doesn't make the pull feel skipped. Your handler runs during that window and the updated tree paints just as
+the spinner hides — there's no separate "done" call to make.
+
+
+
+## Refreshable vs. List
+
+[``](list) has pull-to-refresh built in via its `on-refresh` prop, alongside swipe actions,
+end-reached, and Material3 rows. Reach for `` when you want pull-to-refresh around arbitrary
+content — cards, a [``](column), a dashboard — rather than a list of rows.
+
+## Examples
+
+### Refreshable feed
+
+@verbatim
+```blade static
+
+
+ @foreach($posts as $post)
+
+ {{ $post->title }}
+ {{ $post->excerpt }}
+
+ @endforeach
+
+
+```
+@endverbatim
+
+The handler updates state and returns — the spinner dismisses on its own:
+
+```php
+public function refreshFeed(): void
+{
+ $this->posts = Post::latest()->get();
+}
+```
+
+## Element
+
+```php
+use Native\Mobile\Edge\Elements\Refreshable;
+use Native\Mobile\Edge\Elements\Row;
+use Native\Mobile\Edge\Elements\Text;
+
+Refreshable::make()
+ ->onRefresh('loadLatest')
+ ->addChild(Row::make(Text::make('First')))
+ ->addChild(Row::make(Text::make('Second')));
+```
+
+- `make()` - Create a refreshable container
+- `onRefresh(string $method)` - Component method called on pull-to-refresh
+- `addChild(Element $child)` - Append a child to the scrollable content (inherited from the base `Element`)
diff --git a/resources/views/docs/mobile/4/edge-components/row.md b/resources/views/docs/mobile/4/edge-components/row.md
index a957e7de..b3311954 100644
--- a/resources/views/docs/mobile/4/edge-components/row.md
+++ b/resources/views/docs/mobile/4/edge-components/row.md
@@ -12,7 +12,7 @@ and inline groupings.
```blade
- 4.8 Rating
+ 4.8 Rating
```
@endverbatim
@@ -43,9 +43,9 @@ Everything else from the shared list applies the same as on any element (`w-*`,
@verbatim
```blade
- Title
+ Title
-
+
```
@endverbatim
@@ -55,9 +55,9 @@ Everything else from the shared list applies the same as on any element (`w-*`,
@verbatim
```blade
- One
- Two
- Three
+ One
+ Two
+ Three
```
@endverbatim
diff --git a/resources/views/docs/mobile/4/edge-components/scroll-view.md b/resources/views/docs/mobile/4/edge-components/scroll-view.md
index c438ba56..0ca13434 100644
--- a/resources/views/docs/mobile/4/edge-components/scroll-view.md
+++ b/resources/views/docs/mobile/4/edge-components/scroll-view.md
@@ -6,11 +6,26 @@ order: 340
## Overview
A scrollable container for content that exceeds the available screen space. By default it scrolls vertically, but can
-be configured for horizontal scrolling. On the native side, this uses `LazyColumn`/`LazyRow` on Android and
-`ScrollView` on iOS for efficient rendering.
+be configured for horizontal or two-axis (2D pan) scrolling. On the native side, this uses `LazyColumn`/`LazyRow` on
+Android and `ScrollView` on iOS for efficient rendering.
@verbatim
```blade
+
+
+ @foreach (range(1, 20) as $i)
+ Scrollable row {{ $i }}
+ @endforeach
+
+
+```
+@endverbatim
+
+The fixed `h-[240]` here just gives the preview a bounded viewport so the content overflows and scrolls. On a real
+screen you'd typically let the scroll view fill the page instead:
+
+@verbatim
+```blade static
@foreach($items as $item)
@@ -25,8 +40,14 @@ be configured for horizontal scrolling. On the native side, this uses `LazyColum
All [shared layout and style attributes](layout) are supported, plus:
-- `horizontal` - Scroll horizontally instead of vertically (optional, boolean, default: `false`)
+- `axis` - Scroll direction: `vertical`, `horizontal`, or `both` (optional, default: `vertical`). `both` enables 2D
+ panning for content that's larger than the viewport in both dimensions — give the inner content explicit dimensions
+ larger than the viewport for there to be anything to pan to
+- `horizontal` - Scroll horizontally instead of vertically (optional, boolean, default: `false`). `axis="horizontal"` is
+ the modern equivalent
- `shows-indicators` - Show scroll indicators (optional, boolean, default: `true`) [iOS]
+- `scroll-anchor` - Set to `bottom` for chat-style behavior: the view opens at the latest item and auto-scrolls on new
+ content while the user is near the bottom (optional)
@@ -44,12 +65,15 @@ scrollable content.
### Vertical list
+A full-screen list of posts. `safe-area` keeps the content clear of the notch and home indicator, so this one is
+meant for a real page root — run it in your app to see it edge-to-edge:
+
@verbatim
-```blade
-
-
+```blade static
+
+
@foreach($posts as $post)
-
+
{{ $post->title }}
{{ $post->excerpt }}
@@ -70,10 +94,9 @@ scrollable content.
:width="120"
:height="80"
center
- bg="#F1F5F9"
- :border-radius="12"
+ class="bg-theme-surface-variant rounded-xl"
>
- {{ $category->name }}
+ {{ $category->name }}
@endforeach
@@ -83,9 +106,12 @@ scrollable content.
### Full-page scrollable layout
+Another page-root pattern — `h-full` and `safe-area` only make sense against a real screen, so try this one in your
+app:
+
@verbatim
-```blade
-
+```blade static
+
Welcome
@@ -97,9 +123,44 @@ scrollable content.
```
@endverbatim
+### Chat-style stick-to-bottom
+
+The view opens at the latest message and stays pinned to the bottom as new content arrives. The fixed `h-[200]`
+bounds the preview so there's something to scroll — in a real chat screen you'd use `fill` instead:
+
+@verbatim
+```blade
+
+
+ @foreach($messages as $message)
+ {{ $message->body }}
+ @endforeach
+
+
+```
+@endverbatim
+
When using vertical scrolling, make sure to set width to fill on both the scroll view and its child column so content
stretches across the full screen width.
+
+## Element
+
+```php
+use Native\Mobile\Edge\Elements\ScrollView;
+
+ScrollView::make()
+ ->horizontal()
+ ->both()
+ ->showsIndicators(false)
+ ->autoScrollTo(0);
+```
+
+- `make(Element ...$children)` - Create a scroll view with children
+- `horizontal(bool $value = true)` - Scroll horizontally instead of vertically
+- `both()` - Enable 2D panning on both axes
+- `showsIndicators(bool $value = true)` - Toggle scroll indicators [iOS]
+- `autoScrollTo(int $index)` - Programmatically scroll to the child at `$index`
diff --git a/resources/views/docs/mobile/4/edge-components/select.md b/resources/views/docs/mobile/4/edge-components/select.md
index 2d9cc3a0..ae31ec2b 100644
--- a/resources/views/docs/mobile/4/edge-components/select.md
+++ b/resources/views/docs/mobile/4/edge-components/select.md
@@ -8,19 +8,28 @@ order: 350
A single-choice dropdown picker over a flat list of strings. On iOS, renders as a SwiftUI `Menu` (popover); on
Android, as an M3 `ExposedDropdownMenuBox` with an outlined trigger.
-Per Model 3, colors and borders come from theme tokens.
+Per Material 3, colors and borders come from theme tokens.
@verbatim
```blade
+@php $shippingCountry = null; @endphp
+
```
@endverbatim
+Here `shippingCountry` is a public string property on your component (the `@php` line stands in for
+`public ?string $shippingCountry = null;`) — while it is `null`, the placeholder shows.
+
+Options are a flat list of display strings — pass the strings you want shown, and the selected string is the
+value bound back to your component. An associative `value => label` array is flattened to its labels, so the
+displayed text is what you get.
+
## Props
- `options` - Array of option strings (required, array)
@@ -35,27 +44,48 @@ Per Model 3, colors and borders come from theme tokens.
- `@change` - Component method called when the selection changes. Receives the new option string
+
+
+Margin classes position the picker; its colors and borders come from the theme.
+
+
+
## Two-way Binding
`native:model` binds the selected option to a string property on your component:
@verbatim
```blade
-
+@php $country = 'Canada'; @endphp
+
+
+
+Shipping to: {{ $country }}
```
@endverbatim
+`country` is a public string property on your component (the `@php` line stands in for
+`public string $country = 'Canada';`), and `$countries` is an array of option strings. Picking an option
+syncs the selected string back automatically — no `@change` handler needed — so the echoed text updates
+with the selection.
+
## Examples
### Country picker
@verbatim
```blade
+@php $destination = 'Japan'; @endphp
+
```
@endverbatim
diff --git a/resources/views/docs/mobile/4/edge-components/shapes.md b/resources/views/docs/mobile/4/edge-components/shapes.md
index 0d1d9a57..8aa3d94a 100644
--- a/resources/views/docs/mobile/4/edge-components/shapes.md
+++ b/resources/views/docs/mobile/4/edge-components/shapes.md
@@ -15,21 +15,25 @@ These are typically placed inside a [``](canvas) or used standalo
## Rect
-A rectangle filled with `bg`. All [shared layout and style attributes](layout) apply, so border, radius, opacity,
-and elevation behave as on any other element.
+A rectangle filled with `bg`. All [shared layout and style attributes](layout) apply — use Tailwind-style classes
+(`rounded-*`, `border-*`, `opacity-*`, `shadow-*`) for radius, borders, opacity, and elevation, just as on any
+other element.
@verbatim
```blade
-
+
```
@endverbatim
### Props
-- `left` - X position offset in dp (optional, float). Used inside an absolutely-positioned parent
-- `top` - Y position offset in dp (optional, float)
+The `left` / `top` position props are accepted by the PHP element but are not currently read by the iOS or Android
+renderers. To offset a rect inside a parent, use absolute-positioning classes instead — e.g.
+`class="absolute top-[8] left-[8]"`.
-`` may optionally wrap children if you need to layer content on top of the fill.
+`` is a self-closing element, so it doesn't accept tag children. To layer content on top of the fill,
+overlay the rect and your content inside a [``](stack). In PHP, the `Rect` element accepts children via
+`addChild()` if you'd rather build the layering fluently.
## Circle
@@ -44,33 +48,35 @@ perfect circle use equal `width` and `height`.
### Props
-- `left` - X position offset in dp (optional, float)
-- `top` - Y position offset in dp (optional, float)
+As with ``, the `left` / `top` position props exist on the PHP element but the renderers don't read
+them — position circles with absolute-positioning classes (`class="absolute top-[8] left-[8]"`) or by centering
+them in a [``](stack).
`` is a self-closing element. It does not accept children.
## Line
-A 1pt horizontal rule across the available width.
+A horizontal rule. Style it with `border-*` classes:
@verbatim
-```blade
-
+```blade static
+
```
@endverbatim
### Props
-All [shared layout and style attributes](layout) are supported. The most useful:
+All [shared layout and style attributes](layout) are supported. Set the stroke through classes:
-- `border-color` - Line color as hex string (optional, default: platform separator color)
-- `border-width` - Stroke thickness in dp (optional, float, default: `1`)
+- `border-theme-outline` / `border-[#94A3B8]` - Line color (default: platform separator color)
+- `border` / `border-2` / `border-4` - Stroke thickness in dp (default: `1`)
-`` always paints a centered horizontal stroke across its frame — `from`/`to` coordinates are accepted
-on the PHP element but the iOS and Android renderers don't read them. To position a line, control the parent
-container's frame or use a styled ``.
+On Android, `` paints a centered horizontal stroke across the full width of its frame. On iOS the
+current renderer draws a fixed 100pt stroke pinned to the top of the frame, so it does not reliably span the
+available width — prefer ` ` for a full-width rule. `from`/`to`
+coordinates are accepted on the PHP element but neither renderer reads them.
@@ -86,19 +92,30 @@ container's frame or use a styled ``.
### Colored badge background
+Overlay the label on a filled rect with a ``. Give the rect an explicit frame and center the text on top:
+
@verbatim
```blade
-
+
+
New
-
+
```
@endverbatim
### Decorative separator
+@verbatim
+```blade static
+
+```
+@endverbatim
+
+For a full-width rule that renders consistently on both platforms today, use a divider instead:
+
@verbatim
```blade
-
+
```
@endverbatim
diff --git a/resources/views/docs/mobile/4/edge-components/side-nav.md b/resources/views/docs/mobile/4/edge-components/side-nav.md
index cd393798..4e81ab28 100644
--- a/resources/views/docs/mobile/4/edge-components/side-nav.md
+++ b/resources/views/docs/mobile/4/edge-components/side-nav.md
@@ -21,7 +21,7 @@ order: 370
A slide-out navigation drawer with support for groups, headers, and dividers.
@verbatim
-```blade
+```blade static
-Any `url` that doesn't match the web view's domain will open in the user's default browser.
+Any `url` that doesn't match the web view's domain will open in the user's default browser. Set `open-in-browser` to
+force the browser even for a same-domain `url`.
diff --git a/resources/views/docs/mobile/4/edge-components/slider.md b/resources/views/docs/mobile/4/edge-components/slider.md
index 1ba45ea0..9de495d2 100644
--- a/resources/views/docs/mobile/4/edge-components/slider.md
+++ b/resources/views/docs/mobile/4/edge-components/slider.md
@@ -13,10 +13,15 @@ under control while the user drags.
@verbatim
```blade
-
+@php $brightness = 60; @endphp
+
+
```
@endverbatim
+`brightness` is a public property on your component — the `@php` line stands in for
+`public int $brightness = 60;`.
+
## Props
- `value` - Current value (optional, float)
@@ -24,7 +29,6 @@ under control while the user drags.
- `max` - Maximum value (optional, float, default: `1`)
- `step` - Snap increment (optional, float, default: `0` for continuous)
- `disabled` - Disable the slider (optional, boolean, default: `false`)
-- `size` - `sm | md (default) | lg` (optional, string)
- `a11y-label` - Accessibility label (optional)
- `a11y-hint` - Accessibility hint (optional)
@@ -32,23 +36,36 @@ under control while the user drags.
- `@change` - Component method called when the value changes. Receives the new float value
+
+
+Margin classes position the slider; the active track and thumb colors come from the theme.
+
+
+
## Two-way Binding
@verbatim
```blade
+@php $intensity = 50; @endphp
+
{{-- Every drag tick fires --}}
-
+
{{-- Only fires on drag release --}}
-
+
{{-- Coalesce ticks into one event after 300ms idle --}}
-
+
+
+Intensity: {{ $intensity }}
```
@endverbatim
-`live` is the default and stress-tests the runtime's round-trip; `blur` is the most efficient for unsteady hands;
-`debounce` is the middle ground.
+`intensity` is a public property on your component (`public int $intensity = 50;`). All three sliders bind
+the same property, so dragging any one of them syncs the others and the echo — notice *when* each modifier
+pushes its update. `live` is the default and stress-tests the runtime's round-trip; `blur` is the most
+efficient for unsteady hands; `debounce` is the middle ground. Omit the interval (`native:model.debounce`)
+and it defaults to 300ms.
## Examples
@@ -56,21 +73,30 @@ under control while the user drags.
@verbatim
```blade
+@php $volume = 40; @endphp
+
-
- Volume
- {{ $volume }}%
+
+ Volume
+ {{ $volume }}%
```
@endverbatim
+Declare `volume` as a public property on your component (`public int $volume = 40;`) and the label
+tracks the thumb as it settles.
+
### Stepped picker
@verbatim
```blade
+@php $rating = 3; @endphp
+
+
+Rating: {{ $rating }} / 5
```
@endverbatim
@@ -90,7 +116,6 @@ Slider::make()
- `make()` - Create a slider
- `value(float $val)`, `min(float $val)`, `max(float $val)`, `step(float $val)` - Range / current value
- `disabled(bool $value = true)` - Disable the slider
-- `size(string $value)` - `sm | md | lg`
- `a11yLabel(string $value)`, `a11yHint(string $value)` - Accessibility
- `syncMode(string $mode)`, `debounceMs(int $ms)` - Set by `native:model` modifiers
- `onChange(string $method)` - Component method invoked on change
diff --git a/resources/views/docs/mobile/4/edge-components/spacer.md b/resources/views/docs/mobile/4/edge-components/spacer.md
index 384d87ab..53e77e4b 100644
--- a/resources/views/docs/mobile/4/edge-components/spacer.md
+++ b/resources/views/docs/mobile/4/edge-components/spacer.md
@@ -9,7 +9,7 @@ A flexible space element that expands to fill remaining space within a column or
push elements apart without calculating explicit sizes.
@verbatim
-```blade
+```blade static
```
@endverbatim
@@ -48,8 +48,8 @@ See the full shared list at [Layout & Styling](layout#supported-tailwind-classes
@verbatim
```blade
-
- Welcome
+
+ Welcome
Get started with your app.
@@ -57,14 +57,18 @@ See the full shared list at [Layout & Styling](layout#supported-tailwind-classes
```
@endverbatim
+In a real app this column is usually the page root with `h-full`, so the spacer pushes the button to the bottom of the
+screen. The fixed `h-[220]` here just gives the preview a bounded height — a spacer can only grow when its parent's
+main axis is constrained.
+
### Toolbar with right-aligned trailing icon
@verbatim
```blade
- Title
+ Title
-
+
```
@endverbatim
@@ -74,17 +78,18 @@ See the full shared list at [Layout & Styling](layout#supported-tailwind-classes
@verbatim
```blade
- Section One
-
- Section Two
+ Section One
+
+ Section Two
```
@endverbatim
-Fixed-size spacers are useful, but a margin on the next element (`… `) is
-often more readable.
+A fixed-size spacer needs `flex-grow-0` alongside its height (or width, in a row) — without it, the default
+`flex-grow: 1` takes precedence over the explicit size. Fixed-size spacers are useful, but a margin on the next
+element (`… `) is often more readable.
@@ -93,6 +98,6 @@ often more readable.
```php
use Native\Mobile\Edge\Elements\Spacer;
-Spacer::make(); // flex-grow: 1
-Spacer::make()->height(8); // fixed 8dp vertical
+Spacer::make(); // flex-grow: 1
+Spacer::make()->height(8)->flexGrow(0); // fixed 8dp vertical
```
diff --git a/resources/views/docs/mobile/4/edge-components/stack.md b/resources/views/docs/mobile/4/edge-components/stack.md
index 7f21d368..f4a89d75 100644
--- a/resources/views/docs/mobile/4/edge-components/stack.md
+++ b/resources/views/docs/mobile/4/edge-components/stack.md
@@ -12,10 +12,10 @@ Useful for badges, image overlays, floating labels, and layered UI effects.
@verbatim
```blade
-
-
+
+
- Overlay Text
+ Overlay Text
```
@@ -51,12 +51,15 @@ Everything else from the shared list applies as on any element (`p-*`, `m-*`, `b
@verbatim
```blade
-
+
```
@endverbatim
+In a real app the base layer would typically be a `` avatar (e.g.
+` `).
+
### Badge on an icon
@verbatim
@@ -75,8 +78,8 @@ Everything else from the shared list applies as on any element (`p-*`, `m-*`, `b
@verbatim
```blade
-
-
+
+
Featured Article
Read more about this topic
@@ -100,7 +103,7 @@ use Native\Mobile\Edge\Elements\Image;
use Native\Mobile\Edge\Elements\Text;
Stack::make(
- Image::make('https://example.com/photo.jpg'),
+ Image::make('https://picsum.photos/seed/nativephp/400/300'),
Text::make('Overlay'),
)->width(200)->height(200);
```
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 a7e0f94a..310c1d06 100644
--- a/resources/views/docs/mobile/4/edge-components/tab-row.md
+++ b/resources/views/docs/mobile/4/edge-components/tab-row.md
@@ -12,23 +12,30 @@ Distinct from [``](bottom-nav) — bottom nav is your app's p
URL routing, while `` is an in-screen sectioning control whose tabs swap content within the same
screen.
-Per Model 3, the active tab uses `theme.primary` and the underline is `theme.primary`. Inactive tabs use
+Per Material 3, the active tab uses `theme.primary` and the underline is `theme.primary`. Inactive tabs use
`theme.onSurfaceVariant`.
@verbatim
```blade
+@php $activeTab = 0; @endphp
+
-
+
```
@endverbatim
+`activeTab` is a public int property on your component — the `@php` line stands in for
+`public int $activeTab = 0;`.
+
## Props (Row)
- `value` / `selected-index` - Currently selected tab index (optional, int, default: `0`)
+- `sync-mode` - How `native:model` writes the selected index back to your component (optional)
- `a11y-label` - Accessibility label (optional)
+- `a11y-hint` - Accessibility hint (optional)
## Events
@@ -36,24 +43,31 @@ Per Model 3, the active tab uses `theme.primary` and the underline is `theme.pri
## Two-way Binding
-`native:model` binds the selected index to an integer property on your component:
+`native:model` binds the selected index to an integer property on your component. Tapping a tab syncs the new
+index back automatically:
@verbatim
```blade
-
+@php $currentTab = 0; @endphp
+
+
+
+Selected: {{ ['One', 'Two', 'Three'][$currentTab] }}
```
@endverbatim
+Here `currentTab` stands in for `public int $currentTab = 0;` on your component.
+
## Children
`` declares a single tab. Each accepts:
- `label` - Tab label (required, string). Can also be passed as the first argument to `make()`
-- `icon` - Optional [icon](icons) name rendered above the label
+- `icon` - Optional [icon](icon#icon-name-reference) name rendered above the label
- `a11y-label` - Accessibility label override (optional)
## Examples
@@ -62,7 +76,9 @@ Per Model 3, the active tab uses `theme.primary` and the underline is `theme.pri
@verbatim
```blade
-
+@php $section = 0; @endphp
+
+
@@ -70,30 +86,35 @@ Per Model 3, the active tab uses `theme.primary` and the underline is `theme.pri
@if($section === 0)
-
- Overview content
+
+ Overview content
@elseif($section === 1)
-
- Activity content
+
+ Activity content
@else
-
- Members content
+
+ Members content
@endif
```
@endverbatim
+`section` is a public int property on your component (`public int $section = 0;`). On a real screen you would
+typically add `fill` to the outer column and the content panes so they occupy the remaining screen height.
+
### Tabs with icons
@verbatim
```blade
+@php $filter = 0; @endphp
+
-
+
```
@endverbatim
@@ -107,7 +128,7 @@ use Nativephp\NativeUi\Elements\Tab;
TabRow::make(
Tab::make('Recent')->icon('history'),
Tab::make('Starred')->icon('star'),
- Tab::make('Archived')->icon('archive'),
+ Tab::make('Archived')->icon('archive-box'),
)
->selectedIndex($activeTab)
->onChange('setActiveTab');
@@ -124,4 +145,4 @@ TabRow::make(
### `Tab` methods
- `make(string $label = '')` - Create a tab with a label
-- `icon(string $icon)` - Tab icon
+- `icon(?string $name = null, IosSymbol|string|null $ios = null, AndroidSymbol|string|null $android = null)` - Tab icon. Pass a shared [icon](icon#icon-name-reference) name, or override per platform with the `ios:` / `android:` named arguments
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 32c1662d..45306cd5 100644
--- a/resources/views/docs/mobile/4/edge-components/text-input.md
+++ b/resources/views/docs/mobile/4/edge-components/text-input.md
@@ -5,20 +5,25 @@ order: 430
## Overview
-Native text input fields come in two variants:
+Native text input fields come in three variants:
- `` — bordered field. Default, lower emphasis.
- `` — surface-fill background + bottom indicator line. Higher emphasis.
+- `` — chromeless field with no Material chrome, for chat pills, search bars, and inline
+ editors where the surrounding container supplies the visuals. See [Bare variant](#bare-variant).
-Both share the same prop set and event API. Choose based on emphasis, not behavior.
+All three share the same prop set and event API. Choose the outlined / filled pair based on emphasis, not behavior;
+reach for bare when you want to style the input yourself.
-On iOS they render as SwiftUI `TextField` / `SecureField` with Material3-style chrome; on Android they map to
-`OutlinedTextField` / `TextField` (filled). Per Model 3 there are no per-instance color, font, or border overrides
-— all chrome resolves from the theme. For fully custom input visuals drop to [``](pressable)
-wrapping your own drawing.
+On iOS the outlined and filled variants render as SwiftUI `TextField` / `SecureField` with Material3-style chrome; on
+Android they map to `OutlinedTextField` / `TextField` (filled). Per Material 3 these two have no per-instance color or
+border overrides — all chrome resolves from the theme. For fully custom input visuals reach for the bare variant, or
+drop to [``](pressable) wrapping your own drawing.
@verbatim
```blade
+@php $email = ''; @endphp
+
`](text#line-height))
+- `line-height` / `line-height-px` attributes are an alternative to the `leading-*` classes: `line-height` is a multiplier of the font size, `line-height-px` an absolute override
+
### Sizing & accessibility
- `size` - `sm | md (default) | lg`
@@ -76,7 +97,7 @@ Both variants accept identical props.
-Both variants are self-closing. They do not accept children.
+All three variants are self-closing. They do not accept children.
@@ -95,17 +116,72 @@ Use the `native:model` directive for automatic two-way binding with a component
@verbatim
```blade
-
-
-
+@php $name = 'Ada'; $email = ''; $search = ''; @endphp
+
+
+
+
+
+Hello, {{ $name }}!
```
@endverbatim
+`name`, `email`, and `search` are public string properties on your component — typing syncs them back
+automatically, so the `{{ $name }}` echo updates as you type.
+
`sync-mode` semantics:
- `live` (default) — every keystroke fires `@change`
- `blur` — only fires on focus loss / submit
-- `debounce` — fires after `debounce_ms` of inactivity, or immediately on blur / submit
+- `debounce` — fires after `debounce-ms` of inactivity (300ms when unset), or immediately on blur / submit
+
+## Bare variant
+
+`` is a chromeless input — no outline, no fill, no label, no Material chrome, just the typing
+affordance. It's built for chat input pills, search bars, and inline editors where the surrounding container provides
+the visuals. On iOS it renders as a plain SwiftUI `TextField`; on Android as a Compose `BasicTextField`.
+
+It inherits the full shared prop set — `native:model`, `secure`, `multiline`, `keyboard`, `@submit`,
+`keep-focus-on-submit`, `disabled`, `read-only`, and the rest — so it behaves exactly like the other variants.
+
+Two things set it apart:
+
+- **Class-based styling passes through.** Unlike the filled / outlined variants (which resolve all chrome from the
+ theme), the bare variant lets element-level styling flow to the input directly: `bg`, `rounded-*`, borders, `glass`,
+ opacity, elevation, and padding. So you can style the pill on the input itself, no wrapping row needed.
+- **A `color` attribute** sets the text color — a hex value or a Tailwind token, with `dark:text-*` support for a
+ light/dark pair. Useful when your wrapper overrides the background and the theme's default text color would vanish.
+
+@verbatim
+```blade static
+@php $draft = ''; @endphp
+
+
+```
+@endverbatim
+
+The `color` attribute can be set explicitly or picked up from a `text-*` class on the input:
+
+@verbatim
+```blade static
+@php $query = ''; @endphp
+
+
+
+```
+@endverbatim
+
+
+
+`` is self-closing. It does not accept children.
+
+
## Examples
@@ -113,6 +189,8 @@ Use the `native:model` directive for automatic two-way binding with a component
@verbatim
```blade
+@php $email = ''; $password = ''; @endphp
+
label('Email')
@@ -203,14 +290,24 @@ OutlinedTextInput::make()
->onChange('updateEmail');
```
-Both elements share the same fluent API (defined on `BaseTextInput`):
+All three elements share the same fluent API (defined on `BaseTextInput`):
- `value(string $text)`, `placeholder(string $text)`, `label(string $text)`, `supporting(string $text)`
- `disabled(bool $value = true)`, `readOnly(bool $value = true)`, `error(bool $value = true)`, `loading(bool $value = true)`
- `keyboard(string|int $type)`, `secure(bool $value = true)`, `maxLength(int $length)`
- `multiline(bool $value = true)`, `maxLines(int $lines)`, `minLines(int $lines)`
-- `prefix(string $text)`, `suffix(string $text)`, `leadingIcon(string $name)`, `trailingIcon(string $name)`
+- `keepFocusOnSubmit(bool $value = true)` - Keep the keyboard up after `@submit`
+- `prefix(string $text)`, `suffix(string $text)`
+- `leadingIcon(?string $name = null, IosSymbol|string|null $ios = null, AndroidSymbol|string|null $android = null)` -
+ pass a shared `$name`, or per-platform `$ios` / `$android` symbols for a different icon on each platform
+- `trailingIcon(?string $name = null, IosSymbol|string|null $ios = null, AndroidSymbol|string|null $android = null)` -
+ same per-platform form as `leadingIcon()`
- `size(string $value)` - `sm | md | lg`
+- `font(string $name)` - Custom font (file token or config alias)
- `a11yLabel(string $value)`, `a11yHint(string $value)`
- `syncMode(string $mode)`, `debounceMs(int $ms)`
- `onChange(string $method)`, `onSubmit(string $method)`
+
+`BareTextInput` adds one method on top of the shared API:
+
+- `color(string $color)` - Text color as a hex value or Tailwind token (with `dark:text-*` support)
diff --git a/resources/views/docs/mobile/4/edge-components/text.md b/resources/views/docs/mobile/4/edge-components/text.md
index de099f67..d322ee51 100644
--- a/resources/views/docs/mobile/4/edge-components/text.md
+++ b/resources/views/docs/mobile/4/edge-components/text.md
@@ -9,7 +9,7 @@ Displays text content using platform-native typography. Text content goes betwee
@verbatim
```blade
-
+
Hello, world!
```
@@ -28,9 +28,20 @@ All [shared layout and style attributes](layout) are supported, plus:
- `5` semibold
- `6` bold
- `7` heavy (extrabold)
+
+ Values outside 1-7 are clamped to the nearest supported weight.
- `color` - Text color as hex string (optional, default: `#000000`)
- `text-align` - Alignment: `0`=start, `1`=center, `2`=end (optional, int, default: `0`)
- `max-lines` - Maximum lines before truncating with ellipsis (optional, int)
+- `font-style` - `0`=normal, `1`=italic (optional, int)
+- `font-family` - Typeface: `0`=sans, `1`=serif, `2`=mono (optional, int)
+- `underline` - Underline: `1`=on, `0`=off (optional, int)
+- `line-through` - Strikethrough: `1`=on, `0`=off (optional, int)
+- `text-transform` - Case: `0`=none, `1`=uppercase, `2`=lowercase, `3`=capitalize (optional, int)
+- `letter-spacing` - Tracking in em relative to font size (optional, float)
+- `font` - Custom font: a `resources/fonts/` file token or a config alias like `accent` (optional, string) — see [Custom fonts](#custom-fonts)
+
+Line height is set with `leading-*` classes — see [Line height](#line-height).
@@ -39,6 +50,136 @@ supported as [inline runs](#inline-runs); any other HTML tags in the slot are st
+## Custom fonts
+
+Ship a font with your app and use it by name. Drop `.ttf`, `.otf`, or `.ttc` files
+into your app's `resources/fonts/` directory, then reference one by its filename
+(without the extension) with the `font` attribute:
+
+@verbatim
+```blade
+
+ Custom heading
+
+```
+@endverbatim
+
+So `resources/fonts/Inter-Bold.ttf` becomes `font="Inter-Bold"`. The build bundles
+the files into the native project automatically — no configuration needed. On iOS
+the font is registered and matched by its PostScript name; on Android it's loaded
+from the app's assets. An unresolved name falls back to the system font.
+
+`font` also works on [``](button) and the [text inputs](text-input),
+and is available fluently as `->font('Inter-Bold')`.
+
+### Downloading from Google Fonts
+
+The `native:font` command downloads any [Google Fonts](https://fonts.google.com)
+family straight into `resources/fonts/` — no API key needed:
+
+```bash
+php artisan native:font Lobster
+php artisan native:font "Rock Salt" Inter
+php artisan native:font Inter --weights=400,700 --italic
+```
+
+Files are named `-