UI release notes

What changed in each release of @delacour/react-native-ui, from the changesets that shipped it.

Every version of @delacour/react-native-ui. The component library. It shares one version with the other Delacour packages — see Release channels.

0.8.0

GitHub release

Patch changes

  • #331 Input.Group now bleeds only its outermost blocks through the box padding: the first start block and the last end block. Before, every block in a section took the negative margin, so a second block was pulled over its separator.

  • #331 An inline modal, or one in a custom host, no longer makes OverlayProvider hide the app content from VoiceOver and TalkBack. The content View can hold that modal, so hiding it hid the modal too. Stacking and the back button are unchanged.

  • #331 Drop the unique symbol annotation on the overlay teleport flag. A const initialised with Symbol.for already has a unique symbol type, and the annotation broke source scanners that parse the file with a plain JavaScript grammar.

  • #333 Select.Trigger asChild now passes its className to the child. Before, the classes were dropped.

  • #333 Select.Trigger asChild no longer passes disabled={false} to its child when the select is enabled, so a child that is disabled on its own (<Button isDisabled>) stays disabled and never opens the sheet.

  • #333 Select draws the sheet close button in its own row with the optional title. Before, with no title, the absolutely positioned close sat over the search field or the first option.

  • #333 A controlled Select now clears its search query when the owner sets isOpen to false. Before, only a close from inside the sheet cleared it, so the next open showed the old filtered results.

  • #333 The Select search field no longer takes isDisabled or isInvalid from an enclosing Field. An enabled select inside a disabled field can now be searched, and an invalid value no longer marks the query invalid.

  • #331 Slot now throws for a fragment child. A fragment passes isValidElement but drops the props it is given, so an asChild trigger around a fragment did nothing.

0.7.0

GitHub release

Minor changes

  • #320 DatePicker takes defaultMonth, startMonth and endMonth and passes them to its calendar, so the default anatomy can open near a likely date and bound paging and the year list. Its docs add a date of birth example with minDate and maxDate.

Patch changes

  • #319 Avatar falls back to the accessible name "Avatar" when it is pressable and has no name, fallback or accessibilityLabel, so it is never read as only "button". Initials keep their accent when the name arrives in NFD form.

  • #319 Avatar.Badge draws the presence dot when its children are 0 or "", such as {unread && <Badge />} with no unread items, instead of throwing on a raw string in a View.

  • #319 Avatar remounts its image when the source changes, so a late error from an expired URL no longer marks the refreshed one as failed.

  • #319 Avatar retries its image when the entries of a multi-resolution list change their width, height or scale, and, for a list, when the avatar changes size. imageProps no longer accepts src or srcSet, which would override source and bypass the retry key.

  • #323 Android back closes an overlay open over a BottomSheet, not the sheet. Under an OverlayProvider, the sheet turns closeOnBack off while a modal or anchored overlay is open.

  • #323 BottomSheet.Container finds a Handle or Background written inside a fragment. <>{showHandle && <BottomSheet.Handle />}</> no longer gets a second handle and background added, which gave screen readers two adjustable elements.

  • #323 A BottomSheet with an Overlay hides the app behind it from VoiceOver and TalkBack while it is presented in the root host. BottomSheetProvider hides its children, as OverlayProvider does for a dialog. Inline sheets, sheets in another BottomSheet.Host and enableTouchThrough leave the app reachable.

  • #324 Calendar.Caption now reads an enclosing Field.Label before the month ("Date of birth, October 2026"). The label used to sit only on the grid, which is not accessible and gets no iOS trait from role="grid", so VoiceOver never read it. resolveCaptionLabel is exported from the variants.

  • #324 Calendar.Day now puts "Today", "Range start" and "Range end" in its accessibility label ("Monday 5 October 2026, Today, Range start") instead of the hint, so they are read with VoiceOver hints off. resolveDayAccessibilityLabel is exported from the variants.

  • #324 A read-only Calendar no longer announces every day as disabled ("dimmed"). A read-only day keeps an enabled accessibility state, its tap is still ignored, and it skips the press fade and haptic. CalendarDayState gains isUnavailable for the days that are announced disabled.

  • #327 The test kit no longer mocks native modules the project may not have. Skia, Pulsar, MapLibre, expo-linear-gradient and expo-blur each have a mock of their own under test/mocks/, copied only with a component that installs the module and added to Jest's setupFiles — so a project without chart no longer fails every suite with "Cannot find module 'react-native-skia'".

  • #320 DatePicker stays controlled whenever value is passed, even as undefined, which now reads as empty. A form field that starts undefined or resets to it no longer flips the picker to its own state and leaves a stale day on the trigger.

  • #320 A read-only DatePicker.Trigger reads as text instead of a button. It has no press handler, so VoiceOver no longer offers a double-tap that does nothing. A disabled trigger keeps the button role.

  • #320 A re-tap on a single DatePicker's shown day now confirms it and closes the sheet instead of clearing it. A user who opens a date of birth to check it no longer empties it by tapping it. DatePicker.Clear (isClearable) stays the way to empty a single day.

  • #324 A modal overlay that is animating closed no longer hides the app content from screen readers, so a Dialog or Drawer that moves focus back to its trigger in onExited lands on it. Overlay.Portal takes isExiting; pass useOverlayPresence().phase === "exiting".

  • #324 Screen readers stay inside the top modal overlay. The top open modal's portal frame is modal for VoiceOver, and every overlay drawn under it is hidden from VoiceOver and TalkBack, so a dialog opened over another dialog no longer leaves the lower one reachable. topModalOverlay and isBelowTopModal are exported from overlay.

  • #325 TextAnimation fixes for first-frame painting, negative numbers and font scaling:

    • Rolling picks its roll direction from the magnitude, so a negative number such as −19 → −20 rolls the short way.
    • New reveal text holds its start until the clock has rewound, so it never paints a frame fully shown before it reveals.
    • A Rotating swap holds its start until progress has rewound, so the arriving phrase never flashes in fully shown.
    • Rolling and Rotating cap font scaling at TEXT_MAX_FONT_SIZE_MULTIPLIER by default, like the other parts.

0.6.0

GitHub release

Minor changes

  • #312 Signature and Sketch's toPNG() now return Promise<string>. react-native-skia 3.9 made Image.encodeToBase64 asynchronous, and the peer range moves to >=3.9.0 — await the call.

  • #318 DatePicker takes a description, shown under the sheet's title as a BottomSheet.Description ({testID}-description), so a picker can say why some days cannot be picked.

  • #293 DatePicker takes onClose, called once its sheet has slid all the way down. onOpenChange(false) still fires as the close starts, so a form can move focus to the next field in onClose without the keyboard rising under a sheet that is still leaving.

  • #311 Add component render tests with @testing-library/react-native under Jest. delacour add --tests now copies a component's render suite, the Jest setup that stubs its native modules and a renderWithProviders helper into a Jest project, adds the setup to the project's Jest config, and remembers the choice in native-components.json. Fix a disabled Switch flipping on a screen reader's activation or its Field label press.

  • #306 Select takes isOpen, defaultOpen and onOpenChange to control its sheet, as DatePicker does, and onClose, called once the sheet has slid fully closed after a pick or a dismissal. A form can now open the select when focus moves to it, and move on only after the sheet has left.

  • #291 Upgrade tailwind-variants to v4. lib/tv now builds tv with createTV from tailwind-variants/config, the only v4 entry that accepts twMergeConfig — the default entry throws on it, so a tv.ts copied by an earlier CLI breaks as soon as v4 is installed. tailwind-merge is no longer a dependency: v4 ships its own compiled merge tables. delacour add stops installing it.

    v4's types drop numeric variant keys, so KpiVariantProps["colorIndex"], PlannerVariantProps["colorIndex"] and LeaderboardVariantProps["place"] are now string unions ("1" | "2" | …). The component props — KpiColorIndex, PlannerColorIndex, LeaderboardPlace — stay numeric.

  • #290 TextAnimation.Counting and TextAnimation.Rolling take prefix and suffix props, which win over format's. Rolling keeps each affix as one static cell, so a digit inside one (/100, Q3 ) no longer rolls as a wheel.

Patch changes

  • #310 Avatar.Group counts each avatar inside a fragment as its own face, so max and the +N tile work when the faces come from a fragment or a mapped list wrapped in one. The new flattenAvatarChildren helper opens fragments at any depth and keys each face as a [fragmentKey, faceKey] JSON pair, so keys stay unique however the fragments are keyed.

  • #310 Avatar scales a bundled image (source={require(...)}) to fill the face. The image slot adds size-full, because inset-0 does not override the width and height a bundled asset brings, so a large asset was clipped.

  • #310 Avatar retries its image when any request field changes, the cache mode included. The retry key is now a JSON array of the URI, bundle, method, sorted headers, body and cache mode, so a header value with & or = no longer shares a key with a different request.

  • #298 BottomSheet.Close reaches the 44pt minimum touch target. BOTTOM_SHEET_CLOSE_HIT_SLOP is now 12 instead of 8: the 20pt default icon plus 12pt on each side is 44pt, where 8pt left a 36pt target.

  • #298 BottomSheet honours onSnapPointHaptic={undefined} and onCloseHaptic={undefined} as the documented way to turn a haptic off. A destructuring default had put the Pulsar preset back in place of the explicit undefined.

  • #297 The screen-reader increment and decrement actions on Calendar.Caption no longer page a disabled calendar or the years view, and no longer fire onMonthChange there. They now have the same limits as the arrows.

  • #307 When a parent changes Calendar's controlled month, the new month appears without a slide. Before this fix, the last paging direction stayed set. For example, after the user tapped next, a date picker that reopened on an earlier month slid it in from the right.

  • #297 Calendar and its date helpers always format in the Gregorian calendar that the grid uses. A locale such as fa-IR changes the language and digits, not the calendar. Before this fix, 5 October 2026 was labelled 13 Mehr 1405, and the month picker's names did not match the months they selected.

  • #297 Selecting an outside day with selectOutsideDays no longer pages Calendar past startMonth/endMonth (or minDate/maxDate). The day is still selected, but the grid stays inside the bounds that the arrows and the swipe keep.

  • #297 Calendar's years view scrolls to the visible year each time it opens. Before this fix, if you picked a year and then reopened the years view from the months view, the list started at the earliest year.

  • #297 Calendar moves the visible month to the nearest bound when minDate/maxDate or startMonth/endMonth change and leave it outside, and reports the move through onMonthChange. Before this fix, moving minDate past the visible month disabled both arrows and the swipe, and the calendar could not reach a day that can be picked.

  • #297 Calendar.Grid slides the current month back into view when a swipe's month change is not committed. This happens when a parent holds the controlled month still or passes no onMonthChange. Before this fix, the grid stayed off-screen and the calendar looked empty.

  • #307 resolveWeekStart reads the device locale on engines without Intl.Locale, such as some Hermes builds. Before this fix, a Calendar with no locale on such an engine started the week on Monday, even on an en-US or ja-JP phone.

  • #297 Calendar's years view lists the year the calendar shows. The open side of the year span now reaches from the visible month, not from today, and never past the opposite bound. A calendar with only maxDate in 1900 lists 1800–1900, where the list was empty before.

  • #295 DatePicker cancels the pending auto-close on every selection. Starting a new range, or deselecting the day, within 120 ms of a finished selection no longer closes the sheet on an unfinished or empty selection.

  • #295 DatePicker drops a half-picked range and cancels any pending auto-close when a controlled caller closes the sheet with isOpen={false}. The trigger no longer shows the half range after the sheet closes, and the range does not resume when the sheet opens again. Inline pickers are unaffected.

  • #308 DatePicker.Trigger reads a read-only picker as enabled. Only isDisabled sets accessibilityState.disabled now, so VoiceOver no longer reads a read-only value as dimmed. A trigger that cannot open drops the "Opens calendar" hint, its press handler and its press feedback.

  • #309 While a modal overlay is open, the app content behind it is hidden from screen readers (accessibilityElementsHidden on iOS, importantForAccessibility="no-hide-descendants" on Android), so VoiceOver and TalkBack focus stays inside the overlay.

  • #296 Overlays no longer get stuck when isOpen flips while an enter or exit animation's finish is still queued: a stale finish is dropped before it moves the phase or calls onEntered/onExited. Under reduce motion the scrim and content now fade instead of snapping.

  • #309 Overlay presence timing no longer restarts when the owner re-renders with new inline onEntered/onExited callbacks: they are read through refs, so only isOpen drives the enter and exit animations.

  • #311 Spinner is now one accessible progressbar element, so a screen reader announces it once with its label instead of reaching its inner views separately.

  • #311 Pressable asChild given a text child now throws its documented error ("text and fragments cannot take props") instead of React's generic Children.only message.

  • #311 Fix form controls reacting while disabled, found by their new render tests:

    • Checkbox no longer flips when its Field row is pressed while the checkbox is disabled.
    • OtpInput no longer takes focus from a Field row press while it is disabled.
    • NumberInput.Field and Slider.Thumb ignore screen-reader increment and decrement actions while disabled.
    • Slider no longer calls onChange on mount, and calls it once per screen-reader step instead of twice.
  • #311 Fix screen-reader gaps found by the overlay render tests:

    • Menu and ContextMenu rows can be chosen with VoiceOver and TalkBack. A row now answers onAccessibilityTap on iOS and the activate action on Android, and a disabled row still ignores both.
    • Screen's navbar title and Screen.Error's title are announced as headings.
    • The render-test setup marks AppState as active. React Native's Jest mock otherwise reads as backgrounded, which kept a toast from ever dismissing itself.
    • A composed ImageViewer (Triggers as children, no images) opened with defaultOpen or a controlled open no longer shuts itself on mount before its Triggers register.
  • #311 Tabs: a bar with no Tabs.Content and no defaultValue now opens on its first tab, as documented. It used to start with nothing selected, because its order was still empty when the initial value was seeded.

  • #311 Sortable.Item ignores a Move up / Move down accessibility action on a row that cannot be dragged — a disabled, list-disabled or pinned row. Those rows advertise no actions, but one sent anyway used to reorder them.

  • #311 Post.Author and Post.Community hide their Avatar from assistive technology. The parent's one-stop label already names the person or community, so the labelled avatar inside it was announced a second time on Android.

  • #298 asChild keeps the handler it donates when the child forwards an unset one. <BottomSheet.Trigger asChild><Button onPress={maybe} /></BottomSheet.Trigger> with maybe undefined now opens the sheet; before, the child's undefined replaced the trigger's onPress.

  • #315 TextAnimation fixes for font scaling, retargeting and calm motion:

    • Typing, Counting and the reveal give every text they draw the same font scale cap: cells, the block caret, whitespace and the copies that measure.
    • Counting reserves its width from the number on screen. Rolling retargets each wheel from where it is now and rounds toward the roll's direction, so an interrupted roll keeps the new direction.
    • When calm motion turns on, Rolling settles its wheels and Rotating returns to its first phrase.
    • Typing calls onPhraseTyped for every phrase, even with no hold and no erase or when one frame finishes several, and stops counting the height of a removed phrase.
    • A line reveal waits for its lines to be measured before it starts. onFinished fires once per play, also when calm motion is turned on or off, and setting isActive back to true plays the reveal again.
    • Rolling retargets a wheel whose direction flips on the same digit. Rotating aligns its phrases inside the reserved slot and starts over when its phrase list changes.
    • The clock stops its frame callback once a run finishes.
    • Rows follow the resolved text alignment, so className and an inherited alignment count. capitalize capitalizes each word, not each letter, in Typing and a character reveal. Empty lines from hard breaks keep their height.
    • The block caret's fill is generated in every colour.

