Package Info
Card of package name, version, and a copyable install command (UC-CODE-01 AC-3) — port-at-parity per the PRD verdict, all RNR primitives. Upstream part set at parity (Header/Name/Version/Description/Content/Dependencies/Dependency/ChangeType; name/currentVersion/newVersion/changeType): the version line is the KB's version transition display (current → next, null renders nothing), and the web's five change-type hues compress onto the house status vocabulary with the badge WORD carrying the kind. ONE declared addition: upstream ships no install-command part but AC-3 is binding, so PackageInfoInstall composes the existing Snippet organism inside the card — the new version pins the install when present, otherwise latest. Dependencies render as ONE table (package/version) per the build plan, fixed column sizes, horizontal scroll, mono identifiers via lib/mono.
- v0.2.1
- component
- nativewind
- uniwind
- runs in Expo Go
Install
npx @react-native-reusables/cli@latest add https://rnraielements.com/r/nativewind/package-info.jsonnpx @react-native-reusables/cli@latest add https://rnraielements.com/r/uniwind/package-info.jsonDependencies
No required npm packages — this item installs through the registry dependencies below.
https://reactnativereusables.com/r/nativewind/badge.json |
https://reactnativereusables.com/r/nativewind/text.json |
https://rnraielements.com/r/nativewind/mono.json |
https://rnraielements.com/r/nativewind/snippet.json |
https://rnraielements.com/r/nativewind/status.json |
https://rnraielements.com/r/nativewind/table.json |
https://reactnativereusables.com/r/uniwind/badge.json |
https://reactnativereusables.com/r/uniwind/text.json |
https://rnraielements.com/r/uniwind/mono.json |
https://rnraielements.com/r/uniwind/snippet.json |
https://rnraielements.com/r/uniwind/status.json |
https://rnraielements.com/r/uniwind/table.json |
AI SDK peers:ai@^7.0.89@ai-sdk/react@^4.0.92
Source
import { Badge } from '@/registry/nativewind/components/ui/badge';
import { Text } from '@/registry/nativewind/components/ui/text';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/registry/nativewind/components/ui/table';
import {
Snippet,
SnippetAddon,
SnippetCopyButton,
SnippetInput,
SnippetText,
} from '@/registry/nativewind/components/ai/snippet';
import { monoStyle } from '@/registry/nativewind/lib/mono';
import { cn } from '@/registry/nativewind/lib/utils';
import * as React from 'react';
import { View, type ViewProps } from 'react-native';
import {
installCommand,
packageChangeTypeMeta,
versionTransitionSegments,
type PackageChangeType,
type VersionTransition,
} from './package-info.logic';
/**
* PackageInfo — a package metadata card (UC-CODE-01 AC-3: name, version, and an
* install command that copies to the clipboard).
*
* THE PRD VERDICT IS PORT-AT-PARITY: "Card of package name, version, and a copyable
* install command; all RNR primitives." The upstream part set, from the KB, ported
* part for part: PackageInfo root (name required / currentVersion / newVersion /
* changeType), PackageInfoHeader, PackageInfoName, PackageInfoVersion (the KB's
* "version transition display" — current → next), PackageInfoDescription,
* PackageInfoContent, PackageInfoDependencies, PackageInfoDependency (name, version),
* and PackageInfoChangeType (Badge).
*
* THE ONE DECLARED ADDITION: upstream ships no install-command part, but AC-3 is the
* binding acceptance and the verdict's own words ("a copyable install command") demand
* one — so PackageInfoInstall composes the registry's existing Snippet (the copyable
* command organism) inside the card. The line comes from package-info.logic's
* installCommand(): the NEW version pins the install when present, otherwise latest.
*
* COLOR COMPRESSION rides package-info.logic's map: the web's five change-type hues
* compress onto the house status vocabulary, and the badge text ("major", "added")
* carries the kind — color is never the sole channel.
*
* COMPOSITION: the build plan names `table` as the backing primitive for this surface,
* so PackageInfoDependencies is ONE Table (package/version header, one row per
* dependency) — the same scaffold environment-variables uses. The name and version
* ride the house mono family (lib/mono).
*/
type PackageInfoContextValue = {
name: string;
/** Segmented transition — the upgrade target emphasized over the installed version. */
versionTransition: VersionTransition;
changeType?: PackageChangeType;
/** The copyable line the install part renders. */
installCommandText: string;
};
const PackageInfoContext = React.createContext<PackageInfoContextValue | null>(null);
function usePackageInfo() {
const ctx = React.useContext(PackageInfoContext);
if (!ctx) throw new Error('PackageInfo sub-components must be used within <PackageInfo>');
return ctx;
}
export type PackageInfoProps = Omit<ViewProps, 'children'> & {
/** Required upstream, and kept required — an unnamed package is not package info. */
name: string;
/** What's installed now. */
currentVersion?: string;
/** What the card is announcing (the upgrade target). */
newVersion?: string;
changeType?: PackageChangeType;
/** Overrides the default install line (pnpm add …, bun add …). */
installCommand?: string;
children?: React.ReactNode;
};
function PackageInfo({
name,
currentVersion,
newVersion,
changeType,
installCommand: installCommandOverride,
className,
children,
...props
}: PackageInfoProps) {
const contextValue = React.useMemo<PackageInfoContextValue>(
() => ({
name,
versionTransition: versionTransitionSegments(currentVersion, newVersion),
changeType,
// The NEW version pins the install when present (an upgrade card pins its
// target); otherwise latest — currentVersion describes, it does not pin.
installCommandText: installCommandOverride ?? installCommand(name, newVersion),
}),
[name, currentVersion, newVersion, changeType, installCommandOverride],
);
return (
<PackageInfoContext.Provider value={contextValue}>
<View
className={cn('overflow-hidden rounded-lg border border-border bg-card', className)}
{...props}
>
{children}
</View>
</PackageInfoContext.Provider>
);
}
function PackageInfoHeader({ className, ...props }: ViewProps & { children?: React.ReactNode }) {
return (
<View
className={cn('flex-row items-start justify-between gap-3 px-3 py-3', className)}
{...props}
/>
);
}
/** The package name — mono, like every house surface that shows a code identifier. */
function PackageInfoName({ children, className }: { children?: string; className?: string }) {
const { name } = usePackageInfo();
return (
<Text
numberOfLines={1}
style={monoStyle}
className={cn('text-sm font-medium text-foreground', className)}
>
{children ?? name}
</Text>
);
}
/**
* The transition line, from context. The upgrade case (current → next) renders as
* two spans so the NEW version — what the card is announcing — is emphasized over
* the installed one; single-version cards render plainly. Null context (no versions
* yet) renders NOTHING — never "undefined → undefined" mid-stream. `children`
* overrides with a plain string, as upstream allows.
*/
function PackageInfoVersion({ children, className }: { children?: string; className?: string }) {
const { versionTransition } = usePackageInfo();
if (children !== undefined) {
return (
<Text style={monoStyle} className={cn('text-xs text-muted-foreground', className)}>
{children}
</Text>
);
}
if (versionTransition === null) return null;
if (versionTransition.kind === 'both') {
return (
<View className={cn('flex-row items-baseline', className)}>
<Text numberOfLines={1} style={monoStyle} className="text-xs text-muted-foreground">
{versionTransition.current}
</Text>
<Text style={monoStyle} className="px-0.5 text-xs text-muted-foreground">
→
</Text>
<Text numberOfLines={1} style={monoStyle} className="max-w-28 text-xs font-semibold text-foreground">
{versionTransition.next}
</Text>
</View>
);
}
return (
<Text numberOfLines={1} style={monoStyle} className={cn('text-xs text-muted-foreground', className)}>
{versionTransition.value}
</Text>
);
}
/**
* The change badge: the compressed tone plus its WORD. `children` overrides the label,
* exactly as the web allows. No changeType in the root renders nothing.
*/
function PackageInfoChangeType({ children, className }: { children?: string; className?: string }) {
const { changeType } = usePackageInfo();
if (!changeType) return null;
const meta = packageChangeTypeMeta(changeType);
return (
<Badge variant="outline" className={cn('shrink-0', meta.className, className)}>
<Text>{children ?? meta.label}</Text>
</Badge>
);
}
/** The muted one-liner under the header ("ORM with first-class edge support"). */
function PackageInfoDescription({ className, ...props }: React.ComponentProps<typeof Text>) {
return (
<Text
numberOfLines={2}
className={cn('px-3 pb-3 text-xs text-muted-foreground', className)}
{...props}
/>
);
}
function PackageInfoContent({ className, children, ...props }: ViewProps & { children?: React.ReactNode }) {
return (
<View className={cn('gap-3 border-t border-border p-3', className)} {...props}>
{children}
</View>
);
}
/**
* AC-3's copyable install line, composed on the Snippet organism — the `$` prefix,
* the readOnly field that scrolls a long command, and the flip-and-revert copy button
* are all Snippet's, and none of them are rebuilt here.
*/
function PackageInfoInstall({ className }: { className?: string }) {
const { installCommandText } = usePackageInfo();
return (
<Snippet code={installCommandText} className={className}>
<SnippetAddon>
<SnippetText>$</SnippetText>
</SnippetAddon>
<SnippetInput />
<SnippetCopyButton />
</Snippet>
);
}
/**
* The dependency table: package/version header, one row per PackageInfoDependency.
* Fixed column sizes + the table's own horizontal scroll handle overflowing names.
*/
function PackageInfoDependencies({ className, children }: { className?: string; children?: React.ReactNode }) {
return (
<Table scrollable className={className}>
<TableHeader>
<TableHead className="w-40 shrink-0">
<Text className="text-xs font-medium text-muted-foreground">Package</Text>
</TableHead>
<TableHead className="w-24 shrink-0">
<Text className="text-xs font-medium text-muted-foreground">Version</Text>
</TableHead>
</TableHeader>
<TableBody>{children}</TableBody>
</Table>
);
}
export type PackageInfoDependencyProps = {
/** Required upstream, and kept required. */
name: string;
/** Missing version renders an em dash, never an empty cell. */
version?: string;
className?: string;
};
function PackageInfoDependency({ name, version, className }: PackageInfoDependencyProps) {
return (
<TableRow className={className}>
<TableCell className="w-40 shrink-0">
<Text numberOfLines={1} style={monoStyle} className="text-xs text-foreground">
{name}
</Text>
</TableCell>
<TableCell className="w-24 shrink-0">
<Text numberOfLines={1} style={monoStyle} className="text-xs text-muted-foreground">
{version?.trim() || '—'}
</Text>
</TableCell>
</TableRow>
);
}
export {
PackageInfo,
PackageInfoChangeType,
PackageInfoContent,
PackageInfoDependencies,
PackageInfoDependency,
PackageInfoDescription,
PackageInfoHeader,
PackageInfoInstall,
PackageInfoName,
PackageInfoVersion,
usePackageInfo,
};
/**
* PackageInfo — 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).
*
* COLOR COMPRESSION, DECLARED — same move as commit.logic, one file earlier in the
* wave family. The web paints change types with FIVE escape-hatch hues (major red,
* minor yellow, patch green, added blue, removed gray). The styling contract permits
* exactly three non-token colors, confined to lib/status.ts, so the five compress onto
* the house StatusTone vocabulary — major→error, minor→denied, patch→success,
* added→running (the accent role carries "blue"), removed→pending (the muted pole
* carries "gray"). The badge's own TEXT carries the kind ("major", "added"), so the
* compression loses no information — color is never the sole channel (WCAG 1.4.1).
*
* The version line is the KB's "version transition display": current → next with an
* arrow, each half optional, whitespace-trimmed (streamed metadata arrives dirty).
* The install command is UC-CODE-01 AC-3's copyable line: `npm install <name>` with an
* optional `@version` pin — the NEW version when one is present (an upgrade card pins
* the target), nothing otherwise (a fresh install tracks latest).
*/
import { statusColor } from '@/registry/nativewind/lib/status';
/** The web original's change-type union, verbatim. */
export type PackageChangeType = 'major' | 'minor' | 'patch' | 'added' | 'removed';
/** Every change type, spelled out — exhaustiveness for the meta table. */
export const PACKAGE_CHANGE_TYPE_KEYS = [
'major',
'minor',
'patch',
'added',
'removed',
] as const satisfies readonly PackageChangeType[];
export type PackageChangeTypeMeta = {
/** The badge's default label — the word itself carries the kind. */
label: string;
/** Precomposed text class from the shared statusColor map. */
className: string;
};
/** change type → label + tone, one record, exhaustive by type. */
export const PACKAGE_CHANGE_TYPE_META: Record<PackageChangeType, PackageChangeTypeMeta> = {
major: { label: 'major', className: statusColor.error },
minor: { label: 'minor', className: statusColor.denied },
patch: { label: 'patch', className: statusColor.success },
added: { label: 'added', className: statusColor.running },
removed: { label: 'removed', className: statusColor.pending },
};
export function packageChangeTypeMeta(changeType: PackageChangeType): PackageChangeTypeMeta {
return PACKAGE_CHANGE_TYPE_META[changeType];
}
/**
* The version transition as SEGMENTS: "1.2.3 → 2.0.0" splits so the card can
* emphasize the upgrade target (new version bold/dark — the web's treatment) while
* the old version stays muted. "1.2.3" (installed) / "2.0.0" (announced) render as
* a single segment; null when neither arrives yet — null means render nothing, never
* "undefined → undefined" mid-stream.
*/
export type VersionTransition =
| { kind: 'both'; current: string; next: string }
| { kind: 'single'; value: string }
| null;
export function versionTransitionSegments(currentVersion?: string, newVersion?: string): VersionTransition {
const current = currentVersion?.trim();
const next = newVersion?.trim();
if (current && next) return { kind: 'both', current, next };
const value = current || next;
return value ? { kind: 'single', value } : null;
}
/**
* The flat string form of the same transition, for plain-text callers (a11y labels,
* log lines). Pure join of the segments — one source of truth, never a second parse.
*/
export function formatVersionTransition(currentVersion?: string, newVersion?: string): string | null {
const segments = versionTransitionSegments(currentVersion, newVersion);
if (!segments) return null;
return segments.kind === 'both' ? `${segments.current} → ${segments.next}` : segments.value;
}
/** AC-3's copyable line. `version` pins the install; absent means latest. */
export function installCommand(name: string, version?: string): string {
const pkg = name.trim();
const pinned = version?.trim();
return pinned ? `npm install ${pkg}@${pinned}` : `npm install ${pkg}`;
}
Props
| Name | Type | Default | Description |
|---|---|---|---|
| name | string | — | Required upstream, and kept required — an unnamed package is not package info. |
| currentVersion | string | undefined | — | What's installed now. |
| newVersion | string | undefined | — | What the card is announcing (the upgrade target). |
| changeType | PackageChangeType | undefined | — | — |
| installCommand | string | undefined | — | Overrides the default install line (pnpm add …, bun add …). |
| children | React.ReactNode | — | — |
Theming
Themed via border-border, bg-card, text-foreground, text-muted-foreground.
Usage
() => (
<PackageInfo name="@rnr-ai-elements/registry" currentVersion="0.1.0" newVersion="0.2.0" changeType="patch">
<PackageInfoHeader>
<PackageInfoTitle>Registry</PackageInfoTitle>
<PackageInfoName />
</PackageInfoHeader>
<PackageInfoDescription>The copy-paste registry both engines fan out from</PackageInfoDescription>
<PackageInfoInstall />
</PackageInfo>
)