Row of tinted icon-disc actions with labels — the Send / Top up / Scan pattern under wallet and home headers.
components/ui/quick-actions/quick-actions.tsx@lumioui/core@latest| actions | QuickAction[] | 2–5 items read well on phone widths; each takes equal share. |
| className? | string | |
| itemClassName? | string | Extra classes per item container. |
Row of tinted icon-disc actions with labels — the Send / Top up / Scan pattern under wallet and home headers.
import { QuickActions } from "@/components/ui/quick-actions/quick-actions";
export function Example() {
return (
<QuickActions />
);
} import * as React from "react";
import { forwardRef } from "react";
import { Text, View, type ViewProps, type GestureResponderEvent } from "react-native";
import { cn, useHaptic, usePressAnimation, AnimatedPressable, type ColorTokens, } from "@lumioui/core";
import { Icon, type IconName } from "../icon";
export type QuickActionTone =
| "primary"
| "success"
| "warning"
| "destructive"
| "neutral";
const TONES: Record<QuickActionTone, { disc: string; icon: keyof ColorTokens }> = {
primary: { disc: "bg-primary/10", icon: "primary" },
success: { disc: "bg-success/15", icon: "success" },
warning: { disc: "bg-warning/20", icon: "warning" },
destructive: { disc: "bg-destructive/10", icon: "destructive" },
neutral: { disc: "bg-secondary", icon: "secondaryForeground" },
};
export interface QuickAction {
/** Lucide icon name. */
icon: IconName;
/** Short verb under the disc — keep to one word or two. */
label: string;
onPress?: (e: GestureResponderEvent) => void | Promise<unknown>;
/** Tint of the icon disc. */
tone?: QuickActionTone;
/** Optional badge dot — e.g. an unread count driver. */
badge?: boolean;
}
export interface QuickActionsProps extends Omit<ViewProps, "className"> {
/** 2–5 items read well on phone widths; each takes equal share. */
actions: QuickAction[];
className?: string;
/** Extra classes per item container. */
itemClassName?: string;
}
function QuickActionItem({
action,
className,
}: {
action: QuickAction;
className?: string;
}) {
const haptics = useHaptic();
const press = usePressAnimation({ pressedScale: 0.9 });
const tone = TONES[action.tone ?? "primary"];
const handlePress = (e: GestureResponderEvent) => {
haptics.light();
action.onPress?.(e);
};
return (
<AnimatedPressable
onPress={handlePress}
onPressIn={press.onPressIn}
onPressOut={press.onPressOut}
style={press.animatedStyle}
// 48pt disc + label ≥ 44pt; hitSlop widens the gaps between items.
hitSlop={6}
className={cn("flex-1 items-center gap-1.5", className)}
accessibilityRole="button"
accessibilityLabel={action.label}
>
<View
className={cn(
"h-12 w-12 items-center justify-center rounded-full",
tone.disc
)}
>
<Icon name={action.icon} size="md" color={tone.icon} />
{action.badge ? (
<View className="absolute right-0 top-0 h-2.5 w-2.5 rounded-full border-2 border-background bg-destructive" />
) : null}
</View>
<Text className="text-xs font-medium text-muted-foreground" numberOfLines={1}>
{action.label}
</Text>
</AnimatedPressable>
);
}
/**
* QuickActions — the icon-disc + label row apps put under a header
* (Send / Scan / Top up / More). Equal-width spring-pressed items with
* haptic ticks and per-item tone tinting.
*/
export const QuickActions = forwardRef<
React.ElementRef<typeof View>,
QuickActionsProps
>(function QuickActions({ actions, className, itemClassName, ...props }, ref) {
return (
<View
ref={ref}
className={cn("w-full flex-row", className)}
accessibilityRole="toolbar"
{...props}
>
{actions.map((action) => (
<QuickActionItem key={action.label} action={action} className={itemClassName} />
))}
</View>
);
});
QuickActions.displayName = "QuickActions";