A contemptably stubborn if not dictatorially restrictive navigation rail/menu–I call it a renu. Or maybe a mail. No, a navigrenuail–for Jetpack Compose with a streamlined, DSL-style API.
This “navigrenuail” provides a vertical navigation rail that expands to a full menu drawer. It is designed to be “batteries-included,” providing common behaviors and features out-of-the-box to ensure a consistent look and feel across applications.
Platform parity note. The Kotlin modules (
aznavrailfor Android,aznavrail-cmpfor Compose Multiplatform) are the reference implementation. The React port (aznavrail-react) is versioned separately and currently lags them — the drop-down trigger set, unattached hosts,azItemStateandazPopupare Kotlin-only. See Capabilities & Limitations.
expandWhen, the About screen), plus requirements — Android + React parity.The most recent additions, and where each is documented in full:
| Feature | What it is | Section |
|---|---|---|
| Badges | azItemState(id, badge = "3") drops a count on any item’s corner — dissolving after a second, or persistentBadge = true to stay. Same call sets a per-item spinner and an alert glyph. |
Per-Item Badges & Loading |
| Three highlights | An item can be lit three different ways — active (where you are), focus (what you are touching), and a secondary highlight that is entirely yours. azTheme sets the palette, azHighlight overrides it per item. |
The three highlights |
| Windows that move and fold | Every panel the library floats — popups, hidden menus — is an AzWindow: drag it by its bar, fold it to that bar, close it. |
Windows (AzWindow) |
| About, exactly once | The library tracks every surface that offers an About affordance and draws it in only one of them, so a rail plus a drop-down no longer means About three times over. | In-App About Reader |
| About, already loaded | The reader’s docs and carousel are fetched in the background as the rail composes, so About opens populated instead of spinning. | In-App About Reader |
Add JitPack to your settings.gradle.kts:
CIRCLE, SQUARE, RECTANGLE, TRIANGLE, or the borderless NONE family —
NONE (rectangle footprint), NONE_SQUARE, NONE_CIRCLE. RECTANGLE/NONE auto-size width
(fixed 36dp height); the borderless shapes keep the footprint of the base shape they name, so
dropping the border never reflows the rail.AzButton, AzToggle, AzCycler, AzDivider, AzRoller, AzTextBox, AzForm, AzDropdownMenu.azRailSubHostItem / azMenuSubHostItem), so hosts nest to any depth.AzRailRelocItem allows user drag-and-drop reordering within clusters.
AzTextBox: Modern text box with autocomplete and submit button.AzForm: Group multiple text boxes into a single form with a shared submit button.AzRoller: A dropdown menu that works like a roller or slot machine, cycling through options infinitely.azStatus) and edges (azEdge) carrying instructions, declare goals (azGoal), and activate them on the AzGuidanceController. The reactive engine shows the instruction to reach the next status toward each active goal, auto-advances the instant a target status becomes true (no Next button), re-routes live, and shows every active goal’s callout next to its control. Cross-platform (Android + React), with completed goals persisted.AzDropdownMenu: A standalone, app-icon drop-down declared with the rail’s opinionated DSL (azConfig + azItem/azToggle/azCycler/azDivider). Its overlay panel is styled as the rail or menu (design), width-constrained to match, and pinned to the dockingSide screen edge. Items can carry a route to drive an AzNavHost via a NavController, exactly like the rail — and it accepts no styling the rest of the library doesn’t.headerIconSize.docs/) and renders them inline. On Android the repo is auto-derived from the app namespace (appRepositoryUrl is an optional override); on web appRepositoryUrl is required. (azAbout)androidx.navigation.compose.NavHost for seamless integration.AzNavHost automatically configures directional transitions (slide in/out) based on the docking side (e.g., standard LTR or mirrored for Right dock).azNestedRail allows for secondary popup rails (Vertical or Horizontal) triggered from a rail item.To use this library, add JitPack to your settings.gradle.kts:
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
}
Add the dependency to your app’s build.gradle.kts:
dependencies {
implementation("com.github.HereLiesAz.AzNavRail:aznavrail:VERSION") // Replace VERSION with the latest release
}
Targeting more than Android? The aznavrail-cmp module is a Compose
Multiplatform port of the same rail — one commonMain source set across Android, Desktop (JVM),
Web (wasmJs), and iOS:
commonMain.dependencies {
implementation("com.github.HereLiesAz.AzNavRail:aznavrail-cmp:VERSION")
}
See the module README for the CompositionLocal wiring (there’s no
AzHostActivityLayout off-Android) and the aznavrail-cmp-demo runnable
desktop + web sample.
No matching variant … was foundIf your Gradle sync fails with something like:
Could not resolve com.github.HereLiesAz.AzNavRail:aznavrail-cmp:VERSION.
Required by: project :app
> No matching variant of com.github.HereLiesAz.AzNavRail:aznavrail-cmp:VERSION was found.
…you’re pulling a Kotlin Multiplatform artifact into a plain Android/JVM project. The
multiplatform module (aznavrail-cmp) publishes Gradle Module Metadata with a variant per platform,
and a non-KMP consumer can’t select one through JitPack’s metadata redirect. Switch to the Android
artifact directly:
implementation("com.github.HereLiesAz.AzNavRail:aznavrail:VERSION")
A plain AAR, full-featured — this is the one the setup section above uses, and it includes
AzHostActivityLayout / AzNavHost.
implementation("com.github.HereLiesAz.AzNavRail:aznavrail-cmp-android:VERSION")
The Android variant of the CMP module. Note it has no AzHostActivityLayout / AzNavHost, so
Golden-Sample code won’t compile against it unmodified.
Also make sure you’re not using the old single-module coordinate
com.github.HereLiesAz:AzNavRail:VERSION — it predates the multi-module split and no longer
resolves. The module lives under group com.github.HereLiesAz.AzNavRail.
This is the standard, validated way to initialize AzNavRail. It must be wrapped in AzHostActivityLayout.
import com.hereliesaz.aznavrail.*
@Composable
fun SampleScreen() {
val navController = rememberNavController()
AzHostActivityLayout(
navController = navController,
initiallyExpanded = false
) {
// 1. CONFIGURATION
azConfig(
dockingSide = AzDockingSide.LEFT,
packButtons = true, // Tightly pack rail items
displayAppName = true
)
azTheme(activeColor = Color.Cyan)
// 2. NAVIGATION ITEMS
azRailItem(id = "home", text = "Home", route = "home", content = Icons.Default.Home)
azRailItem(id = "profile", text = "Profile", route = "profile")
// 3. MENU ONLY ITEMS
azMenuItem(id = "settings", text = "Settings", route = "settings")
// 4. ONSCREEN CONTENT
// Use 'onscreen' to define your UI.
// Layout rules (safe zones, padding) are enforced automatically.
onscreen(alignment = Alignment.Center) {
AzNavHost(startDestination = "home") {
composable("home") { Text("Home Screen") }
composable("profile") { Text("Profile Screen") }
composable("settings") { Text("Settings Screen") }
}
}
}
}
AzHostActivityLayout accepts several parameters to customize its behavior:
navController: The NavHostController to use. Required.currentDestination: Explicitly set the current route. If null, it is automatically derived from the navController.isLandscape: Explicitly set the orientation. If null, it is automatically derived from the screen configuration.initiallyExpanded: Set to true to have the rail expanded by default (e.g., for bubble activities).disableSwipeToOpen: Set to true to disable the swipe gesture that opens the menu.pagesEnabled: Toggles the pages Z-ordering system (default true). See below.AzHostActivityLayout enforces a “Strict Mode” layout system:
onscreen block will overlap the rail. Padding is automatically applied based on the docking side.onscreen (e.g., TopStart) are automatically mirrored if the rail is docked to the right.background(weight) DSL to place full-screen content behind the UI (e.g., maps, camera feeds). Backgrounds ignore safe zones.enableEdgeToEdge()), and resolves every safe zone against the system bars union the display cutout — a camera cutout can occlude an edge with no system bar on it, which is exactly the landscape short edge the rail docks against. AzSafeZones therefore carries start/end as well as top/bottom. Every window the library adds to the WindowManager sets LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS — the only value Android 15 left non-deprecated; if you override AzNavRailWindowService.windowParams, keep that assignment. See the Complete Guide §1.1.Both onscreen(...) and background(...) accept a page: Float (default 0f). Pages are a simple Z-ordering system layered on top of standard Compose positioning:
alignment (give them distinct alignments — or compose your own Row/Column inside the content — so they tile without overlapping).page = 1.5f slots a layer between 1f and 2f.background() items form their own book of pages that sits entirely beneath the onscreen book (which sits beneath the rail and nav bar). onscreen pages still respect the safe zones; backgrounds still fill the screen.pagesEnabled = true (default) the system is always active — items with no explicit page simply share the default page 0f. Set pagesEnabled = false to fall back to plain declaration-order rendering (backgrounds by weight), ignoring page.onscreen(alignment = Alignment.Center, page = 0f) { /* base layer */ }
onscreen(alignment = Alignment.Center, page = 1f) { /* drawn BEHIND page 0, can overlap it */ }
background(weight = 0, page = 2f) { /* a background layer, even further back */ }
AzNavRail includes an interactive “Help Mode” (formerly Info Screen), ideal for onboarding or help sections.
helpEnabled = true in azAdvanced.azHelpRailItem(id, text) to place a dedicated help button in the rail.RailItem IDs to help texts to display in the help cards along with the item’s info property. The text from helpList is displayed second.helpList or info is provided.AzTextBox and AzFormAzTextBox is a text input field. AzForm is a container that groups multiple AzTextBox fields, managing them as a single entity with one submit button.
AzTextBox can be configured as a multiline input, which will automatically expand vertically as the user types. The clear and submit buttons remain anchored to the bottom right.secret mode, which masks the input. In this mode, the clear button is replaced by a reveal icon to temporarily show the password.multiline and secret at the same time.AzForm Component:
AzTextBox and AzForm entries support an enabled parameter. When disabled, the input is non-interactive and visual elements are dimmed.import { AzNavRail, AzNavItem, AzButtonShape, AzDockingSide } from '@HereLiesAz/aznavrail-react';
export default function App() {
const [expanded, setExpanded] = useState(false);
const items: AzNavItem[] = [
{
id: "home",
text: "Home",
isRailItem: true,
onClick: () => console.log("Home clicked"),
shape: AzButtonShape.CIRCLE,
},
{
id: "settings",
text: "Settings",
isRailItem: true,
onClick: () => console.log("Settings clicked"),
shape: AzButtonShape.RECTANGLE,
}
];
return (
<View style=>
<AzNavRail
appName="My App"
appIcon={require('./assets/icon.png')}
items={items}
expanded={expanded}
onToggleExpand={() => setExpanded(!expanded)}
settings=
/>
<View style=>
{/* Main Content */}
</View>
</View>
);
}
AzHostActivityLayout enforces a “Constitution” for your UI to ensure consistency and usability:
onscreen is automatically padded to avoid the rail, regardless of docking side or rotation.background(weight) to place content behind the rail (e.g., maps).azRailItem: Always visible.azMenuItem: Visible only in the drawer.azNestedRail: Opens a secondary popup rail (Vertical/Horizontal).azRailRelocItem: Draggable items for user reordering.azRailRelocSubHostItem: A sub-host that is also draggable among its reloc siblings; moves as one block with its children.Manage state directly in the rail without leaving the context.
azRailToggle / azMenuToggle.azRailCycler (multi-state buttons).AzNavRail(...) {
azAdvanced(
isLoading = true // Shows the AzLoad animation in the center of the screen
// ...
)
}
React Implementation:
import { AzNavItem } from '@HereLiesAz/aznavrail-react';
const items: AzNavItem[] = [
{
id: "power",
isRailItem: true,
isToggle: true,
isChecked: isPowerOn,
toggleOnText: "Power On",
toggleOffText: "Power Off",
onClick: () => setPowerOn(!isPowerOn),
// ...
},
{
id: "mode",
isRailItem: true,
isCycler: true,
options: ["Auto", "Cool", "Heat"],
selectedOption: currentMode,
// ...
}
];
You can also use AzLoad directly in your composables.
React Implementation:
import { AzLoad } from '@HereLiesAz/aznavrail-react';
<AzLoad size={48} color="#6200EE" />
The AzButton component (and AzToggle, AzCycler) can be used independently of the rail.
AzButton(
onClick = { /* ... */ },
text = "Save",
modifier = Modifier.fillMaxWidth(), // Now supports modifiers
shape = AzButtonShape.RECTANGLE,
enabled = true, // Can be disabled
isLoading = false, // Shows loading spinner without resizing button
contentPadding = PaddingValues(16.dp) // Custom padding
)
The AzRoller component is a versatile dropdown that behaves like a slot machine but also supports typing and filtering. It extends the functionality of AzTextBox with a unique split-click interaction model.
AzRoller(
options = listOf("Cherry", "Bell", "Bar"),
selectedOption = "Cherry",
onOptionSelected = { /* handle selection (String) */ },
hint = "Select Item",
enabled = true,
isError = false
)
React Implementation:
import { AzRoller } from '@HereLiesAz/aznavrail-react';
<AzRoller
options={["Cherry", "Bell", "Bar"]}
selectedOption="Cherry"
onOptionSelected={(option) => { /* handle selection */ }}
hint="Select Item"
enabled={true}
/>
AzNavRail supports hierarchical navigation with host and sub-items. This allows you to create nested menus that are easy to navigate.
azRailSubHostItem / azMenuSubHostItem), so hosts nest to any depth. Opening a sub-host reveals its children inline while sibling sub-items stay visible. Children attach to their host by hostId reference, not by position.expandWhen): Pass expandWhen = { condition } to any host-item builder to auto-expand or auto-collapse based on a reactive condition. Rising edge (false→true) expands; falling edge (true→false) collapses. The user can still manually collapse a host while the condition is true — the condition re-fires only on the next false→true edge. Useful for the guidance framework, whose callouts need sub-items to be laid out before they can be anchored/highlighted.React Implementation:
const items: AzNavItem[] = [
{
id: "host-1",
text: "Host Item",
isRailItem: true,
isHost: true,
isExpanded: isHost1Expanded,
onClick: () => setHost1Expanded(!isHost1Expanded),
// ...
},
{
id: "sub-1",
text: "Sub Item",
isRailItem: true,
isSubItem: true,
hostId: "host-1",
// ...
}
];
azUnattachedHostItem)A host item does not have to live in the rail. azUnattachedHostItem draws one on its own
somewhere else on the screen; tapping it unfolds its sub-items inline beneath it, exactly as they
would have unfolded inside the rail. Sub-items attach the usual way — hostId — and sub-hosts still
nest to any depth. The host and its whole subtree leave both the rail strip and the drawer.
anchor |
Where it parks |
|---|---|
AzUnattachedAnchor.OPPOSITE (default) |
Side opposite the rail, level with the rail’s own items. |
AzUnattachedAnchor.BOTTOM |
Bottom of the screen, opposite side. |
AzUnattachedAnchor.FLOATING |
Draggable; its position is persisted across launches. |
OPPOSITE/BOTTOM hosts sharing an anchor stack into a fixed column with the rail’s own spacing.
FLOATING hosts each float and dock independently instead:
Each host’s own resting spot is persisted per-launch; rail-to-rail attachments are not.
Long lists scroll. When an expanded host’s sub-items run past the room it has — the window
below the host, minus the safe insets (for OPPOSITE/BOTTOM, also minus one button per host
stacked after it; for FLOATING, down to the bottom safe inset from wherever the rail sits) — the
sub-items scroll under the host, which itself stays fixed. Unfolded relocatable sub-hosts are part
of the same list. With no overflow nothing changes: no clipping, no scroll gesture. A swipe over the
list scrolls it and clicks nothing; a tap still clicks. Long-press-then-drag reordering still works
inside a scrolled list, and dragging an item into the list’s top or bottom edge band auto-scrolls it
so the item can be dropped beyond the visible window. On expansion, the active item (current route,
active classifier or last tapped) is scrolled into view.
azUnattachedHostItem(id = "tools", text = "Tools", anchor = AzUnattachedAnchor.FLOATING)
azRailSubItem(id = "measure", hostId = "tools", text = "Measure") { measure() }
azRailSubToggle(
id = "grid", hostId = "tools",
isChecked = grid, toggleOnText = "Grid On", toggleOffText = "Grid Off",
) { grid = !grid }
azRailRelocItem attaches by hostId here too — a Procreate-style layers panel is exactly this: each
layer row is an azRailRelocItem(hostId = "tools"). Tap, long-press hidden menus and long-press-drag
reordering all work. A group row is an azRailRelocSubHostItem: a host that is one draggable slot
together with its children (see the complete guide, §5).
azItemState)Every item builder takes badge / persistentBadge / isLoading, and any declared item — rail,
menu, sub-item, host, unattached host, nested-rail child — can be decorated by id afterwards:
azRailItem(id = "sync", text = "Sync") { startSync() }
azItemState(id = "sync", isLoading = syncing, badge = pending.takeIf { it > 0 }?.toString())
Loading is per item — the button spins its own AzLoad, scaled to itself and tinted to its own
colour, while the rest of the rail stays live. Null fields leave the item’s existing value alone, so
setting the badge does not clear the spinner.
An item can be lit three ways, and they answer three different questions. Picking the right one is mostly a matter of answering the question the highlight is for:
| Highlight | The question it answers | What lights it |
|---|---|---|
| Active | Where am I? | The item’s route is the current destination, or one of its classifiers is in azConfig(activeClassifiers = …). |
| Focus | What am I touching? | The item is pressed, or it was the last one tapped and carries no route of its own — a toggle, a cycler, an action. |
| Secondary | Whatever you decide. | Only you: azItemState(id, secondary = true) or azConfig(secondaryClassifiers = …). The library never lights it. |
Set the palette once on the rail, and override any of the three on a single item:
azTheme(
activeColor = Color.Cyan, // where you are
focusColor = Color.White, // what you are touching (unset = same as activeColor)
secondaryColor = Color(0xFFFFB300), // yours (unset = the rail's accent)
)
azRailItem(id = "record", text = "Rec")
azHighlight(id = "record", active = Color.Red) // this one goes red when it is the live screen
azItemState(id = "record", secondary = armed) // …and amber while merely armed
How to pick them. activeColor is the loudest thing on the rail, because “where you are” is the
one fact the rail exists to tell you — give it the app’s accent. focusColor is a transient: it is
on screen for as long as a finger is, so it wants contrast against the rail’s own colour rather than
a second hue competing with the accent. Leave it unset and focus simply reuses activeColor, which
is how the rail has always looked. secondaryColor should be a colour that reads as a condition
rather than a place — an amber for “armed”, a green for “synced” — because the user will see it on
an item that is emphatically not where they are.
Precedence, when more than one applies at once: focus beats active beats secondary. A press is the most immediate thing happening, so it wins for as long as it lasts.
AzPopup)An AzPopup is bound to a rail item, and the two share state both ways.
val alerts = rememberAzPopupController()
AzHostActivityLayout(navController = navController) {
azRailItem(id = "sync", text = "Sync") {
alerts.show(itemId = "sync", title = "Syncing", message = "Talking to the server…")
}
azPopup(alerts) {
Text(message ?: "")
AzButton(onClick = { item?.setLoading(false); dismiss() }, text = "Stop")
}
}
The body’s item handle reads the live AzNavItem and writes back to it — setBadge, setLoading,
setAlert, clear — and those writes survive the DSL re-running until the popup clears them.
show() with no itemId binds to the last touched rail item, so a warning raised from a
background job lands on whatever the user just did.
A AzPopupKind.NOTICE / WARNING popup redraws its bound item as a yellow, rounded-corner
triangle outline for as long as it is up, then restores it. The glyph is also available as an
ordinary item shape, AzButtonShape.TRIANGLE.
AzWindow)Every panel this library floats over an app is a window, and they all behave the same way:
AzPopup is drawn in one. So is the hidden menu (hiddenMenu { … } on a reloc item) — which is
the panel that most needed it, since it can hold a text field, and typing into a box you cannot move
off the thing you are typing about is a poor arrangement.
You can raise one yourself:
val panel = rememberAzWindowState()
AzWindow(title = "Layers", state = panel, onDismiss = { showPanel = false }) {
Column(Modifier.padding(16.dp)) { /* anything */ }
}
AzWindowState carries the window’s position and folded state; hoist it (as above) when either has
to outlive the window — a panel that reopens where the user left it. movable = false /
minimizable = false drop the affordances for a window that genuinely shouldn’t have them.
The rail is laid out over the whole window, so what it listens for is what your app underneath does not get. It installs pointer handlers only when it can answer them, and consumes only the events it acts on:
| Situation | What the rail does |
|---|---|
| Menu expanded | A scrim covering everything except the rail collapses it on an outside tap. It exists whenever the drawer is open; dimBehindMenu only decides whether that area is also darkened. |
| Drag on the rail | The drag detector exists only when the rail is floating, enableRailDragging is on, or the menu is swipe-openable — and it consumes the pointer only on the branch that actually undocks the rail or moves the menu. A scroll that starts under the rail still reaches your content. |
| Tap on the collapsed, docked rail | Only the buttons take taps. The gaps between them, and the empty strip above and below, pass through to whatever your app drew underneath. |
| Tap while expanded or floating | The rail is a panel in its own right and swallows stray taps. |
| Nested rail open | A tap-to-dismiss listener covers the strip only while one is open. |
There is deliberately no window-wide tap listener. If the rail appears to eat a gesture your app wanted, that is a bug — please file it.
The rail can be detached and moved around the screen by long-pressing the header icon, which activates “FAB Mode”. To enable this feature, set enableRailDragging = true in the azAdvanced block.
React Implementation:
import { AzNavRailSettings } from '@HereLiesAz/aznavrail-react';
const settings: AzNavRailSettings = {
enableRailDragging: true
};
// Pass settings to AzNavRail
AzDropdownMenu — standalone hamburger menuA drop-down menu is a standalone widget declared with the same opinionated DSL as the rail. In AzNavRail tradition it accepts only what the rest of the library sanctions — no arbitrary panel background, offsets, or free composable escape hatch.
Its trigger is chosen with azConfig(trigger = …) from the sanctioned AzDropdownTrigger set:
MoreVert (three vertical dots — the default), Hamburger, Text("Filter"), Icon(myVector)
(any ImageVector / Painter / URL / Coil model), or AppIcon (the launcher icon). Its size and
clip shape come from headerIconSize / headerIconShape.
By default (azConfig(triggerPlacement = AzDropdownTriggerPlacement.AUTO)) that trigger is placed
next to the big screen title, above the onscreen content area, whenever the drop-down is declared
inside an AzHostActivityLayout. Declare several drop-downs and their triggers line up beside each
other in declaration order. A drop-down used outside a host stays inline; force either behaviour
with TITLE / INLINE.
Tapping it unfolds an overlay panel of the items you declare; tapping outside, pressing back, or tapping an item folds it up.
Configure it through azConfig (mirroring the rail): design picks AzDropdownDesign.RAIL
(compact rail buttons at the collapsed width, ≈100dp) or AzDropdownDesign.MENU (the default;
full-width labeled rows at the expanded width, ≈160dp); dockingSide pins the panel to the
LEFT or RIGHT screen edge; the app-icon headerIconShape/headerIconSize (mirroring the
rail’s azTheme) and vibrate/expandedWidth/collapsedWidth round out the config. The panel
drops from the trigger automatically (downward when it fits, otherwise upward). The MENU design
renders rows at the rail’s menu-item text size and carries the rail’s footer (About / Feedback /
@HereLiesAz, gated by showFooter), just like the expanded menu. Because the dropdown has no
onscreen/host area, tapping About opens a full-screen in-app reader drawn as its own layer
(when inAppAbout = true, the default); set inAppAbout = false to open the repo in a browser
instead. The repo is auto-derived from the app namespace on Android, with appRepositoryUrl as an
optional override (never the AzNavRail library repo).
Items are declared with azItem / azToggle / azCycler / azDivider, accepting only the rail’s
sanctioned per-item knobs (color/textColor/fillColor/shape/enabled/closeOnClick). Each may
carry a route which — like the rail — navigates the supplied navController, so the drop-down
can drive an AzNavHost.
AzDropdownMenu(navController = navController) {
azConfig(design = AzDropdownDesign.MENU, dockingSide = AzDockingSide.LEFT)
azItem("Home", route = "home") { }
azToggle(isChecked = dark, toggleOnText = "Dark", toggleOffText = "Light") { dark = it }
azDivider()
azItem("Sign out") { signOut() }
}
React Implementation:
import { AzDropdownMenu, AzDropdownItem, AzDivider, AzDockingSide, AzDropdownDesign } from '@HereLiesAz/aznavrail-react';
<AzDropdownMenu design={AzDropdownDesign.MENU} dockingSide={AzDockingSide.LEFT} onNavigate={go}>
<AzDropdownItem text="Home" route="home" onClick={() => {}} />
<AzDivider />
<AzDropdownItem text="Sign out" onClick={signOut} />
</AzDropdownMenu>
The dropdown’s app-icon trigger carries an automatic margin, so it never sits flush against neighbouring widgets.
AzNavRail can animate its menu words natively — a staggered turnstile entrance/exit (each item
swings 90° around the docked edge, edge-on → flat, no fade, no vertical slide), a 3D
tilt-on-press, and an itemTextStyle override so the words can be big/light/wide Metro
type. It’s config-driven (preset enums, no free-composable escape hatch). In FAB / floating
mode, where there’s no docked edge to hinge on, the cascade degrades to a vertical up/down slide.
Default timing lets items overlap heavily: entranceDurationMs = 280 and
entranceStaggerMs = 22 mean the next item begins ~22 ms after the previous begins while the
previous is still animating. The stagger is rescaled by item count (AzMotion.CascadeReferenceCount), so a
three-item rail folds and unfolds in the same time as a twelve-item one. One stagger tick after the last item begins, the footer
(About / Feedback / @HereLiesAz) unfolds like an accordion from the top edge — the natural next
beat in the cascade rhythm — so the whole rail-open motion completes as one continuous phrase.
Three surfaces animate:
AzNavHost) sweeps in each time the active screen changes.AzDropdownMenu items do the same.All of it is on by default (the library’s signature look); opt a surface out with
AzEntrance.None / AzExit.None.
Android — the rail uses azKinetics(...); the dropdown uses its own azConfig(...):
AzNavRail {
azKinetics(
itemEntrance = AzEntrance.Turnstile, // default
itemExit = AzExit.Turnstile, // default
tiltOnPress = true, // off by default on the rail (drag-safe)
itemTextStyle = TextStyle(fontSize = 22.sp, fontWeight = FontWeight.Light),
titleEntrance = AzEntrance.Turnstile, // the big screen title
)
// … items …
}
AzDropdownMenu {
azConfig(
itemEntrance = AzEntrance.Turnstile,
itemExit = AzExit.Turnstile,
tiltOnPress = true,
itemTextStyle = TextStyle(fontSize = 28.sp, fontWeight = FontWeight.Light),
)
// … items …
}
React — the rail reads kinetics from settings; the dropdown takes props:
<AzNavRail settings= … />
<AzDropdownMenu itemEntrance={AzEntrance.Turnstile} itemExit={AzExit.Turnstile} tiltOnPress
itemTextStyle=>
…
</AzDropdownMenu>
AzEntrance is None | Fade | SlideUp | Turnstile; AzExit is None | Fade | Turnstile;
AzEasing.Wp7Decelerate is the signature snappy curve (the default easing).
Three developer knobs on azConfig (and on the standalone AzDropdownMenu’s azConfig) shape the
drawer itself. They only affect the expanded-menu drawer labels — small rail-button labels are
unaffected.
dimBehindMenu (default false) plus dimBehindMenuAlpha (default 0.4) — draws a
dim scrim over the rest of the app while the drawer is open. Tapping the scrim collapses it.menuItemAlignment (SIDE default | CENTER) — labels hug the docked edge instead of
center-aligning. SIDE = TextAlign.Start when docked LEFT, TextAlign.End when RIGHT.justifyMenuItems (default true) — runs a hybrid kerning + font-scale solver: fills
the row with letterSpacing alone until tracking would exceed α · fontSize (α = 0.15); once
the cap hits, the font scales up so both letter-spacing and font-size converge on the mix that
lands the label exactly on the row width. Font growth is capped at 1.5×. When the natural
width overflows the row, the solver shrinks the font instead (scale = rowWidth /
naturalWidth, clamped to ≥ 0.5×) so the label fits on one line without breaking mid-word —
line breaks in drawer labels are explicit-only.AzDivider now defaults its color to the surrounding font color
(LocalContentColor.current on Compose, currentColor on the web), and the rail/dropdown/About
call sites pass their accent explicitly — the divider belongs to the same visual family as the
labels next to it, never a muted outline.azConfig(
dimBehindMenu = true,
menuItemAlignment = AzMenuItemAlignment.SIDE, // default
justifyMenuItems = true, // default
)
React:
<AzNavRail settings= … />
By default the app icon in the header sizes itself to the rail width. To pin it to an exact
diameter, set headerIconSize (a Dp on Android, a pixel number on React):
azTheme(headerIconSize = 48.dp)
// or via the combined helper:
azSettings(headerIconSize = 48.dp)
const settings: AzNavRailSettings = { headerIconSize: 48 };
The rail’s About (?) item — and the drawer footer’s About — open a built-in, themed
in-app reader split into two halves:
.md file in the resolved repo’s root and docs/
folder via the GitHub API, builds a table of contents, and renders each doc inline with a markdown
renderer themed to the rail (activeColor links, translucentBackground surfaces).small · medium · LARGE · medium · small. The LARGE (center) item
is the currently active app; its banner (when the repo has docs/banner.png / .webp /
.jpg, or docs/hero.*), name, description, and link buttons (Play / Website / GitHub) render
below the carousel. Scroll the row to change the active app.Repo resolution differs by platform:
com.<owner>.<repo> →
https://github.com/<owner>/<repo> (owner = 2nd package segment, repo = last segment; a trailing
build suffix like .debug is stripped). appRepositoryUrl is therefore optional — set it only
when the namespace doesn’t match the repo. It never falls back to the AzNavRail library repo.appRepositoryUrl is required; when it is unset the About
entry is hidden.The reader’s content is fetched in the background as the rail composes — the docs listing, the first document’s markdown, and the More-from-Az manifest — so opening About shows the page rather than a spinner. A reader opened while a cold-start fetch is still in flight fills itself in the moment it lands. The bottom of the page carries @HereLiesAz, Feedback and hereliesaz.com: About is where someone goes to find out who made this, so that is where the way to reach them belongs.
The carousel snaps. A flick hands focus to the next app or two and settles firmly onto it rather than coasting past a dozen; whatever the gesture leaves half-centred is pulled the rest of the way in; and tapping a card that isn’t centred brings it to the centre.
About appears exactly once. The library keeps track of every surface that could offer it — a
? rail item you declared, the automatic ?, the rail’s expanded-menu footer, a drop-down menu’s
footer — and draws it only in the highest-ranked one, in that order. So an app with both a rail and a
drop-down gets one About, in the rail’s menu, instead of three. Turn it off with
azAbout(dedupeAbout = false) (or azConfig(dedupeAbout = false) on a drop-down) to draw About
wherever it is configured.
The standalone AzDropdownMenu has no onscreen area, so its About opens as its own full-screen
in-app reader (same namespace derivation; inAppAbout = false reverts to a browser link).
// Android: appRepositoryUrl is optional — the repo is derived from the namespace by default.
azConfig(/* appRepositoryUrl = "https://github.com/YourOrg/YourApp" */) // optional override
azAbout(inAppAbout = true) // default; set false to open the repo URL in a browser instead
The rail ends with an About (?) button — a real rail item, appended last — whenever no other
surface is already offering About. With de-duplication on (the default) that means a rail with an
expanded-menu footer carries About there and drops the automatic ?; a noMenu rail, which has no
footer, keeps it. It persists but it is not fixed: declare your own anywhere in the item order to
move, restyle, or rename it — a declared one outranks every other surface — or drop it entirely.
azAboutRailItem(id = "about", text = "?", color = Color.White) // yours, wherever you call it
azAbout(aboutRailItem = false) // or no rail item at all
<AzAboutRailItem id="about" text="?" /> {/* yours, wherever you render it */}
const settings = { aboutRailItem: false }; {/* or no rail item at all */}
Tapping the ? opens the reader; tapping it again closes it. So does tapping any other rail or
menu item, or the app icon — reaching for the rail means you are done reading. The reader never
covers the rail’s own gutter, so the app icon stays tappable behind it.
const settings: AzNavRailSettings = {
appRepositoryUrl: 'https://github.com/YourOrg/YourApp', // required on web
inAppAbout: true,
};
@HereLiesAz onto a second line and making the footer taller than the strip it is
pinned to..azignore: add a .azignore file at your repo root listing docs to exclude from the About
TOC — one pattern per line (# comments; exact paths like CHANGELOG.md, directory prefixes like
docs/internal/, or * globs like *.draft.md).A built-in carousel of the library author’s other apps, reachable from a “More from Az” entry in
the About screen and/or a pinned “More” rail item. You maintain it by pasting GitHub repo
links — one per line, any order — into more-from-az.json. That’s all; a
GitHub Action resolves everything else and bakes a finished manifest the rail just renders:
{ "version": 1, "apps": [
"https://github.com/HereLiesAz/AzNavRail",
"https://github.com/HereLiesAz/CueDetat",
"https://github.com/HereLiesAz/GraffitiXR"
] }
For each repo, .github/workflows/bump-more-from-az.yml (running server-side with the authenticated
GITHUB_TOKEN):
com.<owner>.<repo> (e.g.
HereLiesAz/CueDetat → …?id=com.hereliesaz.cuedetat), keeping it only if the listing exists,WIP,version, committing the baked manifest back with [skip ci].So you never hand-format JSON, never construct a Play link, and never cut a release to add an app. Do
not hand-edit version (CI-managed). The rail parser is lenient, so the list still renders in the
brief window before CI bakes. Grouping is by repository, so paste order never matters.
azAbout(
moreFromAzEnabled = true, // show the "More from Az" entry in the About screen
moreRailItem = true, // also pin a "More" item at the bottom of the rail
moreFromAzJsonUrl = "https://raw.githubusercontent.com/HereLiesAz/AzNavRail/main/more-from-az.json",
)
const settings: AzNavRailSettings = {
moreFromAzEnabled: true,
moreRailItem: true,
moreFromAzJsonUrl: 'https://raw.githubusercontent.com/HereLiesAz/AzNavRail/main/more-from-az.json',
};
Because all resolution happens in CI (not in the app), metadata is identical on Android and web — there’s no client-side CORS limitation, and the runtime never spends GitHub’s rate-limit budget.
AzRailRelocItem is a specialized sub-item that users can reorder via drag-and-drop. This feature is supported on Android, Web, and React Native.
azRailRelocItem(
id = "reloc-1",
hostId = "host-1",
text = "Item 1",
forceHiddenMenuOpen = false, // Programmatically open the hidden menu!
onHiddenMenuDismiss = { /* Menu dismissed */ },
onRelocate = { from, to, newOrder ->
// Handle new order (List<String>)
}
) {
// Hidden Menu (Tap to select -> Long Press to open)
listItem("Action 1") { /* ... */ }
inputItem("Rename", initialValue = "Item 1") { newName -> /* ... */ }
}
hostId may also name an azUnattachedHostItem (Android/CMP only — see below); tap and
long-press-to-open-hidden-menu work the same way there, but onRelocate only fires when hostId
names a rail host, since an unattached host’s stack has no drag-to-reorder gesture of its own.
React Implementation:
import { AzRailRelocItemProps, HiddenMenuScope } from '@HereLiesAz/aznavrail-react';
const relocItem: AzRailRelocItemProps = {
id: "reloc-1",
hostId: "host-1",
text: "Item 1",
isRailItem: false,
isSubItem: true,
forceHiddenMenuOpen: false,
onHiddenMenuDismiss: () => { /* Menu dismissed */ },
onRelocate: (fromIndex, toIndex, newOrder) => {
// Handle new order
},
hiddenMenu: (scope: HiddenMenuScope) => {
scope.listItem("Action 1", () => { /* ... */ });
scope.inputItem("Rename", "Item 1", (newName) => { /* ... */ });
}
};
// Pass this object within the items array to AzNavRail
AzNavRail can function as a system-wide overlay (using SYSTEM_ALERT_WINDOW), so the rail is
reachable from anywhere on the device rather than only inside your app.
Supply the service class through azSettings(overlayService = MyOverlayService::class.java). Doing
so implies enableRailDragging = true, and undocking the rail hands off to that service: the
library requests SYSTEM_ALERT_WINDOW if it hasn’t been granted, then starts the service (as a
foreground service when it extends AzNavRailOverlayService).
Your service extends one of:
| Base class | Permission | Notes |
|---|---|---|
AzNavRailOverlayService |
SYSTEM_ALERT_WINDOW + FOREGROUND_SERVICE |
Foreground service. Implement getNotification(); optionally override getNotificationId(). |
AzNavRailSimpleOverlayService |
SYSTEM_ALERT_WINDOW |
No notification, but the system may reclaim it sooner. |
Declare the permissions and the service in your AndroidManifest.xml
(FOREGROUND_SERVICE_SPECIAL_USE too when targeting API 34+), and call AzNavRail from inside the
service’s content. onOverlayDrag reports (dx, dy) while the overlay is dragged; onRailDrag
reports the same for in-app FAB dragging.
This is Android-only — on Desktop, Web and iOS, undocking stays an in-app floating rail.
The expanded menu text font size (and the footer items text size) is strictly controlled by your app’s MaterialTheme.typography.titleLarge. To adjust the text size inside the side menu drawer, simply customize the titleLarge attribute in your app’s typography theme!
Navigation items support overriding their display text and colors when shown in the menu versus the rail using menuText, menuToggleOnText, menuToggleOffText, menuOptions, textColor, and fillColor properties! By default, the fillColor (translucent background) is automatically computed to be Black (with 25% opacity), unless the item’s main color is Black, in which case it is set to White (with 25% opacity) to ensure proper contrast.
activeColor is not just this rail’s accent — it is what every other AzNavRail surface draws
itself in: a second unattached or floating rail, a drop-down menu, the About reader, the “More from
Az” carousel, the Help overlay, nested-rail popups, popups, and the expanded drawer. Chrome that
belongs to the same navigation system has to look like it; falling back to the app’s theme is how a
rail’s own drop-down ends up a different colour from the rail.
Leave activeColor unset and the accent is derived from the rail’s own items — the colour most
of them are drawn in — before it falls back to the app theme. A rail whose every button is white is a
white rail, whatever MaterialTheme.colorScheme.primary says:
// No azTheme(activeColor = …) anywhere, yet the drop-down, the About reader and the unattached
// rail all come out white, because the rail reads as white.
azRailItem(id = "home", text = "Home", color = Color.White) { }
azRailItem(id = "docs", text = "Docs", color = Color.White) { }
Read it yourself when you need to match it:
val accent = LocalAzRailPalette.current.accent // Color.Unspecified when no rail is present
const accent = useAzAccent() // falls back to the library default
The About reader and the drop-down’s panel also force their ground opaque:
translucentBackground supplies the hue, never the alpha, because a see-through full-screen reader
is an unreadable one — and a menu you can read the app’s artwork through is not a menu. Both then run
a legibility guard over the accent: a colour the library chose is swapped for plain ink when it
fails WCAG’s 3:1 large-text ratio against the panel, while a colour you named on an item is left
exactly as declared.
React Implementation:
// Fonts and colors can be passed as props directly.
// The expanded menu text size can be handled via CSS or React Native styles
// depending on your implementation environment.
const settings: AzNavRailSettings = {
activeColor: '#6200EE'
};
const items: AzNavItem[] = [
{
id: "custom",
text: "Custom Color",
color: '#FF0000',
textColor: '#FFFFFF',
fillColor: 'rgba(255,0,0,0.25)',
// ...
}
];
AzNavRail ships a reactive status-driven guidance framework (it replaces the old scripted scene/card tutorial framework). Instead of authoring a linear walkthrough, you describe your app’s userflow as a flowchart of statuses (string-id nodes) joined by edges (transitions that each carry an instruction). You declare goals (target statuses) and activate them; the engine then shows the instruction to reach the next status toward each active goal, auto-advancing the instant a target status becomes true (no Next button), re-routing live, and showing every active goal’s instruction at once as a callout next to its control.
azStatus(id) { predicate } — a named flowchart node; true while its reactive predicate is true.azEdge(from, to, text, ...) — a transition carrying the instruction text shown to make it. You hand-author edges only into your custom statuses; rail affordances are auto-edged.azGoal(id, target, ...) — a target status; inert until activated. The moment its target becomes true the goal completes (and is recorded as completed).Built-in az.* statuses are auto-published: az.app.ready (always true), az.rail.expanded /
az.rail.collapsed, az.rail.floating, az.host.<id>.expanded, az.screen.<route>,
az.item.<id>.active, az.nestedRail.<id>.open, az.help.open, az.onscreen.<id>.visible.
Auto-edges are generated for rail affordances (e.g. az.rail.collapsed → az.rail.expanded →
“Open the menu”, tap host → az.host.<id>.expanded, tap nested-rail → az.nestedRail.<id>.open, tap
routed item → az.screen.<route>).
Predicate timing: predicates reading Compose/React state are observed instantly; predicates over
non-reactive sources (a StateFlow.value, a plain var, a ref, an external store) are observed
within a ~300 ms poll. Goal activation is developer-driven (no built-in end-user picker);
autoStartWhen self-activates onboarding goals.
The guidance DSL sits inside AzHostActivityLayout { ... }, which returns an AzGuidanceController:
import com.hereliesaz.aznavrail.tutorial.*
val controller = AzHostActivityLayout(navController = nav, currentDestination = route) {
azRailHostItem(id = "features", text = "Features")
azRailSubItem(id = "feature-a", hostId = "features", text = "Feature A", route = "feature-a")
// Custom status + an edge INTO it (rail affordances are auto-edged):
azStatus("profileComplete") { viewModel.profile.isComplete }
azEdge(
from = "az.screen.feature-a",
to = "profileComplete",
text = "Fill in your name and email to finish setup.",
title = "Complete your profile",
highlightItemId = "feature-a"
)
// Goals — autoStartWhen self-activates onboarding once:
azGoal(id = "onboarding", target = "profileComplete", label = "Finish onboarding",
autoStartWhen = { !guidance.isCompleted("onboarding") })
azGoal(id = "find-features", target = "az.host.features.expanded", label = "Discover Features")
}
// Drive goals imperatively (also: LocalAzGuidanceController.current / rememberAzGuidanceController()):
controller.activate("find-features")
controller.deactivate("find-features")
controller.markReached("profileComplete")
val active: List<String> = controller.activeGoals
val done: List<String> = controller.completedGoals
AzGuidanceController (package com.hereliesaz.aznavrail.tutorial) exposes enabled, activeGoals,
completedGoals, enable(), disable(), activate(id), deactivate(id), markReached(id),
isCompleted(id). Completed goals persist in SharedPreferences under key
az_navrail_completed_goals.
The DSL is expressed as JSX children of the rail; the controller is read with a hook:
import {
AzGuidanceProvider, useAzGuidanceController,
AzStatus, AzEdge, AzGoal,
} from '@HereLiesAz/aznavrail-react';
<AzNavRail /* … */>
<AzRailHostItem id="features" text="Features" />
<AzRailSubItem id="feature-a" hostId="features" text="Feature A" route="feature-a" />
<AzStatus id="profileComplete" predicate={() => viewModel.profile.isComplete} />
<AzEdge from="az.screen.feature-a" to="profileComplete"
text="Fill in your name and email to finish setup."
title="Complete your profile" highlightItemId="feature-a" />
<AzGoal id="onboarding" target="profileComplete" label="Finish onboarding"
autoStartWhen={() => !done.includes('onboarding')} />
<AzGoal id="find-features" target="az.host.features.expanded" label="Discover Features" />
</AzNavRail>
// elsewhere:
const ctrl = useAzGuidanceController();
ctrl.activate('find-features');
const active: string[] = ctrl.activeGoals;
const done: string[] = ctrl.completedGoals;
The hook returns the same surface as Android (enabled, activeGoals, completedGoals, enable,
disable, activate, deactivate, markReached, isCompleted). Also exported: AzGuidanceProvider,
AzInstructionOverlay, useActiveStatuses, computeBuiltinStatuses, nextHop, routeInstructions,
computeAutoEdges. Completed goals persist to localStorage (and AsyncStorage on RN) under
az_navrail_completed_goals. (Parity note: the React/web overlay draws an accent ring around each
target over a light dim, rather than a true punch-out spotlight.)
See docs/TUTORIAL_FRAMEWORK_PROPOSAL.md for the complete API reference and docs/AZNAVRAIL_COMPLETE_GUIDE.md for end-to-end usage examples.
AzNavRail ships a four-detent bottom-sheet shell ported from LogKitty. One DSL line registers it inside AzHostActivityLayout; the sheet draws above the rail, above the menu, and above the onscreen area, spans the full screen width edge-to-edge, and extends all the way to the bottom of the screen so the HIDDEN-detent touch strip is reachable from the system-navigation-bar edge — a swipe-up from the gesture/nav-bar area reveals it. It is not a background().
val sheetController = rememberAzSheetController(initial = AzSheetDetent.PEEK)
AzHostActivityLayout(navController = nav, currentDestination = currentRoute) {
azConfig(dockingSide = AzDockingSide.LEFT)
azMenuItem(id = "home", text = "Home", route = "home", onClick = { /* … */ })
onscreen { AzNavHost(startDestination = "home") { /* … */ } }
azBottomSheet(controller = sheetController) {
Column(Modifier.fillMaxSize().padding(16.dp)) {
Text("Hello sheet")
Button(onClick = { sheetController.stepUp() }) { Text("Expand") }
}
}
}
Detents — HIDDEN, PEEK, HALF, FULL — and the accumulated-delta drag gesture mirror LogKitty exactly. Dragging down steps the sheet down one detent at a time (FULL → HALF → PEEK → HIDDEN), mirroring the up-drag’s one-step expand. At HIDDEN, the strip stays visible with a dimmed drag-handle (28dp) and tapping the strip steps up to PEEK. At PEEK, a transparent tap overlay catches taps to step down to HIDDEN. At HALF/FULL, a dim scrim covers the background and tapping it steps down one detent. Inside AzBottomSheetWindowHost, the sheet content now receives the real window insets, so Modifier.navigationBarsPadding() (and WindowInsets.navigationBars) resolve correctly without the consumer measuring the nav bar; you can still pad inside your content lambda or use AzBottomSheetInsetAware. For Service-hosted overlays (LogKitty’s use case), use the AzBottomSheetWindowHost flavor — calling updateConfig(newConfig) resizes the live overlay window immediately at HIDDEN/PEEK (e.g. recomputing the nav-bar inset on rotation/split-screen). Both flavors share state, theming, and gesture handling so the visual is identical.
Navigation-bar awareness. Set AzSheetConfig.drawBehindNavBar = true to make the sheet draw behind the system navigation bar on button-navigation devices: the exposed height above the bar is unchanged, but the nav-bar background is forced see-through so the sheet content shows through it (in-tree this makes the host Activity’s nav bar transparent — below Android 15 only, since navigationBarColor is deprecated in API 35 and ignored for apps targeting SDK 35, where the bar is already transparent; in the overlay flavor it paints AzNavBarDecorWindow semi-transparent). It is a no-op in gesture navigation. Independently — and automatically, with no flag — the host imposes no bottom margin on on-screen content when the device uses gesture navigation (content runs edge-to-edge); button-navigation devices keep the usual safe-zone/inset bottom margin.
See docs/DSL.md, docs/API.md, and docs/AZNAVRAIL_COMPLETE_GUIDE.md for the full reference, and docs/MIGRATION_GUIDE.md for the LogKitty migration recipe.
azRailRelocItem items remember the order the user dragged them into for the lifetime of the host activity. The library stores per-host orderings in AzNavRailScopeImpl.savedRelocOrders and re-applies them after every recomposition (the DSL block is re-evaluated on each composition, so without this layer the original declaration order would always win). The onRelocate(from, to, newOrder) callback still fires on drop so you can persist the order externally (DataStore, room, etc.) and seed your DSL with the same arrangement on next launch.
The interactive help overlay is scoped to where it was triggered from: tap azHelpRailItem in the main rail → cards for main-rail items; tap azHelpRailItem/azHelpSubItem inside an azNestedRail block → cards only for that nested rail’s children. Cards whose rail item is currently off-screen (scrolled out of the rail’s own scroll area, in a collapsed menu, or in a closed nested popup) are suppressed entirely — line, card, and all. The library tracks bounds via per-item DisposableEffect so the cache reflects “what’s currently on screen”, not “what was ever on screen”. Connector lines update in real-time as the user scrolls either the rail or the cards Column.
The repo ships two showcase apps:
SampleApp/ — Android MainActivity that demonstrates every public capability: rail/menu items at every shape, both nested rail alignments, reloc items with keepNestedRailOpen, live theming, help overlay, status-driven guidance, FAB-mode rail drag with AzNavRailSimpleOverlayService, bottom sheets, forms. See SampleApp/COVERAGE.md for the symbol-by-symbol audit.sample-pwa/ — Vite + React + react-native-web showcase deployed to GitHub Pages at https://HereLiesAz.github.io/AzNavRail/ via .github/workflows/jekyll-gh-pages.yml. Demonstrates the React port’s bottom sheet, shape options, both nested alignments, and the help overlay.The library includes a comprehensive Complete Guide (docs/AZNAVRAIL_COMPLETE_GUIDE.md) containing:
Copyright 2024 The AzNavRail Authors
Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.