0.5.0

GitHub release

Minor changes

  • #252 Flow draws its edges, background, connection preview and minimap with Skia instead of react-native-svg, so react-native-skia is now an optional peer it needs, as Chart and Sketch do. The background is a runtime shader that stays sharp at any zoom. useThemeColors resolves several theme colours in one lookup.

  • #285 ScrollFade takes keyboardAware, which lifts the end fade above an open keyboard and keeps it there while the content scrolls. Without it the end fade sat under the keyboard, and over a KeyboardAwareScrollView it cleared for good after the first scroll. scrollEdgeOpacity takes an optional coveredEnd, and resolveKeyboardLift is the new pure resolver behind the lift.

  • #249 Signature: the pad draws its strokes with Skia instead of react-native-svg — the finished strokes as one recorded picture and the stroke under the finger as a path traced on the UI thread, with no path string built and parsed per frame. traceStroke is exported for drawing a stroke into a Skia path builder; smoothStroke, toSVG() and toPaths() are unchanged. react-native-skia is now needed by Signature.

    Signature: the ref gains toPNG(), the signature as a base64 PNG framed and coloured exactly as toSVG() with the same options, at scale pixels per point. resolveSignatureExport and signaturePngSize are exported.

  • #251 Move to react-native-skia 3, the package @shopify/react-native-skia was renamed to. The optional react-native-skia peer replaces @shopify/react-native-skia and needs >=3.7.3, which needs Android minSdkVersion 26. Sketch's toPNG() renders on Skia.Surface.MakeRaster.

  • #281 SlideButton takes isLoading: the handle's glyph gives way to a spinner, the drag is refused and the control announces itself busy — Button's isLoading, on the handle.

Patch changes

  • #265 An Attachment chip stretched wider than its content — a vertical composer tray — now fills its width, so the remove cross sits at the far edge instead of beside the name.

    A chip's upload bar is clipped to the chip's rounded corner, so the end of an indeterminate sweep no longer hangs below the curve.

  • #269 Combobox opens its sheet at its cap and holds that height while it is open, instead of sizing it to the filtered rows — typing no longer resizes the sheet, and the keyboard arriving no longer moves it. resolveComboboxSheetHeight is the new pure resolver behind it.

  • #269 Combobox keeps its keyboard up while the results scroll; only a drag that carries the sheet down, or a close, puts it away. The sheet's scrollables now take a caller's keyboardDismissMode instead of always using interactive.

  • #278 BottomSheet takes keyboardLiftLimit="highestSnapPoint": the keyboard lifts the sheet no higher than its highest snap point, so a sheet resting there holds still and its body shrinks by the keyboard instead. Combobox opens at its full height with it, so its sheet no longer opens half-height, jumps up when the search field's keyboard rises, and drops back as the keyboard leaves on close. resolveComboboxSheetHeight takes only topInset and windowHeight now.

  • #266 Fab.Group's actions now grow out of the trigger and collapse back into it along its centre line, instead of scaling about the row's centre and swinging in from the label's side.

  • #283 Input takes the enclosing Field.Label's text as its accessibilityLabel when none is given, so <Field><Field.Label>Email</Field.Label><Input /></Field> announces "Email" to a screen reader instead of an unnamed text field. An explicit accessibilityLabel still wins. Textarea inherits it.

  • #261 Input.Group.Block no longer stacks the box's vertical padding on its own. A toolbar above the field now sits as far from the border as from its separator, and a footer below it loses the extra space under its counter.

  • #259 MarkdownEditor's preview keeps the write pane's corners: square top corners under a bar toolbar, and the write pane's radius at every size.

  • #284 Components no longer pass dependency arrays to Reanimated's useAnimatedStyle, useDerivedValue, useAnimatedReaction and useAnimatedScrollHandler. Reanimated ignores them on native and warned once per app in development: "Dependencies should only be used on the web and are always ignored on native". Nothing changes at runtime; the worklets still rebuild from their closures.

  • #247 SectionProgress's pill now eases to each section's title width instead of jumping, and the titles slide out and in as the section changes — up for a later section, down for an earlier one. Reduce motion snaps the width and only fades. useSectionProgress() gains index, the active section's position.

  • #247 SectionProgress's pill now grows into its card as one surface — width, height and corners together, the list fading in as the card opens — instead of swapping a capsule for a scaled-in card. The card hugs a short list and stops at six rows, past which it scrolls. Reduce motion snaps the size and only fades the list.

  • #247 SectionProgress draws its ring with Skia instead of react-native-svg: a track circle and a fill path trimmed to the progress shared value, with the same size, stroke, colours and twelve o'clock start. react-native-skia (already an optional peer) is now needed by section-progress. resolveRingDashOffset is replaced by resolveRingTrimEnd (the clamped 0–1 trim) and resolveRingOval (the stroke's centre-line box).

  • #277 A searchable Select opens its sheet at its cap and holds that height while it is open, instead of sizing it to the filtered rows — typing no longer resizes the sheet, and the keyboard covers its bottom rather than moving its top. A Select without search still sizes to its rows. resolveSelectListMinHeight is the new pure resolver behind it.

  • #262 BottomSheet takes keyboardLiftLimit. Set to "maxDynamicContentSize", a sheet sized to its content is lifted by a keyboard only as far as its cap, decided on the UI thread from the keyboard's first frame. Select opts in, so a searchable select no longer shoots to the top of the screen when the keyboard opens and then settles back to its cap.

  • #277 A searchable Select keeps its keyboard up while the results scroll, as Combobox does; only a drag that carries the sheet down, or a close, puts it away.

  • #251 Fix Skia canvases — Chart, Flow, SectionProgress, Signature, Sketch, Tour — that came up blank after a JS reload (the dev menu's, or Updates.reloadAsync after an OTA update) until something re-rendered them. react-native-skia 3.7.3 allocates canvas ids natively, unique for the life of the process, so the optional react-native-skia peer now needs >=3.7.3.

  • #281 SlideButton's trail now fills the whole rail at the end of the travel instead of stopping under the handle's middle.

  • #248 Tour draws its dimmed mask with Skia instead of react-native-svg: a window-sized rect with the spotlight clipped out, its rounded-rectangle hole derived on the UI thread from the existing shared values rather than rebuilt as a path string every frame. react-native-skia is now a peer of Tour. resolveMaskHole is exported; buildMaskPath is deprecated and no longer used by the tour.

    Between steps the hole now crossfades — the old one fades closed while the new one opens — instead of sliding across the screen, and the card stays mounted and glides to its new place instead of blinking out and fading back in. Under reduce motion the card jumps; the hole still fades. Opening the tour now has an entrance: the dim fades up around the first hole and the card follows a beat later, rising a few points into place (reduce motion keeps the fade and drops the rise). TOUR_CARD_FADE_MS is now 240. TOUR_HOLE_FADE_MS, TOUR_ENTER_MS, resolveTourHoleFadeDuration, resolveTourCardEntrance and isSameHoleRect are exported.

0.4.1

GitHub release

Patch changes

  • #238 Calendar: a secondary range's start and end circles are one even colour — the band now runs under the whole circle instead of half of it.

  • #244 add --tests copies each component's bun test suites beside it, records them in the lock, and asks for @types/bun; update keeps copied tests current and update --tests brings them in. The registry ships each item's tests in a separate tests list, so older CLIs are unaffected.

  • #246 add --tests writes copied suites for the project's own runner — Vitest, Jest or Bun, read from package.json or set with tests.runner in native-components.json — and asks for that runner's packages instead of @types/bun. The shipped suites keep to the API all three share, and the registry build fails on one that does not. add --tests and doctor check that the runner can run them — its packages, a Jest config or a Vitest alias, and tsconfigRaw on Expo's tsconfig — and name the fix.

  • #241 Item, ListGroup.Item and GridItem: asChild takes a single child element as written. The text wrap (and GridItem's background ordering) always rebuilt the children as an array, which Pressable's Children.only rejects even with one element, so <Item asChild onPress={…}><View /></Item> threw. Under asChild the children now pass through untouched, and without it a lone element comes back as that same element. The wrap is one shared helper, lib/wrap-text-children.

  • #243 Copied components no longer assume Bun or cite tests that are not copied. add strips maintainer-only notes and test-file rows from each component's AGENTS.md, the registry build fails on any shipped file that names Bun or an unshipped test, and the CLI prints Expo commands for the project's own package manager. The agent skill shows npx with its pnpm, yarn and bun equivalents.

  • #240 Pressable: TalkBack's double-tap-and-hold now reaches onLongPress through a longpress accessibility action on Android, behind the same disabled / busy guard as the press. Screen-reader activation and long press are dispatched by dispatchPressableAccessibilityAction, which fires each handler once and never on a gated control.

  • #242 Export every public variant axis from its component's entry point. BADGE_MATERIALS / BadgeMaterial, BUTTON_MATERIALS / ButtonMaterial, SURFACE_MATERIALS / SurfaceMaterial and SCROLL_SHADOW_ANCHORS / ScreenScrollShadowAnchor were declared but only reachable by importing a component's internal files; @delacour/react-native-ui/text-animation now also exports TextAnimationEffectKind, TextAnimationCaretKind, TextAnimationSlideFrom and a new TEXT_ANIMATION_SLIDE_DIRECTIONS. A package test now fails for any axis left unexported.

0.4.0

GitHub release

