Install

npx @react-native-reusables/cli@latest add https://rnraielements.com/r/nativewind/commit.json

Dependencies

https://reactnativereusables.com/r/nativewind/avatar.json
https://reactnativereusables.com/r/nativewind/badge.json
https://reactnativereusables.com/r/nativewind/button.json
https://reactnativereusables.com/r/nativewind/collapsible.json
https://reactnativereusables.com/r/nativewind/icon.json
https://reactnativereusables.com/r/nativewind/text.json
https://rnraielements.com/r/nativewind/item.json
https://rnraielements.com/r/nativewind/mono.json
https://rnraielements.com/r/nativewind/status.json

AI SDK peers:ai@^7.0.89@ai-sdk/react@^4.0.92

Source

commit.tsx
import { Avatar, AvatarFallback } from '@/registry/nativewind/components/ui/avatar'; import { Button } from '@/registry/nativewind/components/ui/button'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/registry/nativewind/components/ui/collapsible'; import { Icon } from '@/registry/nativewind/components/ui/icon'; import { Item } from '@/registry/nativewind/components/ui/item'; import { Text, TextClassContext } from '@/registry/nativewind/components/ui/text'; import { monoStyle } from '@/registry/nativewind/lib/mono'; import { statusColor } from '@/registry/nativewind/lib/status'; import { cn } from '@/registry/nativewind/lib/utils'; import { CheckIcon, ChevronDownIcon, CopyIcon, FileIcon, GitCommitIcon, MinusIcon, PlusIcon, } from 'lucide-react-native'; import * as React from 'react'; import { Platform, View, type ViewProps } from 'react-native'; import Animated, { ReduceMotion, useAnimatedStyle, useSharedValue, withTiming, } from 'react-native-reanimated'; import * as Clipboard from 'expo-clipboard'; import { commitFileStatusMeta, formatRelativeDate, } from './commit.logic'; import type { CommitFileStatus as CommitFileStatusValue } from './commit.logic'; /** * Commit — a commit reference row with its changed files behind a disclosure (UC web * original: a Collapsible card — hash, message, author, timestamp, and a file list * with per-file +/- counts). UC-CODE-01 AC-2 is the binding acceptance: view hash, * message, and author, and copy the hash with one tap. * * COMPOSITION: the container is the web's Collapsible with the card look (rounded-lg * border bg-background — the web writes `bg-background` on the root, commit.tsx:26; * queue.tsx's QueueSection is the same composition). File rows compose the `item` * atom — the house list row. The hash is the web's bare mono text + inline git-commit * mark (commit.tsx:59-60 — no chip chrome; the web spans inherit the header's * foreground, encoded explicitly for dark mode); `shortSha(hash)` from commit.logic * is the expected text. * * TOUCH ADAPTATIONS, DECLARED: * - the web's affordance for "this header opens files" is a cursor hover — dead under * a thumb — so the header carries the house rotating chevron (queue/task precedent, * ReduceMotion.System); * - the web's CommitActions wraps clicks in stopPropagation so pressing Copy does not * toggle the disclosure. React Native's responder system hands the press to the * deepest touchable and does not bubble it to the trigger, so no propagation dance * exists — a plain nested Button is already correct; * - the author avatar gets the persona.tsx hardening (explicit numeric size beside * the class): the wave-7 device incident was EXACTLY an avatar's fallback blowing * up to full-screen when a stale stylesheet dropped its size class mid-transcript. * * TIMESTAMP: web's Intl.RelativeTimeFormat is hand-rolled in commit.logic (Hermes * variance; Vitest-ownable with injected time) and rendered as plain text. */ type CommitContextValue = { open: boolean }; const CommitContext = React.createContext<CommitContextValue | null>(null); function useCommit() { const ctx = React.useContext(CommitContext); if (!ctx) throw new Error('Commit sub-components must be used within <Commit>'); return ctx; } type CommitProps = Omit<ViewProps, 'children'> & { /** The web Collapsible's default: closed. */ defaultOpen?: boolean; onOpenChange?: (open: boolean) => void; children?: React.ReactNode; }; function Commit({ defaultOpen = false, onOpenChange, className, children, ...props }: CommitProps) { const [open, setOpen] = React.useState(defaultOpen); function handleOpenChange(next: boolean) { setOpen(next); onOpenChange?.(next); } return ( <CommitContext.Provider value={{ open }}> <Collapsible open={open} onOpenChange={handleOpenChange} className={cn('overflow-hidden rounded-lg border border-border bg-background', className)} {...props} > {children} </Collapsible> </CommitContext.Provider> ); } /** House chevron (queue/task precedent): 250ms open, 200ms close, system-reduced. */ function Chevron({ open }: { open: boolean }) { const rotation = useSharedValue(open ? 1 : 0); React.useEffect(() => { rotation.value = withTiming(open ? 1 : 0, { duration: open ? 250 : 200, reduceMotion: ReduceMotion.System, }); }, [open, rotation]); const style = useAnimatedStyle(() => ({ transform: [{ rotate: `${rotation.value * 180}deg` }], })); return ( <Animated.View style={style}> <Icon as={ChevronDownIcon} size={14} className="shrink-0 text-muted-foreground" /> </Animated.View> ); } function CommitHeader({ className, children, ...props }: ViewProps & { children?: React.ReactNode }) { const { open } = useCommit(); return ( <CollapsibleTrigger className={cn( 'flex-row items-center gap-3 p-3 text-left active:bg-accent', Platform.select({ web: 'transition-opacity hover:opacity-80' }), 'active:opacity-80', className, )} {...props} > <View className="min-w-0 flex-1 flex-row items-center gap-3">{children}</View> <Chevron open={open} /> </CollapsibleTrigger> ); } /** * The hash: the git-commit mark plus the caller's short SHA in the house mono — the * web's bare `font-mono text-xs` span with an inline size-3 icon (commit.tsx:59-60); * the invented Badge chip is gone. The web spans inherit the header's foreground; * RN icons carry no inherited text color, so the mark states `text-foreground` * explicitly and the Text rides the default foreground. */ function CommitHash({ children, className }: { children: string; className?: string }) { return ( <View className={cn('shrink-0 flex-row items-center', className)}> <Icon as={GitCommitIcon} size={12} className="text-foreground" /> <Text style={monoStyle} className="ml-1 text-xs"> {children} </Text> </View> ); } function CommitMessage({ className, ...props }: React.ComponentProps<typeof Text>) { return <Text numberOfLines={2} className={cn('text-sm font-medium text-foreground', className)} {...props} />; } /** Metadata row. Publishes the muted-xs pair so its Text children style without classes. */ function CommitMetadata({ className, children, ...props }: ViewProps & { children?: React.ReactNode }) { return ( <TextClassContext.Provider value="text-xs text-muted-foreground"> <View className={cn('flex-row flex-wrap items-center gap-2', className)} {...props}> {children} </View> </TextClassContext.Provider> ); } function CommitSeparator({ children, className }: { children?: string; className?: string }) { return ( <Text className={cn('text-xs text-muted-foreground', className)}>{children ?? '•'}</Text> ); } function CommitInfo({ className, children, ...props }: ViewProps & { children?: React.ReactNode }) { return ( <View className={cn('min-w-0 flex-1 gap-1', className)} {...props}> {children} </View> ); } function CommitAuthor({ className, children, ...props }: ViewProps & { children?: React.ReactNode }) { return ( <View className={cn('flex-row items-center gap-2', className)} {...props}> {children} </View> ); } type CommitAuthorAvatarProps = { /** The initials. Required — the fallback is the whole avatar until a URI ships. */ initials: string; className?: string; }; function CommitAuthorAvatar({ initials, className }: CommitAuthorAvatarProps) { return ( // Numeric style sizing beside the class is persona.tsx's hardening rule: this is // the exact element whose size class was observed dropping on a stale uniwind // rebuild, ballooning a percent-sized fallback to full-screen mid-transcript. <Avatar alt={initials} className={cn('size-8', className)} style={{ width: 32, height: 32 }}> <AvatarFallback> <Text className="text-xs font-medium">{initials}</Text> </AvatarFallback> </Avatar> ); } /** * The relative day label ("yesterday", "5 days ago"), computed inline from the pure * formatter. The web's `<time dateTime>` attribute is DOM-only; the visible text is the * behavior. `children` overrides, exactly as the web allows. */ function CommitTimestamp({ date, children, className }: { date: Date; children?: string; className?: string }) { return ( <Text className={cn('text-xs text-muted-foreground', className)}> {children ?? formatRelativeDate(date)} </Text> ); } /** * Trailing controls. RN's responder system gives the press to the deepest touchable — * the nested buttons never toggle the disclosure, which is what the web's * stopPropagation wrapper was for. */ function CommitActions({ className, children, ...props }: ViewProps & { children?: React.ReactNode }) { return ( <View className={cn('flex-row items-center gap-1', className)} {...props}> {children} </View> ); } type CommitCopyButtonProps = { /** The hash to copy — short or full, the caller's choice. */ hash: string; timeout?: number; onCopy?: () => void; onError?: (error: unknown) => void; className?: string; }; /** UC-CODE-01 AC-2's one-tap copy. Flip-and-revert per the code-block precedent. */ function CommitCopyButton({ hash, timeout = 2000, onCopy, onError, className }: CommitCopyButtonProps) { const [copied, setCopied] = React.useState(false); const timer = React.useRef<ReturnType<typeof setTimeout> | null>(null); React.useEffect(() => () => { if (timer.current) clearTimeout(timer.current); }, []); async function copy() { try { await Clipboard.setStringAsync(hash); setCopied(true); onCopy?.(); timer.current = setTimeout(() => setCopied(false), timeout); } catch (error) { // Never silent — the web original calls onError and so do we. onError?.(error); } } return ( <Button variant="ghost" size="icon" onPress={copy} accessibilityLabel={copied ? 'Copied' : 'Copy commit hash'} // House formula (confirmation.tsx): RNR's h-10 (40pt) + hitSlop 2/side = the 44pt // platform minimum, with no pixel of extra chrome. hitSlop={{ top: 2, bottom: 2, left: 2, right: 2 }} className={cn('shrink-0 rounded-full', className)} > <Icon as={copied ? CheckIcon : CopyIcon} size={14} className="text-muted-foreground" /> </Button> ); } function CommitContent({ className, children, ...props }: ViewProps & { children?: React.ReactNode }) { return ( <CollapsibleContent className={cn('border-t border-border p-3', className)} {...props}> {children} </CollapsibleContent> ); } function CommitFiles({ className, children, ...props }: ViewProps & { children?: React.ReactNode }) { return ( <View className={cn('gap-1', className)} {...props}> {children} </View> ); } /** * The file row, composed on the item atom — the house list row (queue precedent). * Children are the fine-grained parts below, exactly as the web composes its div row. */ function CommitFile({ className, children, ...props }: ViewProps & { children?: React.ReactNode }) { return ( <Item className={cn('gap-2 rounded-sm px-1 py-0.5', className)} {...props}> {children} </Item> ); } function CommitFileInfo({ className, children, ...props }: ViewProps & { children?: React.ReactNode }) { return ( <View className={cn('min-w-0 flex-1 flex-row items-center gap-2', className)} {...props}> {children} </View> ); } function CommitFileIcon({ className }: { className?: string }) { return <Icon as={FileIcon} size={14} className={cn('shrink-0 text-muted-foreground', className)} />; } /** * status → letter + tone from the pure map. The letter is the web original's default * (A/M/D/R); `children` overrides, exactly as the web allows. The tone is the * compressed map's — the letter keeps color from being the sole channel (WCAG 1.4.1). */ function CommitFileStatus({ status, children, className }: { status: CommitFileStatusValue; children?: string; className?: string }) { const meta = commitFileStatusMeta(status); return ( <Text style={monoStyle} accessibilityLabel={`Status: ${status}`} className={cn('shrink-0 text-xs font-medium', meta.className, className)} > {children ?? meta.label} </Text> ); } function CommitFilePath({ children, className }: { children: string; className?: string }) { return ( <Text style={monoStyle} numberOfLines={1} className={cn('flex-1 text-xs text-foreground', className)}> {children} </Text> ); } function CommitFileChanges({ className, children, ...props }: ViewProps & { children?: React.ReactNode }) { return ( <TextClassContext.Provider value="text-xs"> <View className={cn('flex-row shrink-0 items-center gap-1', className)} {...props}> {children} </View> </TextClassContext.Provider> ); } /** The web's +count span: NULL at zero and below — a file with no additions shows no +0. */ function CommitFileAdditions({ count, className }: { count: number; className?: string }) { if (count <= 0) return null; return ( <View className={cn('flex-row items-center gap-0.5', className)}> <Icon as={PlusIcon} size={12} className={statusColor.success} /> <Text style={monoStyle} className={cn('text-xs', statusColor.success)}> {count} </Text> </View> ); } /** The deletions twin — the web's red, our text-destructive escape hatch. */ function CommitFileDeletions({ count, className }: { count: number; className?: string }) { if (count <= 0) return null; return ( <View className={cn('flex-row items-center gap-0.5', className)}> <Icon as={MinusIcon} size={12} className={statusColor.error} /> <Text style={monoStyle} className={cn('text-xs', statusColor.error)}> {count} </Text> </View> ); } export { Commit, CommitActions, CommitAuthor, CommitAuthorAvatar, CommitContent, CommitCopyButton, CommitFile, CommitFileAdditions, CommitFileChanges, CommitFileDeletions, CommitFileIcon, CommitFileInfo, CommitFilePath, CommitFileStatus, CommitFiles, CommitHash, CommitHeader, CommitInfo, CommitMessage, CommitMetadata, CommitSeparator, CommitTimestamp, useCommit, };
commit.logic.ts
/** * Commit — pure logic. Zero react-native imports, so the Vitest tier owns it (see the * header of lib/reasoning-lifecycle.ts for why component files themselves cannot load * under Node). * * The web original formats the timestamp with `Intl.RelativeTimeFormat('en')` over a * rounded day delta. The format is hand-rolled here instead, byte-compatible for the * cases the web produces ("yesterday", "5 days ago"), because Hermes' Intl build is a * device-variable surface a display component must not lean on — and so the Vitest * tier can own the format with injected time. * * COLOR COMPRESSION, DECLARED: the web paints file statuses with FOUR escape-hatch * hues (added green, deleted red, modified yellow, renamed blue). The styling contract * permits exactly three non-token colors, confined to lib/status.ts. The port maps the * four statuses onto the house StatusTone vocabulary — added→success, modified→running, * deleted→error, renamed→pending — and leans on the rule that carries the whole design: * color is never the sole channel (WCAG 1.4.1). The web's own status LETTERS (A/M/D/R) * render beside the tone, so the compressed map loses no information. */ import { statusColor, type StatusTone } from '@/registry/nativewind/lib/status'; /** File change kinds, verbatim from the web original's union. */ export type CommitFileStatus = 'added' | 'modified' | 'deleted' | 'renamed'; /** Every status, spelled out — UC-AGENT-style exhaustiveness for the meta table. */ export const COMMIT_FILE_STATUS_KEYS = [ 'added', 'modified', 'deleted', 'renamed', ] as const satisfies readonly CommitFileStatus[]; export type CommitFileStatusMeta = { /** The web original's one-letter label: A, M, D, R. */ label: string; tone: StatusTone; /** Precomposed text class from the shared statusColor map. */ className: string; }; /** * status → letter + tone, one record, exhaustive by type: a status added here without a * row there fails tsc. Letters are the web original's; tones are the compressed map. */ export const COMMIT_FILE_STATUS_META: Record<CommitFileStatus, CommitFileStatusMeta> = { added: { label: 'A', tone: 'success', className: statusColor.success }, modified: { label: 'M', tone: 'running', className: statusColor.running }, deleted: { label: 'D', tone: 'error', className: statusColor.error }, renamed: { label: 'R', tone: 'pending', className: statusColor.pending }, }; export function commitFileStatusMeta(status: CommitFileStatus): CommitFileStatusMeta { return COMMIT_FILE_STATUS_META[status]; } /** One file in the commit. Counts are optional; `commitDiffstat` normalizes them. */ export type CommitFileData = { path: string; status: CommitFileStatus; additions?: number; deletions?: number; }; /** * The short SHA for the hash chip. Trims and truncates at `length` (git's default * abbreviated form). No-throw: an empty or short hash comes back as-is rather than * throwing — a streamed commit object may still be arriving. */ export function shortSha(hash: string, length = 7): string { return hash.trim().slice(0, length); } /** * The relative day label the web CommitTimestamp renders, hand-rolled over the same * rounded-day delta. `now` is injectable so tests (and callers with a fixed clock) own * time. Negative diffs are the past ("yesterday", "5 days ago"), zero is "today", * positive the future ("tomorrow", "in 3 days"). */ export function formatRelativeDate(date: Date, now: Date = new Date()): string { const days = Math.round((date.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)); if (days === 0) return 'today'; if (days === 1) return 'tomorrow'; if (days === -1) return 'yesterday'; return days > 0 ? `in ${days} days` : `${-days} days ago`; } /** Totals across a commit's files — the summary line's +N −N math, undefined → 0. */ export function commitDiffstat(files: readonly CommitFileData[]): { additions: number; deletions: number; files: number; } { return { additions: files.reduce((sum, f) => sum + (f.additions ?? 0), 0), deletions: files.reduce((sum, f) => sum + (f.deletions ?? 0), 0), files: files.length, }; } /** * The signed per-file counts. The web renders `+{count}` spans and returns NULL at zero * and below — a file with no additions shows no +0. Same rule here: null means render * nothing. (The row itself renders the web's icon-plus-count pair; these strings are * for callers composing a summary line.) */ export function signedDiffCount(count: number): string | null { if (count <= 0) return null; return `+${count}`; } /** The deletions twin of signedDiffCount. */ export function signedDeletionCount(count: number): string | null { if (count <= 0) return null; return `-${count}`; }

Props

NameTypeDefaultDescription
defaultOpenboolean | undefinedfalseThe web Collapsible's default: closed.
onOpenChange((open: boolean) => void) | undefined——
childrenReact.ReactNode——

Theming

Themed via bg-background, border-border, text-muted-foreground, bg-accent, text-foreground, text-destructive.

Usage

() => ( <Commit defaultOpen> <CommitHeader> <CommitAuthor> <CommitAuthorAvatar initials="JR" /> <Text className="text-sm text-foreground">Justin Rich</Text> </CommitAuthor> <CommitTitle>swap mvp planning binder for one-page plan</CommitTitle> <CommitDescription>59 files deleted, one README left standing</CommitDescription> </CommitHeader> <CommitContent> <CommitFile path="packages/registry/registry.json"> <CommitFileChanges additions={6} deletions={2} /> </CommitFile> <CommitFile path="scripts/check-tokens.ts"> <CommitFileChanges additions={190} deletions={0} /> </CommitFile> </CommitContent> <CommitActions> <CommitCopyButton hash="ad2267d" /> </CommitActions> </Commit> )