Skip to content

Releases: iv-stpn/rn-motion-ui

rn-motion-ui@5.2.0

Choose a tag to compare

@github-actions github-actions released this 10 Aug 10:08
69459ab

Minor Changes

  • b56144c: Draggable: collision algorithms, axis constraint, drag bounds, and handle sub-component

    • New collisionAlgorithm prop ('intersect' | 'contain' | 'center') switches zone hit testing from point-based to rect-vs-rect. The draggable's live rect (computed from its lift-time box offset by pointer delta) is tested against each zone's measured box using the chosen strategy. Falls back to the existing point-in-rect test when unset or when no sourceRect is available.
    • New dragAxis prop ('x' | 'y' | 'both') constrains pointer movement to a single axis during the drag. The ghost and zone targeting respect the clamped position; onDragMove still receives the raw (unclamped) point.
    • New dragBoundsRef prop accepts a ref to a boundary View. The drag ghost is clamped inside that view's window-coordinate rect on every frame. Pan-transport only (touch on web, native); HTML5 drags are controlled by the browser.
    • New <Draggable.Handle> sub-component restricts drag initiation to a sub-area. Multiple handles per draggable are supported; as long as at least one is mounted, the host's GestureDetector is suppressed.

    Dragzone: skipRectMeasure for programmatic hit testing

    Zones that compute hit testing through another mechanism (e.g. arithmetic position in SortableList) can now set skipRectMeasure={true}. The zone is never measured, never participates in measure sweeps, and always passes the spatial hit test — the consumer's accepts predicate is the sole gate.

    SortableList: Reanimated-powered UI-thread animations

    SortableList now drives item position animations entirely on the UI thread via react-native-reanimated shared values and useAnimatedReaction. Insertion index updates write directly to a SharedValue without triggering React re-renders; items read the shared values in worklets and animate translateY with withTiming. The commit (on drop) snaps items to their new canonical positions via a dropVersion shared value bump in a useLayoutEffect — the user never sees an intermediate frame. The activeIndex (for renderItem's isDragging flag) stays as React state, so only the dragged item re-renders on lift/drop.

  • ee276b3: New IconButton component — a purpose-built icon-only button superceding Button size="icon"

    IconButton is a standalone component for icon-only actions. It shares the same 8 visual variants as Button (primary, secondary, ghost, danger, special, inverse, outlineDanger, ghostDanger) and adds the icon / iconBackgroundColor / iconColor API from MenuItem for coloured icon tiles (iOS Settings style).

    Key differences from <Button size="icon">:

    • icon prop takes a ComponentType<IconProps> — the icon component itself, not a pre-built element
    • iconBackgroundColor optionally wraps the icon in a coloured rounded-square tile
    • iconColor overrides the variant-derived icon stroke colour
    • accessibilityLabel is required — an icon-only button needs an accessible name
    • No children, leftAdornment, or rightAdornment — the icon IS the content
    • Sizes: 'sm' | 'md' | 'lg' (24×24, 32×32, 40×40 px squares)

    Existing <Button size="icon"> continues to work. ButtonSpinner is now exported from button-internals to power the loading state.

  • d8bf5e6: OtpInput: refreshed API with alpha/alphanumeric types, ref handle, and renamed props

    BREAKING prop renames (pre-release):

    • lengthnumberOfDigits
    • masksecureTextEntry
    • onChangeonTextChange
    • onCompleteonFilled
    • status / OTPStatusOtpInputStatus

    New features:

    • type prop'numeric' | 'alpha' | 'alphanumeric'. Controls which characters each slot accepts. The sanitize and applyEdit logic functions now accept a type parameter.
    • ref handle (OtpInputRef) — exposes focus(), blur(), and clear() imperatively.
    • autoComplete prop — forwarded to the hidden TextInput.
    • stickBlinkMs prop — customise the cursor blink interval.

    Internal: applyEdit now takes a single options object ({ prev, raw, length, anchor, type }) instead of positional arguments. Tests updated accordingly.

  • 9b9b09c: ReorderableList: remove ghost mode (indicator-only)

    ReorderableList is now indicator-mode only. The mode prop, renderPreview prop, and all ghost-mode state (previewKeys, ghostKey, flipRects, movedKey) are removed.

    • Breaking: mode prop removed — 'ghost' is no longer accepted
    • Breaking: renderPreview prop removed
    • FLIP animation system removed (Animated.View wrappers, measureInWindow tracking, easing curves)
    • Items now render in plain View wrappers instead of Animated.View
    • ReorderableItem adds a mount-time measure effect so zone rects are populated before a drag can land
    • isPastThreshold helper removed from reorderable-list-reorder; insertionPosition no longer accepts ghostHeight

    For real-time visual reordering during drag, use the new SortableList component (rn-motion-ui/sortable-list).

  • 43fb467: Rename DnDListReorderableList

    The ./dnd-list export is replaced by ./reorderable-list. All associated types and helpers are renamed accordingly:

    • DnDListReorderableList — the main container component
    • DnDItemReorderableItem — individual draggable rows
    • dndReorderreorderableListReorder — the reorder helper

    Import from rn-motion-ui/reorderable-list instead of rn-motion-ui/dnd-list. The old path is removed.

  • d6be9ea: New SortableList component

    A drag-to-reorder list where items visually reorder in real-time during the drag — the dragged item is dimmed at its preview position while other items animate to close the gap or make room.

    Built on the existing gesture primitives (Draggable, Dragzone, DragManager), so it inherits their transport story and isolation model. Each item computes its visual position as a pure function of (index, activeIndex, insertionIndex) and animates translateY to reach it — no rect measurement, no FLIP snapshots, no tree reordering during the drag.

    • New export: rn-motion-ui/sortable-list
    • Requires a fixed itemHeight prop (every item must share the same height)
    • Isolates itself inside a <DragManager isolate> — two lists on the same page are independent
    • Supports renderPreview for a custom drag ghost
    • The reorder commits on drop; cancelling reverts items to their original positions
  • a37019b: Table: move border and background colours out of the component into configurable className props

    The Table component previously hardcoded border-border border-b on rows, headers, cards, and footer, as well as bg-surface-selected on the selected-row overlay, bg-border on skeleton pulses, and bg-primary/bg-danger on row insert/delete buttons. Those are now removed from the component internals and exposed as new className props:

    • selectedClassName — classes merged onto the selected row/card background overlay
    • dropIndicatorClassName — classes merged onto the column-reorder drop indicator
    • skeletonClassName — classes merged onto the skeleton pulse bars during loading
    • emptyClassName — classes merged onto the empty-state wrapper

    Stories preserve the classic appearance via a CLASSIC_TABLE defaults object spread onto each <Table> instance. Remove or override individual entries to customise.

  • 1c8a226: ToggleGroup: replace Button children with items prop; add pill variant; fix bordered outer border

    BREAKING: ToggleGroup no longer accepts Button children. Replace children with the new items prop ({ value: string; label: ReactNode }[]). The selectedVariant, unselectedVariant, and pressMode props are removed. The size prop is now 'sm' | 'md' | 'lg' (drops 'icon').

    BREAKING: Button and ElevatedButton no longer accept a value prop. This was only used by the old ToggleGroup pattern and is now removed from BaseButtonProps.

    New pill variant: a bg-muted rounded-full container with a spring-animated sliding indicator (bg-surface-3 / dark:bg-black) that glides behind the selected item's text. Respects useReducedMotion().

    Fixed bordered variant: now renders a visible outer border (border border-border rounded-interactive) in addition to the existing inner divider borders.

    Items are now flat Pressable + Text surfaces (no longer Button components), with uniform px-3 horizontal padding.

Patch Changes

  • c7992e0: Button: prune outline, ghostPrimary variants; tighten label + ripple colours

    The outline and ghostPrimary variants are removed. All internal and story usages of outline switch to ghost. Label colour map simplified: primary now uses text-foreground, secondary uses text-surface-1, ghost uses text-foreground. Filled-ripple set updated (secondary added, primary removed) so the white shimmer only fires on opaque dark fills. Spinner colour resolution consolidated.

    New helpers in button-scale.ts: buttonRadiusClass() (CSS twin of buttonRadius()), STATE_ICON_SIZE, and STATE_BUTTON_GAP_CLASSNAME for proportional state-icon spacing per size.

  • c7992e0: Extract shared CloseButton component; add close button to MorphingModal

    A new CloseButton component replaces the inline Pressable + CloseLine icon pattern used across AdaptiveModal, FullSheet, and MorphingModal. The component is a simple themed close icon button with consistent hit slop and accessibility label.

    MorphingModal gains a showClose prop that renders a CloseButton in the top-right corner of the panel. FullSheet's closeIcon prop now accepts any ReactNode (previously just an icon override) — pass a <CloseButton> or any custom element.

  • 30569fe: **Drag overlay: ghost fade-out, HTML5 positioning, S...

Read more

rn-motion-ui@5.1.0

Choose a tag to compare

@github-actions github-actions released this 09 Aug 01:56
b70f6a8

Minor Changes

  • 2a3f08b: Breaking: token rename — --spacing-button-*--spacing-interactive-*, --radius-button-*--radius-interactive

    The box geometry tokens (height, horizontal padding, corner radius) that were previously named after buttons have been renamed because they are shared by Button, Input, OtpInput, Tabs, and ButtonGroup. Consumers who overrode --spacing-button-md, --radius-button-md or their CSS utility classes h-button-*, rounded-button-*, px-button-pad-* must update to the new names.

    New: four-category corner radius system

    Radius is now split into --radius-interactive (buttons, inputs, tabs), --radius-card, --radius-menu, and --radius-modal — each family independently tunable. A new shared lib/radius.ts module exports the corresponding pixel constants and Tailwind class strings, replacing the definitions that lived inside button-scale.ts.

    Menu: staggered item entry, faster exit, size-aware separators

    Each menu item now fades in with a 25ms stagger delay, driven by a new MOTION_MENU_ENTER spring preset. Exit duration was cut to 150ms, enter scale deepened to 0.85, and per-item offset increased to 12px. MenuSeparator and MenuLabel now accept a size prop so their thickness and font size track the menu scale.

    Button: press animation modes and continuous spinner

    Buttons gain a pressMode prop — scale (default, uniform), scaleY (vertical compression for segmented controls), and none. The loading spinner was rewritten from declarative MotiView loop to imperative Reanimated withRepeat so it no longer restarts on every parent re-render.

    Tabs: new size prop

    Tabs now accepts size (sm | md | lg) to control trigger height via the interactive surface family tokens, aligning with Button and Input at the same size.

    Input: size-aware text and padding

    The Input text box now scales its font size and horizontal padding per the size prop, matching the interactive surface family.

    Dock, Table, Loader, FeedbackWidget, ButtonGroup, and others

    All components updated to use the renamed tokens and consolidated radius constants.

Patch Changes

  • b561c15: Tighten interactive surface sizing

    Interactive component heights reduced by 4px per tier (sm: 28, md: 36, lg: 44) and horizontal padding trimmed by 2px (sm: 10, md: 14, lg: 18). MenuItem, MultiStepMenu sidebar, and OTP slot line height updated accordingly. CSS spacing tokens synced across tokens.css and storybook/demo/tokens.css.

  • f5b3a55: refactor: migrate inline style props to className where possible

    Inline style props (flexDirection, overflow, width, opacity, textAlign) moved to Tailwind utility classes across Marquee, SwipeableList, Table, Button, and ElevatedButton. CHECKBOX_COL_WIDTH renamed to CHECKBOX_COLUMN_WIDTH and relocated from table-types.ts to table-utils.ts. No behavioural changes.

rn-motion-ui@5.0.3

Choose a tag to compare

@github-actions github-actions released this 08 Aug 12:58
6931d26

Patch Changes

  • 4754dab: - ButtonGroup: new form component for grouping buttons with segmented,
    toolbar, and grid layouts
    • FeedbackWidget: refactored morphing animation using shared layout springs;
      replaced AnimatePresence wrapper with coordinated scale/translate
      transitions on individual views (SPRING_SWAP, SPRING_LAYOUT); container
      now animates width instead of just borderRadius
    • Input: added outline-none to the text field; fixed iOS text vertical
      alignment via textAlignVertical: 'center' and lineHeight: 0
    • FileSystem header: removed bottom border
    • HoverMenu: width="trigger" now sets minWidth from the trigger
      measurement instead of a fixed width, allowing panels to grow wider than the
      trigger when content overflows
    • MorphingModal: added elevation prop and storybook elevation control
    • AdaptiveModal: fixed missing label on the elevation Choice control in
      storybook
  • 1cc4430: - check-readme script: --fix now auto-inserts missing component rows into
    packages/ui/README.md by extracting PascalCase exports from each component's
    source, so the UI components table stays in sync without manual edits
    • Husky pre-push hook: runs check-readme.mjs --fix automatically,
      regenerating stale README blocks and inserting unpublished component rows
      before every push
  • 880c1b7: - AnimatePresence: removed unused presenceAffectsLayout prop (was accepted
    for API compatibility but never implemented)
  • df6a662: - Table: new columnLayoutStyle() utility for consistent column width
    resolution across header, row cells, and skeleton pulses; containerWidth
    removed from HeaderCell, RowCell, TableRow, and SkeletonCellPulse
    each now uses columnLayoutStyle(column.width, colWidth) internally
    • Table: horizontal ScrollView now only wraps the header + body when
      columns actually overflow the container; when they fit, no scroll wrapper is
      added, avoiding responder-tree interference with long-press menus and the
      column-reorder drop indicator
    • Table: FlatList performance tuned with windowSize,
      maxToRenderPerBatch, initialNumToRender, updateCellsBatchingPeriod, and
      nestedScrollEnabled for smoother large-table rendering
    • BottomSheet: replaced flex-1 with grow in the sheet body for UniWind
      v4 compatibility
    • Replaced template-literal className concatenation with the cn() utility
      across FeedbackWidget, Checkbox, StarRating, Switch,
      AdaptiveDropdown, AdaptiveModal, BottomSheet, FullSheet, HoverMenu,
      MorphingModal, and Popover

rn-motion-ui@5.0.2

Choose a tag to compare

@github-actions github-actions released this 08 Aug 00:41
af87a6d

Patch Changes

  • 01a8bf9: fix: replace workspace:* protocol with ^0.0.2 for rn-motion-ui-icons dependency so upstream consumers can resolve it

rn-motion-ui@5.0.1

Choose a tag to compare

@github-actions github-actions released this 08 Aug 00:17
958149a

Patch Changes

  • Updated dependencies [646025b]
    • rn-motion-ui-icons@0.0.2

rn-motion-ui@5.0.0

Choose a tag to compare

@github-actions github-actions released this 08 Aug 00:02
48d295e

Major Changes

  • 44a0672: FileSystem: drag and drop now runs on Draggable/Dragzone/MultiDragManager.

    Every view had brought its own drag. The list and icons grids shared one hook, the
    columns pane had a second one that mirrored its architecture, web had a third for
    the HTML5 half and external drops a fourth — five hooks, each measuring boxes,
    hit-testing points and tracking a session, and each with its own idea of which
    folder a release belonged to. Adding a view meant writing a sixth.

    They are gone, replaced by the components the library already ships. An entry is a
    <MultiDraggable>, a folder is a <Dragzone>, and the panes and the background are
    zones too — so the ladder a drop falls down (entry, then the column under it, then
    the open folder) is expressed as zone priority rather than as branches inside a
    resolver. props are unchanged: draggable, onMove and onExternalDrop mean
    exactly what they did.

    What changes is behaviour that used to differ per view, and now cannot:

    • The three draggable views drag identically — list, icons and columns — because
      none of them implements dragging any more. They resolve a drop through one hit
      test rather than three that agreed by hand.
    • A multi-select drag carries the selection, via MultiDragManager: drag one of
      three selected rows and all three move. The members left behind fade, and lifting
      an unselected entry still moves just it.
    • Autoscroll while dragging near an edge now works in the columns panes too,
      each scrolling on its own, where before only the list and icons grid had it.
    • A drop is resolved from measured boxes, so a touch pan and a mouse drag land
      on the same folder. Previously the web path read the DOM drop target and the
      pan path hit-tested rows, which could disagree at a row boundary.

    Two fixes fall out of the same work:

    • onExternalDrop now fires for an in-library <Draggable> from elsewhere on the
      page
      , not just for an OS file drag. Its documented contract always covered "a
      custom element on the page that sets drag data"; a payload with no FileSystem
      entries in it is foreign whether or not this library started the drag, and it
      reaches the consumer either way.
    • The hover highlight stands down for the length of a drag, in the list, icons
      and columns views alike, so it cannot mark one cell while a zone outlines
      another. It comes back on the first pointer move after the drop. A mouse drag is
      an HTML5 drag and the browser stops the pointer stream while one runs, so the
      highlight now takes the lift itself as its cue rather than waiting for a
      pointercancel that not every engine sends.

    FS_DRAG_CONTAINER_TEST_ID still names each draggable view's scroll surface, and
    every entry answers to <root>-entry-<path> in all four views. The internal hooks
    useFileSystemDrag, useFileSystemColumnsDrag, useFileSystemIconsDrag,
    useFileSystemDragWeb and useFileSystemExternalDrop are deleted; none was
    exported from the package.

  • 706dac3: Breaking: rn-motion-ui/icons is gone. Icons live in rn-motion-ui-icons.

    The 109 icons this package used to re-export were a subset of Lucide,
    hand-picked because each one had to be committed as generated source. That is a
    bad deal for a consumer: the icon you want is either in that list or it does not
    exist for you, and the list only grew when a component here happened to need
    something.

    rn-motion-ui-icons replaces it with the whole MingCute set — 3335 icons, one
    subpath each. Every icon this package renders internally now comes from there,
    so what components use and what you can use are the same set.

    -import { Check, ChevronRight } from 'rn-motion-ui/icons';
    +import { CheckLine } from 'rn-motion-ui-icons/icons/check-line';
    +import { RightLine } from 'rn-motion-ui-icons/icons/right-line';

    IconProps moved too, and is no longer exported from this package at all:

    -import type { IconProps } from 'rn-motion-ui/icons';
    +import type { IconProps } from 'rn-motion-ui-icons/icon-props';

    Install it alongside this package — rn-motion-ui depends on it, so anything
    that takes an icon (ThemedIcon, CommandIcon, BloomIcon, FileSystem's
    action icons) is already typed against the new IconProps and needs no change
    beyond the import.

    strokeWidth is gone from IconProps. Lucide's geometry is stroked and
    took a width; MingCute ships fill and stroke variants with the weight baked into
    the path, so there is nothing to widen. Drop the prop — it is a type error now.
    Where this package passed strokeWidth={2.5} for a slightly heavier check
    (Input, OTPInput, StatefulButton, AnimatedBadge), those icons now render
    at MingCute's own weight, which is a visible but deliberate change.

    Names do not carry over: MingCute names its own icons, and most differ from
    Lucide's. Every icon is suffixed -line or -fill (1667 line, 1668 fill), and
    the component name is the PascalCase of the file — icons/check-line exports
    CheckLine. The mapping used for the internal migration, if you were relying on
    the same names:

    was (Lucide) now (MingCute)
    AlertCircle, Info icons/information-lineInformationLine
    AlertTriangle icons/alert-lineAlertLine
    Check icons/check-lineCheckLine
    ChevronDown / Up / Left / Right icons/down-line / up-line / left-line / right-line
    Circle icons/round-lineRoundLine
    FileText, ScrollText icons/file-lineFileLine
    FolderClosed, FolderKanban icons/folder-lineFolderLine
    GripVertical icons/dots-vertical-lineDotsVerticalLine
    LoaderCircle icons/loading-lineLoadingLine
    MoreHorizontal icons/more-1-lineMore1Line
    Plus icons/add-lineAddLine
    Trash2 icons/delete-2-lineDelete2Line
    User icons/user-2-lineUser2Line
    X icons/close-lineCloseLine

    The rest resolve the same way: kebab-case the concept, add -line or -fill,
    and the export is its PascalCase.

Minor Changes

  • a4e9e3e: AdaptiveDropdown: rename headerRightheaderSuffix, remove showClose prop; simplify header layout (no fixed height, no border, no built-in close button).

    BottomSheet: increase top corner radius to rounded-t-2xl.

  • 99e42b1: Breadcrumbs: new component — the trail FileSystem already drew, now its own.

    A breadcrumb trail: the levels above the current one, each a way back, with the
    current one as plain text at the end. It knows nothing about what a level is
    pass the segments outermost-first and read back the pressed id, so a folder
    path, a route key and a wizard step are all the same thing to it.

    import { Breadcrumbs } from "rn-motion-ui/breadcrumbs";
    
    <Breadcrumbs
      items={[
        { id: "", label: "Files" },
        { id: "documents", label: "Documents" },
        { id: "documents/reports", label: "Reports" },
      ]}
      onNavigate={navigateTo}
    />;

    A deep trail scrolls horizontally by default, keeping one line. Set maxVisible
    instead to hold it to a fixed number of levels: the middle folds behind a
    that says how much it hides and hands those levels back when pressed, so nothing
    becomes unreachable. scrollable={false} wraps instead of scrolling.

    The rest of the surface: separator replaces the chevron with any node, size
    picks the text scale ('sm' | 'base') and takes the separator and icons with
    it, an icon per item rides ahead of its label, and currentId picks which level
    is the destination — null makes every level pressable, for a trail whose leaf is
    not where you are. className, contentClassName and itemClassName reach the
    container, the segment row and each segment.

    Accessibility: the container is a list named Breadcrumb, every earlier level
    is a button named Go to {label} (override per item with accessibilityLabel),
    and the current level is text — being the one unpressable segment is what marks
    it as current. RN has no aria-current, so the trail does not claim one.

    FileSystem now renders this component instead of its own private trail. No
    API change and no visual change: same placement under the header, same hiding at
    the root, same rootLabel as the leading segment, and the same Go to {label}
    names its stories already query. Both trails — the bar and the per-row ones under
    search results — are now built from one buildCrumbs.

  • ae616b8: Card: pass onPress and the surface becomes pressable

    A card that stands for something you can open had to be wrapped in a Pressable
    by hand, which meant a second element around the one that already draws the
    frame. Give Card an onPress and it renders as the Pressable itself:

    <Card elevation={2} onPress={() => open(project.id)}>
      <Text>{project.name}</Text>
    </Card>

    Omit it and nothing changes — t...

Read more

rn-motion-ui-icons@0.0.2

Choose a tag to compare

@github-actions github-actions released this 08 Aug 00:16
958149a

Patch Changes

  • 646025b: Icon components now forward all remaining props to the underlying <Svg> element via ...props. IconProps extends SvgProps from react-native-svg, so consumers can pass any SVG prop (hitSlop, onLayout, pointerEvents, opacity, etc.) through to the root SVG.

rn-motion-ui@4.0.0

Choose a tag to compare

@github-actions github-actions released this 01 Aug 17:31
41b7cd7

Major Changes

  • b430163: Remove AvailabilityScheduler component.

  • 45fd462: Remove deprecated visible/onClose props and clean up internal comments

    Breaking: visible and onClose props have been removed from BottomSheet, FullSheet, AdaptiveModal, and ActionFeedbackModal. These were deprecated aliases introduced in the previous minor. Migrate to open and onOpenChange:

    // Before
    <BottomSheet visible={open} onClose={close} />
    <FullSheet visible={open} onClose={close} />
    <AdaptiveModal visible={open} onClose={close} />
    <ActionFeedbackModal visible={open} onClose={close} />
    
    // After
    <BottomSheet open={open} onOpenChange={close} />
    <FullSheet open={open} onOpenChange={close} />
    <AdaptiveModal open={open} onOpenChange={close} />
    <ActionFeedbackModal open={open} onOpenChange={close} />

    Breaking: state?: never has been removed from MotiPressableProps. It was a no-op guard and carries no runtime effect.

    Internal call sites (AdaptiveDropdown, CommandPalette, MultiStepMenu) have been updated to the new API. The PopoverCtx internal type is renamed to PopoverContext (unexported; no public API change). Inline RN FALLBACK vs web implementation notes have been removed from component files.

  • fc3b682: Remove NotFound component.

Minor Changes

  • a2ff66d: FileSystem: the background context menu now opens over the empty area

    getBackgroundContextMenuActions used to need a view to right-click. The placeholder that stands in for the file area — an empty folder, a search with no hits, filters that match nothing, a folder still loading — is now mounted in the same background surface the list and icons views use, so a right-click (web) or long-press (native) anywhere in it opens the background menu. An empty folder is exactly where a "New folder" action matters most.

    It uses the same single-open coordination as the views, so opening it closes any other file-system menu.

    Also: the background menu's title at the root now comes from the title prop instead of a hardcoded 'Files'. Inside a folder it is the folder name, as before.

  • 22b260f: FileSystem: multi-selection — Ctrl/Cmd-click, Shift-range, long-press, and a selection box

    selectionMode="multiple" lets more than one entry be selected at a time, with the gestures a file browser is expected to have:

    • Ctrl-click (Cmd-click on macOS), or a long-press on touch: toggle the entry under the pointer in or out of the selection.
    • Shift-click: take the contiguous run from the anchor — the last entry picked without Shift — to the entry pressed. The anchor stays put, so shift-clicking around grows and shrinks one run rather than accumulating; hold Ctrl/Cmd as well to add the run to what is already selected.
    • A selection box dragged across empty space, web only, in all four views — the list, the icons grid, any columns pane, and the gallery filmstrip (which bands horizontally, being a horizontal list). Everything the band touches is selected live as it is drawn; hold Ctrl/Cmd as you start it to add rather than replace. A box only starts from a point that is not on an entry, so a drag that begins on a row still moves that row.

    A plain press still replaces the selection, and a press on the background still clears it. All four views paint the selection, and the status bar counts it with a Clear affordance once there is more than one.

    The ordering a Shift-range runs through comes from the view you pressed, not from the store: the list view runs through its rows as drawn (an expanded folder's children included, since they sit between their parent and its next sibling), and the columns view keeps each pane to itself, so a range never jumps across the trail into a sibling folder.

    The selected set arrives through a new onSelectedItemsChange(items), in the order the entries were picked. onSelectionChange(item) is unchanged and now follows the lead — the entry added most recently — which is what the columns trail, the columns preview pane and the gallery stage keep showing. renderBody gains selectedEntries, and renderFooter gains selectedCount and clearSelection.

    Dragging an entry that belongs to a multi-selection now moves the whole selection: onMove reports every path in one sources array instead of firing per entry. Members the drop would not actually move — the destination itself, entries already inside it, a folder dropped into its own subtree — are filtered out first, and nothing fires when that leaves the list empty. Dragging an unselected entry is still a single-entry drag.

    Two things to know before switching it on:

    • Long-press is already the entry context menu's trigger on touch, and multi-selection takes it over. With getContextMenuActions the menu still opens on right-click on web, but on touch it becomes unreachable — so pick one, or surface those actions elsewhere.
    • With draggable, a hold on native starts a drag (at 300 ms) before a long press resolves (at 500 ms), so the toggle gesture is effectively web-only in the list and icons views.

    Two fixes fall out of the same work, and apply whatever selectionMode is set to:

    • Entry rows and tiles now carry aria-selected. They only ever set accessibilityState={{ selected }}, which react-native-web does not map to anything, so on web the highlight fill was the only thing saying an entry was picked — assistive tech was told nothing at all.
    • A drag in the grid view now only lifts a tile when the press actually landed on one. It used to resolve the press to the nearest tile, so a press in the padding or in a gutter between tiles would lift a neighbour you had not touched.

    The default is selectionMode="single", which behaves exactly as before — except that re-selecting the entry you had already selected before navigating away and back no longer fires a duplicate onSelectionChange.

  • dd54f5d: FileSystem: new renderEmptyState slot

    Replaces the placeholder that stands in for the file area when there is nothing to show, so "This folder is empty" is no longer the only option. args.reason says which of the four cases you are drawing — 'empty-folder', 'no-search-results', 'no-filter-matches', or 'loading' — and args.label carries the copy the built-in placeholder would have used, ready to reuse. The rest of the args (currentPath, folderName, view, searchValue, isSearching, hasActiveFilters) describe the state that emptied it.

    The slot is per-reason rather than all-or-nothing: return undefined to fall through to the built-in placeholder for that state, so you can take over the empty folder and leave the loading spinner and the no-results message alone. Return null to draw nothing.

    Like renderBody, it is called as a plain function rather than rendered as a component — don't call hooks directly in it, put them in a component you render inside the returned tree.

    <FileSystem
      items={items}
      renderEmptyState={({ reason, folderName }) =>
        reason === "empty-folder" ? (
          <DropZone folder={folderName} onPick={upload} />
        ) : undefined
      }
    />

    FileSystemEmptyStateArgs and FileSystemEmptyStateReason are exported alongside it. Whatever the slot returns is mounted in the same background surface the built-in placeholder uses, so getBackgroundContextMenuActions still opens over it.

  • 643d0ff: Add MenuItem — a shared menu-row primitive, now exported as rn-motion-ui/menu-item

    CommandPalette and MultiStepMenu each carried their own near-identical menu-row markup (leading icon, label, active highlight, trailing slot). That row is now a single component with two visual modes selected by iconBackgroundColor:

    • Default — CommandPalette style: animated bg-surface-selected overlay, 16 px themed icon, py-2 padding, text-sm label.
    • iOS-style (iconBackgroundColor set) — Settings/MultiStepMenu style: coloured rounded-square icon, bg-primary/75 active highlight, h-11 row, text-base label.
    import { MenuItem } from "rn-motion-ui/menu-item";
    
    <MenuItem
      icon={Bell}
      label="Notifications"
      active={isActive}
      onPress={select}
    />;

    MultiStepMenu's MenuRow and CommandPalette's internal CommandRow are now thin wrappers over it — no public API change to either, beyond MenuRowProps['icon'] being typed as the exported MenuItemIcon (structurally identical to the previous local IconRenderer) and CommandIconProps becoming an alias of the shared IconProps (widened with the optional strokeWidth, style and accessibilityLabel fields; existing icon renderers stay assignable).

    BottomSheet's sheet container moves onto cn() + the SURFACE_CLASSNAME ladder. Two visual consequences: it now carries shadow-elevated-3 alongside bg-surface-3, and its non-full-sheet top radius changes from rounded-t-2xl to rounded-t-lg.

    Also folded template-literal class concatenation into cn() in ActionFeedbackModal, dropped the now-unneeded useSortedClasses biome-ignore comments, and rewrote the AdaptiveDropdown / HoverMenu stories to use the shared row instead of local one-off copies.

  • b2d501d: CardChoiceRadioCard, now animating per card, plus a new multi-select CheckboxCard

    Breaking: CardChoice has been renamed to RadioCard to say what it is — a
    card-shaped radio — and to pair with the new CheckboxCard. The subpath moved
    with it; there are no deprecated aliases.

    Old New
    rn-motion-ui/card-choice rn-motion-ui/radio-card
    CardChoice RadioCard
    CardChoiceGroup RadioCardGroup
    CardChoiceGroupProps RadioCardGroupProps
    CardChoiceProps ...
Read more

rn-motion-ui@3.4.0

Choose a tag to compare

@github-actions github-actions released this 29 Jul 03:24
771e2ad

Minor Changes

  • 281ac6a: feat(a11y): accessibility sweep of the overlay, carousel, progress and decorative components, plus a writing-direction primitive

    Modal semantics. BottomSheet and ActionFeedbackModal now expose role="dialog" with aria-modal, take an accessibilityLabel, and contain keyboard focus on the web through the new useFocusTrap hook — react-native-web renders Modal as an ordinary fixed <div>, so Tab previously walked straight out of an open sheet and into the page behind it, where a keyboard user could operate controls they could not see. Native already had containment from Modal itself, so the hook is a no-op there.

    BottomSheet also gains closeAccessibilityLabel (default 'Close'): the backdrop is now a labelled button, because the drag handle it sits next to is a pointer-only affordance and was the only way to dismiss the sheet. The handle itself is now hidden from assistive technology.

    Announcements. ActionFeedbackModal wraps its state content in a persistent live region, so a spinner resolving to success or error is announced instead of changing silently. iOS gets an explicit announceForAccessibilityaccessibilityLiveRegion is Android-only and VoiceOver does not re-read a subtree that mutated under it.

    Values. CylinderCarousel is now an adjustable control with a position value and working increment/decrement actions, giving it a non-pointer way to change slides for the first time. ScrollProgress reports role="progressbar" and a live percentage, mirrored off the UI thread in 5% steps so the indicator stays frame-driven.

    RangeSlider is fixed as part of this: it set React Native's nested accessibilityValue, which react-native-web does not read at all — it forwards only the flat aria-value* props — so on the web the slider announced no value whatsoever. Every value-bearing component now emits both spellings.

    Decorative content. Skeleton and Marquee's duplicated track are hidden from assistive technology on native as well as web. The marquee previously read its entire contents out twice on iOS and Android.

    Writing direction. New rn-motion-ui/hooks/use-direction (useDirection, useIsRTL) and rn-motion-ui/hooks/direction-provider (DirectionProvider). These exist because I18nManager.isRTL cannot be the answer on its own: react-native-web's I18nManager is a stub whose isRTL is hard-coded false, so any component branching on it is silently LTR-only in every browser. The hook reads the right source per platform, and the provider states it explicitly for a subtree.

    Marquee is the first component wired up: direction accepts the logical values 'start' (new default, identical to the old 'left' under LTR) and 'end' alongside the existing physical ones, and mirrors its travel under RTL — where the platform flips the belt's own row and the old direction tore a gap open in the loop instead of cycling.

    Tabs was audited and needs no change: its indicator and slide direction are both computed from measured geometry, which the platform mirrors along with the layout, so they come out right in either direction. That is now covered by an RTL story rather than left as an assumption. TabsList gained an optional testID — the sliding indicator is exposed as ${testID}-indicator, so its position can be asserted.

    RangeSlider now mirrors under RTL: minimum on the right, filling leftwards, the way a native slider does in an RTL locale. Four things flip together — the pointer mapping (locationX is measured from the physical left edge whichever way the page reads, so without this the slider painted mirrored and then jumped to the wrong value on the first press), the fill's growth origin, the thumb's travel, and the tick positions. A new optional writingDirection prop opts out, for a track whose axis is a thing rather than a quantity — a timeline or a seek bar.

    Table cell alignment now follows the writing direction when a column does not set align. Previously the default paired a direction-relative alignItems: 'flex-start' with a hard-left textAlign, so under RTL the text sat on the left inside a right-aligned cell. An explicit align: 'left' | 'right' stays physical — a column of numbers asking for right means right. Column order is untouched and now documented as the consumer's call: the table renders the columns array as given, since whether the first column belongs on the right depends on what the data means.

    Table column drag-to-reorder now mirrors as well. Its drop boundaries are accumulated from column widths in column order rather than measured, so unlike Tabs it could not inherit the platform's mirroring — the boundary table describes the logical axis while the pointer's pageX is physical, and under RTL the two run opposite ways. Dropping a column on the trailing physical edge now appends it in both directions, and the drop indicator lands on the boundary it marks rather than a column away. The row and column action overlays follow the trailing edge too, instead of pinning to the right.

    That geometry moved out of the hook into three new pure exports on rn-motion-ui/table-utilscolumnBoundaries, dropIndexAt, dropIndicatorX — so the same drop-target maths a custom header needs is available without reimplementing it, and is unit-testable without a gesture.

    No breaking changes: every new prop is optional and the defaults preserve current behaviour.

  • 6c97690: feat(FileSystem): renderBody slot for wrapping the file area

    renderBody decorates the file area instead of replacing it. Where renderHeader and renderFooter hand you a state snapshot and take whatever you return, this one also hands you state.content — the active view, or the empty/loading placeholder standing in for it — so a drop hint, an upload overlay or a details rail can sit alongside the four views without reimplementing any of them. Returning state.content unchanged is a no-op.

    <FileSystem
      renderBody={({ content, isEmpty }) => (
        <View className="flex-1">
          {content}
          {isEmpty ? <DropHint /> : null}
        </View>
      )}
    />

    The snapshot is the state that produced the content — currentPath, entries, view, selectedEntry, searchValue, isSearching, hasActiveFilters, isLoadingCurrentFolder, isEmpty — exported as FileSystemBodyState, so a wrapper tracks the same selection and folder the views do without recomputing any of it.

    isEmpty is not the same as "the placeholder is showing": the columns view keeps its panes over an empty folder, since that is how Finder lets you walk back up a trail, so it only yields to the placeholder while searching or filtering.

    Unlike the header and footer slots, renderBody is called as a plain function rather than mounted as a component. An inline arrow is a new function identity on every render, and a component whose type changes remounts its entire subtree — here that subtree is the active view, so every keystroke in the search field would have reset its scroll offset, its panes and any in-flight drag. Calling it keeps the returned elements in the parent's own tree, where reconciliation compares them by position as usual. The consequence for callers: don't call hooks directly inside renderBody — put them in a component you render inside the returned tree.

    The wrapper renders inside the file-area node rather than around it, so bodyClassName still applies and the area keeps its flex sizing and web text-selection guard however you nest things. Give the returned tree flex-1 (or size-full) if it should fill the area the way the built-in views do.

  • 6c97690: feat(elevated): export SURFACE_CLASSNAME, and drop the built-in frame from FileSystem and the AdaptiveDropdown panel

    New SURFACE_CLASSNAME on rn-motion-ui/elevated — a level-indexed map pairing each surface background with the matching elevation shadow, so a custom surface can take both halves of the ladder at one level without calling surfaceBackground and elevatedShadow separately.

    import { SURFACE_CLASSNAME } from "rn-motion-ui/elevated";
    
    <View className={SURFACE_CLASSNAME[5]} />; // bg-surface-5 shadow-elevated-5

    It is a plain record, not a function, so it is indexed rather than clamped: surfaceBackground and elevatedShadow still take any number and clamp it into range, while an out-of-range index here is a type error and, from untyped JS, undefined. Reach for the functions when the level is computed at runtime.

    Visual change. FileSystem's root no longer draws rounded-xl border border-border, and AdaptiveDropdown's floating panel no longer draws border border-border. Both now render an unframed surface, leaving the frame to the container they sit in — a FileSystem inside a card or a pane of its own was stacking two borders, and there was no way to opt out.

    FileSystem takes the old chrome back through className="rounded-xl border border-border"; the shared cn resolves consumer classes last-wins, so it applies. The dropdown panel has no such escape hatch — contentClassName reaches the body inside the panel, not the panel itself — so its border cannot currently be restored from the outside. It keeps its rounded-2xl and its elevation shadow, which is what separates it from the page.

    Internally, the per-file cn copies in Card, Skeleton and AdaptiveModal — each a comment claiming the package ships no shared cn — are replaced by the real src/lib/cn.ts. Those copies only concatenated, so a consumer class and a component default targeting the same utility group both survived into the class string and the winner came down to stylesheet order. They now resolve last-wins in the consumer's favour, which is what their prop docs already promised.

  • 58c7e45: feat(hooks): export useSafeInsets at `rn-motion-ui...

Read more

rn-motion-ui@3.3.0

Choose a tag to compare

@github-actions github-actions released this 28 Jul 11:14
9428121

Minor Changes

  • 465ac98: feat: useBreakpoint — width breakpoints without resize re-renders

    New rn-motion-ui/hooks/use-breakpoint exports useBreakpoint() and
    useBreakpointAtLeast(value). Both subscribe to Dimensions but store only the
    resolved tier, so a component re-renders when the breakpoint flips rather than
    on every resize frame the way useWindowDimensions does.

    The scale (base / sm / md / lg / xl / 2xl) mirrors Tailwind's default
    screens and is the single source of truth for responsive decisions in the
    package — the pure helpers live in rn-motion-ui/breakpoints for components that
    measure their own container instead of the window.

    Every component that previously hard-coded a cutoff now accepts an override:

    • AdaptiveModal, FullSheetwideBreakpoint (default 'sm', was a literal 640)
    • AdaptiveDropdownwideBreakpoint (default 'md', was a literal 768)
    • FileSystembreakpoints={{ minimal, compact, tablet }} for its
      container-measured header tiers (defaults 360 / 560 / 768), plus
      contextMenuWideBreakpoint (default 'md', was a literal 768) for the window
      width at which entry context menus open as a cursor-anchored panel rather than
      a bottom sheet

    Each takes a breakpoint name or a raw pixel number. Defaults are unchanged, so
    this is additive.

  • ab84da1: One shared box for the whole button family, driven by tokens. Button, ElevatedButton, GlossyButton and ActionSwapButton had each grown their own height/padding/radius table, so an md of one type didn't line up with an md of another. They now all read the same geometry from tokens.css--spacing-button-{sm,md,lg} (32/40/48px), --spacing-button-pad-{sm,md,lg} (12/16/20px) and --radius-button-{sm,md,lg} (8/10/12px) — so a row of mixed button types has one baseline, and overriding a token retunes every type at once.

    ActionSwapButton joins the family properly: it takes a shape prop ('pill' | 'rounded', default 'pill' so existing buttons look the same), its size is now the family's ButtonSize, and its label uses the family's type ramp instead of a duplicate of it. ActionSwapButtonSize is now an alias of ButtonSize and ActionSwapButtonShape of ButtonShape — both still exported.

    Visible changes, per type:

    • Buttonmd and lg lose 4px of horizontal padding (20→16, 24→20); the rounded shape moves off a flat 12px radius onto the 8/10/12 ramp; icon grows from 32 to 40px so it squares the md height.
    • ElevatedButton — padding grows 2–4px per size (10→12, 14→16, 16→20); icon grows from 32 to 40px. Radii are unchanged (AlignUI's 8/10/12 is what the shared ramp was drawn from), and its 14px label is still the documented opt-out from the type ramp.
    • GlossyButtonmd grows 36→40px and lg 44→48px to join the family's height ramp; padding drops at md/lg (20→16, 24→20) and grows at sm (10→12); icon grows 36→40px; the rounded shape moves off a flat 12px radius onto the ramp. The 2px inset around the label is gone, so a glossy label sits at the same inset as a flat one.
    • ActionSwapButton — same height and padding as before at every size. Its content gap is now a flat 8px (was 6 at sm and 10 at lg).

    Adornment spacing is one value across the family now (8px). ElevatedButton previously spaced its content at 12px and pulled icons back in by 4px, which netted the same 8px beside a label — the difference only showed with two adornments.

    StatefulButton's success/error padding squeeze is derived from the shared padding rather than tabulated, so it stays proportional if a token is overridden.

  • de66bc8: feat(ui): FileSystem headless header/footer slots + per-region classNames

    renderHeader and renderFooter replace the built-in toolbar and status bar
    with your own UI. Each receives the same state the default region renders from,
    so a custom header wires navigation, search, sort and filters without
    reimplementing any of the logic:

    <FileSystem
      items={items}
      renderHeader={({ folderName, canGoBack, goBack, searchValue, setSearchValue, layout }) => (
        <MyToolbar  />
      )}
    />

    The state shapes are exported as FileSystemHeaderState and
    FileSystemStatusState. Both include the responsive hints the built-in header
    uses (layout, isCompact), so a custom region can collapse at the same widths.

    For the common case of restyling rather than replacing, four class hooks merge
    onto the built-in regions: headerClassName, bodyClassName, footerClassName
    and the existing className. The two render* props take precedence over their
    matching *ClassName.

    Defaults are unchanged — omit everything and the component renders exactly as
    before.

  • 19c5bbc: HoverMenu's render-prop trigger now receives { open, toggle } instead of just { open }, matching AdaptiveDropdown. A trigger that is pressable in its own right (a Button, a Pressable) claims the press, so the wrapper's own toggle never fires — toggle is what lets such a trigger open the menu. Also adds triggerIsPressable: set it and the wrapper drops its button role, aria-expanded, onPress and tab stop, since the trigger already carries all four. Without it, web renders a <button> inside a <button> and keyboard users get two tab stops for one control. Hover stays on the wrapper either way, so web hover-open is unaffected. Both are additive — a plain node trigger keeps the wrapper-owns-the-press behaviour unchanged.

    Stories: add the glossy trigger kind to the shared story TriggerButton, which gives every overlay playground that showcases trigger variants (ActionFeedbackModal, AdaptiveModal, BottomSheet, CommandPalette, FullSheet, MorphingModal) a GlossyButton chip. The HoverMenu and AdaptiveDropdown playgrounds gain that same Trigger chip row, so all four launch styles can be swapped under one live overlay; each keeps its previous plain-node trigger in a section of its own to demonstrate the wrapper-owns-the-press path.

  • f2d4ba4: feat(overlays): safe-area insets on by default for full-screen overlays

    FullSheet, BottomSheet, Drawer, and AdaptiveModal now accept a safeArea prop (default true) that applies device safe-area insets — status-bar top and home-indicator bottom — to the overlay content.

    When react-native-safe-area-context is installed and a <SafeAreaProvider> is present in the tree, real device insets are used. If the package is absent, insets fall back to zero so existing consumers without it are unaffected.

    Pass safeArea={false} to opt out and manage insets yourself.

  • 8d996ce: Breaking — StatefulButton's elevated prop is replaced by chip. elevated was a boolean with one alternative to the flat button; there are now two chip keys, so the flag becomes a mode:

    -<StatefulButton elevated onPress={submit}>Save</StatefulButton>
    +<StatefulButton chip="elevated" onPress={submit}>Save</StatefulButton>

    Omitting chip renders the flat button, exactly as omitting elevated did. elevated is gone rather than deprecated — it shipped one release ago in 3.2.0, and keeping a boolean that means "one particular chip" beside the mode it is a subset of reads worse than the rename costs.

    The new value is chip="glossy": the GlossyButton key (domed SVG gradient, inset bevel, OKLCH-derived cast) driven through the same machine. Either key keeps its full appearance through loading/success/error instead of greying out, and each state adopts the matching variant — idle/loading map the flat variant onto that key's palette (danger family → danger, special/inverse carry over, everything else → the key's neutral fill), success switches to the success key, error to the danger key. Full fill, gloss, rim and cast, not a flat overlay: neither chip paints the flat button's crossfaded colour plate, because it has a variant to switch instead. Glossy dims whole-key via opacity rather than recolouring its label, so its idle content colour comes from glossyContentColor and holds constant across states.

    The success/error horizontal padding squeeze is now derived from the family's shared --spacing-button-pad-* rather than tabulated per size, so retuning a padding token keeps the squeeze proportional.

  • fd1d111: Tabs gains a choice of content-panel animation. contentAnimation on Tabs sets it for every panel, and animation on a single TabsContent overrides it for that panel only:

    • fade (default) — the existing cross-fade with a 4 px settle, unchanged, so nothing shifts for current consumers.
    • slide — the panel you land on travels a full container width in from the side the selection moved towards, while the panel you left is pushed out the opposite way, so the pair reads as one page displacing another rather than as a nudge. Sized for mobile screens and modals. Direction is read off the triggers' measured rects rather than the order the panels were declared in, so it also holds for controlled changes: a programmatic jump to a tab slides the same way a press on that tab would. Travel distance is measured on the Tabs root, so the first panel — which has no previous page to push out — just fades in.
    • dropIn — the panel falls from above on a springy scale-up.

    fade and dropIn are enter-only: TabsContent renders nothing for the tab it isn't showing, so a switch is an unmount plus a fresh mount, with no exiting layer to co-ordinate. slide is the exception, since a page swap only reads as one if the page you left is visibly pushed aside. The outgoing panel keeps its subtree mounted for the length of the push, leaves the layout flow immediately so it can't displace the panel replacing it, and finishes the trip as an absolutely positioned layer over the spot it held — hidden from assistive tech and non-interactive while it travels, t...

Read more