Minor changes

  • #211 Add Breadcrumb — the path to the current screen, links back up each level and the current page last.

    • Breadcrumb.List counts its direct Breadcrumb.Items and puts a separator between every pair, none after the last. A hand-placed Breadcrumb.Separator stands in for the automatic one beside it, and the root's separator replaces the chevron everywhere.
    • Past maxItems the middle folds into an ellipsis, keeping itemsBeforeCollapse and itemsAfterCollapse at each end; onEllipsisPress makes it a button and hands over the hidden indices. overflow="scroll" runs the trail on in a horizontal scroller opened at the current page.
    • Breadcrumb.Link is a Pressable announced as a link with a 44-point target and takes an onPress — no router coupling. Breadcrumb.Page is announced selected; separators are hidden from screen readers; chevrons flip in right-to-left layouts.
    • sm and md sizes, on the caption and body text steps.
    • delacour add breadcrumb copies it into a project.
  • #216 Add CircularText — text set around a circle, optionally turning

    The text sits on a react-native-svg TextPath, laid out once by the platform's shaper and closed exactly with textLength. span is a full ring, an arc or natural spacing; side stands the glyphs outside or inside the circle; repeat="fill" repeats a phrase until the ring closes. spin is a steady loop in degrees per second, driven by a frame-callback integrator so a pause (by press or by state) eases to a halt and resumes without a snap, or a controlled angle from a shared value. centerContent stays upright and independently accessible, the ring reads once with separators stripped, and a loop holds still under reduce motion or isMotionCalm.

  • #182 Add ColorPicker — pick a colour by dragging, typing or tapping a preset.

    • Compound anatomy: ColorPicker.Area (saturation and brightness), .Hue, .Alpha, .Preview, .Input (a hex field), .Swatches / .Swatch, .Content and .Trigger. A bare <ColorPicker /> draws the area, the hue track and the field.
    • Every drag runs on the UI thread through one shared value, so nothing re-renders while a finger moves. onValueChange fires per frame, onValueCommit once a change settles.
    • Accepts hex (3, 4, 6, 8 digits), rgb(), rgba(), hsl() and hsla(); reports in format — hex, rgb or hsl. Greys and black keep their hue, and a controlled echo never drifts it.
    • hasAlpha adds an opacity track whose fade keeps the same RGB end to end. presentation="sheet" puts the surface in a BottomSheet behind a trigger row drawn on Input's box.
    • Joins Field, takes Input's sizes and variants, and is adjustable for screen readers in every axis, announcing on commit.
    • The color-picker subpath imports BottomSheet, so it needs @delacour/react-native-bottom-sheet installed even inline.
    • delacour add color-picker copies it into a project.
  • #206 Add Compare, two layers of one frame with a seam the finger drags across — before and after, one screen in two themes, a redesign against the original. The front layer is revealed by a sliding mask rather than resized, so both halves stay in register; the handle is an adjustable with stepped assistive actions, snap points on release, haptics at stops and bounds, horizontal or vertical orientation, and a grab mode that leaves vertical scrolling to the list

  • #190 Add Direction, a per-subtree LTR/RTL wrapper with useDirection and useDirectionSign.

    • <Direction dir="rtl"> mirrors its subtree on the next frame — rows reverse, start/end insets swap — with no restart and no remount, and nests: an inner <Direction dir="ltr"> holds a phone number's order inside an RTL card.
    • Uniwind's rtl: and ltr: variants resolve against the nearest Direction, not the app.
    • useDirection() and useDirectionSign() report the direction to gesture and translateX maths. Outside any Direction they fall back to I18nManager.isRTL rather than throwing.
    • Worklet-safe helpers — resolveDirection, directionSign, physicalSide, mirrorX, logicalDelta — for code that runs on the UI thread.
    • delacour add direction copies it into a project.
  • #222 Add ImageViewer: a fullscreen gallery that opens from a thumbnail, with pinch, pan and double-tap zoom, swipe between images and drag-to-dismiss, plus the media / media-foreground colour tokens.

  • #208 Add MessageScroller, a conversation list anchored at the newest message

    It follows new messages while the reader is at the end and leaves them alone once they have scrolled up, with a floating "jump to latest" button carrying an unread count. Older history loads when the reader reaches the top, guarded to one request per page, and lands without moving the viewport. The list rises with the keyboard, draws day separators and an unread divider, and passes each message's place in its sender's run to renderMessage. useMessageScrollerState drives the same behaviour on another list, such as Screen.ChatList.

  • #218 Add Message, a chat bubble with incoming and outgoing sides, run-aware logical corners, an avatar gutter, author and timestamp, delivery receipts with retry, reactions, actions and a typing indicator — read by a screen reader as one composed label

  • #205 Add Pagination, for moving through the pages of a result set. The numbers variant draws a fixed-length run of page numbers and gaps, so the number under the reader's thumb never slides away as the window moves; a gap never hides a single page, and tapping one jumps pageJump pages. compact draws one "3 / 12" readout that VoiceOver swipes a page at a time, and simple the two labelled arrows alone. Pagination.Summary and Pagination.Status ("41–60 of 472") sit at the leading edge. A controlled page outside [1, count] is drawn clamped and reported once through onPageChange. sm targets reach 44 points with hit slop. delacour add pagination copies it in.

  • #200 Add Post, a social card in four shapes — feed, vote, compact and media — with an author, a tokenised body with tags, mentions and links, a fixed-shape media frame with a scrim, stats whose glyph fills and whose count rolls, and a score pill; all counts and votes stay caller state

  • #219 Add QRCode: a string a camera can read, drawn as one react-native-svg path, with an optional QRCode.Logo in the middle

    The encoder is in-house and dependency-free — numeric, alphanumeric and UTF-8 byte modes, versions 1–40, levels L/M/Q/H — and is exported as encodeQr and useQRCode for custom renderers. Three module styles (square, rounded, dots) keep the finders concentric; modules snap to whole device pixels; a logo forces level H and grows the version until every block stays recoverable. The tile is dark-on-light in both themes, and an inverted or low-contrast pair warns in development. The registry now also skips *.test-helpers.ts and *.fixtures.ts alongside tests.

  • #195 Add Questionnaire — a guided form, one question at a time. Six question kinds (single and multiple choice with a free-text "Other", text, rating, a numbered scale and yes/no), conditional questions that drop their stale answers, Back that follows the questions actually visited, deliberate skips, a dimmed-not-disabled Next that explains a blocked answer, async submit, controlled answers and navigation, and bar, stepper or count progress. The flow's rules are exported as pure functions, so a server can re-validate a submission.

  • #223 Add SectionProgress — a floating pill over a long reading screen: a ring filling with scroll progress beside the title of the section being read, opening a card that lists every section.

    • Reads useScrollSections: scroll drives the ring and the reveal, value takes activeId, onValueChange takes scrollTo. The ring runs on the UI thread, so scrolling renders nothing; a jump eases over 180ms.
    • Six placements against the safe area, an offset, and a revealAt that latches once crossed. The panel grows out of the pill, scrolls past six rows, and closes on an outside tap, the escape gesture or Android back.
    • SectionProgress.Item takes a color for its marker and for the ring while it is active. progress overrides the scroll; isOpen / onOpenChange control the panel; haptic plays on a section change.
    • VoiceOver reads "Sections, Pricing, 42 percent"; opening moves focus to the active section. Reduce motion turns the panel's scale into a fade.
    • delacour add section-progress copies it into a project, with use-scroll-sections.
  • #214 Add SectionRail — a thin rail of bars along a screen edge, one per section of a long screen, that opens into a labelled outline.

    • SectionRail.Trigger holds one SectionRail.Bar per section and SectionRail.Content one SectionRail.Item per row; level shortens a bar and indents its row by the same step. The active bar's colour eases in, and the active row is drawn in the foreground.
    • Tap the rail to open the outline, or scrub: drag along it and the row under the finger highlights — mapped on the UI thread, with an optional haptic tick per section crossed — and releasing jumps there. A pick lingers for closeDelay before closing; an outside tap or Android's back closes it.
    • placement (end, start) and align (center, bottom) are logical and mirror in right-to-left; the rail clears the safe area by offset. It fills its container and passes every other touch through, so the content under it keeps scrolling.
    • Pairs with useScrollSections: value={sections.activeId} and onValueChange={sections.scrollTo}. value={undefined} stays controlled, so a hook's empty first value does not flip modes.
    • One adjustable element to VoiceOver, named "Sections" and valued by the active title; swiping steps through the sections and focus lands on the active row when the outline opens.
    • delacour add section-rail copies it into a project, along with use-scroll-sections. The registry builder now reads a registryDependencies list from ITEM_META for edges no import records.
  • #221 Add Sortable — a vertical list the user reorders by dragging.

    • Rows move by transform on the UI thread, so a drag renders nothing; onReorder(order, { id, from, to }) fires once, after the row lands, and the transforms clear in the commit that renders the new order.
    • activation="handle" drags from Sortable.Handle; "long-press" lifts the whole row after longPressDelay. A row changes slot when its edge crosses the next row's midpoint, at any heights.
    • isPinned rows never move and the others reorder around them; isDisabled rows cannot be lifted.
    • scrollRef auto-scrolls the enclosing scroller near its edges. haptic plays one feedback on lift and one per slot passed.
    • Screen readers get Move up / Move down actions and "Position 2 of 5", with the new position announced.
    • reorderItems and useSortableItem are exported. delacour add sortable copies it into a project.
  • #179 Add SplitView, two stacked panes with a drag row that settles on named snap points.

  • #177 Add Splitter, resizable panes divided by draggable, adjustable handles, sized in percent.

  • #198 Add TagInput — free-form tags typed into one field box, each a removable chip

    Return or a delimiter commits the text as a chip and keeps the keyboard up; a paste holding delimiters lands as several tags. Duplicates, a max and a validate hook refuse tags through onReject with a reason, and allowOverflow accepts past max while marking the field invalid. Backspace on an empty field arms the last chip, and a second removes it. The box is Input's, it joins an enclosing Field, and tags and draft are each controlled or uncontrolled.

  • #220 Add TextAnimation — five ways a piece of text or a number arrives, on one clock.

    The root reveals a string by word, character or line with a fade, slide or scale, staggered forward, in reverse, from the centre or in a seeded random order. TextAnimation.Typing types a phrase or cycles several with a bar or block caret, TextAnimation.Rotating swaps one word in place inside a sentence, TextAnimation.Counting tweens a formatted number on the UI thread, and TextAnimation.Rolling rolls an odometer whose wheels take the short way round. Nothing reflows while it plays, a screen reader hears the final text once, and under reduce motion every part shows its final text on the first frame.

  • #212 Add Tour — a guided walkthrough: the screen dims, a hole opens around one control, and a card beside it explains it.

    • Wrap each control in a Tour.Step with an order and a title; the tour sorts the steps, measures the current one in window coordinates and re-measures on rotation. onStepChange fires first, so a target can be scrolled into view.
    • The hole is an SVG cutout, rect or circle, that glides between targets on the UI thread and jumps under reduce motion. shape, padding, radius and placement are set on the tour and overridden per step.
    • The card sits below the hole when it fits and above otherwise, centred and clamped to the safe area. Next, Back, Skip and Done, a "2 of 5" progress, and every label replaceable.
    • A React Native Modal by default; isInteractive draws inside the tour's root and passes touches inside the hole through to the control.
    • Open with defaultOpen, the isOpen triple or useTour().start(). onFinish and onSkip are where an app records that the tour was seen.
    • Focus moves to the card on each step; VoiceOver's escape and Android back skip when dismissible.
    • delacour add tour copies it into a project.
  • #207 Add Tree — a hierarchy opened one level at a time, with branches that expand and collapse and optional single or multiple selection. Depth comes from context, closed groups unmount, and hasChildren marks a lazy branch before its children load.

  • #183 Add AnimatedBadge, a status pill that rolls its content and springs its width when the status changes.

  • #192 Add Attachment — a file, image, video or recording with a thumbnail, size, upload state and remove

    AttachmentMedia is discriminated by kind, so an image without a source is a type error. Three layouts — chip for a composer, tile for a photo tray, card for inside a message — at three sizes. Upload stays the app's: status is idle, uploading (determinate or looping), error with the app's message, or done, and onRemove / onRetry compose their controls in beside the press target so a remove tap never opens the attachment. A screen reader meets one element with a composed label, progress in ten-point steps and remove / retry as actions. Attachment.Group is a horizontal tray that fades attachments in and out and slides the rest along, stilled under calm motion. delacour add attachment installs it.

  • #175 Add Combobox — a long or remote list behind a field-shaped trigger, filtered as you type in a bottom sheet.

    • The sheet opens with its search field focused once it has settled. The default filter matches the label and keywords case- and accent-insensitively; filter={{ kind: "custom", match }} takes a predicate and { kind: "none" } shows the options as given, for a server. The matched run of each label is bold, through accents.
    • Single selection by default, where a pick closes the sheet; selectionMode="multiple" keeps it open and shows removable chips on the trigger (folding into +N) and above the search field, with a two-step Backspace remove. value and onValueChange are typed per mode; maxSelected disables the rest once reached.
    • The query is controlled separately with inputValue and onInputValueChange, which reports type, select, clear or close. A picked option stays on the trigger after a server's results move on. allowCustomValue offers the typed text as a value.
    • Built from Select's leaves — the same trigger box and option rows — and joins Field the same way. isClearable, isReadOnly, isLoading, Combobox.Empty and a replaceable Combobox.Trigger cover the rest; the result count is announced to screen readers.
    • Needs @delacour/react-native-bottom-sheet and BottomSheetProvider. delacour add combobox copies it into a project, along with select, chip and bottom-sheet.
  • #174 Add Countdown, the time left until a fixed moment, read from the wall clock on every tick

    It reads the clock rather than decrementing a counter, so it is right after the app was backgrounded or the device slept, and it re-reads on returning to the foreground. Seconds round up — the final second shows 0:01 and onComplete fires once at zero, re-arming when to moves into the future. units picks which units show, the largest absorbing everything above it; trim drops leading zero units. variant is segmented — boxed digits that slide per changed digit, with captions from labels — or inline, one tabular run. urgentBelow turns the digits destructive. The timer label is coarse and names seconds only in the final minute. bunx delacour add countdown copies it into a project.

  • #172 Add DatePicker, a field-shaped trigger that shows a date (mode="single"), a range ("range") or several ("multiple") and opens a Calendar in a bottom sheet to change it. The sheet closes when the pick is finished — on a day, on a range's second end, or on Done for several — and a half-picked range is held inside the sheet rather than reported. The trigger is the Input box at every size and variant; its text is formatted from the value through the calendar's Intl formatters, with format and formatRange overrides and a long-form screen-reader value. minDate/maxDate/disabledDates, locale and week start pass to the calendar; isOpen/onOpenChange and the value work controlled or uncontrolled; Clear, read-only, presentation="inline", and isInvalid/isDisabled/label/label-tap from a Field. Needs BottomSheetProvider. bunx delacour add date-picker copies it.

  • #199 Add DateTimePicker, one field for a date and a time of day picked together: an Input-shaped trigger that opens a Calendar and TimePicker's wheel in one bottom sheet with one Done, or draws them in place with presentation="inline". The value is a whole { date, time } or null, never half — a day picked first takes the time from the clock, rounded up onto minuteStep; a time picked first lands on today or the nearest allowed day — and every pick and settle reports it, so Done only closes and announces. layout="tabs" (default) puts Date and Time behind tabs and moves to Time once after the first date pick; layout="stacked" draws the calendar over a time row that opens the wheel. minDateTime/maxDateTime are one window that bounds the calendar to its days and the wheel to its times on those days only, re-clamping the time when the date moves; plus disabledDates, hourCycle, showSeconds, locale, a custom format, isClearable, controlled or uncontrolled value and open state, and the Field cascade. The subpath exports the pure DateTimeValue helpers — toNativeDate/fromNativeDate, formatDateTime, serialiseDateTime/parseDateTime, clampDateTime, timeBoundsForDate and more. Needs BottomSheetProvider. bunx delacour add date-time-picker copies it.

  • #226 Move to Expo SDK 58: React Native 0.88, React 19.3, Gesture Handler 3

    The peer floors are now react-native >=0.88 and react-native-gesture-handler >=3.3. React Native 0.88 types its components through the strict API by default, so every ref is typed as the host instance it receives — ViewInstance, TextInputInstance, ScrollViewInstance — rather than as the component, and ScreenScrollViewRef is now ScrollViewInstance. Those types do not exist before 0.88. The sheet's and the drawer's pans keep the builder API under Gesture Handler 3's LegacyPanGesture type. The CLI's verify:expo scaffolds against SDK 58.

  • #170 Add FlipCard — two faces of one card and a turn between them

    FlipCard.Front sits in flow and sizes the card, FlipCard.Back fills it, and FlipCard.Trigger turns it under every trigger. trigger is a discriminated union: "press" (a tap anywhere), "swipe" (a drag that scrubs the turn, rubber-bands past either rest and settles on the nearer face or the way it was flicked; swipeDistance exists only here) and "manual" (only face, the ref's flip() / flipTo() or a trigger). The face is state and the rotation follows it: the hidden face leaves the accessibility tree and stops taking touches the moment a turn is committed, which face is drawn is decided by the angle rather than backfaceVisibility, the settled face is announced, and a gesture card always offers an activate action. Under reduce motion or isMotionCalm the faces crossfade instead of turning. delacour add flip-card installs it.

  • #173 Add Flow, a pan-and-zoom canvas of nodes joined by edges that route themselves — handles, groups, drag-to-connect, controls, a minimap and assistive move/connect/delete actions, all reporting to an app that owns the graph.

  • #202 Add Frame, a titled shell with a header action, a divided tray of rows, labelled sections and a footer.

  • #189 Add GridItem and GridItem.Group, a bento tile grid with whole-cell spans

    GridItem.Group measures its width, sizes a cell from columns, gap and aspect or rowHeight, and places each direct child first-fit in writing order from its colSpan and rowSpan, on pixel-snapped edges and the logical start inset. It reserves its height before the first measure, so nothing jumps, and glides tiles to new cells on a relayout unless reduce motion is on. GridItem takes four variants and two sizes, renders a Pressable only when it has a press handler, and announces itself as one stop labelled from its title, value and description. Parts: Background, Media, Title, Value, Description, Footer and Actions; useGridItem() and useGridItemGroup() expose the tile's box and the group's cell size, and bunx delacour add grid-item copies it into a project.

  • #186 Add Input.Group.Button, Input.Group.Separator and Input.Group.Block, decorative decorators, and Textarea inside a group

    • Input.Group.Prefix / .Suffix take isDecorative, which hides them from screen readers and lets touches through to the field. It defaults to true when every child is bare text or an Icon.
    • Input.Group.Button is a Button sized one step below the box (square when icon-only), disabled with the group without fading twice, and warns in development when icon-only with no label.
    • Input.Group.Separator draws a hairline whose direction comes from where it is written: vertical in the row, horizontal between a block and the row.
    • Input.Group.Block adds a full-width section above (position="start") or below ("end") the field inside the same border and focus ring. The group always renders an inner row, so adding a block never remounts the field.
    • Textarea reads the group's context and drops its own box, so a message composer is one box.
  • #176 Add Leaderboard: ranked people or teams with standard competition ties, the top three on a podium of growing bars or lifted cards, a movement column, and the highlighted entry pinned below the limit cut with its true rank.

    Its podium and rows read entries in any order and rank them stably, order="asc" for times; unit is spoken after every value. bunx delacour add leaderboard copies it into a project.

  • #191 Add MarkdownEditor, a Markdown field with a formatting toolbar, a monospaced write pane and a rendered preview

    The toolbar wraps a selection or inserts markers at the caret, toggles a format off when pressed again, applies headings, quotes and lists to every touched line, and lights from the caret. Return continues a list. The preview draws the toolbar's subset on the Text presets and opens a link only for http, https, mailto and tel. It joins Field like Input, comes in a bar or pill toolbar, and ships applyMarkdownCommand, continueMarkdownList and parseMarkdown as plain functions. The CLI adds it with delacour add markdown-editor.

  • #188 Add Marker, a centred inline note for steps and day breaks between conversation turns

    It comes as a bare default row, a hairline border capsule, or a separator label ruled to both edges. With no handler it is a plain view read as text; give it onPress or onLongPress and it becomes a pressable announced as a button. Marker.Content shimmer breathes the text on the UI thread while a step runs and announces the marker busy, stilled under reduce motion. bunx delacour add marker copies it in.

  • #196 Add Marquee: content that travels across its container on a loop at a speed in points per second — horizontal or vertical, edge fades, held on touch, Marquee.Group to keep rows in step, and a still, hand-scrollable strip under reduce motion.

  • #203 Add Support: the chrome around a help conversation — availability, channels, topics, past requests, articles, and a thread's agent, ticket, handoff, suggested replies, note and resolution.

  • #184 Add NumberInput — a number between a minus and a plus

    Tap a stepper to move by step, hold it to repeat after 400ms and accelerate, or type: while focused the field takes half-typed text ("", "-", "1.") and commits on blur or submit, so it never fights the person typing. Every path goes through one reconcile — snapped to the step, held inside [min, max], rounded to the step's precision — and onValueChange fires only on a committed change. The box is Input.Group's, so it takes Input's variants and sizes and an enclosing Field's label, invalid and disabled state. formatOptions formats the value at rest through Intl; commitMode="validate" keeps an out-of-range number and flags it instead of clamping. The field is adjustable with increment and decrement actions for VoiceOver and TalkBack. The parse, format and step maths are exported as pure helpers. bunx delacour add number-input installs it.

  • #215 Add OtpInput, a one-time code or PIN drawn as a row of cells over one hidden TextInput, so the number pad, a paste, iOS's QuickType code from Messages and Android's SMS autofill all fill every cell at once. Every change is sanitised to pattern (digits, alphanumeric, alphabetic or a one-character RegExp) and cut to length; onComplete fires once per transition to a full code with a success haptic. groupSize draws separators between groups, or write the anatomy out with OtpInput.Group, OtpInput.Slot and OtpInput.Separator. isMasked draws dots without switching off autofill, the caret holds still under reduce motion, the row is one accessible element, and isInvalid / isDisabled cascade from a Field. bunx delacour add otp-input copies it.

  • #217 Add PageHeader: the head of a profile or place screen — a cover that reaches under the status bar, an avatar lifted over its edge, a title block, actions and a strip of stats that compacts on screen and reads in full. Add a 2xl (96pt) Avatar size.

  • #178 Add Planner: a month of days, each showing what falls on it — category dots and icons, tinted tiles or named blocks — with swipe paging, a virtualised week scroller, a legend with counts and a bottom sheet of the selected day.

  • #194 Add ScrollBlur — a progressive blur at a scrollable's edges that keeps floating controls legible over any background, falling back to a gradient wash with no engine or with Reduce Transparency on. Ships with ScrollBlurLayerProvider, the expo/blur-layer engine (ExpoBlurLayer, behind the new optional peer expo-blur), useReduceTransparency and withAlpha in lib/color. The CLI gains scroll-blur, blur-layer and use-reduce-transparency registry items, and classifies expo-blur as an expo install package.

  • #209 Add ScrollCanvas, an image in a still frame that moves as the page scrolls — parallax, zoom, reveal, or a sequence of frames scrubbed by the scroll. It follows a Screen with no wiring or a useScrollSource source at any depth, takes a progress shared value to drive it directly, and draws each effect's resting state under Reduce Motion.

  • #213 Add ScrollFade, a gradient at a scrollable's edges while content runs past them

    @delacour/react-native-ui/scroll-fade overlays any scrollable — a card row, a list in a Card, a sheet body — and fades its start and end edges only while there is content past them, on either axis. It reads a ScrollSource from useScrollSource, or needs no wiring at all inside a Screen. The fade is a colour, so name the surface with color; edges, size, fadeInDistance and insetStart / insetEnd tune it. The CLI registry gains the scroll-fade item.

  • #201 Add ScrollHeader, a collapsing large title with a pinned bar, cover and snap.

  • #210 Add ScrollText, text that resolves unit by unit as the screen scrolls past it. Four effects — colour, fade, rise and highlight — over a word, character (by grapheme) or line split, staggered by overlap inside a viewport window. Progress comes from the enclosing Screen, from any scroll view's offset (read against its own bounds with viewport), or from a shared value already in 0..1. Every frame runs on the UI thread and writes only opacity, transform and colour; un-revealed text never drops below a legible floor, reduce motion and isMotionCalm show it fully revealed, and the block reads as one accessibility element with the whole string. delacour add scroll-text copies it in.

  • #197 Add SearchBar, the query field at the top of a list, on Input.Group with no chrome of its own. A clear button that keeps focus and the keyboard, a Cancel action shown never, on focus or always that empties the text and dismisses the keyboard, debounce with onDebouncedChange (flushed by the search key, immediate on clear), isLoading swapping clear for a spinner, and tokens drawn as removable chips with Backspace removing the last. pill and rounded shapes over Input's variants and sizes, and it joins an enclosing Field. delacour add search-bar copies it.

  • #171 Add Select — a closed set of choices behind a field-shaped trigger, picked from a bottom sheet.

    • Single selection by default, where a pick closes the sheet; selectionMode="multiple" keeps it open, draws a box on each row and closes from a Done footer. value and onValueChange are typed per mode — a string or null, or an array — and maxSelected disables the remaining rows once reached.
    • Options come as data through options, or as Select.Item and Select.Group children, and both are drawn from one normalised, virtualised list. isSearchable adds a search field that matches case- and accent-insensitively, hides groups it empties and clears on close.
    • The trigger is Input's own box at every variant and size. Inside a Field the select takes its invalid and disabled state, reads Field.Label as its accessible name and sheet title, and opens when the label is tapped. isReadOnly, isLoading, renderValue and a replaceable Select.Trigger cover the rest.
    • Rows are radios in single mode and checkboxes in multiple to a screen reader; group headings are headers. SelectOptionRow is exported as the leaf a later picker reuses.
    • Needs @delacour/react-native-bottom-sheet and BottomSheetProvider. delacour add select copies it into a project, along with bottom-sheet.
  • #187 Add Signature, a pad to sign with a finger or a stylus. Strokes are drawn on the UI thread and smoothed into ink, a tap leaves a dot, and React renders once per stroke rather than per move. Signature.Undo, .Redo and .Clear disable themselves when they would do nothing, and onRequestAlternative adds a "Can't sign? Type your name" link for anyone who cannot draw. The value is plain JSON that re-fits a pad of another size; the ref adds toSVG() and toPaths(). Four sizes, two variants, read-only and disabled states, and isInvalid / isDisabled cascade from a Field. bunx delacour add signature copies it.

  • #204 Add Sketch: a drawing surface with pen, shapes, eraser, colours, size and undo, drawn with Skia and exported as SVG or PNG. Needs @shopify/react-native-skia, now an optional peer.

  • #181 Add Table, rows and columns of short values lined up by one set of column rules

    columns on the table declares each column once — { flex } or { width }, with an align — and every row's Nth cell reads the Nth rule, so a header, the body and a footer line up without a width on any cell. Bare text is set in tabular figures. Table.Head takes sort to become a button that draws and announces its direction while the caller owns the order; Table.Row takes onPress and isSelected to become one selectable button named by its cells, and each cell announces its column's heading. size, variant="outline" and striped set density and chrome; minWidth scrolls a wide table sideways with edge fades that dissolve into the surface behind it; rows render from a FlatList with index and isLast; Table.Empty keeps the header; Table.Frame puts the table in a card under a title and an action. bunx delacour add table copies it into a project.

  • #185 Add ThemeSelector, a System, Light or Dark choice read from the live Uniwind theme and applied to it. System is selected while the app follows the device, so a theme changed anywhere else re-selects it. segmented joins the options into one attached run; cards stands them apart as tiles. applyTheme={false} with value turns it into a presentation layer so the app can persist the preference itself — there is no storage dependency — and restoreThemeMode validates a stored string before it reaches Uniwind.setTheme. useThemeSelection reads and sets the mode with no selector mounted. delacour add theme-selector copies it, with toggle-button, icon and field as registry dependencies.

  • #193 Add TimePicker, a time of day on snapping wheels — hour, minute, optional second and, on a 12-hour clock, AM/PM. It presents as an Input-shaped trigger that opens a BottomSheet, or inline with presentation="inline". The value is always a 24-hour TimeValue; hourCycle and locale change only how it reads. It takes minuteStep, minTime/maxTime (out-of-bounds rows dim and a wheel resting on one moves back), showSeconds, visibleRows, a custom format, clearable, controlled or uncontrolled value and open state, and joins an enclosing Field. Parts are TimePicker.Trigger, .Value, .Icon, .Content, .Wheel, .Done and .Clear; TimePickerWheel renders the bare wheel on its own, and the subpath exports the pure time helpers (parseTime, serialiseTime, formatTime, snapTime, clampTime and more). Each wheel column is one adjustable element to screen readers. The subpath imports the sheet engine, so it needs @delacour/react-native-bottom-sheet installed. The CLI can now add time-picker.

  • #180 Add Timeline, a sequence of dated events down a rail or across a snapping strip

    Timeline lays out an order's history, a project's milestones or a match's key moments: each Timeline.Item is a marker on a rail with a title, a date, a description and optional side columns. One zero-based value says how far along the sequence is — events before it completed, at it current, after it upcoming — and the rail fills off that value alone, so it is never half drawn between two events. tone says what kind an event is (info, success, warning, destructive) and tints its marker whatever its progress. Five variants (dot, icon, numbered, card, compact) and two sizes. Horizontally it is a swipeable strip of columns that can snap, fires onColumnChange and a selection haptic as each column reaches the reading edge, and collapses an empty column to a tick. Timeline.List virtualises a long history over a FlatList and supplies each item's position itself. Timeline.Masthead and Timeline.Stats head the sequence. Each event is one VoiceOver stop, read as "Event 2 of 6, completed, success".

    bunx delacour add timeline copies it into your project.

Patch changes

  • #233 Fix Item and ListGroup.Item throwing with asChild and one child. Their text wrapping always returned an array, and Pressable's asChild path calls Children.only, which rejects an array of one — so <Item asChild onPress={…}><View /></Item> threw. A single child now passes through as itself.

  • #234 ListGroup.Item renders a plain view when it has neither onPress nor onLongPress, so a static row is no longer announced as a button and runs no press feedback. Rows with a handler keep button semantics, disabled ones included.

  • #235 Pressable wires screen-reader activation to onPress behind the same disabled / busy guard as the tap: onAccessibilityTap on iOS, so a VoiceOver double-tap no longer relies on UIKit synthesising a touch, and an activate action on Android, where a TalkBack double-tap previously fell through to a click with no listener. A caller that lists activate itself still owns it, and a caller's other actions are merged in. Every control built on Pressable — Button, ListGroup.Item, back and close buttons — inherits it.

  • #232 Screen scrollables compose a caller's onScroll with their own scroll tracking. Screen.FlatList, SectionList and LegendList used to drop it; Screen.ScrollArea let it replace the tracking, freezing ScrollShadow and the navbar border. A plain callback and a useAnimatedScrollHandler result are both accepted.

0.3.0

GitHub release

Minor changes

  • #126 Add Carousel: swipeable slides with peek, loop, vertical, coverflow, autoplay, dots and arrows. Adds lib/paging, the shared worklet-safe paging maths.

  • #163 Add the scroll family's shared base: useScrollSource, scroll-maths and scroll-edges

    @delacour/react-native-ui/hooks/use-scroll-source gives any scrollable a ScrollSource — its offset, content size and layout size as shared values, plus its axis — and the props to spread onto it; ScrollSourceProvider shares one and useResolvedScrollSource is how an effect finds it. lib/scroll-maths holds the worklet maths an edge affordance needs (scrollEdgeOpacity, scrollProgress, readScrollEvent) and lib/scroll-edges the band geometry. A Screen now publishes its own body as a scroll source, so scroll-linked effects inside one need no wiring. The CLI registry gains the three items, and screen now depends on use-scroll-source.

  • #164 Add useScrollSections — section-aware scroll tracking for long screens.

    • useScrollSections({ readingLine }) returns a scrollHandler for an Animated.ScrollView, a register(id) that hands back each section's onLayout, the activeId (React state that changes only when the reader crosses into another section), progress and scrollY as shared values, and scrollTo(id).
    • The active section is resolved on the UI thread and crosses to React only on a change, so scrolling never re-renders the screen.
    • The pure resolvers behind it — resolveActiveSection, resolveScrollProgress and resolveSectionScrollTarget — are worklet-safe and exported from lib/scroll-sections.
    • delacour add use-scroll-sections copies the hook and its resolvers into a project.

0.2.0

GitHub release

Minor changes

  • #159 Add Map — a themed map for places and paths, at @delacour/react-native-ui/map

    Markers that are React views and anchor on their pin's tip, labels, themed popups, routes, arcs and floating zoom, compass, recenter and locate controls, on MapLibre (@maplibre/maplibre-react-native). The basemap is OpenStreetMap vector tiles from OpenFreeMap — free, no key — painted from the theme's tokens on both platforms, so light, dark and any retuned theme carry through to the map; mapStyle or buildMapStyle swap in other tiles. Props set the opening view; a ref moves the camera with flyTo, fitBounds and recenter. @maplibre/maplibre-react-native is a new optional peer, resolved only by an app that imports /map; delacour add map copies the component into a project and expo installs it.

  • #131 cn() now runs on cn instead of clsx + tailwind-merge: the same output (held to tailwind-merge's on every variant class), much faster on repeated render calls. clsx is no longer a dependency; tailwind-merge stays, as tailwind-variants' peer for tv(). Components added with delacour add now install cn.

  • #131 Add the etched material, the tray and the kicker — opt-in, the default look is unchanged

    Surface and Card take material: flat (default), etched (a one-pixel highlight set into the edge, with a very soft drop) and tray (a muted frame, 4pt padding, 2xl corner, whose panels take the xl corner and the card fill). Button and Badge take material="etched" — the primary button gains a white inset top edge, the quiet fills the light/dark edge, and an etched badge squares to the small corner with 8%/16% alpha soft status fills. Input and Textarea gain an etched variant that rings on focus. Text.Kicker is the ten-point, 0.2em-tracked uppercase mono label.

    New tokens in tokens.css: --text-kicker, --tracking-kicker, --shadow-etched, --shadow-etched-dark, --shadow-etched-primary, --shadow-focus and --shadow-focus-dark, registered with tailwind-merge. theme.css is untouched.

0.1.0

GitHub release

Minor changes

  • #97 Add Meter — a measurement on a fixed scale, coloured by where it falls.

    • Built on Progress: Meter.Header, Meter.Label, Meter.Output, Meter.Track and Meter.Fill are the progress bar's parts, with a root that clamps the reading and judges it. A bare <Meter value={68} /> draws the scale on its own.
    • Judge a reading by low, high and optimum — good, worse and worst regions painted success, warning and destructive, with optimum saying which end is good — or by a thresholds list naming the colour from points along the scale. The prop type allows one or the other, never both.
    • segments draws the scale as whole blocks that fade on and off; any reading above the floor lights at least one.
    • valueLabel takes a function handed the judged reading and its region, so the judgement is put into words that are drawn and spoken alike rather than left to colour. One accessible element with the progressbar role, spoken the way Progress speaks its value.
    • delacour add meter copies it into a project, along with progress.
  • #88 Add Progress — a bar showing how far a task has got, or a looping segment while it is under way.

    • Compound anatomy: Progress.Header, Progress.Label, Progress.Output, Progress.Track and Progress.Fill; a bare <Progress value={40} /> draws the track and fill on its own.
    • The fill animates on the UI thread as a clipped translateX, with no layout pass per frame. isIndeterminate sweeps a segment across the track, and breathes in place instead when Reduce Motion is on.
    • Six colours and three sizes, shared with Slider. formatOptions, a function child on Progress.Output, or valueLabel word the readout.
    • One accessible element with the progressbar role: the value is spoken as a percentage over 0–100 and as a count (18 of 24) over any other range, and an indeterminate bar reports busy with no value.
    • delacour add progress copies it into a project.
  • #137 Add SelectionMode — pick several things at once, then act on them from a bar. A long press enters the mode with the pressed item picked; taps then toggle. SelectionMode.Item wraps any row, avatar or swatch with a round leading mark, a ring or nothing; .Header counts the picks and offers select-all and the way out; .Bar and .Action act on the selection; .Group lays items out as a stacked card, a grid or a horizontal strip. Selection is a set of ids, controllable alongside the mode, with an optional max.

    delacour add selection-mode copies it into a project.

  • #135 Add SlideButton, a control confirmed by dragging a handle across a rail

    The handle tracks the finger exactly and only the release is sprung. A release confirms past threshold (0.9 by default) with a small look-ahead, but a flick from halfway never does, and threshold={1} demands the far end. Controlled, the handle waits at the end until isCompleted answers; a rejected onComplete promise takes it home. Variants secondary, destructive and success; sizes match Button. Screen readers confirm with one named action, and the control flips under RTL.

  • #132 Add StackCard — a deck taken one card at a time by throwing the top one off.

    • Compound anatomy: StackCard.Card, StackCard.Stamp, StackCard.Empty, StackCard.Actions and StackCard.Action. The root sorts its children by type, and anything it does not recognise is a card.
    • One shared value — the top card's offset — drives the drag, the tilt, the stamps and the cards behind, so nothing re-renders during a drag and the next card is already in place when the top one leaves.
    • directions picks which ways a drag throws; a horizontal-only deck lets vertical scrolls through. A flick throws from a short drag, and a disallowed direction gives a little and returns.
    • layout draws the cards behind as a stack, a fan or flat, depth of them. Only a window around the top card is mounted, so a deck of five hundred costs what a deck of five does.
    • Undo brings the last card back from the side it left, and a controlled deck that leaves index where it was declines the throw and the card flies back.
    • Each allowed direction, and undo, is a screen-reader action on the top card, named by directionLabels; the cards behind are hidden. Under Reduce Motion a card fades rather than flies.
    • delacour add stack-card copies it into a project.
  • #94 Add Alert, a status message on a Surface

    Five statuses — default, info, success, warning and destructive — pick the leading glyph and colour it and the title from one token, while the description stays muted. variant="soft" washes the surface in the status's soft fill; variant="surface" keeps a neutral fill that steps off whatever it sits in. Three sizes move padding, type and glyph together. Composed from Alert.Indicator, Alert.Content, Alert.Title, Alert.Description, Alert.Action and Alert.CloseButton; isDismissible adds the close control, uncontrolled or through isOpen / onOpenChange, and useAlert().dismiss closes it from an action. bunx delacour add alert copies it into a project, with surface alongside.

  • #92 Add Avatar

    A person as a picture. The initials of name are painted first and the image goes on top, so the circle is never empty while a picture loads or after it fails, and a failed source retries on its own when its URI or headers change. variant and color paint the fallback on the six colours Badge takes; sm, md, lg and xl are fixed edges.

    Avatar.Badge pins a count or, left empty, a presence dot to a corner without the circle clipping it. Avatar.Group overlaps its faces with the first on top, rings each in the page background, and counts the people past max — or up to total — in a trailing +N tile that onOverflowPress can make a button.

    delacour add avatar copies it in.

  • #139 Button.Group takes isAttached and isFullWidth. isAttached={false} keeps the shared variant, size, isDisabled and feedback but drops the joined shape: every member keeps its own corners, no seam overlaps, the run takes a gap, and a press scales again. isFullWidth spans the parent and splits it equally between members, whatever their labels say. A custom member reads both through useButtonGroupItem(), as isAttached and isStretched.

  • #124 Add Calendar, an always-visible month grid for picking one day (mode="single"), several ("multiple") or a range ("range"), controlled or uncontrolled. Always six weeks, so the height never moves; swipe or tap the arrows to page, and tap the caption for month and year jump views. minDate/maxDate and disabled matchers (a date, a span, weekdays or a predicate) limit what can be picked; names and the first day of the week come from the locale. Two variants, three sizes on the input scale, optional Surface chrome, custom day content, and isInvalid / isDisabled / the label cascade from a Field. Days are plain CalendarDates — { year, month, day }, no time zone — with the helpers to compare, add, format, parse and serialise them, so no date library is needed. bunx delacour add calendar copies it.

  • #117 Add isMotionCalm to DelacourProvider, and useCalmMotion()

    An E2E build passes isMotionCalm and every decorative loop holds still, exactly as it does under the OS reduce-motion setting — so a test runner that waits for the screen to settle before each gesture (Argent, Detox, Maestro) never waits out a shimmering Skeleton. Motion that is the behaviour keeps moving: Spinner and an indeterminate Progress are untouched.

    useCalmMotion() (@delacour/react-native-ui/hooks/use-calm-motion) is the one question a component asks before it loops — reduce motion, or the app asking — and CalmMotionProvider sets the app's half for a hand-composed root. Skeleton and Skeleton.Group now read it.

  • #95 Add Card, a content surface with a header, a body and a footer

    Built on Surface, so it takes the same four fills and steps to the next one when nested. Six parts — Card.Header, Card.Title, Card.Description, Card.Action, Card.Content and Card.Footer — share one inset through context across three sizes, and the padding lives on the parts, so media placed straight in the card reaches its edges. Card.Action is pinned to the header's corner wherever it is written, the title follows the foreground token of the card's fill, and Card.Footer variant="band" sets the footer into the card on the next fill down. useCard() exposes the size and fill to custom parts, and bunx delacour add card copies it into a project, with surface alongside.

  • #90 Add Chip, an interactive pill for filters, tags and removable tokens

    What a chip does follows from its props. With no handler it is a plain tag; with onPress it is a button; with isSelected, defaultSelected or onSelectedChange it is a filter that toggles — controlled or uncontrolled, with a selection haptic, announced to a screen reader as selected or not. onClose adds a remove control with a press of its own, so removing a chip never also toggles it.

    import { Chip } from "@delacour/react-native-ui/chip";
    
    <Chip isSelected={open} onSelectedChange={setOpen} variant="outline">
      <Icon icon={IconFilter1} />
      <Chip.Label>Open only</Chip.Label>
    </Chip>;

    It shares Badge's colours, sizes and tones — an unselected soft or outline chip is painted exactly as the badge beside it — and selection swaps in a solid fill in the chip's colour without changing its size, so a wrapping row never reflows. Parts: Chip.Label, Chip.StartContent, Chip.EndContent, Chip.CloseButton, and useChip() for a child that restyles itself on selection.

    bunx delacour add chip copies it in, along with badge, whose tones it reads.

    Pressable now multiplies a className's opacity into its press feedback instead of overwriting it, so a disabled Button, Badge or Chip fades as its variant says it should.

  • #87 Add Collapsible — one section of content shown and hidden by its own trigger. Controlled (isOpen / onOpenChange) or uncontrolled (defaultOpen), with Collapsible.Trigger, .Title, .Description, .Indicator and .Content. The panel's height is measured and sprung, so whatever sits below follows it; the panel mounts on first open and stays mounted, and a closed panel leaves the accessibility tree. Shares Accordion's variants, sizes and motion, and bunx delacour add collapsible copies it without pulling in an accordion.

  • #134 Add ContextMenu — the actions that belong to a piece of content, reached by holding it. A hold opens the menu at the finger or against the whole trigger; a short press runs the trigger's onPress, and never both. ContextMenu.Preview lifts a copy of the held content over the scrim, with the panel beside it. The rows and the panel are Menu's own, and Menu.Content gains a backdrop prop for drawing over its scrim.

  • #125 Add Dialog, a centred card over a dimmed app that asks for a decision or a short input

    @delacour/react-native-ui/dialog, and delacour add dialog. A compound root with Trigger, Content, Close, Header, Title, Description, Body and Footer, drawn through the overlay foundation, so it sits above every bottom sheet and the navigator's header. isOpen / defaultOpen / onOpenChange, and isDismissible={false} for an alert dialog that only its own actions close. Four sizes (sm, md, lg, full), a plain or panel footer that stacks on sm, a card that lifts just clear of the keyboard, a fade-only entrance under Reduce Motion, and accessibility focus that moves to the title on open and back to the trigger on close.

    Also: Overlay.Portal's missing-provider warning no longer names the npm package, so a copied overlay carries no reference to it.

  • #129 Add Drawer, a panel that slides in from an edge and covers the app until dismissed

    @delacour/react-native-ui/drawer, and delacour add drawer. A compound root with Trigger, Content, Header, Title, Description, Body, Footer and Close, drawn through the overlay foundation, so it sits above every bottom sheet and the navigator's header. Four sides — start and end swap under RTL, plus top and bottom — and four sizes as a capped fraction of the window. Swipe toward the edge to dismiss: past 40% or on a fling it carries on from the finger, short of that it eases back, a selection haptic marks the threshold and the scrim thins with the drag. The panel pads the safe-area insets on its screen sides, fades instead of sliding under Reduce Motion, and moves accessibility focus to the title on open and back to the trigger on close.

  • #86 Add EmptyState

    A placeholder for a list or screen with nothing in it. EmptyState.Header stacks Media, Title and Description, and EmptyState.Content holds the actions. variant="card" draws a dashed block for an empty section beside populated ones, and size scales the padding, media and type together. A bare Icon in Media inherits its size and colour, and Media variant="icon" tints a box behind it.

    delacour add empty-state copies it into a project.

  • #136 Add Fab, one primary action floating over the screen it belongs to. placement pins it to the bottom start, centre or end of its nearest positioned ancestor, clear of the safe area, or leaves it in flow when omitted; sizes are 44, 56 and 64pt from new --spacing-fab-* tokens, in primary, secondary, surface and destructive. isExtended with Fab.Label makes a labelled stadium. Fab.Group unfolds a dial of Fab.Actions over a scrim from one spring with a stagger, turns its plus into a cross, unmounts the actions once closed, closes on the scrim, an action or Android back, and takes isOpen / defaultOpen / onOpenChange. delacour add fab copies it in.

  • #130 Add Feedback, a dialog for writing — the field in a recessed well, the actions on the band around it

    @delacour/react-native-ui/feedback, and delacour add feedback. Built on Dialog, so open state, portal, scrim, back, escape, focus and the keyboard lift are Dialog's. A compound root with Trigger, Content, Panel, Title, Close, Field, Footer, Action, Cancel and Submit, and useFeedback(). The draft is value / defaultValue / onValueChange on the root and survives close and reopen until clear(). Submit is gated by canSubmitFeedback (empty and whitespace are empty, canSubmitEmpty for chips or a rating), passes the trimmed text, shows its loading state and makes the field read-only while a returned promise is in flight, and never closes on its own. The well eases to its measured height when a multi-step flow swaps its content, so the card never jumps; under Reduce Motion it changes height at once. The field grows from minRows (6) to maxRows (12) before it scrolls, and takes the title's text as its label.

  • #93 Add Item, a row of media, text and actions for lists and settings. It renders as a Pressable when given onPress and as a plain view otherwise, has default, outline and muted surfaces, sm/md/lg sizes, a vertical orientation for cards, isDisabled and isSelected states, and Item.Media (bare icon, icon tile or clipped image), Item.Content, Item.Title, Item.Description, Item.Actions, Item.Header, Item.Footer, Item.Group and Item.Separator. Dropped into a ListGroup it takes the group's size, draws no surface of its own and lines up with the group's dividers. The CLI can now add item.

  • #98 Add Kpi, one number, its change and a sparkline of how it got there

    Built on Card, so it takes the card's four fills and three sizes and sits on the card's inset. Kpi.Trend takes a signed percentage and colours it by what it means rather than by its sign — goodDirection on the card says whether up, down or neither is good news, and every trend inside follows it — as a line of text or a badge with an arrow, announced as one string. Kpi.Sparkline is a Chart line with no axes in the card's series colour, under the number or in a fixed column beside it, and holding it scrubs a point: the KPI keeps the scrubbed index as controllable state that useKpi() reads. isLoading holds placeholders of each part's size, and Kpi.Group lays several metrics out in a row or a column, optionally on one surface with rules between. Chart gains frameClassName, which sizes the plot's frame. bunx delacour add kpi copies it into a project, with card and chart alongside.

  • #85 Add Label, a form control's name with required, invalid and disabled states

    Label renders Text.Label and takes a colour, never a scale, so it keeps the type scale the rest of a form uses. isRequired appends a destructive asterisk as a nested run behind a no-break space, so a label that wraps carries the mark after its last word, and announces the label as "Email, required" rather than reading the asterisk aloud. isInvalid turns it destructive and isDisabled fades it and reports it disabled to assistive technology.

    import { Label } from "@delacour/react-native-ui/label";
    
    <Label isRequired>Email</Label>;

    bunx delacour add label copies it in, bringing text and tv with it.

  • #133 Add Menu — a list of actions anchored to the control that opens it. Rows run an action and close; checkbox rows stay open, radio rows close; Menu.Sub expands in place. The panel flips above the trigger near the bottom of the screen, stays inside the safe area and scrolls past the room. resolveMenuPlacement is exported for a later popover to adopt.

  • #120 Add OverlayProvider, the layer every overlay is drawn on

    @delacour/react-native-ui/overlay mounts the one teleport host that overlays and bottom sheets share, and a registry that orders them in bands — every overlay above every sheet, an anchored panel above the dialog that opened it, a toast above both — and gives Android's back button to the topmost overlay only. Overlay.Portal, Overlay.Scrim, useOverlayPresence (keeps an overlay mounted through its exit animation) and useOverlayBackHandler are exported for building more. BottomSheetProvider now detects an OverlayProvider above or below it and mounts teleport's provider only once; the engine's provider takes hasPortalProvider to make that possible. bunx delacour add overlay copies it into a project.

  • #127 Add Popover — a small panel anchored to its trigger that flips and shifts to stay inside the safe area and above the keyboard, with an arrow that keeps pointing at the trigger, a title, a description, a close control, an optional scrim, width="trigger" and a scrollable body. Its anchoring pieces — resolveAnchoredPosition, useAnchorMeasure, useAnchoredContent and AnchoredArrow — are exported for other anchored overlays. delacour add popover copies it.

  • #138 Add ProgressButton, a button held rather than tapped to confirm. A fill grows from the leading edge over holdDuration (2000 ms by default) and onComplete fires only when it reaches the end, read from the animation's own finish rather than a timer. Released early, the fill plays back at the same rate, and a second press resumes from where it is. Every variant (primary, secondary, destructive, success) rests on the same surface and carries its colour in a label drawn twice, so the text stays readable across the wipe. It takes the button's sizes and a pill or rounded shape, draws a tick on completion or a caller's ProgressButton.Done, rewinds by travelling back with isAutoReset or a controlled isCompleted, steps in fifths under reduced motion, and completes on a screen reader's activate. delacour add progress-button copies it in.

  • #91 Add Rating, a row of stars that reads or sets a score. Tap a star or drag along the row to set it, in whole stars or any step that divides one (step={0.5} for halves); a read-only rating draws any value, filling a fraction of a star. allowClear lets a tap on the value held withdraw it, Rating.Output shows the value, and the row is one adjustable control to screen readers. Six colours, three sizes, and isInvalid / isDisabled cascade from a Field. bunx delacour add rating copies it.

  • #116 BottomSheet is rewritten on @delacour/react-native-bottom-sheet, this repository's own headless engine, with the same compound shape — Trigger, Portal, Overlay, Container, Content, ScrollView, Footer, Close, Title, Description — and eight new parts: Background, Handle, FlatList, SectionList, LegendList, TextInput, Steps / Step, Host and Provider. @gorhom/bottom-sheet is no longer imported anywhere; its peer entry leaves in the next release.

    Install. The engine is an optional peer, like @delacour/react-native-charts: bun add @delacour/react-native-bottom-sheet@alpha and expo install react-native-teleport, then rebuild the dev client — teleport is a native module and needs the new architecture. delacour add bottom-sheet does both.

    Provider. DelacourProvider no longer mounts a sheet provider. Mount BottomSheetProvider from @delacour/react-native-ui/bottom-sheet once, inside DelacourProvider and around the navigator. Without it a Portal renders where it is written, as an inline sheet.

    Renamed and moved props. The engine's names win, and sizing and behaviour move from Container to the root:

    • <BottomSheet.Container enableDynamicSizing={false} snapPoints={…} maxDynamicContentSize={…}> → <BottomSheet dynamicSizing={false} snapPoints={…} maxDynamicContentSize={…}>. Container takes className, style and the three *ClassName props only.
    • keyboardBehavior, keyboardBlurBehavior, enablePanDownToClose, enableOverDrag and the other gorhom modal props move to the root under the same names; android_keyboardInputMode is gone (keyboard-controller owns the window). New on the root: keyboardScope, detached, topInset, bottomInset (defaults to the safe-area bottom), stackBehavior, closeOnBack, keepMounted, animation, onSnapPointHaptic / onCloseHaptic / onOverDragHaptic.
    • <BottomSheet.Overlay isCloseOnPress={false}> → <BottomSheet.Overlay pressBehavior="none">. Omitting Overlay still draws no scrim.
    • BottomSheet.ScrollView no longer needs dynamicSizing={false} or snapPoints: a list's content size is the dynamic snap point, capped by maxDynamicContentSize.
    • BottomSheetHandle (the ref type) is now BottomSheetRef; the old name is a deprecated alias. useBottomSheetInput() stays as an alias of the engine's useBottomSheetTextInput(). resolveSheetBottomInset / resolveSheetScrollEndPadding are re-exported from the engine's ./core.
    • Removed: BOTTOM_SHEET_KEYBOARD_DEFAULTS, resolveFooterPlacement, and the container and portal contexts.
  • #96 Add Skeleton, a placeholder for content that is still loading

    It comes in three shapes — rect, line and circle — and either shimmers a glint across itself or pulses its opacity, on the UI thread. Pass the real content as children and the placeholder takes its exact size, then fades the content in when isLoading flips off with nothing moving. Skeleton.Lines draws a paragraph, and Skeleton.Group keeps every skeleton inside it on one clock and one loading flag. Motion stops under the OS reduce-motion setting, and a placeholder stays hidden from screen readers unless it has a label. bunx delacour add skeleton copies it in.

  • #118 Detents are snap points everywhere, matching the snapPoints prop. DetentSpec is SnapPointSpec, onDetentHaptic is onSnapPointHaptic, useBottomSheetAnimated()'s detents is snapPoints, the internal detentCount is snapPointCount, and the core helpers follow (normalizeSnapPoints, parseSnapPoint, dynamicSnapPoint, crossedSnapPoint, snapPointUnder). The handle's accessibility value reads "Snap point 1 of 2". The docs page moves to /docs/bottom-sheet/snap-points.

  • #99 Add Steps, a stepper for multi-step flows

    Steps shows where someone is in a flow: numbered indicators joined by a line, each step completed, current or upcoming against one zero-based value. It runs horizontally, with titles under the indicators, or vertically, with titles and descriptions beside them, in two variants and three sizes. The connectors are drawn for you — the root counts its Steps.Items and every one but the last draws a line to the next, filled once the value has passed it.

    A step can be loading (a spinner in the indicator), invalid (a cross and a destructive title) or disabled, and completed overrides whether it reads as done. Steps take presses and move the value there; isLinear keeps the steps ahead closed, and a controlled value with no onValueChange is a read-only progress display whose steps are not announced as buttons. Steps.Panel holds a step's form or summary below its title, outside the tap target, so its controls stay reachable by a screen reader. Each step is announced as its title with a value such as "Step 2 of 3, completed".

    bunx delacour add steps copies it into your project.

  • #83 Add Surface, a rounded container on the theme's fill ladder

    Four fills — default, secondary, tertiary and transparent — and four padding steps. A surface that names no variant steps to the next fill from the one it sits in, so a panel inside a card never vanishes into it, and a transparent surface passes the plane beneath it through. useSurface() exposes the resolved fill to custom children, and padding="none" clips, for content bled to the corners. bunx delacour add surface copies it into a project.

  • #123 Add Swipe, a row that slides aside to reveal actions behind it. Swipe.Start and Swipe.End hold Swipe.Action tiles; a drag far past them fires the outermost (isFullSwipe, on by default), Swipe.Group keeps one row open at a time, and a ref opens or closes the row from code. Every action is also published as an accessibility action on the row.

  • #89 Add Textarea, a multiline text field sized in rows. It renders Input's box — variants, sizes, focus, invalid, disabled and the Field cascade — and adds a height derived from rows and the size's line height, autoGrow up to maxRows, and a showCount character count against maxLength that turns destructive at the limit. delacour add textarea copies it, with input and text as registry dependencies.

  • #122 Add Toast — a brief message shown with toast() from anywhere, including outside React, and drawn by one <ToastViewport /> mounted inside OverlayProvider. Toasts stack at the top or bottom edge, three drawn with the newest in front and the rest queued; they sit above the home indicator and the keyboard, pause their clock while touched or backgrounded, and swipe away toward their edge or sideways. Statuses, glyphs and title colours are Alert's; toast.promise turns one loading toast into the success or the failure in place; render composes a custom card from <Toast>'s parts. Each toast is announced as it appears and stays at least ten seconds under a screen reader. Alert's status glyphs move to the alert-glyphs.ts leaf so the two share them. delacour add toast copies it.

  • #84 Add ToggleButton, a button that stays pressed. On its own it is controlled with isSelected and onSelected or holds its own state from defaultSelected; inside ToggleButton.Group the group owns one array of selected values, in multiple or single selection mode, with isSelectionRequired to keep the last choice from being cleared. Each state draws with one of the button's own variants (default, outline, ghost), so sizes, icons, loading and press feedback are the button's. An attached group joins into one run like Button.Group; a detached one wraps with a gap. The state is announced as a toggle button that is checked, or as a selected radio in a single-choice group. delacour add toggle-button copies it in.

  • #128 Add Tooltip — a short label anchored to its control that opens on a long press (or a press, with openOn="press"), hides itself after duration, and closes on any outside tap while letting that tap through to what it landed on. It flips and shifts like Popover, has an inverted and a surface variant, an arrow that points at the trigger, and reads its label to a screen reader on the trigger without opening. OverlayProvider now hears every touch that starts beneath it, through subscribeTouchStart, without claiming any. delacour add tooltip copies it.

  • #48 Publish the libraries under the @delacour org

    delacour-react-native-ui is now @delacour/react-native-ui, and delacour-react-native-charts is now @delacour/react-native-charts. The old names are deprecated and take no further versions. (Both were briefly spelled @delacour/native-ui and @delacour/charts in this repository; neither reached npm, so the line above is the whole story for anyone installing.) Nothing about the components changed — swap the package and the import prefix:

    bun remove delacour-react-native-ui delacour-react-native-charts
    bun add @delacour/react-native-ui@alpha @delacour/react-native-charts@alpha
    - import { Button } from "delacour-react-native-ui/button";
    + import { Button } from "@delacour/react-native-ui/button";

    and in global.css, @import '@delacour/react-native-ui/styles';.

    The CLI keeps its name. delacour add chart now installs @delacour/react-native-charts.

  • #22 Add bar, scatter, candlestick and pie charts, stacked areas and horizontal bars

    @delacour/react-native-charts gains four marks and a second root. ChartBar draws one bar per datum on a cubic-cornered rect path, so a corner radius animates without snapping; sibling bars share a step and bars naming one stackId stack in data space, so the y domain covers the running totals rather than the tallest series. ChartArea takes the same stackId. ChartScatter is one Skia path per series, and ChartCandlestick draws every candle through every sentiment path so a colour flip is a morph rather than a cut. orientation="horizontal" swaps the axis roles at the model, so bars grow rightward from a category axis. PolarChart is the new root, with PieSlices on a fixed-verb path that morphs between any two data sets and a scrub-free tap that resolves a slice index. @delacour/react-native-charts/core exports the bar, scatter, candle and slice geometry alongside the scales.

    @delacour/react-native-ui/chart skins all of it. Chart.Bar, Chart.Scatter and Chart.Candlestick join Chart.Line and Chart.Area; bars group by being siblings, stack by sharing a stackId, round their value end from --radius, and take labels. Candles borrow success, destructive and muted-foreground for their sentiment. Over bars or candles Chart.Tooltip.X becomes a band one step wide. PieChart is a second root — PieChart.Slice, .Label, .Center, .Tooltip and .Legend — whose categories are its rows, with innerRadius for a donut and a tap-driven readout.

    Series colours now dedupe before the theme lookup, so twenty slices walking the five-token ramp resolve five tokens rather than throwing past the eighth.

    The CLI's chart registry item picks up the new files and names the new marks in its description.

  • #21 Add Button.Group, which joins several controls into one segmented run — with Button.Group.Separator for a rule between two members and Button.Group.Text for a chunk that says something rather than doing something. An Input joins the same way.

    <Button.Group variant="outline">
      <Button onPress={archive}>Archive</Button>
      <Button onPress={report}>Report</Button>
      <Button onPress={snooze}>Snooze</Button>
    </Button.Group>
    
    <Button.Group>
      <Button onPress={save}>Save</Button>
      <Button.Group.Separator />
      <Button accessibilityLabel="More" size="icon-md" onPress={openMenu}>
        <Icon icon={IconChevronDownSmall} />
      </Button>
    </Button.Group>

    Each member squares the pair of corners crossing a seam and overlaps its neighbour by a point, so two adjacent borders draw as one hairline. React Native has no sibling selector, so a member's place is computed in JavaScript and published through context rather than matched with CSS — which also means a control this package has never heard of can join a run by reading useButtonGroupItem().

    The group owns its members' axes: their step outright, since controls of different heights do not join, and variant, isDisabled and feedback as defaults a member may override. A member keeps its own shape, so a square button still works inside a run — an icon-md member of an sm group comes out icon-sm.

    Nothing existing changes behaviour: a button outside a group draws exactly the corner it drew before.

    • New exports: BUTTON_GROUP_ORIENTATIONS, BUTTON_GROUP_POSITIONS, BUTTON_GROUP_SEPARATOR_ORIENTATION, BUTTON_FEEDBACK, BUTTON_GROUP_FEEDBACK, useButtonGroup, useButtonGroupContext, useButtonGroupItem, useButtonGroupItemContext, ButtonGroupProvider, ButtonGroupItemProvider, and the pure resolvers resolveGroupPositions, resolveGroupSeams, resolveButtonFeedback, resolveGroupedButtonSize and resolveButtonSizeStep.
  • #53 Name both libraries for what they are

    @delacour/react-native-ui and @delacour/react-native-charts — the names they published under before the scope, with the delacour- prefix traded for @delacour/ rather than dropped. Every subpath keeps its spelling (@delacour/react-native-ui/button, @delacour/react-native-charts/core), so only the package half of an import changes.

    The pair now matches: the charts package was briefly @delacour/charts, which did not say React Native and did not sit beside its sibling. Neither intermediate spelling reached npm.

    The CLI stays delacour, unscoped, because it is the thing people type: bunx delacour@alpha add button.

    @types/react is pinned in the workspace catalog as part of this: four packages declared three different ranges, so two copies were always installed and only hoisting order decided which reached the root. The rename changed that order, split Ref types across two copies and collapsed every ComponentRef<typeof Animated.View> to never.

  • #25 One theme file, the same shape everywhere

    theme.css is now the one file in styles/ that is yours, and its header says so. What delacour init ships, what the docs site's /theme page emits with no preset, and what delacour theme writes are held to the same shape, declaration for declaration.

    delacour theme converts theme.css in place. Paste a shadcn or tweakcn globals.css over the file and run the command with no argument: a file already in Uniwind's shape is left alone, a shadcn-shaped one is rewritten, and anything else is named. delacour doctor fails on a theme.css still in shadcn's :root / .dark shape, which Uniwind reads as a utility class named dark and a dark theme that never arrives.

    The converter fills what shadcn v4 stopped declaring. --destructive-foreground — which Button, Badge, Switch, Slider and Checkbox all paint with — and the shadow scale are now derived when a source omits them, and the light --elevated derivation follows the card, as the shipped file already did.

    The shipped defaults are shadcn's current ones. The chart ramp is the neutral greys shadcn init writes today, and the default typeface is each platform's own sans rather than Geist, which a fresh app had never loaded. The /theme page shows theme.css first, with shadcn's globals.css in a second tab for a web app sharing the theme.

  • #22 Add charts: a headless Skia engine, and the Chart component that skins it

    @delacour/react-native-charts is new — a token-free charting engine for React Native, drawn with Skia, animated with Reanimated and driven by Gesture Handler. It ships CartesianChart with Line, Area, Grid and both axes, a scrub whose dot rides the drawn curve rather than hopping between data points, and path morphing that never falls back to snapping. @delacour/react-native-charts/core is every scale, tick, curve and solver in it, importable with no Skia in the module graph.

    @delacour/react-native-ui/chart is that engine wearing the theme. A shadcn-shaped config names each series and assigns --chart-1 … --chart-5 by position, so a call site writes <Chart.Line yKey="revenue" /> and never a colour. Parts are placed rather than configured: Chart.Grid, Chart.Line, Chart.Area, Chart.XAxis and Chart.YAxis draw into the canvas, while Chart.Tooltip and Chart.Legend are React Native views layered over and under it.

    Also new: --spacing-chart-sm/md/lg, because a canvas has no intrinsic height and a dashboard's rows only line up if every chart agrees on one.

    This needs a dev-client rebuild. @shopify/react-native-skia is a native module and is new to the workspace — run expo prebuild --clean and rebuild before running the playground.

    The CLI learns two things: how to install a Skia-backed component, and that @delacour/react-native-charts publishes to the alpha tag while this repository is in pre mode, since a bare bun add of it would resolve latest and find nothing.

  • #19 Breaking. Button's isIconOnly prop is removed. A square footprint is now a size: size="icon-sm", size="icon-md" or size="icon-lg".

    // before
    <Button accessibilityLabel="Favourite" isIconOnly size="sm" variant="ghost">
    
    // after
    <Button accessibilityLabel="Favourite" size="icon-sm" variant="ghost">

    Each icon-* size is its labelled step with the horizontal padding traded for a width off the same token, which is shadcn's spelling and makes padding and width mutually exclusive by construction rather than by rule.

    • BUTTON_SIZES now holds all six values, and is derived from the new BUTTON_LABEL_SIZES and BUTTON_ICON_SIZES tuples. ButtonSize widens to match; ButtonLabelSize and ButtonIconSize are exported alongside it.
    • buttonVariants no longer accepts isIconOnly; pass the size instead. Its signature now matches the public prop exactly, so buttonVariants({ size: "icon-md" }) works.
    • ButtonLayout loses isIconOnly, and resolveButtonLayout no longer takes it — it now folds only isLoading and spinnerPlacement.
    • useButton() reports size verbatim, so an icon button returns "icon-lg" rather than "lg". Match against BUTTON_ICON_SIZES where a child needs to know it sits inside a square one.

Patch changes

  • #117 Fix Accordion and Collapsible panels that never opened on their first expand in Release builds

    The item's effect decided "has the panel measured?" by reading the measured height's shared value on the JS thread, straight after the panel's onLayout had written it. That write is queued onto the UI runtime and the read does not drain the queue, so a Release build read the unmeasured sentinel every time, bailed, and nothing re-ran it — the trigger said expanded and the panel stayed shut. Debug builds were slow enough to hide it. "Measured" is now React state, the decision is a pure accordionTravelTarget / collapsibleTravelTarget, and a test sweeps both components for any JS-thread read of the height.

  • #112 Chip, Badge and Alert take a closeTestID and forward it to the close control they compose in, so a test or automation can press "Remove" or "Dismiss" by id rather than by its label.

  • #103 Pressable multiplies a className's opacity into its press feedback instead of overwriting it, so a disabled Button, Badge, Checkbox, ListGroup.Item or Input.Group now fades as its variant asks.

  • #62 End BottomSheet.ScrollView at a sticky footer's top edge, so the scroll indicator and the overscroll bounce stop above the footer instead of running on underneath it

  • #100 Slider.Thumb now has an accessible name. It defaults to the enclosing Field.Label's text — the label alone for one thumb, "…, minimum" and "…, maximum" for a range's two — and an accessibilityLabel on the thumb wins outright. A range with no label still reads "Minimum" and "Maximum". Field.Label registers its text with the field's context (label / registerLabel), so any control can read it; resolveFieldLabelText and resolveThumbAccessibilityLabel are exported.

  • #46 Fix the corner seam on a checked Checkbox

    The fill sat exactly inside the border, so its outer curve and the border's inner curve were the same curve rasterised on two layers. Antialiased independently they under-cover where they meet, letting the box's own bg-card bleed through as a dull arc at each corner — on iOS and Android alike, and at every size. The straight edges are pixel-aligned, which is why only the corners showed it.

    The fill now overlaps the border ring instead of meeting it: -inset-px reaches past the padding box, and it wears the box's own corner rather than one a border width tighter. There is no longer a shared edge to leave a seam.

0.1.0-alpha.5

Patch changes

  • #103 Pressable multiplies a className's opacity into its press feedback instead of overwriting it, so a disabled Button, Badge, Checkbox, ListGroup.Item or Input.Group now fades as its variant asks.

  • #100 Slider.Thumb now has an accessible name. It defaults to the enclosing Field.Label's text — the label alone for one thumb, "…, minimum" and "…, maximum" for a range's two — and an accessibilityLabel on the thumb wins outright. A range with no label still reads "Minimum" and "Maximum". Field.Label registers its text with the field's context (label / registerLabel), so any control can read it; resolveFieldLabelText and resolveThumbAccessibilityLabel are exported.

0.1.0-alpha.4

Patch changes

  • #62 End BottomSheet.ScrollView at a sticky footer's top edge, so the scroll indicator and the overscroll bounce stop above the footer instead of running on underneath it

0.1.0-alpha.3

Minor changes

  • #53 Name both libraries for what they are

    @delacour/react-native-ui and @delacour/react-native-charts — the names they published under before the scope, with the delacour- prefix traded for @delacour/ rather than dropped. Every subpath keeps its spelling (@delacour/react-native-ui/button, @delacour/react-native-charts/core), so only the package half of an import changes.

    The pair now matches: the charts package was briefly @delacour/charts, which did not say React Native and did not sit beside its sibling. Neither intermediate spelling reached npm.

    The CLI stays delacour, unscoped, because it is the thing people type: bunx delacour@alpha add button.

    @types/react is pinned in the workspace catalog as part of this: four packages declared three different ranges, so two copies were always installed and only hoisting order decided which reached the root. The rename changed that order, split Ref types across two copies and collapsed every ComponentRef<typeof Animated.View> to never.

0.1.0-alpha.2

Minor changes

  • #48 Publish the libraries under the @delacour org

    delacour-react-native-ui is now @delacour/react-native-ui, and delacour-react-native-charts is now @delacour/react-native-charts. The old names are deprecated and take no further versions. Nothing about the components changed — swap the package and the import prefix:

    bun remove delacour-react-native-ui delacour-react-native-charts
    bun add @delacour/react-native-ui@alpha @delacour/react-native-charts@alpha
    - import { Button } from "delacour-react-native-ui/button";
    + import { Button } from "@delacour/react-native-ui/button";

    and in global.css, @import '@delacour/react-native-ui/styles';.

    The CLI keeps its name. delacour add chart now installs @delacour/react-native-charts.

Patch changes

  • #46 Fix the corner seam on a checked Checkbox

    The fill sat exactly inside the border, so its outer curve and the border's inner curve were the same curve rasterised on two layers. Antialiased independently they under-cover where they meet, letting the box's own bg-card bleed through as a dull arc at each corner — on iOS and Android alike, and at every size. The straight edges are pixel-aligned, which is why only the corners showed it.

    The fill now overlaps the border ring instead of meeting it: -inset-px reaches past the padding box, and it wears the box's own corner rather than one a border width tighter. There is no longer a shared edge to leave a seam.

0.1.0-alpha.1

Minor changes

  • #22 Add bar, scatter, candlestick and pie charts, stacked areas and horizontal bars

    @delacour/react-native-charts gains four marks and a second root. ChartBar draws one bar per datum on a cubic-cornered rect path, so a corner radius animates without snapping; sibling bars share a step and bars naming one stackId stack in data space, so the y domain covers the running totals rather than the tallest series. ChartArea takes the same stackId. ChartScatter is one Skia path per series, and ChartCandlestick draws every candle through every sentiment path so a colour flip is a morph rather than a cut. orientation="horizontal" swaps the axis roles at the model, so bars grow rightward from a category axis. PolarChart is the new root, with PieSlices on a fixed-verb path that morphs between any two data sets and a scrub-free tap that resolves a slice index. @delacour/react-native-charts/core exports the bar, scatter, candle and slice geometry alongside the scales.

    @delacour/react-native-ui/chart skins all of it. Chart.Bar, Chart.Scatter and Chart.Candlestick join Chart.Line and Chart.Area; bars group by being siblings, stack by sharing a stackId, round their value end from --radius, and take labels. Candles borrow success, destructive and muted-foreground for their sentiment. Over bars or candles Chart.Tooltip.X becomes a band one step wide. PieChart is a second root — PieChart.Slice, .Label, .Center, .Tooltip and .Legend — whose categories are its rows, with innerRadius for a donut and a tap-driven readout.

    Series colours now dedupe before the theme lookup, so twenty slices walking the five-token ramp resolve five tokens rather than throwing past the eighth.

    The CLI's chart registry item picks up the new files and names the new marks in its description.

  • #21 Add Button.Group, which joins several controls into one segmented run — with Button.Group.Separator for a rule between two members and Button.Group.Text for a chunk that says something rather than doing something. An Input joins the same way.

    <Button.Group variant="outline">
      <Button onPress={archive}>Archive</Button>
      <Button onPress={report}>Report</Button>
      <Button onPress={snooze}>Snooze</Button>
    </Button.Group>
    
    <Button.Group>
      <Button onPress={save}>Save</Button>
      <Button.Group.Separator />
      <Button accessibilityLabel="More" size="icon-md" onPress={openMenu}>
        <Icon icon={IconChevronDownSmall} />
      </Button>
    </Button.Group>

    Each member squares the pair of corners crossing a seam and overlaps its neighbour by a point, so two adjacent borders draw as one hairline. React Native has no sibling selector, so a member's place is computed in JavaScript and published through context rather than matched with CSS — which also means a control this package has never heard of can join a run by reading useButtonGroupItem().

    The group owns its members' axes: their step outright, since controls of different heights do not join, and variant, isDisabled and feedback as defaults a member may override. A member keeps its own shape, so a square button still works inside a run — an icon-md member of an sm group comes out icon-sm.

    Nothing existing changes behaviour: a button outside a group draws exactly the corner it drew before.

    • New exports: BUTTON_GROUP_ORIENTATIONS, BUTTON_GROUP_POSITIONS, BUTTON_GROUP_SEPARATOR_ORIENTATION, BUTTON_FEEDBACK, BUTTON_GROUP_FEEDBACK, useButtonGroup, useButtonGroupContext, useButtonGroupItem, useButtonGroupItemContext, ButtonGroupProvider, ButtonGroupItemProvider, and the pure resolvers resolveGroupPositions, resolveGroupSeams, resolveButtonFeedback, resolveGroupedButtonSize and resolveButtonSizeStep.
  • #25 One theme file, the same shape everywhere

    theme.css is now the one file in styles/ that is yours, and its header says so. What delacour init ships, what the docs site's /theme page emits with no preset, and what delacour theme writes are held to the same shape, declaration for declaration.

    delacour theme converts theme.css in place. Paste a shadcn or tweakcn globals.css over the file and run the command with no argument: a file already in Uniwind's shape is left alone, a shadcn-shaped one is rewritten, and anything else is named. delacour doctor fails on a theme.css still in shadcn's :root / .dark shape, which Uniwind reads as a utility class named dark and a dark theme that never arrives.

    The converter fills what shadcn v4 stopped declaring. --destructive-foreground — which Button, Badge, Switch, Slider and Checkbox all paint with — and the shadow scale are now derived when a source omits them, and the light --elevated derivation follows the card, as the shipped file already did.

    The shipped defaults are shadcn's current ones. The chart ramp is the neutral greys shadcn init writes today, and the default typeface is each platform's own sans rather than Geist, which a fresh app had never loaded. The /theme page shows theme.css first, with shadcn's globals.css in a second tab for a web app sharing the theme.

  • #22 Add charts: a headless Skia engine, and the Chart component that skins it

    @delacour/react-native-charts is new — a token-free charting engine for React Native, drawn with Skia, animated with Reanimated and driven by Gesture Handler. It ships CartesianChart with Line, Area, Grid and both axes, a scrub whose dot rides the drawn curve rather than hopping between data points, and path morphing that never falls back to snapping. @delacour/react-native-charts/core is every scale, tick, curve and solver in it, importable with no Skia in the module graph.

    @delacour/react-native-ui/chart is that engine wearing the theme. A shadcn-shaped config names each series and assigns --chart-1 … --chart-5 by position, so a call site writes <Chart.Line yKey="revenue" /> and never a colour. Parts are placed rather than configured: Chart.Grid, Chart.Line, Chart.Area, Chart.XAxis and Chart.YAxis draw into the canvas, while Chart.Tooltip and Chart.Legend are React Native views layered over and under it.

    Also new: --spacing-chart-sm/md/lg, because a canvas has no intrinsic height and a dashboard's rows only line up if every chart agrees on one.

    This needs a dev-client rebuild. @shopify/react-native-skia is a native module and is new to the workspace — run expo prebuild --clean and rebuild before running the playground.

    The CLI learns two things: how to install a Skia-backed component, and that @delacour/react-native-charts publishes to the alpha tag while this repository is in pre mode, since a bare bun add of it would resolve latest and find nothing.

  • #19 Breaking. Button's isIconOnly prop is removed. A square footprint is now a size: size="icon-sm", size="icon-md" or size="icon-lg".

    // before
    <Button accessibilityLabel="Favourite" isIconOnly size="sm" variant="ghost">
    
    // after
    <Button accessibilityLabel="Favourite" size="icon-sm" variant="ghost">

    Each icon-* size is its labelled step with the horizontal padding traded for a width off the same token, which is shadcn's spelling and makes padding and width mutually exclusive by construction rather than by rule.

    • BUTTON_SIZES now holds all six values, and is derived from the new BUTTON_LABEL_SIZES and BUTTON_ICON_SIZES tuples. ButtonSize widens to match; ButtonLabelSize and ButtonIconSize are exported alongside it.
    • buttonVariants no longer accepts isIconOnly; pass the size instead. Its signature now matches the public prop exactly, so buttonVariants({ size: "icon-md" }) works.
    • ButtonLayout loses isIconOnly, and resolveButtonLayout no longer takes it — it now folds only isLoading and spinnerPlacement.
    • useButton() reports size verbatim, so an icon button returns "icon-lg" rather than "lg". Match against BUTTON_ICON_SIZES where a child needs to know it sits inside a square one.

On this page