Context
The context-window budget chip and its press-opened breakdown (the PRD verdict turns the web's hover card into a popover; the data displayed is unchanged). Composes RNR popover and progress; ContextContent passes insets through to the anchored content, the consumer's per-screen ritual. Cost is INJECTED, not computed — the web prices with tokenlens, a dependency this registry does not take, so rows and footer take caller-formatted costText and an unsupplied cost renders the web's own unknown-marker instead of a lying $0.00. The SVG progress ring is a themed PieChartIcon: a raw Svg cannot receive className without engine-specific cssInterop, which the styling contract forbids in registry source. Zero-token usage rows render nothing, upstream trap parity.
- v0.2.1
- component
- nativewind
- uniwind
- runs in Expo Go
Install
npx @react-native-reusables/cli@latest add https://rnraielements.com/r/nativewind/context.jsonnpx @react-native-reusables/cli@latest add https://rnraielements.com/r/uniwind/context.jsonDependencies
No required npm packages — this item installs through the registry dependencies below.
https://reactnativereusables.com/r/nativewind/button.json |
https://reactnativereusables.com/r/nativewind/icon.json |
https://reactnativereusables.com/r/nativewind/popover.json |
https://reactnativereusables.com/r/nativewind/progress.json |
https://reactnativereusables.com/r/nativewind/text.json |
https://rnraielements.com/r/nativewind/mono.json |
https://reactnativereusables.com/r/uniwind/button.json |
https://reactnativereusables.com/r/uniwind/icon.json |
https://reactnativereusables.com/r/uniwind/popover.json |
https://reactnativereusables.com/r/uniwind/progress.json |
https://reactnativereusables.com/r/uniwind/text.json |
https://rnraielements.com/r/uniwind/mono.json |
AI SDK peers:ai@^7.0.89@ai-sdk/react@^4.0.92
Source
import { Button } from '@/registry/nativewind/components/ui/button';
import { Icon } from '@/registry/nativewind/components/ui/icon';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/registry/nativewind/components/ui/popover';
import { Progress } from '@/registry/nativewind/components/ui/progress';
import { Text } from '@/registry/nativewind/components/ui/text';
import { monoStyle } from '@/registry/nativewind/lib/mono';
import { cn } from '@/registry/nativewind/lib/utils';
import { PieChartIcon } from 'lucide-react-native';
import * as React from 'react';
import { View, type ViewProps } from 'react-native';
import {
contextUsedPercent,
formatContextPercent,
formatContextRatio,
formatContextTokens,
usageRowVisible,
type ContextUsage,
} from './context.logic';
/**
* Context — the context-window budget chip (what the web original shows as a
* hover-card trigger reading "62.5%" beside the composer).
*
* THE PRD VERDICT IS PORT-ADAPTED, PURE-RNR: "Token and cost breakdown is revealed on
* hover in the web version. On touch it becomes a press-opened popover; the data
* displayed is unchanged." The web composition maps 1:1 onto RNR's popover —
* Context wraps Popover exactly as it wraps HoverCard, ContextTrigger wraps
* PopoverTrigger, ContextContent renders PopoverContent:
*
* <Context usedTokens={…} maxTokens={…} usage={…}>
* <ContextTrigger />
* <ContextContent insets={contentInsets}>
* <ContextContentHeader />
* <ContextContentBody>
* <ContextInputUsage /> <ContextOutputUsage />
* <ContextReasoningUsage /> <ContextCacheUsage />
* </ContextContentBody>
* <ContextContentFooter costText="…?" />
* </ContextContent>
* </Context>
*
* ContextContent takes the RNR popover content props — insets FIRST among them
* (anchored content hides under the notch/nav bar without them; the consumer's
* per-screen contentInsets ritual applies, as with every popover in the house).
*
* COST IS INJECTED, NOT COMPUTED. The web prices tokens with `tokenlens`, a model
* pricing database this registry does not depend on. Every cost surface takes a
* caller-formatted string instead (the renderMarkdown seam precedent): row
* `costText` and footer `costText`. An unsupplied cost renders "—" — the web
* original's own unknown-marker — never "$0.00", which would be a lie.
*
* THE RING GLYPH IS A DECLARED SUBSTITUTION. The web trigger draws an SVG
* stroke-dashoffset progress ring; a raw react-native-svg element cannot receive
* className without engine-specific cssInterop, which the styling contract forbids in
* registry source, and an unthemeable stroke would fail the palette-flip proof. The
* trigger ships the themed PieChartIcon instead — same at-a-glance "portion of whole"
* semantics, tokens intact. The PRECISE bar is ContextContentHeader's RNR Progress,
* which is theme-reachable.
*
* Two upstream behaviors preserved verbatim: the provider THROW
* ("Context components must be used within Context"), and the zero-token rows
* rendering nothing (usageRowVisible) — a breakdown never shows "Input 0".
*/
type ContextSchema = {
usedTokens: number;
maxTokens: number;
usage?: ContextUsage;
modelId?: string;
};
const ContextContext = React.createContext<ContextSchema | null>(null);
function useContextValue(): ContextSchema {
const ctx = React.useContext(ContextContext);
if (!ctx) throw new Error('Context components must be used within Context');
return ctx;
}
type ContextProps = ContextSchema & {
children?: React.ReactNode;
};
function Context({ usedTokens, maxTokens, usage, modelId, children }: ContextProps) {
const contextValue = React.useMemo<ContextSchema>(
() => ({ usedTokens, maxTokens, usage, modelId }),
[usedTokens, maxTokens, usage, modelId],
);
return (
<ContextContext.Provider value={contextValue}>
<Popover>{children}</Popover>
</ContextContext.Provider>
);
}
type ContextTriggerProps = {
/** Overrides the default percent + gauge button (web: children on the trigger). */
children?: React.ReactNode;
className?: string;
};
function ContextTrigger({ children, className }: ContextTriggerProps) {
const { usedTokens, maxTokens } = useContextValue();
return (
<PopoverTrigger asChild>
{children ?? (
<Button
variant="ghost"
accessibilityLabel={`Model context usage, ${formatContextPercent(usedTokens, maxTokens)}`}
className={cn('gap-1.5', className)}
>
<Text className="font-medium text-muted-foreground">
{formatContextPercent(usedTokens, maxTokens)}
</Text>
<Icon as={PieChartIcon} size={20} className="text-muted-foreground" />
</Button>
)}
</PopoverTrigger>
);
}
type ContextContentProps = React.ComponentProps<typeof PopoverContent> & {
children?: React.ReactNode;
};
/**
* The press-opened breakdown. PopoverContent props flow through — insets, align,
* sideOffset, portalHost — because the safe-area ritual belongs to the caller's
* screen, not to this file.
*
* THE PORTAL RE-PROVIDE (the @rn-primitives/popover Portal precedent): the portal
* transport stores this subtree's ELEMENTS in a zustand store and PortalHost —
* mounted at the root layout, an ANCESTOR of the screen rendering <Context> —
* renders them there, so React context resolves against the host's fiber and
* ContextContext.Provider inside <Context> never reaches the portalled children
* (the header/body/footer call useContextValue() and would throw
* "Context components must be used within Context" on-device). So the value is
* read OUTSIDE the portal — here, where composition still sits under Context's
* provider — and re-provided INSIDE the boundary, exactly as the popover
* primitive re-provides its own RootContext around the portalled children.
*/
function ContextContent({ className, children, ...props }: ContextContentProps) {
const contextValue = useContextValue();
return (
<PopoverContent className={cn('w-60 gap-0 p-0', className)} {...props}>
<ContextContext.Provider value={contextValue}>{children}</ContextContext.Provider>
</PopoverContent>
);
}
type ContextContentHeaderProps = ViewProps & { children?: React.ReactNode };
/** Percentage, compact "used / total" in mono, and the progress bar. */
function ContextContentHeader({ className, children, ...props }: ContextContentHeaderProps) {
const { usedTokens, maxTokens } = useContextValue();
const usedPercent = contextUsedPercent(usedTokens, maxTokens);
return (
<View className={cn('w-full gap-2 border-b border-border p-3', className)} {...props}>
{children ?? (
<>
<View className="flex-row items-center justify-between gap-3">
<Text className="text-xs">{formatContextPercent(usedTokens, maxTokens)}</Text>
<Text style={monoStyle} className="text-xs text-muted-foreground">
{formatContextRatio(usedTokens, maxTokens)}
</Text>
</View>
<Progress value={usedPercent * 100} className="bg-muted" />
</>
)}
</View>
);
}
type ContextContentBodyProps = ViewProps & { children?: React.ReactNode };
function ContextContentBody({ className, children, ...props }: ContextContentBodyProps) {
return (
<View className={cn('w-full gap-2 p-3', className)} {...props}>
{children}
</View>
);
}
type ContextContentFooterProps = ViewProps & {
/** Caller-priced total (see header note — tokenlens is not a dependency). */
costText?: string;
children?: React.ReactNode;
};
/**
* The total row on the secondary surface. The web original prints "$0.00" when it
* cannot price; that is a lie on a port with no pricing data, so an unsupplied cost
* renders the web original's own unknown-marker, "—".
*/
function ContextContentFooter({ costText, className, children, ...props }: ContextContentFooterProps) {
return (
<View
className={cn(
'flex-row w-full items-center justify-between gap-3 bg-secondary p-3',
className,
)}
{...props}
>
{children ?? (
<>
<Text className="text-xs text-muted-foreground">Total cost</Text>
<Text className="text-xs">{costText ?? '—'}</Text>
</>
)}
</View>
);
}
type ContextUsageRowProps = ViewProps & {
/** Caller-priced cost for this slice — "• $0.0042" when supplied. */
costText?: string;
children?: React.ReactNode;
};
/**
* One usage slice: label left, compact tokens (+ optional injected cost) right.
* Children replace the token readout exactly as upstream allows; a zero or missing
* count renders NOTHING (upstream trap parity).
*/
function ContextUsageRow({
label,
tokens,
costText,
className,
children,
...props
}: ContextUsageRowProps & { label: string; tokens: number | undefined }) {
if (children) {
return (
<View className={cn('flex-row items-center justify-between', className)} {...props}>
{children}
</View>
);
}
if (!usageRowVisible(tokens)) return null;
return (
<View className={cn('flex-row items-center justify-between', className)} {...props}>
<Text className="text-xs text-muted-foreground">{label}</Text>
<Text className="text-xs">
{formatContextTokens(tokens as number)}
{costText ? (
<Text className="text-xs text-muted-foreground"> • {costText}</Text>
) : null}
</Text>
</View>
);
}
function ContextInputUsage(props: ContextUsageRowProps) {
const { usage } = useContextValue();
return <ContextUsageRow label="Input" tokens={usage?.inputTokens} {...props} />;
}
function ContextOutputUsage(props: ContextUsageRowProps) {
const { usage } = useContextValue();
return <ContextUsageRow label="Output" tokens={usage?.outputTokens} {...props} />;
}
function ContextReasoningUsage(props: ContextUsageRowProps) {
const { usage } = useContextValue();
return <ContextUsageRow label="Reasoning" tokens={usage?.reasoningTokens} {...props} />;
}
function ContextCacheUsage(props: ContextUsageRowProps) {
const { usage } = useContextValue();
return <ContextUsageRow label="Cache" tokens={usage?.cachedInputTokens} {...props} />;
}
export {
Context,
ContextCacheUsage,
ContextContent,
ContextContentBody,
ContextContentFooter,
ContextContentHeader,
ContextInputUsage,
ContextOutputUsage,
ContextReasoningUsage,
ContextTrigger,
};
export type { ContextUsage };
/**
* Context — pure logic. Zero react-native imports, so the Vitest tier owns it (see the
* header of sources.logic.ts for why component files themselves cannot load under
* Node).
*
* The usage shape mirrors the AI SDK's LanguageModelUsage structurally, declared
* locally per the ToolStatus precedent (the ONE kind of type allowed to mirror an
* external contract, with no runtime dependency on the SDK).
*
* Cost does NOT compute here. The web original prices tokens with `tokenlens` — a model
* pricing database we do not take as a dependency, so cost strings are injected by the
* caller (the renderMarkdown seam precedent: an injected render over a hard binding).
* Every formatter here is number-in/string-out and no-throw: a streamed usage object
* with zero or missing fields is a rendering question, never a crash.
*/
/** Structurally identical to the AI SDK `LanguageModelUsage` fields the rows read. */
export type ContextUsage = {
inputTokens?: number;
outputTokens?: number;
reasoningTokens?: number;
cachedInputTokens?: number;
};
/**
* The consumed fraction of the window, clamped to [0, 1]. The web original divides
* bare and would render NaN% on a zero max; a budget of zero means nothing used, not
* infinity.
*/
export function contextUsedPercent(usedTokens: number, maxTokens: number): number {
if (!maxTokens || maxTokens <= 0) return 0;
const ratio = usedTokens / maxTokens;
if (Number.isNaN(ratio)) return 0;
return Math.min(1, Math.max(0, ratio));
}
/**
* The trigger and header percentage — the web original's exact formatter
* (`Intl.NumberFormat('en-US', { maximumFractionDigits: 1, style: 'percent' })` over
* the ratio), so 0.625 renders "62.5%" and 1 renders "100%".
*/
export function formatContextPercent(usedTokens: number, maxTokens: number): string {
return new Intl.NumberFormat('en-US', {
maximumFractionDigits: 1,
style: 'percent',
}).format(contextUsedPercent(usedTokens, maxTokens));
}
/**
* Compact token counts — the web original's exact formatter
* (`Intl.NumberFormat('en-US', { notation: 'compact' })`), byte-parity with its
* "12.3K / 200K" header readout.
*/
export function formatContextTokens(tokens: number): string {
return new Intl.NumberFormat('en-US', { notation: 'compact' }).format(tokens);
}
/**
* The usage-row visibility rule, upstream trap parity: a row whose token count is
* missing or zero renders NOTHING — not a zero line. "Input — 0" is noise in a
* breakdown the user opens to see where the window went.
*/
export function usageRowVisible(tokens: number | undefined): boolean {
return Boolean(tokens);
}
/**
* The composed "used / total" header readout, compact on both sides exactly as the
* web original writes it (`{used} / {total}` with the spaces around the slash).
*/
export function formatContextRatio(usedTokens: number, maxTokens: number): string {
return `${formatContextTokens(usedTokens)} / ${formatContextTokens(maxTokens)}`;
}
Props
| Name | Type | Default | Description |
|---|---|---|---|
| children | React.ReactNode | — | — |
Theming
Themed via text-muted-foreground, border-border, bg-muted, bg-secondary.
Usage
() => (
<Context usedTokens={142_318} maxTokens={200_000} modelId={MODEL_ID}>
<ContextTrigger />
</Context>
)