@@ -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..ffc1c318 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 from `resources/fonts/`, by filename without extension (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 (filename without extension)
+- `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..bae76506 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..12e8423e 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..d21c5b52 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..154c7722 100644
--- a/resources/views/docs/mobile/4/edge-components/introduction.md
+++ b/resources/views/docs/mobile/4/edge-components/introduction.md
@@ -66,6 +66,8 @@ interactive forms and navigation chrome.
- **[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
+- **[Lazy Grid](lazy-grid)** - Self-scrolling grid that only materializes visible cells
+- **[Refreshable](refreshable)** - Standalone scrolling container with native pull-to-refresh
### Overlays
@@ -80,7 +82,7 @@ interactive forms and navigation chrome.
## How It Works
@verbatim
-```blade
+```blade static
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..1c3a59c2 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..989f8fcf 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..12e89f8c 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 (filename without extension)
- `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..ca1c20a0 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 name — a font file from `resources/fonts/` without its extension (optional, string) — see [Custom fonts](#custom-fonts)
+
+Line height is set with `leading-*` classes — see [Line height](#line-height).
@@ -39,6 +50,122 @@ 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 `-