# Commands (/docs/native/cli/commands) ```bash delacour [components...] [options] ``` Every command that reads the registry also takes `--registry`, `--ref`, `--offline` and `--cwd` — see [Shared options](#shared-options). ## `init` [#init] Sets the project up and writes `native-components.json`. Covered in full at [Setting up a project](/docs/native/cli/init). ```bash delacour init [components...] ``` ## `add` [#add] Copies components in, with everything they need. ```bash delacour add button input field delacour add --all ``` The order is deliberate: resolve the dependency graph, plan every file, ask **once** about conflicts, write, then install. Installing first would leave a project holding new native dependencies and no components if you answered "no" to the overwrite prompt. A file already on disk with byte-identical content is not a conflict — that is the normal state of a utility two components share, and `add` says so rather than asking. Only a genuine difference is worth a prompt, because you may well have edited it. `expo install` for native modules and anything pinned to the SDK, your package manager for plain JavaScript, and `registryDependencies` — which are other components to copy, not packages to install. Non-interactively (`--yes`, or a pipe), a conflict is an error naming the files rather than a silent overwrite. Pass `--overwrite` when you mean it. ## `list`, `search`, `view` [#list-search-view] Browsing works before `native-components.json` exists — which is exactly when you are deciding whether to run `init`. ```bash delacour list delacour search sheet delacour view button ``` `view` prints the item's files, the components it copies in, and the packages it installs. All three take `--json`. ## `diff` [#diff] Shows what has moved upstream since you copied a component. ```bash delacour diff # every component you have delacour diff button ``` Read-only, and deliberately so. Once a component is in your repository it is yours, and the changes on each side are both legitimate — a fix upstream, a tweak locally. Printing both and stopping is the only behaviour that does not quietly pick a winner. `add --overwrite` takes the registry's copy when that is what you want. Which components you hold is judged from the files on disk, not from anything recorded at `add` time — a manifest of "what was installed" would be one more thing to drift from reality. ## `doctor` [#doctor] Checks that the app is wired up for these components. Every check is a failure mode that produces no error on its own — see [Troubleshooting](/docs/native/cli/troubleshooting). ```bash delacour doctor delacour doctor --fast # skip the checks that shell out to the Expo CLI delacour doctor --json ``` Exits non-zero when a check fails, so it works in CI. ## `info` [#info] The resolved config and what was detected: package manager, Expo and React Native versions, whether Uniwind and Tailwind are installed, the workspace and app roots, and the path aliases found in `tsconfig.json`. ```bash delacour info ``` Start here when the CLI is writing files somewhere you did not expect. ## `mcp` [#mcp] Runs as an MCP server over stdio so a coding agent can browse and add components. See [Agents](/docs/native/cli/mcp). ## Shared options [#shared-options] `--cwd` decides which `native-components.json` applies: the nearest one, walking upwards. The walk stops at the repository root — past it lie your other projects. See [The registry](/docs/native/cli/registry) for pinning a ref and pointing at a fork. # native-components.json (/docs/native/cli/configuration) Written by `init`, next to your components. The nearest one wins, walking up from `--cwd`. ```json title="native-components.json" { "$schema": "https://raw.githubusercontent.com/delacournz/delacour-ui/main/registry/config.schema.json", "framework": "expo", "typescript": true, "registry": {}, "registries": {}, "paths": { "ui": "src/components/ui", "lib": "src/lib", "hooks": "src/hooks", "styles": "src/styles", "icons": "src/lib/icons" }, "aliases": { "ui": "@/components/ui", "lib": "@/lib", "hooks": "@/hooks", "styles": "@/styles", "icons": "@/lib/icons" }, "app": { "root": ".", "css": "src/styles/global.css", "metroConfig": "metro.config.js", "uniwindTypes": "src/uniwind-types.d.ts" } } ``` The `$schema` URL is generated from the same definitions the CLI validates against, so editor completions cannot describe a shape the CLI would then reject. ## `paths` and `aliases` are not the same thing [#paths-and-aliases-are-not-the-same-thing] This is the one part of the file worth understanding. They answer two different questions: * **`paths`** — where a file lands on disk, relative to `native-components.json`. * **`aliases`** — what other files import it as. They coincide only when the project has path aliases configured. Many Expo apps do not, and Metro resolves `tsconfig` paths only with `experiments.tsconfigPaths` turned on. Keeping them separate means two things: `add` never has to parse a `tsconfig` to work out where anything went, and a project with no aliases at all still gets imports that resolve. Leave a namespace out and imports into it are written as **relative paths**, computed from where the files actually landed. Delete `aliases` entirely and every import is relative. ```tsx // aliases.ui set import { Icon } from "@/components/ui/icon"; // aliases.ui absent import { Icon } from "../icon"; ``` `init` fills `aliases` in by reading a wildcard mapping like `"@/*": ["./src/*"]` out of `tsconfig.json`. It never writes to that file. ### The five namespaces [#the-five-namespaces] ## `package` [#package] ```jsonc { "package": { "name": "@acme/ui" } } ``` Present only when the components live in a shared package, and written by `init` when you name one. Its presence is what makes `add` maintain the package's `exports` map and record native modules as peers — see [Monorepos](/docs/native/cli/monorepo). ## `app` [#app] Which Expo app to wire up. Every path here is relative to `app.root`, and `app.root` itself is relative to `native-components.json`. This block exists because the components and the app are not always the same package. Metro, the Tailwind entry and the native dependencies always belong to the **app**, even when the components live in a shared package — see [Monorepos](/docs/native/cli/monorepo). ## `registry` [#registry] ```json { "registry": { "ref": "v0.2.0" } } ``` Covered in [The registry](/docs/native/cli/registry). ## The rest [#the-rest] ## Changing it later [#changing-it-later] Edit the file and re-run `add` for anything already copied — the CLI writes to whatever `paths` says now, so old files stay where they were. `delacour info` prints the resolved absolute paths, which is the quickest way to check a change landed the way you meant. # Overview (/docs/native/cli) `delacour` copies the library's source into your repository. You end up owning `button.tsx` — you can read it, edit it, and delete the parts you do not want. There is no package to upgrade and no release cadence to wait on. ```bash bunx delacour@latest init bunx delacour@latest add button ``` That writes real files into your app: ```txt src/ ├── components/ui/button/ button.tsx, button.variants.ts, index.ts, AGENTS.md, … ├── lib/ cn.ts, tv.ts └── styles/ tokens.css, theme.css, base.css, index.css ``` ## Which delivery should you use? [#which-delivery-should-you-use] Both are supported and they hold the same components. The choice is about who owns the code. | | [Install the package](/docs/native/getting-started/installation) | This CLI | | ------------------- | ---------------------------------------------------------------- | -------------------------------- | | Upgrades | `bun update` | `delacour diff`, then decide | | Editing a component | Fork or wrap it | Edit the file | | Import path | `@delacour/native-ui/button` | `@/components/ui/button` | | New components | Arrive with the package | `delacour add` when you want one | | Bundle | Only the subpaths you import | Only the files you added | The components are identical — the CLI copies the same source the package ships. Running `delacour add button` in a project that already installs `@delacour/native-ui` gives you a local copy to edit; delete the package import and point at the new path. ## Why this is not just shadcn for React Native [#why-this-is-not-just-shadcn-for-react-native] The copy-in model is shadcn's. What is different is everything Expo makes different, and each of these is a failure that produces **no error at all** when it goes wrong: `bun add react-native-reanimated` fetches the newest release, which on any older SDK does not compile. The CLI asks the SDK what it supports. `withUniwindConfig` installs the transform that compiles `className`. A wrapper applied after it can replace the transformer and leave every component unstyled. Tailwind's scanner does not follow the symlink a monorepo creates, and a glob that misses your components strips their classes from the release build. New Architecture, `tsconfigPaths`, the gesture root, duplicate native modules. Nine checks, each with the fix. ## What `init` will and will not touch [#what-init-will-and-will-not-touch] ### Writes `native-components.json` [#writes-native-componentsjson] Where components go, and how they should import each other. See [Configuration](/docs/native/cli/configuration). ### Wires Metro and your Tailwind entry [#wires-metro-and-your-tailwind-entry] `metro.config.js` gets the Uniwind wrapper; `global.css` gets an `@import` and the `@source` globs, inside a marked block it is safe to re-run over. ### Copies the theme [#copies-the-theme] The `styles` item — tokens, palette, and the Uniwind type shim — so a fresh project renders before you have chosen a single component. ### Prints what it will not do [#prints-what-it-will-not-do] `tsconfig.json` and `app.config.ts` are read, never written. Three things need a human, and `delacour doctor` re-checks every one of them. ## Next [#next] # Setting up a project (/docs/native/cli/init) ```bash bunx delacour@latest init ``` Two questions, both skippable with `--defaults`: where the components should live, and what your source directory is called. Everything else is read off the project — the package manager from the lockfile, the Expo SDK from `package.json`, the path aliases from `tsconfig.json`. You can add components in the same run: ```bash bunx delacour@latest init button input field ``` ## What it changes [#what-it-changes] ### `native-components.json` [#native-componentsjson] Written next to your components. In a monorepo that is the package you ran it from — see [Monorepos](/docs/native/cli/monorepo). ### `metro.config.js` [#metroconfigjs] Wrapped with Uniwind's config, **outermost**: ```js title="metro.config.js" const { withUniwindConfig } = require("uniwind/metro"); const { getDefaultConfig } = require("expo/metro-config"); const config = getDefaultConfig(__dirname); module.exports = withUniwindConfig(config, { cssEntryFile: "./src/styles/global.css", dtsFile: "./src/uniwind-types.d.ts", }); ``` If there is no Metro config, one is written. If the export is not a shape the CLI can rewrite safely, it prints the snippet and asks you to add it rather than guessing at an unfamiliar file. `withUniwindConfig` installs the transformer that compiles `className` into styles. A wrapper applied **after** it can replace `config.transformer`, and every component then renders unstyled with nothing logged. `delacour doctor` checks the ordering, not just the presence. ### Your Tailwind entry [#your-tailwind-entry] An `@import` for the theme and one `@source` per directory holding copied code, inside a marked block: ```css title="src/styles/global.css" @import "tailwindcss"; @import "uniwind"; @source "../**/*.{ts,tsx}"; /* delacour:start — managed by `delacour init`, edit outside this block */ @import "./index.css"; @source "../components/ui"; @source "../hooks"; @source "../lib"; /* delacour:end */ ``` Anything you write outside the markers is left alone, and re-running `init` replaces the block rather than appending a second one. ### The theme [#the-theme] `init` adds the `styles` item — `tokens.css`, `theme.css`, `base.css` and the `uniwind-env.d.ts` type shim — and installs `tailwindcss` and `uniwind`. ## What it does not change [#what-it-does-not-change] `tsconfig.json` and `app.config.ts` are **read, never written**. For `tsconfig.json` that is a deliberate trade. The CLI looks for a wildcard alias like `"@/*": ["./src/*"]` and, if it finds one, records the matching alias per directory. If it finds none, imports are written as relative paths instead — which Metro resolves whether or not `experiments.tsconfigPaths` is on. Creating an alias you did not ask for is a larger change than the imports it saves, and in a monorepo the same alias often already means something else. ## The three things left for you [#the-three-things-left-for-you] `init` prints these, and `delacour doctor` checks all of them afterwards. ### Import the CSS entry [#import-the-css-entry] ```tsx title="app/_layout.tsx" import "@/styles/global.css"; ``` First statement of the root layout. `withUniwindConfig`'s `cssEntryFile` names the file for the transformer; it does not put it in the bundle. Skip this and the app builds, boots and renders every component **completely unstyled**, with nothing logged. ### Mount the provider [#mount-the-provider] Every pressable in the library is a Gesture Handler detector, and a detector outside a `GestureHandlerRootView` never receives a touch. Nothing throws — the button just does not respond. ```tsx title="app/_layout.tsx" import "@/styles/global.css"; import { DelacourProvider } from "@/components/ui/provider"; export default function RootLayout() { return {children}; } ``` `delacour add provider` gets you that component; see [Provider](/docs/native/getting-started/provider) for what each of its layers is doing. ### Turn on `tsconfigPaths` — only if you use aliases [#turn-on-tsconfigpaths--only-if-you-use-aliases] ```ts title="app.config.ts" experiments: { tsconfigPaths: true, } ``` Metro does not read `tsconfig` path mappings without it, so `@/components/ui/button` fails to resolve. If `native-components.json` has an empty `aliases` object, your imports are relative and this does not apply. ### Rebuild after a native module lands [#rebuild-after-a-native-module-lands] `add` tells you when it installed one. A JavaScript reload will not pick it up — the module is not in the binary yet. ```bash npx expo run:ios # or run:android ``` ## Verify [#verify] ```bash bunx delacour add button bunx delacour doctor ``` ```tsx import { Button } from "@/components/ui/button"; ; ``` A string child is wrapped in a `Button.Label` for you. If the button renders but does not respond, the provider is missing above it. # MCP server (/docs/native/cli/mcp) ```bash delacour mcp ``` An MCP server over stdio, exposing the same functions the commands call. Ask an agent for "a button" without it and you get plausible JSX — the right shape, and none of the parts that matter here: the icon that inherits its size from the button's context, the spinner that *replaces* the icon so the label does not shift, the `expo install` route for the native modules underneath. With it, the agent reads the real component and copies it. ## Wiring it up [#wiring-it-up] ```bash claude mcp add delacour -- bunx delacour@latest mcp ``` ```json title=".cursor/mcp.json" { "mcpServers": { "delacour": { "command": "bunx", "args": ["delacour@latest", "mcp"] } } } ``` ```json { "command": "bunx", "args": ["delacour@latest", "mcp", "--cwd", "/path/to/your/app"] } ``` `--cwd` decides which `native-components.json` applies. Omit it when the client already runs the server from your project root. `--registry` and `--ref` work here too, so an agent can be pointed at a fork or pinned to a version. ## The tools [#the-tools] `check_project` is the one worth pointing an agent at explicitly. When a component renders unstyled or a press does nothing, there is no error message to read — the agent will otherwise start rewriting working code. Those failures are [configuration](/docs/native/cli/troubleshooting), and this names them. ## Copying beats generating [#copying-beats-generating] `get_component` returns the component's `AGENTS.md` alongside its source, rewritten to cite your own import paths. That document is where the reasoning lives — why `isLoading` swaps the icon rather than joining it, why a `View` cannot cascade colour to a `Text`, why the separator stretches instead of taking a percentage width. An agent that has read it edits the component correctly. An agent that has not writes something that renders and is subtly wrong. `add` writes each component's `AGENTS.md` next to its source, so the notes are in your repository whether or not the MCP server is running. That is a real advantage of owning the code: the design rules travel with it. ## Without MCP [#without-mcp] Every tool has a command behind it, so an agent with shell access needs nothing installed: ```bash delacour list --json delacour view button --json delacour add button --yes delacour doctor --json ``` See also [`llms.txt`](/docs/native/getting-started/llms) for handing an agent the whole library as context, and [Agents](/docs/native/getting-started/agents) for the conventions it should follow when editing these components. # Monorepos (/docs/native/cli/monorepo) Components can live in the app that uses them, or in a package several apps share. The CLI decides from where you run it. ```bash cd apps/mobile && bunx delacour init # into the app cd packages/ui && bunx delacour init # into a shared package ``` Running inside a package that is not the app **is** the answer — there is no prompt and no flag. From the repository root, `init` asks. ## What `init` creates [#what-init-creates] In the shared-package layout it writes a real, importable workspace package — not just a directory with components in it: ```jsonc title="packages/ui/package.json" { "name": "@acme/ui", "type": "module", "private": true, "exports": { "./button": "./src/components/ui/button/index.ts", "./lib/cn": "./src/lib/cn.ts", "./styles": "./src/styles/index.css" }, "peerDependencies": { "react-native-reanimated": "*", "tailwind-variants": "*" } } ``` Then the app imports it by name, and `init` adds `"@acme/ui": "workspace:*"` to the app so the package manager links it: ```tsx import { Button } from "@acme/ui/button"; ``` `@acme/ui` on its own does not resolve, deliberately. A barrel would make every app pull in every component — and every optional native peer with it. `add` regenerates the map from what is on disk, so it cannot drift from the components you actually have. Native modules are recorded as **peer** dependencies of the package and installed into the **app**. The app owns the versions, because `expo install` pins them against the SDK, and one copy of each native module is the difference between working and a crash on the first press. ## What changes between the two [#what-changes-between-the-two] `native-components.json` lives wherever the components live. What moves is `app.root`, and with it every file the CLI has to touch but does not own. ```json title="apps/mobile/native-components.json" { "paths": { "ui": "src/components/ui", "lib": "src/lib" }, "app": { "root": ".", "css": "src/styles/global.css" } } ``` ```txt apps/mobile/ ├── native-components.json ├── metro.config.js └── src/ ├── components/ui/button/ ├── lib/ └── styles/global.css ``` ```json title="packages/ui/native-components.json" { "paths": { "ui": "src/components/ui", "lib": "src/lib" }, "package": { "name": "@acme/ui" }, "app": { "root": "../../apps/mobile", "css": "src/styles/global.css" } } ``` The `package` block is the whole signal for this layout: its presence is what makes `add` maintain the exports map and record peers rather than dependencies. ```txt packages/ui/ ├── native-components.json └── src/components/ui/button/ apps/mobile/ ├── metro.config.js ← still wired here └── src/styles/global.css ← and here ``` Metro, the Tailwind entry and the native dependencies are the app's, whatever package the components sit in. `init` finds the Expo app — up from where you ran it, then across `apps/` — and wires that one. `expo install` runs there too: two copies of a native module register twice and break at runtime. ## The symlink that costs you every style [#the-symlink-that-costs-you-every-style] This is the failure worth reading twice, because nothing reports it and it only shows up in a release build. Tailwind compiles the classes it finds by scanning source text. Your package manager links a workspace package into `node_modules`, and **Tailwind's scanner does not follow symlinks**. A `@source` pointing at the linked copy therefore contributes nothing at all: no error, no warning, and every class your components use is dropped from the build. ```css title="apps/mobile/src/styles/global.css" /* Silently scans nothing. */ @source "../../node_modules/@acme/ui/src"; /* The real directory. This is what init writes. */ @source "../../../../packages/ui/src/components/ui"; ``` `init` computes those globs from where the files actually landed on disk, so the real path is what comes out. `delacour doctor` re-checks the coverage afterwards, resolving symlinks as it goes. ## One copy of each native module [#one-copy-of-each-native-module] A monorepo invites a second copy: the package manager can materialise `react-native-reanimated` under the app while the workspace root holds another. Two registrations of a native module break at runtime. `init` writes this into the app's `metro.config.js` for you in the shared-package layout, and `delacour doctor` checks it: ```js title="apps/mobile/metro.config.js" config.resolver.extraNodeModules = { "react-native": path.resolve(workspaceRoot, "node_modules/react-native"), "react-native-reanimated": path.resolve(workspaceRoot, "node_modules/react-native-reanimated"), "react-native-gesture-handler": path.resolve(workspaceRoot, "node_modules/react-native-gesture-handler"), }; ``` Keep `withUniwindConfig` the outermost wrapper around whatever else you add — `init` places the resolver block before it, and `doctor` checks the ordering. Add a `bunfig.toml` at the workspace root with `linker = "hoisted"`. Bun's default isolated layout hides packages under `node_modules/.bun/…`, which Metro cannot follow and which makes TypeScript resolve the package's copy of React Native to a different realpath than the app's — every `ref` in the library then fails to typecheck, naming neither cause. ```toml title="bunfig.toml" [install] linker = "hoisted" ``` ## Several apps, one package [#several-apps-one-package] Point each app's Tailwind entry at the package and give each its own Metro wrapper. `native-components.json` names one app in `app.root` — the one `init` and `doctor` work against — so run `doctor` from each app's directory to check the others: ```bash bunx delacour doctor --cwd apps/mobile bunx delacour doctor --cwd apps/tablet ``` `--cwd apps/tablet` finds the nearest `native-components.json` walking upwards. If your second app has no config of its own, it resolves the shared package's — which names the *first* app in `app.root`. Give each app a `native-components.json` when they diverge. ## Sharing without a package [#sharing-without-a-package] You do not need a workspace package at all. Point two apps at the same directory with `paths` containing `../`, and both get the same files with no publishing step: ```json title="apps/mobile/native-components.json" { "paths": { "ui": "../../shared/components/ui", "lib": "../../shared/lib" }, "app": { "root": "." } } ``` `aliases` will be empty unless a `tsconfig` mapping covers that directory, so the imports come out relative — which resolves without `experiments.tsconfigPaths`. # The registry (/docs/native/cli/registry) The registry is committed to the repository and read straight off `raw.githubusercontent.com`. There is no service to host and nothing to keep running — and nothing to be down when you run `add`. ```txt registry/ ├── registry.json the index: names, descriptions, dependencies ├── r/button.json one item: what it is, what it needs, which files ├── files/ui/button/button.tsx the files themselves └── config.schema.json the JSON Schema behind native-components.json's $schema ``` An item **references** its files rather than carrying them. shadcn inlines each file as a string inside the item JSON; the cost of that is a megabyte of escaped source that no reviewer can read and `git` cannot diff. Written out, `files/ui/button/button.tsx` is the TypeScript it looks like — you can open it on GitHub and see exactly what `add` is about to copy. `add button` resolves the graph from the index, fetches the handful of items it needs, then their files. Everything fetched is cached under your cache directory and revalidated with an ETag, so a repeated `add` costs a 304 and an `add` on a flaky connection still works from what is already there. The ref pins the whole tree at once, so an item and its files can never disagree. ## An item [#an-item] ```json title="registry/r/button.json" { "name": "button", "type": "registry:ui", "title": "Button", "description": "A pressable action composed from parts, with variants, sizes and a loading state.", "registryDependencies": ["icon", "pressable", "spinner", "text", "tv"], "dependencies": ["tailwind-variants"], "expoDependencies": [], "devDependencies": [], "files": [{ "path": "files/ui/button/button.tsx", "target": "button/button.tsx", "namespace": "ui" }] } ``` `path` is where the file lives in the registry; `namespace` and `target` are where it lands in your project. A file that inlines `content` is rejected rather than ignored, so a shadcn-shaped item fails with a message rather than a 404 halfway through a copy. The dependency list is split three ways on purpose, and it is the split that decides whether an Expo build compiles: Expo pins every native module to a version its SDK can build. `bun add react-native-reanimated` fetches the newest release instead, which on any older SDK is a package that fails at the linker rather than at install time. A single `dependencies` field cannot express that difference. Each item also carries the component's `AGENTS.md`, rewritten to cite your paths rather than the package's — so an agent working in your repository gets the design rules next to the code. ## Pinning a version [#pinning-a-version] The CLI is published with the git ref it was built from baked in, so a given version always reads the registry it shipped against. Override it two ways, in this order: ```bash bunx delacour add tabs --ref main # 1. the flag ``` ```json title="native-components.json" { "registry": { "ref": "v0.2.0" } } // 2. the config ``` `--ref main` is how you take a component that has landed since your CLI was published. `bunx delacour@latest` gets you both the newest CLI and its matching registry. ## Reading from somewhere else [#reading-from-somewhere-else] `--registry` accepts three shapes, and so does `registry.url` in the config. ```bash bunx delacour add button --registry github:acme/ui --ref main ``` Expands to `raw.githubusercontent.com/acme/ui//registry`. A ref in the shorthand (`github:acme/ui#next`) wins over `--ref`. ```bash bunx delacour add card --registry https://ui.acme.com/r ``` Used exactly as given — a hosted registry owns its own layout. It must serve `registry.json`, `r/.json` and every `path` those items name, at that base. ```bash bunx delacour add button --registry ./registry ``` Reads from disk, skipping the network and the cache entirely. This is how the CLI's own tests run, and it is the fastest way to try a change to a component before publishing it. Everything fetched is validated before a byte is written — a `target` that tries to escape its namespace is rejected rather than followed. That guard is real, but it is not a substitute for trusting the registry you point at. ## Building your own [#building-your-own] The registry is **derived** from the library's source rather than maintained beside it: one item per component folder, dependencies read off the imports, and imports rewritten into portable placeholders. There is no hand-written index to drift. ```bash bun --filter delacour run registry:build ``` The build refuses to guess. A component folder with no metadata fails it, and so does an npm import nobody has classified as `expo install` or plain — because the default would be the one that installs a native module at a version the SDK cannot build. Imports are canonicalised at build time and substituted at `add` time: ```tsx // in the library import { Icon } from "../icon"; import { cn } from "../../lib/cn"; // in the registry import { Icon } from "@registry/ui/icon"; import { cn } from "@registry/lib/cn"; // in your project import { Icon } from "@/components/ui/icon"; import { cn } from "@/lib/cn"; ``` That is why `add` needs no TypeScript parser: the parsing happened once, in the registry build, against source we control. An import that lands in the same directory is left alone, so a copied file reads the way the original does. ## Verifying a registry [#verifying-a-registry] ```bash bun --filter delacour run verify:expo ``` Scaffolds a real Expo app in a temp directory, installs Uniwind and Tailwind, runs `init`, adds **every** item, and then runs `tsc --noEmit` over the result. A clean typecheck is the proof that every copied component resolves every import it makes — which no fixture can establish, because a fixture has no dependency tree to resolve against. It also asserts what a build alone cannot: ```bash bun --filter delacour run verify:expo --no-install # structure only, offline bun --filter delacour run verify:expo --bundle # ...and compile it with Metro bun --filter delacour run verify:expo --simulator # ...and render it on a device bun --filter delacour run verify:expo --only button # one component and its closure ``` Uniwind compiles `className` in a **Metro transform**, so a class the scanner never saw is dropped from the build with nothing logged — and `tsc` cannot see that at all. `--bundle` runs `expo export`, which is the whole pipeline short of the native build, and is the cheapest stage that catches it. `--simulator` goes one further and builds a dev client, because Reanimated, Gesture Handler and the keyboard controller are native modules Expo Go cannot load. CI runs the offline pass on every push and the full pass in its own job. If you maintain a fork or your own registry, point it at yours with `--registry`. # Troubleshooting (/docs/native/cli/troubleshooting) ```bash bunx delacour doctor ``` ```txt ✓ Expo SDK ~57.0.15, react-native 0.86.2 ✓ New Architecture enabled ✓ Uniwind uniwind and tailwindcss installed ✓ Metro wrapped, outermost, pointing at the configured entry ✗ Tailwind sources not scanned: src/components/ui → Classes in those files are dropped from release builds. ! Gesture Handler no GestureHandlerRootView found → Wrap your root layout in . ``` Every check here exists because the thing it catches produces **no error**. A React Native app with any of these wrong builds fine, boots fine, and is quietly wrong — which is why this command exists at all and shadcn has no equivalent. `--fast` skips the two checks that shell out to the Expo CLI. `--json` prints the results for CI; the exit code is non-zero when a check fails. ## Symptom → cause [#symptom--cause] ### My components render, but with no styles [#my-components-render-but-with-no-styles] Three causes, in the order worth checking. **Nothing imports the CSS entry.** `withUniwindConfig`'s `cssEntryFile` tells the transformer which file to compile — it does not put that file in the bundle. If no module imports it, the app builds, boots and renders every component completely unstyled, with nothing logged. ```tsx title="app/_layout.tsx" import "@/styles/global.css"; ``` First statement, in the root layout. `delacour doctor` checks this, and is careful to ignore a doc comment that merely names the import. The signature is unmistakable once you have seen it: no padding, no gaps, no colours, and spinners drawn at their natural SVG size rather than the icon scale. Every component is *mounting* — it is only the classes that never arrived. **Tailwind's `@source` coverage**, and usually only in a release build. Tailwind compiles the classes it finds in source text, so a glob that does not reach your components means those classes were never compiled. In a monorepo, the usual cause is a `@source` pointing through `node_modules` — the scanner does not follow symlinks, so it scans nothing. See [Monorepos](/docs/native/cli/monorepo). ```bash bunx delacour doctor # "Tailwind sources — not scanned: …" bunx delacour init -f # rewrites the managed @source block ``` **Metro's wrapper ordering.** If `withUniwindConfig` is present but not the **outermost** wrapper, something applied after it can replace `config.transformer`, and `className` stops compiling entirely. `doctor` checks the ordering, not just the presence. ### Presses do nothing [#presses-do-nothing] No `GestureHandlerRootView` above the component. Every pressable in this library is a Gesture Handler detector, and a detector outside the root never receives a touch — no error, no warning. ```tsx title="app/_layout.tsx" import { DelacourProvider } from "@/components/ui/provider"; export default function RootLayout() { return {children}; } ``` ### `Unable to resolve "@/components/ui/button"` [#unable-to-resolve-componentsuibutton] Metro does not read `tsconfig` path mappings unless you ask it to: ```ts title="app.config.ts" experiments: { tsconfigPaths: true } ``` `doctor` only raises this when `native-components.json` actually has aliases. If it does not, your imports are relative and this is not your problem. ### A red box naming a module I just installed [#a-red-box-naming-a-module-i-just-installed] A native module is not live until the app is rebuilt — a JavaScript reload cannot pick it up, because the module is not in the binary. `add` warns when it installs one. ```bash npx expo run:ios # or run:android ``` ### Something breaks only in a monorepo [#something-breaks-only-in-a-monorepo] Two copies of a native module registering twice. `doctor` warns when it finds `react-native-reanimated` (or friends) under both the app and the workspace root; pin them in Metro's `resolver.extraNodeModules`. ### A worklet error, or Reanimated refusing to build [#a-worklet-error-or-reanimated-refusing-to-build] Reanimated 4 is New Architecture only. `doctor` reads your resolved Expo config and fails if `newArchEnabled` is off. ### `add` overwrote something I had edited [#add-overwrote-something-i-had-edited] It asks first — unless you passed `--overwrite`, or ran non-interactively without it, in which case it refuses and names the files. To see what you had changed before deciding: ```bash bunx delacour diff button ``` ### The registry does not have a component I can see on this site [#the-registry-does-not-have-a-component-i-can-see-on-this-site] Your CLI is pinned to the ref it was published from. Take the newest: ```bash bunx delacour@latest add tabs --ref main ``` See [The registry](/docs/native/cli/registry). ### Every `className` is a type error in a monorepo [#every-classname-is-a-type-error-in-a-monorepo] ``` Type '{ children: Element; className: string; … }' is not assignable to type 'IntrinsicAttributes & Omit>, "ref">' ``` `uniwind-env.d.ts` is one line — `/// ` — and it is what gives a React Native component a `className` prop in TypeScript at all. In a shared package it lands beside the components, **outside the app's `tsconfig` include**, and nothing imports it: a declaration file holding only a triple-slash reference cannot be reached by an import. So the augmentation never loads for the app. `init` writes a copy into the app for you, and `doctor` fails when it is missing. If you are adopting a package by hand, add it yourself: ```ts title="apps/mobile/uniwind-env.d.ts" /// ``` ### `Ref`, or two copies of React Native's types [#refnever-or-two-copies-of-react-natives-types] ``` Type 'Ref | undefined' is not assignable to type 'AnimatedComponentRef' ``` A Bun workspace installing with the default isolated layout. Packages sit under `node_modules/.bun/…` and are linked from each package, so the shared package resolves React Native at a different realpath than the app — TypeScript treats them as different modules and generics collapse to `never`. Metro cannot follow that layout either. ```toml title="bunfig.toml" [install] linker = "hoisted" ``` ### A copied component does not typecheck [#a-copied-component-does-not-typecheck] That is a registry defect rather than something wrong with your project — an import the registry did not account for, or a package it failed to declare. Confirm it against a clean app: ```bash bun --filter delacour run verify:expo --only ``` If that run is clean, the difference is your project: check `delacour info` for where files landed, and `tsconfig.json` for an alias that resolves somewhere unexpected. ## The full check list [#the-full-check-list] ## When doctor is not enough [#when-doctor-is-not-enough] ```bash bunx delacour info ``` Prints the resolved config and everything that was detected — package manager, workspace root, app root, the path aliases found in `tsconfig.json`, and the absolute directory for each namespace. Start there when files are landing somewhere you did not expect. # Accordion (/docs/native/components/accordion) ```tsx import { Accordion } from "@delacour/native-ui/accordion"; ``` ```tsx Shipping Rates and delivery times Free over $50. Two to four working days. ``` **Variants:** `default`, `secondary`, `tertiary`, `transparent` — `ListGroup`'s set, because an accordion is the same kind of thing. **Sizes:** `sm`, `md`, `lg`. ## Anatomy [#anatomy] | Part | What it is | | ----------------------- | ------------------------------------------------------------------------------- | | `Accordion` | The surface. Owns the selection mode and the shared axes | | `Accordion.Item` | One section: a trigger, and the panel it discloses | | `Accordion.Trigger` | The row that opens the item. A `Pressable`, so it inherits the whole vocabulary | | `Accordion.Title` | The trigger's primary line. Bare string children become one automatically | | `Accordion.Description` | The trigger's secondary line, stacked under the title | | `Accordion.Indicator` | The glyph that turns as the panel opens. Composed in when a trigger holds none | | `Accordion.Content` | The measured, clipped panel. Mounts on first expand and stays mounted | Dividers between adjacent items are inserted for you, the way `ListGroup` does it. A `Separator` placed by hand is never doubled; `isDivided={false}` turns the feature off. ## Selection [#selection] The root is a discriminated union on `selectionMode`. ```tsx ``` In `single` mode the value is `string | null` — `null` is a controlled *empty*, not an absence. In `multiple` mode it is `string[]`. With `isCollapsible={false}` the **last** open item is refused. In `multiple` mode an item still closes while another is open. Reading it as "no item may ever close" would make a multiple accordion add-only. A refused tap returns its own input by identity, so it neither re-renders nor reports an `onValueChange` for a change that did not happen. ## The animation [#the-animation] The panel's height, the panel's opacity and the indicator's rotation all read **one** `progress` shared value, so they cannot drift out of step by a frame. The spring is critically damped: an overshoot would draw the panel taller than its content measured, flashing the surface behind it for a frame at the end of every expand. The fade runs ahead of the height (`ACCORDION_CONTENT_FADE`). A panel whose opacity tracked its height linearly would be half transparent at the midpoint of every expand, which reads as content struggling to arrive rather than as a panel opening. `ACCORDION_UNMEASURED` is **negative**, and that is load-bearing. A panel that measured `0` is a real answer — a panel whose content rendered nothing — and treating it as "still waiting" would leave the indicator stuck pointing the wrong way. Only a value no layout can produce can mean *unmeasured*. ## Custom indicators [#custom-indicators] An indicator has to be a **direct** `Accordion.Indicator` child of the trigger — the trigger finds it by element type, so a wrapped one is invisible and a second default gets composed in beside it. Use its render function to swap the glyph on state, and `isAnimated={false}` to opt out of the rotation when the glyph itself carries the change (a plus becoming a minus). ```tsx Details {({ isExpanded }) => } ``` The disabled fade lands on the **item**, never on the trigger. The trigger is a `Pressable` whose `Animated.View` writes `opacity` every frame, so a class there is silently overwritten. An item is expected to hold an `Accordion.Content`. One without a panel has nothing to measure, so its indicator stays put. ## API [#api] # Badge (/docs/native/components/badge) ```tsx import { Badge } from "@delacour/native-ui/badge"; ``` ```tsx Active ``` ## Two axes, not one [#two-axes-not-one] `variant` says how the surface is painted — `solid`, `soft`, `outline`, `ghost`. `color` says what it means — `default`, `primary`, `success`, `warning`, `danger`, `info`. **Sizes:** `sm`, `md`, `lg`. `Button` collapses the two into a single enum and a badge deliberately does not: six semantic colours are the point of this component rather than an afterthought, and one axis would need thirteen names to say what two say with ten. Neither axis paints a surface alone, so all twenty-four pairings live in `compoundVariants`. A test asserts every cell is distinct — two cells collapsing means a caller can set an axis and see nothing change. ## Content until given something to do [#content-until-given-something-to-do] With no `onPress` and no `onLongPress` the root is a plain `View`. Mounting a `GestureDetector` regardless would put one under every tag in a list of fifty and announce each of them to assistive technology as a button with no action. Supply either handler and the root becomes a `Pressable`, inheriting `feedback`, `haptic` and the rest; only the default differs — `scale`. ## Dismissing [#dismissing] ```tsx Design ``` `onClose` is its own pressable — a `Badge.CloseButton` composed in at the end — so its tap is claimed by the inner detector and never also fires the badge's `onPress`. It presses with `fade` rather than the root's `scale`: a spring on a glyph that small reads as a jitter. Reach for the part by hand only to place it somewhere other than last. ## A size is padding, never a height [#a-size-is-padding-never-a-height] `Text` respects OS font scaling, so a fixed height clips the label at a large accessibility step instead of growing with it. A test asserts the root carries no `h-*` at any size. `self-start` is load-bearing. A badge is sized by its content, and inside a gap column every child is stretch-aligned by default — without it a one-word badge spans the whole screen. The border is reserved on every variant, transparent until `outline` colours it — declaring it only where it shows would make the badge two points wider the moment a caller switched variant. ## Icons and text inherit [#icons-and-text-inherit] The root wraps its subtree in an `IconDefaultsProvider` and a `TextClassProvider`, so a bare `` or `` inside a badge comes out at the right size and colour with nothing said at the call site. String children are wrapped in a `Badge.Label` automatically. ## API [#api] A badge is a `Pressable`, so `feedback`, `haptic`, `onPress` and the rest are inherited rather than restated — see [Pressable](/docs/native/components/pressable). `asChild`, `busy` and `disabled` are withheld: use `isDisabled`, which the parts read through context. ### `Badge.Label` [#badgelabel] The badge's text. Picks its colour and type scale from the badge's variant, colour and size. Extends React Native's `TextProps`. ### `Badge.CloseButton` [#badgeclosebutton] The dismiss pressable. Defaults its `accessibilityLabel` to `"Remove"` and its `feedback` to `fade` — a badge that scaled on the close button's press would flex the whole chip. Extends `PressableProps`. ### `Badge.StartContent` / `Badge.EndContent` [#badgestartcontent--badgeendcontent] Centred wrappers for leading and trailing content that is not an `Icon`. An `Icon` needs neither — it inherits the badge's icon size and its colour from the subtree. # BottomSheet (/docs/native/components/bottom-sheet) ```tsx import { BottomSheet } from "@delacour/native-ui/bottom-sheet"; ``` ```tsx Filters Narrow the list down. ``` There is no size axis and no colour axis: a sheet has one shape and one surface. ## Anatomy [#anatomy] | Part | What it is | | ------------------------- | -------------------------------------------------------------------------- | | `BottomSheet` | State only — the root renders no view at all | | `BottomSheet.Trigger` | The control that opens the sheet. `asChild` to make a `Button` the trigger | | `BottomSheet.Portal` | Everything drawn above the app. Lifts the overlay out to the container | | `BottomSheet.Overlay` | The scrim. Written beside the container, drawn as its backdrop | | `BottomSheet.Container` | The sheet itself. Every gorhom prop passes through it | | `BottomSheet.Content` | The sheet's body, sized to itself under `enableDynamicSizing` | | `BottomSheet.ScrollView` | A scrolling body. Needs `enableDynamicSizing={false}` and snap points | | `BottomSheet.Close` | The dismiss control, positioned out of the content's flow | | `BottomSheet.Title` | The sheet's heading — a `Text.Header` with the close control's clearance | | `BottomSheet.Description` | Supporting copy under the title | | `BottomSheet.Footer` | Controls at the bottom. `sticky` pins them above the keyboard | That is the component. `Portal` hoists `Overlay` into gorhom's `backdropComponent`, and `Container` hoists a sticky `Footer` into its `footerComponent`. Writing them in the tree is what keeps the anatomy readable; hoisting them is what makes gorhom render them correctly. ## One callback for every path [#one-callback-for-every-path] `onOpenChange` fires whenever the sheet opens or closes — a trigger, a swipe down, a press on the backdrop, `BottomSheet.Close`, or a controlled `isOpen`. Deliberately one callback: gorhom's own `onClose` fires for the gesture alone, which is the shape that makes a caller wire three handlers and still miss one. ## Text input inside a sheet [#text-input-inside-a-sheet] Use `useBottomSheetInput()` and spread what it returns onto the field. The `ref` is not optional polish. ```tsx import { useBottomSheetInput } from "@delacour/native-ui/bottom-sheet"; const input = useBottomSheetInput(); ; ``` The scrim is also rebuilt for this reason: gorhom's full-screen backdrop mounts a `Gesture.Tap()` that steals a `TextInput`'s tap. `BottomSheet.Overlay` passes `pressBehavior="none"` and lays a `Pressable` only over the band **above** the sheet, sized from its animated position. That covers the keyboard half. A *drag* across a field can still be claimed by the sheet's content pan — pass `enableContentPanningGesture={false}` on the container when that bites. Do not put a `KeyboardStickyView` inside a sheet. It is a third mechanism moving the same view, and it fights the other two every frame. ## Accessibility [#accessibility] `BottomSheet.Content` sets `accessible={false}` by default, and it is the least obvious line in the component. gorhom marks its content container `accessible`; on iOS an `accessible` container collapses its whole subtree into **one** element, so every field, button and line of copy inside becomes unreachable to VoiceOver. ## Gotchas [#gotchas] Use `backgroundClassName`. The container's own `style` is forwarded to gorhom, which passes `[StyleSheet.absoluteFill, backgroundStyle]` — dropping it renders the sheet transparent over the app. `BottomSheet.ScrollView` requires `enableDynamicSizing={false}` and explicit `snapPoints` on the container. It also puts its classes on an inner `View` rather than gorhom's content container: Uniwind compiles `contentContainerClassName` into an array, gorhom reads `paddingBottom` off a flattened style, and the last row would hide behind a pinned footer. ## Requirements [#requirements] `@gorhom/bottom-sheet` is a **required** peer, and the app must mount [`DelacourProvider`](/docs/native/components/provider) — it supplies the modal provider the sheet presents into. ## API [#api] The root takes no `ViewProps` — it renders no view. Layout and styling belong on `Container`, `Content` and `Footer`. ### `BottomSheet.Trigger` [#bottomsheettrigger] Opens the sheet. A `Pressable`, so it inherits the whole vocabulary — `asChild` renders into your own element instead of emitting a `View`. ### `BottomSheet.Container` [#bottomsheetcontainer] The modal itself. Extends `@gorhom/bottom-sheet`'s `BottomSheetModalProps` — `snapPoints`, `enableDynamicSizing`, `index` and the rest pass straight through. The component slots (`backdropComponent`, `backgroundComponent`, `footerComponent`, `handleComponent`) and `onDismiss` are withheld: the compound parts fill them. ### `BottomSheet.Content` [#bottomsheetcontent] The padded region inside the sheet. Extends `ViewProps`. ### `BottomSheet.ScrollView` [#bottomsheetscrollview] Scrollable content that hands its gesture to the sheet at the top of the scroll. Extends Gorhom's `BottomSheetScrollViewProps`; `contentContainerStyle` and `enableFooterMarginAdjustment` are withheld — the first has a `className` equivalent, the second is managed for you. ### `BottomSheet.Footer` [#bottomsheetfooter] Pinned below the content. ### `BottomSheet.Overlay` [#bottomsheetoverlay] The backdrop. Composed in with sensible defaults when the sheet holds none. ### `BottomSheet.Title` / `Description` / `Close` / `Handle` / `Background` / `Portal` [#bottomsheettitle--description--close--handle--background--portal] `Title` and `Description` are `Text` presets and take its props. `Close` is a `Pressable` that dismisses the sheet, defaulting its `accessibilityLabel` to `"Close"` and its `feedback` to `fade`. `Handle` and `Background` replace the grabber and the surface outright — reach for `handleClassName` and `backgroundClassName` on `Container` first. `Portal` takes only `children`. # Button (/docs/native/components/button) ```tsx import { Button } from "@delacour/native-ui/button"; ``` ```tsx ``` A string child is wrapped in a `Button.Label` automatically. React Native crashes on bare text outside a ``, so every component here that takes free-form children does the same. ## Variants [#variants] ```tsx ``` The strongest action on the screen. One per view. ```tsx ``` ```tsx ``` ```tsx ``` ```tsx ``` ```tsx ``` ```tsx ``` A destructive action that is not the primary one on the screen. ## Sizes [#sizes] ```tsx ``` A size is one axis driving four values: the height (`--spacing-button-*`), the label's type scale (`--text-button-*`), the icon step (`--spacing-icon-*`) and the horizontal padding. ## Icons [#icons] Icons are **composed, never passed as props**. Put an `Icon` in the children, before or after the label. ```tsx import { Icon } from "@delacour/native-ui/icon"; import { IconArrowRight, IconTrash } from "@delacour/native-ui/icons/central"; ``` The button wraps its subtree in an `IconDefaultsProvider` carrying the size and the variant's foreground token, so a bare `` comes out right with nothing said at the call site. An explicit `size` or `color` on the icon still wins. `Button.StartContent` and `Button.EndContent` are for wrapping leading or trailing content that is **not** an `Icon`. ### Icon-only [#icon-only] ```tsx ``` Always pair `isIconOnly` with an `accessibilityLabel`. There is no text for a screen reader to fall back on. ## Loading [#loading] ```tsx ``` `isLoading` composes a `Spinner` in and blocks presses. **The spinner replaces the icon, it does not join it.** It takes the place of the composed `Icon` on the side `spinnerPlacement` names — the first at `start`, the last at `end` — so the label does not shift when work begins and shift back when it ends. The swap costs no layout because both glyphs are drawn at the button's own `size-icon-*` token. Only a bare `Icon` is swapped; a `Button.StartContent` wraps content of unknown height, and replacing one could resize the button. ### Placement [#placement] ```tsx ``` `only` drops the children and centres the spinner in the footprint the button already has, carrying the label onto `accessibilityLabel` so a screen reader still has a name to read. It does not square the button on its own — pair it with `isIconOnly` when a square is what you want. `isLoading` blocks the press and announces the button as *busy*, but keeps full contrast. The spinner already says the press landed, and dimming reads as "this control is unavailable". Pass `isDimmedWhileLoading` to opt into the faded treatment. A stretched button keeps its width while loading, because `only` keeps its footprint. Inside a `flex-row` it still shrinks to the spinner, and that snap is un-animated on purpose: `Pressable`'s `Animated.View` already runs a `useAnimatedStyle` on `opacity` and `transform`, and a native layout transition on the same view fights it for prop ownership. Pin the width (`w-full`, `min-w-*`) if you need it stable. ## Press feedback and haptics [#press-feedback-and-haptics] A button **is** a `Pressable`. `feedback`, `haptic`, `pressedScale` and the rest are inherited rather than restated; only the default differs — `scale`. ```tsx ``` Do not add ripple, ink, glow or highlight overlays. There are no wash layers on pressables in this library. ## Reading the button's state [#reading-the-buttons-state] ```tsx import { useButton } from "@delacour/native-ui/button"; function ButtonCount({ count }: { count: number }) { const { variant, size, isDisabled } = useButton(); // … } ``` ## Anatomy [#anatomy] ## API [#api] ### `ButtonProps` [#buttonprops] Extends `PressableProps`, minus `busy`, `children`, `disabled`, `pressedOpacity` and `pressedScale`. ### `ButtonLabelProps` [#buttonlabelprops] React Native's `TextProps`, plus `className`. ### `useButton()` [#usebutton] ## Exported constants [#exported-constants] ```tsx import { BUTTON_VARIANTS, BUTTON_SIZES, BUTTON_SPINNER_PLACEMENTS, BUTTON_FOREGROUND_TOKEN, buttonVariants, } from "@delacour/native-ui/button"; ``` `BUTTON_FOREGROUND_TOKEN` maps each variant to the theme token its icons and text are drawn in. `buttonVariants` is the `tv()` slot set — useful when building a component that has to match a button exactly. # Checkbox (/docs/native/components/checkbox) ```tsx import { Checkbox } from "@delacour/native-ui/checkbox"; ``` ```tsx Accept the terms ``` **Colours:** `default`, `primary`, `success`, `warning`, `danger`, `info`. **Sizes:** `sm`, `md`, `lg`. **Alignment:** `start`, `end`. There is no `variant` axis: a checkbox has one shape, and a second way to paint it would be a second thing to keep in step with the radio beside it. ## The root draws the box [#the-root-draws-the-box] `` on its own is a complete control. Anything composed inside lands *beside* the box and shares its tap target — which is exactly why `Checkbox.Label` exists next to `Field.Label`. `Field.Label` names a control from a row away; this one is inside the pressable, so tapping the words toggles the box. Use `Field.Label` in a horizontal `Field`, and `Checkbox.Label` everywhere else. ## Groups [#groups] ```tsx Email Push ``` The group's state is one array of the children's `value`s. A grouped checkbox with no `value` throws by name — group membership is invisible in the child's props at compile time, so it cannot be a type error. The group is a plain `View` with no role. Lay the children out any other way with a `className` — `flex-row flex-wrap` for a row. ### Precedence: `own ?? group ?? field ?? default` [#precedence-own--group--field--default] Deliberately not `Input.Group`'s order. `Checkbox.Group` owns no box — it is a state controller that also carries shared defaults, which makes it the same kind of thing as `Field`. "Make the group `lg`" and "make this one danger" are different questions and both get an answer. ## Indeterminate [#indeterminate] `isIndeterminate` paints the surface and swaps the glyph, and reports `checked="mixed"` — so a "select all" row says what it means rather than claiming a half-truth. `isFilled`, not `isChecked`, is the `tv` axis: checked and indeterminate both paint the surface and only the glyph tells them apart. ## Colour paints the indicator, not the box [#colour-paints-the-indicator-not-the-box] An unticked box is `border-input bg-card` at every colour — the same chrome a field wears, because it is the same kind of thing. Only the border has to know both states. **Invalid outranks the colour**, on the border and the fill, ticked or not. ## The animation [#the-animation] Three gestures off one shared value, so they cannot drift: 1. **The fill** fades and scales **from the centre**. A box is filled, not slid into — there is no edge a checkbox is filled *from*, so a `translateX` here would read as a panel arriving. 2. **The tick** sits behind a container whose width opens from the box's left edge, so the stroke is drawn on when ticking and taken back when unticking. It is held by `tickDelay` until the surface it is drawn on is most of the way there. 3. **The border** comes last, held by `borderDelay` until the surface is near the edge, so it reads as the fill *arriving* at the border rather than as an outline changing on its own. Reduce-motion takes Reanimated's default `System` policy here, unlike `Spinner`: the state change is the point and the travel is decoration. ## Press defaults [#press-defaults] The root **is** a `Pressable`. Two defaults differ and only two: `feedback="fade"` (a spring on a 20pt square reads as a jitter) and `haptic="selection"` (a checkbox is a state toggle, and the tick landing is the confirmation). Both are ordinary props — `haptic={false}` silences it. `onPress` is `Omit`ed rather than forwarded: the press **is** the toggle, and `onCheckedChange` is where a side effect goes. ## Sizing [#sizing] The box mints no scale of its own — it reads `--spacing-icon-*` two steps above its own glyph: 18/14, 20/16, 24/18. The test pins the *offset*, not the points. Only a **bare** box gets `hitSlop`. Once there is a label the row is already a wide target, and slop on top of that would overlap the row below. ## API [#api] A checkbox is a `Pressable`, so `feedback`, `haptic` and the rest are inherited — see [Pressable](/docs/native/components/pressable). `onPress` is withheld in favour of `onCheckedChange`, and `disabled` in favour of `isDisabled`, which the label reads through context. ### `Checkbox.Group` [#checkboxgroup] Owns the selection for the boxes inside it, and publishes the shared axes so each one does not restate them. Extends `ViewProps`. ### `Checkbox.Label` [#checkboxlabel] The checkbox's text, wired as its accessible name. Extends React Native's `TextProps`. Inside a group, a checkbox's `value` is required — the group tracks selection by it. A box without one is invisible to the group's `checked` array, so it renders but never reports. # Field (/docs/native/components/field) ```tsx import { Field } from "@delacour/native-ui/field"; ``` ```tsx Email We only use this to send receipts. ``` ## The state cascade [#the-state-cascade] `isInvalid` and `isDisabled` live on the `Field` and reach the control inside it. ```tsx Email {error} ``` The label turns danger, and so does the `Input` — with nothing said on the input itself. A control opts out explicitly: ``. On the web, shadcn does this with `group-data-[invalid=true]/field:` — a parent-scoped selector. Uniwind has no equivalent: its compiler reads `data-*` off a **single flat selector**, and its runtime matches them against props on the component **carrying the class**. So no class on a `Field` can reach the `Input` inside it. There is no `group-*`, no `peer-*`, no `:has()`. A data-attribute class would also leave `bun test` behind. Even for a part styling itself, `data-invalid:text-danger` moves the decision into Uniwind's runtime matcher where no unit test can see it. The parts style themselves from `tv()` booleans; the context is only for crossing a component boundary. ## The whole row drives the control [#the-whole-row-drives-the-control] Once a control inside offers a press, the field becomes a `Pressable` with `feedback="none"` and the whole row calls it. ```tsx Accept the terms You can withdraw consent at any time. ``` Tapping the label — or the description under it — ticks the checkbox. A checkbox in a form is a small square next to a sentence, and the sentence is what people aim at. A field of static text registers nothing and stays a `View`. Mounting a gesture detector regardless would put one under every label in a form. The row is `accessible={false}` so the control stays the element a screen reader sees, and the inner detector claims a tap on the box itself rather than firing both. ## Orientation [#orientation] ```tsx ``` `vertical` stacks the label over the control. `horizontal` puts them side by side — use `Field.Content` to group the label with its description as one block. ## Sets and groups [#sets-and-groups] ```tsx Notifications Choose what reaches you, and how. Email Push ``` `Field.Legend` takes `variant="label"` for a nested set, so it matches the fields under it rather than titling them. `content` 0.5 → `root` 1.5 → `set` 4 → `group` 5. A label attaches to the control beneath it rather than the one above purely because the gap inside a field is tighter than the gap between two, and nothing else is doing that work. The test pins the **ordering**, not the numbers. `Field.Group` inserts no dividers, unlike `ListGroup`. A list of rows without lines is a wall of text; fields are already held apart by whitespace, and a rule between every one is noise. **A set holds no state.** `isInvalid` and `isDisabled` live on each `Field`, because a whole section turning danger says less than the one field that is actually wrong. ## Separators [#separators] ```tsx or ``` A labelled separator draws **two** rules with the label between them, not one rule with the label on top. The web version absolutely-positions a single rule and punches a hole in it with an opaque `bg-background` label — invisible only while the separator sits on exactly that colour. On a card or a sheet the hole shows as a block of the wrong shade. Two rules assume nothing about what is behind them. ## Errors [#errors] ```tsx {error} ``` `Field.Error` renders nothing when it has no children, so the line removes itself once the value is fixed. It is deliberately **not** gated on `isInvalid`: a part that swallowed children you actually wrote, because of a prop on a sibling, would be a part whose absence is unexplainable from the call site. Only the label fades when disabled. The control dims itself, and a dimmed description stacked on a dimmed control reads as two problems rather than one state. The description stays muted when invalid too, so an appearing `Field.Error` is the one line that changed. ## Reading the field's state [#reading-the-fields-state] ```tsx import { useField, useFieldContext } from "@delacour/native-ui/field"; const { isInvalid, isDisabled, orientation } = useField(); ``` `useField()` throws outside a ``. Use `useFieldContext()` where the field is optional — as every control does. ## Anatomy [#anatomy] ## API [#api] ### `FieldProps` [#fieldprops] React Native's `ViewProps`, plus: ### `FieldTextProps` [#fieldtextprops] `TextProps` with `variant` omitted — each part already is one, and naming a second would let a caller turn a description into a heading and lose the type scale the component exists to keep. ### `FieldLegendProps` [#fieldlegendprops] `FieldTextProps`, plus: ### `useField()` [#usefield] ## What it deliberately does not have [#what-it-deliberately-does-not-have] **No `Field.Title`.** On the web it exists because a `
` is not a `