/** * StackCard — a pile of cards, taken one at a time by throwing the top one off. * * ```tsx * decide(people[index], direction)}> * Yes * No * {people.map((person) => ( * * {person.name} * * ))} * * Nobody left * * * ``` * * For a queue of things each answered with one decision and then gone: a * review queue, a set of flashcards, an inbox of suggestions. The gesture is * the answer, which is what makes it quicker than a list of rows with buttons * on them — and what makes it wrong for anything the reader has to compare, * skim or come back to. A deck shows one card and hides the rest. * * For a run of slides the reader browses rather than disposes of, use * [Carousel](../carousel); for one row's actions in a list, [Swipe](../swipe). * * ## The pile is one drag, read by everything * * The top card's `x` and `y` are the only values a gesture writes, and every * other moving part is derived from them: each stamp fades in on how far the * drag has carried the card toward its own direction, and the cards behind * climb toward the top position on the furthest of those. * * That derivation is also what makes a dismissal seamless. By the time the top * card has been carried far enough to leave, the second card is already * exactly where the top card sits — so when the deck advances there is nothing * left for it to move, and no frame in which the pile re-arranges itself. * * ## The deck moves on the UI thread, and React catches up * * Which card is on top is held twice: as React state, for what is mounted and * for the callbacks, and as a shared value, for what is drawn. A throw is * handled entirely on the UI thread — the card leaves on the frame the finger * lets go, and once it is off the screen the top card, the offset and the fade * all move in one step — and only then is the new index requested from React. * * Nothing on the screen waits for a render, so a busy JavaScript thread cannot * stall a throw or leave a frame where the two halves disagree. What React * renders afterwards is kept from showing before the UI thread agrees with it: * a stamp is only drawn on the card the UI thread has on top. * * A controlled deck is still the owner's to decide. An owner that declines the * new index gets the card back, flown in from the way it went; one that * accepts it sees nothing move at all, because it already has. * * One card behind the pile's stated depth stays mounted so it can fade in as * it takes the last visible place, and one card ahead of the top stays mounted * so `undo` has something to fly back in. The rest are unmounted, which is * what makes a deck of five hundred cost what a deck of five costs. * * ## A deck is not reachable by a gesture alone * * A throw is not available to a screen reader, and neither is a card that can * only be answered by throwing it. So the top card publishes an accessibility * action for every direction the deck accepts, and `StackCard.Action` renders * the same decisions as ordinary buttons — which sighted people reach for too, * on the card they are not sure about. */ import { type ReactNode } from 'react'; import { View, type ViewProps } from 'react-native'; import { type SharedValue } from 'react-native-reanimated'; import { type VariantProps } from 'tailwind-variants'; import { type StackCardDirection } from './stack-card-geometry.js'; export type { StackCardDirection } from './stack-card-geometry.js'; /** * The deck's state, for a control that lives outside the pile — a counter, a * progress bar, a button in a toolbar. * * `release` is a shared value running 0 to 1 as the top card is carried toward * leaving, so something beside the deck can move with the drag rather than * starting a second animation next to it. */ export declare function useStackCard(): { /** How many cards have been answered, which is also the top card's index. */ index: number; /** How many cards the deck was given. */ count: number; /** How many are left, the top one included. */ remaining: number; /** Whether there is a card to bring back. */ canUndo: boolean; release: SharedValue; send: (direction: StackCardDirection) => void; undo: () => void; reset: () => void; }; /** How the cards behind the top one are arranged. */ export type StackCardLayout = 'stack' | 'fan' | 'flat'; export interface StackCardHandle { /** Send the top card away as though it had been thrown that way. */ swipe: (direction: StackCardDirection) => void; /** Bring the last card back, and with it the decision that removed it. */ undo: () => void; /** Put every card back. */ reset: () => void; } export interface StackCardProps extends Omit { /** * A `StackCard.Card` for each card, plus any of `StackCard.Stamp`, * `StackCard.Empty` and `StackCard.Actions`, in any order. Anything else is * laid out under the pile. */ children?: ReactNode; /** * Which card is on top, when the caller holds it. Leave unset to let the * deck keep its own. A controlled deck that declines a request stays where * it is and the thrown card comes back, so this is also how a decision is * confirmed before it is taken. */ index?: number; /** Which card an uncontrolled deck starts on. */ defaultIndex?: number; /** Fires whenever the deck asks to move, with the index it is asking for. */ onIndexChange?: (index: number) => void; /** * Fires when a card leaves, with the way it went and the index it was at. * It fires before `onIndexChange` asks for the next index. Not called by * `undo` — the index going back is what reports that. */ onSwipe?: (direction: StackCardDirection, index: number) => void; /** Fires once when the last card leaves. */ onEmpty?: () => void; /** * Which ways a card may be thrown. Left and right by default. * * A direction left out still follows the finger a little and then comes * back, rather than refusing to move at all — a card that does not budge * reads as a frozen screen. */ directions?: readonly StackCardDirection[]; /** * How the cards behind the top one are arranged. `stack` steps them down and * back; `fan` turns them alternately, like a hand of cards; `flat` hides them * entirely, for full-bleed cards where a peeking edge is only clutter. */ layout?: StackCardLayout; /** How many cards are drawn behind the top one. Two is a pile; five is a mess. */ depth?: number; /** * How far a card has to be taken for a release to send it away, as a * fraction of the card. Momentum counts toward it, so a flick clears it * without travelling. */ threshold?: number; /** Stop the deck taking a gesture, without changing how it looks. */ disabled?: boolean; /** A tick when a drag first reaches the point of no return, and a knock as the card goes. */ haptics?: boolean; /** * What a screen reader is offered for each direction, in place of "Swipe * left". Name the decision — `{ left: 'Skip', right: 'Save' }`. */ directionLabels?: Partial>; /** Classes for the whole control. Give it a height; the pile fills what is left. */ className?: string; /** Classes for the box the cards are laid out in. */ pileClassName?: string; } export interface StackCardCardProps extends ViewProps { className?: string; children?: ReactNode; } /** * A stamp is a filled block of colour with a word on it, turned a few degrees * so it reads as pressed onto the card rather than laid out on it. * * The fill is the status colour at full strength rather than a tint of it: a * stamp exists only for the moment it appears, and a six-per-cent wash of the * card's own surface is a smudge rather than an answer. The word is carried in * white, which is what the status colours are chosen to take — a status's * `-foreground` token is its darker text form, meant for a neutral surface, * and over the fill it is the same hue twice. * * It sits in the corner the card is being pulled away from, which is also the * corner the thumb is not over. */ declare const stampVariants: import("tailwind-variants").TVReturnType<{ color: { default: { pill: string; label: string; }; primary: { pill: string; label: string; }; success: { pill: string; label: string; }; warning: { pill: string; label: string; }; info: { pill: string; label: string; }; destructive: { pill: string; label: string; }; }; direction: { left: { root: string; pill: string; }; right: { root: string; pill: string; }; up: { root: string; }; down: { root: string; }; }; }, { root: string; pill: string; label: string; }, undefined, { color: { default: { pill: string; label: string; }; primary: { pill: string; label: string; }; success: { pill: string; label: string; }; warning: { pill: string; label: string; }; info: { pill: string; label: string; }; destructive: { pill: string; label: string; }; }; direction: { left: { root: string; pill: string; }; right: { root: string; pill: string; }; up: { root: string; }; down: { root: string; }; }; }, { root: string; pill: string; label: string; }, import("tailwind-variants").TVReturnType<{ color: { default: { pill: string; label: string; }; primary: { pill: string; label: string; }; success: { pill: string; label: string; }; warning: { pill: string; label: string; }; info: { pill: string; label: string; }; destructive: { pill: string; label: string; }; }; direction: { left: { root: string; pill: string; }; right: { root: string; pill: string; }; up: { root: string; }; down: { root: string; }; }; }, { root: string; pill: string; label: string; }, undefined, unknown, unknown, undefined>>; export type StackCardStampColor = 'default' | 'primary' | 'success' | 'warning' | 'info' | 'destructive'; export interface StackCardStampProps extends Omit, VariantProps { className?: string; /** The word, or anything else to draw on the stamp. */ children?: ReactNode; /** Which direction the stamp answers for. Also where on the card it goes. */ direction?: StackCardDirection; /** Extra classes for the label, when the stamp is given a string. */ labelClassName?: string; } export interface StackCardEmptyProps extends ViewProps { className?: string; children?: ReactNode; } export interface StackCardActionsProps extends ViewProps { className?: string; children?: ReactNode; } declare const actionVariants: import("tailwind-variants").TVReturnType<{ color: { default: { root: string; }; primary: { root: string; }; success: { root: string; }; warning: { root: string; }; info: { root: string; }; destructive: { root: string; }; }; size: { sm: { root: string; }; md: { root: string; }; lg: { root: string; }; }; }, { root: string; }, undefined, { color: { default: { root: string; }; primary: { root: string; }; success: { root: string; }; warning: { root: string; }; info: { root: string; }; destructive: { root: string; }; }; size: { sm: { root: string; }; md: { root: string; }; lg: { root: string; }; }; }, { root: string; }, import("tailwind-variants").TVReturnType<{ color: { default: { root: string; }; primary: { root: string; }; success: { root: string; }; warning: { root: string; }; info: { root: string; }; destructive: { root: string; }; }; size: { sm: { root: string; }; md: { root: string; }; lg: { root: string; }; }; }, { root: string; }, undefined, unknown, unknown, undefined>>; export interface StackCardActionProps extends Omit, VariantProps { className?: string; /** What pressing it does: send the top card that way, or bring the last one back. */ action: StackCardDirection | 'undo'; /** The glyph. Sized and tinted by the button — pass neither. */ icon?: ReactNode; /** * What a screen reader is offered. Falls back to "Undo", or to the plain * name of the direction. */ label?: string; /** Run after the deck has been told, for a sound or a log. */ onPress?: () => void; } export declare const StackCard: import("react").ForwardRefExoticComponent> & { Card: import("react").ForwardRefExoticComponent>; Stamp: import("react").ForwardRefExoticComponent>; Empty: import("react").ForwardRefExoticComponent>; Actions: import("react").ForwardRefExoticComponent>; Action: import("react").ForwardRefExoticComponent>; }; //# sourceMappingURL=index.d.ts.map