@thewhileloop/whileui 4.2.1 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +1235 -663
  2. package/dist/agent/catalog.json +1737 -0
  3. package/dist/agent/llms-components.txt +2787 -0
  4. package/dist/agent/llms-full.txt +2962 -0
  5. package/dist/agent/llms.txt +152 -0
  6. package/dist/blocks/chat/chat.d.ts +10 -2
  7. package/dist/blocks/chat/chat.js +1 -1
  8. package/dist/blocks/chat/index.d.ts +1 -1
  9. package/dist/blocks/commerce/feature-gate.js +2 -2
  10. package/dist/blocks/commerce/metric-card.js +1 -1
  11. package/dist/blocks/commerce/subscription-card.d.ts +7 -0
  12. package/dist/blocks/commerce/usage-bar.d.ts +4 -0
  13. package/dist/blocks/layout/app-shell.d.ts +4 -0
  14. package/dist/blocks/layout/empty-state.js +1 -1
  15. package/dist/blocks/layout/error-state.js +1 -1
  16. package/dist/blocks/layout/page-skeleton.d.ts +1 -0
  17. package/dist/blocks/layout/pull-to-refresh-scroll-view.d.ts +1 -0
  18. package/dist/blocks/layout/screen-skeleton.d.ts +5 -0
  19. package/dist/blocks/layout/sheet.js +1 -1
  20. package/dist/blocks/lists/list-item.js +1 -1
  21. package/dist/blocks/lists/notification-item.js +1 -1
  22. package/dist/blocks/media/smart-image.js +4 -2
  23. package/dist/blocks/navigation/bottom-nav.js +1 -1
  24. package/dist/blocks/navigation/drawer-menu.js +2 -2
  25. package/dist/blocks/navigation/floating-bottom-nav.js +3 -1
  26. package/dist/blocks/navigation/tab-bar.js +1 -1
  27. package/dist/components/alert/alert.d.ts +7 -0
  28. package/dist/components/alert/alert.js +23 -4
  29. package/dist/components/button/button.d.ts +3 -0
  30. package/dist/components/button/button.js +2 -1
  31. package/dist/components/form-field/form-field.d.ts +18 -0
  32. package/dist/components/form-field/form-field.js +25 -4
  33. package/dist/components/input/input.js +2 -2
  34. package/dist/components/numeric-input/numeric-input.d.ts +3 -0
  35. package/dist/components/numeric-input/numeric-input.js +2 -2
  36. package/dist/components/otp-input/otp-input.d.ts +8 -0
  37. package/dist/components/pressable/pressable.d.ts +1 -0
  38. package/dist/components/segmented-control/segmented-control.d.ts +1 -0
  39. package/dist/components/surface/surface.d.ts +5 -0
  40. package/dist/components/textarea/textarea.js +2 -2
  41. package/dist/components/toast/toast.d.ts +10 -10
  42. package/dist/components/toast/toast.js +53 -35
  43. package/dist/index.d.ts +1 -0
  44. package/dist/index.js +1 -0
  45. package/dist/lib/frosted-surface.d.ts +3 -0
  46. package/dist/lib/theme-colors.d.ts +4 -3
  47. package/dist/lib/theme-colors.js +26 -1
  48. package/dist/lib/theme-contract.js +12 -12
  49. package/dist/lib/theme-presets.js +9 -9
  50. package/dist/lib/theme-seeds.d.ts +40 -0
  51. package/dist/lib/theme-seeds.js +143 -0
  52. package/dist/lib/visual-tokens.js +13 -0
  53. package/dist/metro.cjs +12 -5
  54. package/dist/tokens.d.ts +1 -0
  55. package/dist/tokens.js +1 -0
  56. package/package.json +3 -2
@@ -0,0 +1,152 @@
1
+ # WhileUI 4.3.0
2
+
3
+ > React Native components and blocks for iOS, Android and web, styled with Uniwind and
4
+ > themed through a typed token contract. Package: `@thewhileloop/whileui`.
5
+
6
+ Files in this folder:
7
+ - llms-components.txt — every component and block with its API section
8
+ - llms-full.txt — the complete README, including setup and theming
9
+ - catalog.json — the same data, structured (served by `@thewhileloop/whileui-mcp`)
10
+
11
+ Components import from `@thewhileloop/whileui`. Copy-paste templates are not in the
12
+ package; copy them from https://github.com/whileloophq/whileui/tree/main/apps/showcase/catalog/templates.
13
+
14
+ ## Quick Reference (AI / Code Generation)
15
+
16
+ - **Full-screen:** `AppShell` + `Header` in `header` + `BottomNav` in `bottomNav` + content in `children`
17
+ - **No-jump loading:** keep `AppShell` mounted, set `loading` + `skeleton` (e.g., `PageSkeleton`/`ScreenSkeleton`)
18
+ - **Layout:** `Stack` (vertical), `Row` (horizontal) — both support `gap`, `align`, `justify`
19
+ - **Auth callbacks:** Auth templates use objects: `onSubmit({ email, password })`, `onSubmit({ firstName, lastName, email, password })`, etc. Copy templates from `apps/showcase/catalog/templates/auth/`.
20
+ - **PortalHost:** Add `<PortalHost />` at app root for Select, Popover, Tooltip, HoverCard.
21
+ Inside any WhileUI overlay they render into that overlay's layer without configuration.
22
+ - **Motion:** Wrap the root in `<MotionProvider>`; use `useMotionPolicy()` for semantic
23
+ durations, the responsive spring, and reduced-motion behavior.
24
+ - **Metro:** `withWhileUIMetro(config, { cssEntryFile: './global.css' })` from
25
+ `@thewhileloop/whileui/metro` — wraps Uniwind and keeps icon fonts loading on device.
26
+ `global.css` at app root, imported in `App.tsx`. Installing from npm also needs
27
+ `@source './node_modules/@thewhileloop/whileui/dist';` or every component renders unstyled.
28
+ - **States:** `LoadingState`, `EmptyState` and `ErrorState` share one shape; `ErrorState`
29
+ takes `onRetry`. `Text variant="eyebrow"` is the muted section label above a title.
30
+ - **Reference:** Block props in `packages/ui/src/blocks` (core) and `apps/showcase/catalog/templates/` (auth, profile); flow patterns in README "Flow Patterns" section.
31
+
32
+ ### Composition grammar
33
+
34
+ - Start with one page canvas and flat sections. Add a `Surface` only when grouping or
35
+ elevation communicates something; do not wrap every section in a card.
36
+ - Use `level="base"` for grouped content, `raised` for selectable or floating regions,
37
+ and `overlay` for content above the page. Avoid nesting more than one surfaced level.
38
+ - Express hierarchy through `Text` variants before custom font-size classes. Keep one
39
+ primary action per region and constrain long-form content with `Container`.
40
+ - Forms use `FormField`; its disabled and invalid state flows into `Input`, `Textarea`,
41
+ `NumericInput`, and `Select`. Pass a control prop only to override that inherited state.
42
+ - Chat mechanics belong to `ChatConversation` + `SmartInput`. Message payloads, tools,
43
+ reasoning, approvals, sources, and action rows are application-owned compositions.
44
+ - Import Markdown and code rendering from their optional subpaths. Do not make the root
45
+ package pay for content rendering when the screen does not use it.
46
+
47
+ ## Components and blocks
48
+
49
+ - **Text** (Primitives) — Themed text with variant support
50
+ - **Surface** (Primitives) — Token-backed base, raised, and overlay container
51
+ - **View** (Primitives) — Themed view wrapper
52
+ - **Pressable** (Primitives) — Themed pressable wrapper
53
+ - **Stack** (Primitives) — Vertical flex layout with gap
54
+ - **Row** (Primitives) — Horizontal flex layout with gap
55
+ - **MotionProvider** (Root Providers) — System reduced motion, explicit preview modes, and deterministic testing
56
+ - **Button** (Form Controls) — 4 sizes, ButtonText & ButtonIcon sub-components
57
+ - **Input** (Form Controls) — TextInput wrapper with themed styling
58
+ - **NumericInput** (Form Controls) — Numeric input with prefix/suffix slots, optional steppers, and compact size
59
+ - **OTPInput** (Form Controls) — Verification code input with auto-focus, paste, secure mask, loading skeleton, error shake
60
+ - **FormField** (Form Controls) — Compound API: FormField, FormLabel, FormControl, FormHint, FormMessage
61
+ - **LabeledField** (Form Controls) — Field wrapper with label/helper/error plus left/right slots
62
+ - **Textarea** (Form Controls) — Multi-line text input
63
+ - **Checkbox** (Form Controls) — Controlled/uncontrolled, accessibility roles
64
+ - **Switch** (Form Controls) — Controlled/uncontrolled, accessibility roles
65
+ - **RadioGroup** (Form Controls) — RadioGroup + RadioGroupItem
66
+ - **Select** (Form Controls) — Uses `SelectOption` type `{value, label}`. Includes SelectGroup, SelectLabel, SelectSeparator
67
+ - **Label** (Form Controls) — Form field label
68
+ - **SegmentedControl** (Form Controls) — SegmentedControl, SegmentedControlItem, SegmentedControlItemText with wrapping layout support
69
+ - **Toggle** (Form Controls) — ToggleText sub-component
70
+ - **ToggleGroup** (Form Controls) — Group of toggle items
71
+ - **Card** (Display) — Compound: Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter
72
+ - **Surface** (Display) — Shared radius, padding, border, shadow, and optional frosted treatment
73
+ - **Badge** (Display) — BadgeText sub-component
74
+ - **FAB** (Display) — Floating action button. `label` required; pass your own icon as children
75
+ - **Alert** (Display) — AlertTitle & AlertDescription
76
+ - **Avatar** (Display) — AvatarImage + AvatarFallback
77
+ - **DataRow** (Display) — DataRow, DataRowLeft/Center/Right, DataRowLabel/Description/Value
78
+ - **Separator** (Display) — Themed divider
79
+ - **Progress** (Display) — Value-based progress bar with accessibility
80
+ - **Spinner** (Display) — ActivityIndicator wrapper
81
+ - **Skeleton** (Display) — Loading placeholder (pulse = opacity fade, shimmer = sweep)
82
+ - **AspectRatio** (Display) — Maintain aspect ratio container
83
+ - **Tabs** (Layout) — TabsList, TabsTrigger, TabsContent
84
+ - **Accordion** (Layout) — AccordionItem, AccordionTrigger, Content
85
+ - **Collapsible** (Layout) — CollapsibleTrigger, CollapsibleContent
86
+ - **Container** (Layout) — Centred content column with a max width
87
+ - **Dialog** (Overlays & Menus) — Modal dialog with Header, Footer, Title, Description
88
+ - **AlertDialog** (Overlays & Menus) — Confirmation dialog with Action/Cancel buttons
89
+ - **Popover** (Overlays & Menus) — Position-aware popover (requires PortalHost)
90
+ - **Tooltip** (Overlays & Menus) — Position-aware tooltip (requires PortalHost)
91
+ - **DropdownMenu** (Overlays & Menus) — DropdownMenuTrigger, Content, Item, Label, Separator
92
+ - **ContextMenu** (Overlays & Menus) — Long-press context menu
93
+ - **HoverCard** (Overlays & Menus) — Position-aware hover card (requires PortalHost)
94
+ - **Menubar** (Overlays & Menus) — Horizontal menu bar
95
+ - **Toast** (Feedback) — ToastProvider, ToastContainer, useToast() hook
96
+ - **SignInForm** (Auth, copy-paste template) — Email/password sign in with callbacks
97
+ - **SignUpForm** (Auth, copy-paste template) — Registration form with callbacks
98
+ - **ForgotPasswordForm** (Auth, copy-paste template) — Password reset request
99
+ - **ResetPasswordForm** (Auth, copy-paste template) — Set new password
100
+ - **VerifyEmailForm** (Auth, copy-paste template) — Email verification code input
101
+ - **SocialConnections** (Auth, copy-paste template) — OAuth provider buttons
102
+ - **UserMenu** (Auth, copy-paste template) — Profile dropdown for auth flows
103
+ - **AppShell** (Navigation) — Layout shell with header/footer/bottomNav slots
104
+ - **Header** (Navigation) — Top app bar with back/actions
105
+ - **BottomNav** (Navigation) — Tab-style bottom navigation bar
106
+ - **FloatingBottomNav** (Navigation) — Elevated bottom nav with safe area support
107
+ - **TabBar** (Navigation) — Top tab bar with indicator
108
+ - **DrawerMenu** (Navigation) — Drawer with sections and items
109
+ - **ActionBar** (Layout) — Sticky bottom action row with safe-area padding
110
+ - **ConfirmActionSheet** (Layout) — Reusable destructive confirmation sheet
111
+ - **Sheet** (Layout) — Bottom sheet modal with header/content/footer slots
112
+ - **FormModalScreen** (Layout) — Modal scaffold for forms with loading states
113
+ - **ContentSkeleton** (Layout) — Page/content placeholder with variants (list, card, generic)
114
+ - **PageSkeleton** (Layout) — Variant-based page layouts (dashboard, list, settings, card, generic)
115
+ - **ScreenSkeleton** (Layout) — Header slot/placeholder + PageSkeleton content in one block
116
+ - **ErrorBoundary** (Layout) — React ErrorBoundary that renders ErrorState by default
117
+ - **EmptyState** (Layout) — Empty content placeholder
118
+ - **ErrorState** (Layout) — Error display with retry
119
+ - **LoadingState** (Layout) — In-flow loading placeholder for a section still fetching
120
+ - **LoadingScreen** (Layout) — Full-screen loading indicator
121
+ - **PullToRefreshScrollView** (Layout) — Themed ScrollView with RefreshControl (useThemeTokens for colors)
122
+ - **KeyboardAwareScreen** (Layout) — One screen-level keyboard policy for bottom-docked controls
123
+ - **DockedInput** (Layout) — Safe-area-aware bottom dock that reports its complete height
124
+ - **SmartInput** (Layout) — Compose input with slots, variants, and optional rotating examples
125
+ - **SplashScreen** (Layout) — Branded splash (fade/scale/slide variants)
126
+ - **MinimalSplash** (Layout) — Minimal monochrome splash
127
+ - **BrandedSplash** (Layout) — Splash with brand imagery
128
+ - **Chat** (Chat) — Opinionated messages, suggestions, and SmartInput composition
129
+ - **ChatConversation** (Chat) — Constrained conversation viewport that follows streaming output
130
+ - **ChatMessageBubble** (Chat) — Message bubble (user/assistant, big/small text)
131
+ - **ChatSuggestions** (Chat) — Suggestion chips when empty
132
+ - **ProfileHeader** (Profile & Settings, copy-paste template) — Profile header with stats
133
+ - **AccountCard** (Profile & Settings, copy-paste template) — Account summary card
134
+ - **SettingsSection** (Profile & Settings, copy-paste template) — Section header with optional action
135
+ - **SettingsItem** (Profile & Settings, copy-paste template) — Row for toggles/links/settings
136
+ - **ListItem** (Lists) — Title/subtitle row
137
+ - **NotificationItem** (Lists) — Notification row with metadata
138
+ - **TimelineFeed** (Lists) — Vertical feed with connecting lines
139
+ - **ProductCard** (Commerce) — Product card with badge/media
140
+ - **PricingCard** (Commerce) — Pricing tiers with feature list
141
+ - **CheckoutSummary** (Commerce) — Cart summary with line items
142
+ - **MetricCard** (Commerce) — Stats/progress card for dashboards
143
+ - **SubscriptionCard** (Commerce) — Current plan card with manage/upgrade actions
144
+ - **FeatureGate** (Commerce) — Locked feature state with upgrade CTA
145
+ - **UsageBar** (Commerce) — Quota meter with warning/exceeded states
146
+ - **PlanToggle** (Commerce) — Monthly vs annual plan switcher
147
+ - **UpgradeBanner** (Commerce) — Inline upgrade banner with action/dismiss
148
+ - **SmartImage** (Media) — Image with explicit loading, loaded, fallback, and error lifecycle
149
+ - **DatePickerModal** (Date Picker) — Bottom sheet modal with calendar, compact trigger
150
+ - **DatePickerInline** (Date Picker) — Inline calendar for forms or dashboards
151
+ - **DateRangePickerModal** (Date Picker) — Range selection modal with period marking
152
+ - **TimePickerModal** (Date Picker) — Hour and minute wheels in the same bottom sheet
@@ -14,11 +14,16 @@ export interface ChatSuggestionsProps {
14
14
  className?: string;
15
15
  }
16
16
  export interface ChatProps {
17
+ /** Messages to show, each `{ id, role, content, secondary?, contentSize? }`. */
17
18
  messages: ChatMessage[];
19
+ /** Input value. */
18
20
  value: string;
21
+ /** Input change. */
19
22
  onChangeText: (text: string) => void;
23
+ /** Send handler. */
20
24
  onSend: () => void;
21
25
  placeholder?: string;
26
+ /** Chips when empty. */
22
27
  suggestions?: string[];
23
28
  onSuggestionPress?: (text: string) => void;
24
29
  leftSlot?: React.ReactNode;
@@ -40,9 +45,12 @@ export interface ChatProps {
40
45
  className?: string;
41
46
  messagesClassName?: string;
42
47
  }
43
- export declare function ChatMessageBubble({ message, contentClassName, }: {
48
+ export interface ChatMessageBubbleProps {
49
+ /** The message to render; its `role` decides alignment and colour. */
44
50
  message: ChatMessage;
51
+ /** Classes for the bubble's content wrapper. */
45
52
  contentClassName?: string;
46
- }): React.JSX.Element;
53
+ }
54
+ export declare function ChatMessageBubble({ message, contentClassName }: ChatMessageBubbleProps): React.JSX.Element;
47
55
  export declare function ChatSuggestions({ suggestions, onSelect, className }: ChatSuggestionsProps): React.JSX.Element;
48
56
  export declare function Chat({ messages, value, onChangeText, onSend, placeholder, suggestions, onSuggestionPress, leftSlot, rightSlot, loading, header, emptyTitle, emptyDescription, exampleMessage, renderMessage, loadingIndicator, inputSafeArea, keyboardVerticalOffset, className, messagesClassName, }: ChatProps): React.JSX.Element;
@@ -25,7 +25,7 @@ const chatMessageBubbleVariants = tv({
25
25
  },
26
26
  defaultVariants: { role: 'assistant' },
27
27
  });
28
- export function ChatMessageBubble({ message, contentClassName, }) {
28
+ export function ChatMessageBubble({ message, contentClassName }) {
29
29
  const role = message.role === 'system' ? 'system' : message.role;
30
30
  const isUser = message.role === 'user';
31
31
  const contentRole = CONTENT_SIZE_ROLE[message.contentSize ?? 'default'];
@@ -1,2 +1,2 @@
1
- export { Chat, ChatMessageBubble, ChatSuggestions, type ChatMessage, type ChatProps, type ChatSuggestionsProps, } from './chat';
1
+ export { Chat, ChatMessageBubble, ChatSuggestions, type ChatMessage, type ChatMessageBubbleProps, type ChatProps, type ChatSuggestionsProps, } from './chat';
2
2
  export { ChatConversation, type ChatConversationProps } from './chat-conversation';
@@ -11,7 +11,7 @@ export function FeatureGate({ title, description, icon, buttonLabel = 'Upgrade',
11
11
  const body = typographyStyle(visual, 'body');
12
12
  const label = typographyStyle(visual, 'label');
13
13
  if (!children) {
14
- return (_jsx(Card, { className: cn('border-dashed border-border', className), padding: "lg", style: style, ...props, children: _jsxs(View, { className: "items-start gap-3", children: [icon ? _jsx(View, { children: icon }) : null, _jsxs(View, { className: "gap-1", children: [_jsx(Text, { className: "font-semibold text-foreground", style: body, children: title }), _jsx(Text, { className: "text-muted-foreground", style: label, children: description })] }), _jsx(Button, { onPress: onUpgrade, disabled: !onUpgrade, children: _jsx(ButtonText, { children: buttonLabel }) })] }) }));
14
+ return (_jsx(Card, { className: cn('border-dashed border-border', className), padding: "lg", style: style, ...props, children: _jsxs(View, { className: "items-start gap-3", children: [icon ? _jsx(View, { "aria-hidden": true, children: icon }) : null, _jsxs(View, { className: "gap-1", children: [_jsx(Text, { className: "font-semibold text-foreground", style: body, children: title }), _jsx(Text, { className: "text-muted-foreground", style: label, children: description })] }), _jsx(Button, { onPress: onUpgrade, disabled: !onUpgrade, children: _jsx(ButtonText, { children: buttonLabel }) })] }) }));
15
15
  }
16
16
  return (_jsxs(View, { className: cn('overflow-hidden border border-border', className), style: [
17
17
  {
@@ -19,5 +19,5 @@ export function FeatureGate({ title, description, icon, buttonLabel = 'Upgrade',
19
19
  borderWidth: visual.borderWidthHairline,
20
20
  },
21
21
  style,
22
- ], ...props, children: [_jsx(View, { className: "absolute inset-0 justify-center opacity-45", children: children }), _jsxs(View, { className: "items-center justify-center bg-background/70 px-6", style: { paddingVertical: surfacePadding(visual, 'default') }, children: [icon ? _jsx(View, { className: "mb-3", children: icon }) : null, _jsx(Text, { className: "text-center font-semibold text-foreground", style: body, children: title }), _jsx(Text, { className: "mt-1 text-center text-muted-foreground", style: label, children: description }), _jsx(Button, { className: "mt-4", onPress: onUpgrade, disabled: !onUpgrade, children: _jsx(ButtonText, { children: buttonLabel }) })] })] }));
22
+ ], ...props, children: [_jsx(View, { className: "absolute inset-0 justify-center opacity-45", children: children }), _jsxs(View, { className: "items-center justify-center bg-background/70 px-6", style: { paddingVertical: surfacePadding(visual, 'default') }, children: [icon ? (_jsx(View, { "aria-hidden": true, className: "mb-3", children: icon })) : null, _jsx(Text, { className: "text-center font-semibold text-foreground", style: body, children: title }), _jsx(Text, { className: "mt-1 text-center text-muted-foreground", style: label, children: description }), _jsx(Button, { className: "mt-4", onPress: onUpgrade, disabled: !onUpgrade, children: _jsx(ButtonText, { children: buttonLabel }) })] })] }));
23
23
  }
@@ -26,7 +26,7 @@ function MetricCard({ className, label, value, subtitle, progress, segments, ico
26
26
  if (loading) {
27
27
  return _jsx(MetricCardSkeleton, { className: className, variant: variant ?? 'default' });
28
28
  }
29
- return (_jsxs(Card, { className: cn(metricCardVariants({ variant }), className), ...props, children: [_jsxs(View, { className: "flex-row items-start justify-between mb-2", children: [_jsx(Text, { variant: "caption", className: "text-muted-foreground", children: label }), icon && _jsx(View, { children: icon })] }), _jsx(Text, { variant: "heading", className: "mb-1", children: value }), subtitle && (_jsx(Text, { variant: "caption", className: "text-muted-foreground", children: subtitle })), progress !== undefined && (_jsx(View, { className: "mt-3", children: _jsx(View, { className: "h-2 bg-muted overflow-hidden", style: { borderRadius: visual.radiusSm }, children: _jsx(View, { className: "h-full bg-primary", style: {
29
+ return (_jsxs(Card, { className: cn(metricCardVariants({ variant }), className), ...props, children: [_jsxs(View, { className: "flex-row items-start justify-between mb-2", children: [_jsx(Text, { variant: "caption", className: "text-muted-foreground", children: label }), icon && _jsx(View, { "aria-hidden": true, children: icon })] }), _jsx(Text, { variant: "heading", className: "mb-1", children: value }), subtitle && (_jsx(Text, { variant: "caption", className: "text-muted-foreground", children: subtitle })), progress !== undefined && (_jsx(View, { className: "mt-3", children: _jsx(View, { className: "h-2 bg-muted overflow-hidden", style: { borderRadius: visual.radiusSm }, children: _jsx(View, { className: "h-full bg-primary", style: {
30
30
  borderRadius: visual.radiusSm,
31
31
  width: `${Math.min(100, Math.max(0, progress))}%`,
32
32
  } }) }) })), segments && segments.length > 0 && (_jsx(View, { className: "mt-3 flex-row h-3 overflow-hidden", style: { borderRadius: visual.radiusSm }, children: segments.map((seg) => (_jsx(View, { className: "h-full", style: { width: `${seg.value}%`, backgroundColor: seg.color } }, `${seg.color}:${seg.value}`))) }))] }));
@@ -1,11 +1,18 @@
1
1
  import { type ViewProps } from 'react-native';
2
2
  export interface SubscriptionCardProps extends ViewProps {
3
+ /** Current plan label. */
3
4
  planName: string;
5
+ /** Plan price. */
4
6
  price: string;
7
+ /** Billing period label. */
5
8
  period?: string;
9
+ /** Renewal/expiry date text. */
6
10
  expiresAt?: string;
11
+ /** Active/inactive badge state. */
7
12
  isActive?: boolean;
13
+ /** Manage action. */
8
14
  onManage?: () => void;
15
+ /** Upgrade action. */
9
16
  onUpgrade?: () => void;
10
17
  manageLabel?: string;
11
18
  upgradeLabel?: string;
@@ -1,9 +1,13 @@
1
1
  import { type ViewProps } from 'react-native';
2
2
  export type UsageBarVariant = 'default' | 'warning' | 'exceeded';
3
3
  export interface UsageBarProps extends ViewProps {
4
+ /** Usage label. */
4
5
  label: string;
6
+ /** Used amount. */
5
7
  used: number;
8
+ /** Quota limit. */
6
9
  limit: number;
10
+ /** Optional explicit visual state. */
7
11
  variant?: UsageBarVariant;
8
12
  className?: string;
9
13
  }
@@ -1,9 +1,13 @@
1
1
  import React from 'react';
2
2
  import { type ViewProps } from 'react-native';
3
3
  export interface AppShellProps extends ViewProps {
4
+ /** Header slot. */
4
5
  header?: React.ReactNode;
6
+ /** Footer slot. */
5
7
  footer?: React.ReactNode;
8
+ /** Bottom navigation slot. */
6
9
  bottomNav?: React.ReactNode;
10
+ /** Wrap in SafeAreaView. */
7
11
  safeArea?: boolean;
8
12
  /** Keep shell mounted while swapping content with skeleton. */
9
13
  loading?: boolean;
@@ -14,5 +14,5 @@ function DefaultEmptyIcon() {
14
14
  }
15
15
  export function EmptyState({ icon, title, description, action, secondaryAction, className, style, ...props }) {
16
16
  const visual = useVisualTokens();
17
- return (_jsxs(View, { className: cn('flex-1 items-center justify-center', className), style: [{ padding: surfacePadding(visual, 'lg') }, style], ...props, children: [_jsx(View, { className: "mb-6", children: icon ? _jsx(View, { className: "text-muted-foreground", children: icon }) : _jsx(DefaultEmptyIcon, {}) }), _jsx(Text, { className: "mb-2 text-center font-semibold text-foreground", style: typographyStyle(visual, 'title'), children: title }), description && (_jsx(Text, { className: "mb-6 max-w-xs text-center text-muted-foreground", children: description })), action && (_jsx(Button, { onPress: action.onPress, className: "mb-3", children: _jsx(ButtonText, { children: action.label }) })), secondaryAction && (_jsx(Button, { variant: "ghost", onPress: secondaryAction.onPress, children: _jsx(ButtonText, { children: secondaryAction.label }) }))] }));
17
+ return (_jsxs(View, { className: cn('flex-1 items-center justify-center', className), style: [{ padding: surfacePadding(visual, 'lg') }, style], ...props, children: [_jsx(View, { className: "mb-6", children: icon ? (_jsx(View, { "aria-hidden": true, className: "text-muted-foreground", children: icon })) : (_jsx(DefaultEmptyIcon, {})) }), _jsx(Text, { className: "mb-2 text-center font-semibold text-foreground", style: typographyStyle(visual, 'title'), children: title }), description && (_jsx(Text, { className: "mb-6 max-w-xs text-center text-muted-foreground", children: description })), action && (_jsx(Button, { onPress: action.onPress, className: "mb-3", children: _jsx(ButtonText, { children: action.label }) })), secondaryAction && (_jsx(Button, { variant: "ghost", onPress: secondaryAction.onPress, children: _jsx(ButtonText, { children: secondaryAction.label }) }))] }));
18
18
  }
@@ -7,5 +7,5 @@ export function ErrorState({ icon, title = 'Something went wrong', description,
7
7
  const errorMessage = description ||
8
8
  (error instanceof Error ? error.message : error) ||
9
9
  'An unexpected error occurred';
10
- return (_jsxs(View, { className: cn('flex-1 items-center justify-center p-8', className), ...props, children: [icon && _jsx(View, { className: "mb-6 text-destructive", children: icon }), _jsx(Text, { className: "mb-2 text-center text-xl font-semibold text-foreground", children: title }), _jsx(Text, { className: "mb-6 max-w-xs text-center text-muted-foreground", children: errorMessage }), onRetry && (_jsx(Button, { onPress: onRetry, children: _jsx(ButtonText, { children: retryLabel }) }))] }));
10
+ return (_jsxs(View, { className: cn('flex-1 items-center justify-center p-8', className), ...props, children: [icon && (_jsx(View, { "aria-hidden": true, className: "mb-6 text-destructive", children: icon })), _jsx(Text, { className: "mb-2 text-center text-xl font-semibold text-foreground", children: title }), _jsx(Text, { className: "mb-6 max-w-xs text-center text-muted-foreground", children: errorMessage }), onRetry && (_jsx(Button, { onPress: onRetry, children: _jsx(ButtonText, { children: retryLabel }) }))] }));
11
11
  }
@@ -4,6 +4,7 @@ export type PageSkeletonVariant = 'dashboard' | 'list' | 'settings' | 'card' | '
4
4
  export type PageSkeletonPadding = 'none' | 'sm' | 'default' | 'lg';
5
5
  export type PageSkeletonHeaderPlaceholder = 'compact' | 'default';
6
6
  export interface PageSkeletonProps extends ViewProps {
7
+ /** Layout preset. */
7
8
  variant: PageSkeletonVariant;
8
9
  /** Rows/items for list (default 3) or settings (default 4) variant. */
9
10
  count?: number;
@@ -1,5 +1,6 @@
1
1
  import { type ScrollViewProps } from 'react-native';
2
2
  export interface PullToRefreshScrollViewProps extends Omit<ScrollViewProps, 'refreshControl'> {
3
+ /** Whether refresh is in progress. */
3
4
  refreshing: boolean;
4
5
  /**
5
6
  * Refreshing is almost always async, so the signature says so. Nothing here awaits it —
@@ -2,10 +2,15 @@ import { type ViewProps } from 'react-native';
2
2
  import { type ReactNode } from 'react';
3
3
  import { type PageSkeletonHeaderPlaceholder, type PageSkeletonPadding, type PageSkeletonVariant } from './page-skeleton';
4
4
  export interface ScreenSkeletonProps extends Omit<ViewProps, 'children'> {
5
+ /** Skeleton content preset. */
5
6
  variant?: PageSkeletonVariant;
7
+ /** Rows/items for list/settings variants. */
6
8
  count?: number;
9
+ /** Inner content padding. */
7
10
  padding?: PageSkeletonPadding;
11
+ /** Real header slot. */
8
12
  header?: ReactNode;
13
+ /** Placeholder header. */
9
14
  headerPlaceholder?: boolean | PageSkeletonHeaderPlaceholder;
10
15
  className?: string;
11
16
  }
@@ -79,7 +79,7 @@ export function Sheet({ open, onOpenChange, children, maxHeight = 'full', maxWid
79
79
  widthStyle,
80
80
  frostedSurface.surfaceStyle,
81
81
  ];
82
- return (_jsx(OverlayHost, { open: mounted, animationType: "none", onRequestClose: onClose, children: _jsxs(View, { className: "flex-1 justify-end", children: [_jsx(Animated.View, { style: [
82
+ return (_jsx(OverlayHost, { open: mounted, animationType: "none", onRequestClose: open ? onClose : undefined, children: _jsxs(View, { className: "flex-1 justify-end", children: [_jsx(Animated.View, { style: [
83
83
  StyleSheet.absoluteFill,
84
84
  { backgroundColor: frosted ? 'transparent' : colors.overlay },
85
85
  backdropStyle,
@@ -27,5 +27,5 @@ export function ListItem({ icon, title, subtitle, description, rightIcon, rightT
27
27
  return (_jsxs(Pressable, { accessibilityRole: props.onPress || props.onLongPress ? 'button' : undefined, className: cn('flex-row items-center bg-card', focusRing, showBorder && 'border-border', 'active:bg-muted', className), style: (state) => {
28
28
  const callerStyle = typeof styleProp === 'function' ? styleProp(state) : styleProp;
29
29
  return [rowStyle, callerStyle];
30
- }, ...props, children: [icon && _jsx(View, { className: "mr-3 text-muted-foreground", children: icon }), _jsxs(View, { className: "flex-1", children: [_jsx(Text, { className: "text-foreground", style: typographyStyle(visual, 'body'), children: title }), subtitle && (_jsx(Text, { className: "text-muted-foreground", style: typographyStyle(visual, 'label'), children: subtitle })), description && (_jsx(Text, { className: "mt-1 text-muted-foreground", style: typographyStyle(visual, 'label'), numberOfLines: 2, children: description }))] }), rightText && (_jsx(Text, { className: "mr-2 text-muted-foreground", style: typographyStyle(visual, 'label'), children: rightText })), rightIcon && _jsx(View, { className: "text-muted-foreground", children: rightIcon }), action, !action && !rightIcon && (_jsx(Text, { className: "text-muted-foreground", style: typographyStyle(visual, 'emphasis'), children: "\u203A" }))] }));
30
+ }, ...props, children: [icon && (_jsx(View, { "aria-hidden": true, className: "mr-3 text-muted-foreground", children: icon })), _jsxs(View, { className: "flex-1", children: [_jsx(Text, { className: "text-foreground", style: typographyStyle(visual, 'body'), children: title }), subtitle && (_jsx(Text, { className: "text-muted-foreground", style: typographyStyle(visual, 'label'), children: subtitle })), description && (_jsx(Text, { className: "mt-1 text-muted-foreground", style: typographyStyle(visual, 'label'), numberOfLines: 2, children: description }))] }), rightText && (_jsx(Text, { className: "mr-2 text-muted-foreground", style: typographyStyle(visual, 'label'), children: rightText })), rightIcon && (_jsx(View, { "aria-hidden": true, className: "text-muted-foreground", children: rightIcon })), action, !action && !rightIcon && (_jsx(Text, { className: "text-muted-foreground", style: typographyStyle(visual, 'emphasis'), children: "\u203A" }))] }));
31
31
  }
@@ -27,5 +27,5 @@ export function NotificationItem({ icon, avatarUrl, avatarFallback, title, messa
27
27
  return (_jsxs(Pressable, { accessibilityRole: props.onPress || props.onLongPress ? 'button' : undefined, className: cn('flex-row items-start active:bg-muted', focusRing, !read && 'bg-primary-soft-subtle', className), style: (state) => {
28
28
  const callerStyle = typeof styleProp === 'function' ? styleProp(state) : styleProp;
29
29
  return [rowStyle, callerStyle];
30
- }, ...props, children: [avatarUrl || avatarFallback ? (_jsxs(Avatar, { className: "h-10 w-10", children: [avatarUrl && _jsx(AvatarImage, { src: avatarUrl }), _jsx(AvatarFallback, { children: avatarFallback })] })) : (icon && (_jsx(View, { className: "h-10 w-10 items-center justify-center rounded-full bg-muted", children: icon }))), _jsxs(View, { className: "ml-3 flex-1", children: [_jsxs(View, { className: "flex-row items-center justify-between", children: [_jsx(Text, { className: cn(!read ? 'font-semibold text-foreground' : 'text-foreground'), style: typographyStyle(visual, 'label'), children: title }), _jsxs(View, { className: "flex-row items-center gap-2", children: [_jsx(Text, { className: "text-muted-foreground", style: typographyStyle(visual, 'caption'), children: time }), !read && showDot && _jsx(View, { className: "h-2 w-2 rounded-full bg-primary" })] })] }), _jsx(Text, { className: "mt-0.5 text-muted-foreground", style: typographyStyle(visual, 'label'), numberOfLines: 2, children: message })] })] }));
30
+ }, ...props, children: [avatarUrl || avatarFallback ? (_jsxs(Avatar, { className: "h-10 w-10", children: [avatarUrl && _jsx(AvatarImage, { src: avatarUrl }), _jsx(AvatarFallback, { children: avatarFallback })] })) : (icon && (_jsx(View, { "aria-hidden": true, className: "h-10 w-10 items-center justify-center rounded-full bg-muted", children: icon }))), _jsxs(View, { className: "ml-3 flex-1", children: [_jsxs(View, { className: "flex-row items-center justify-between", children: [_jsx(Text, { className: cn(!read ? 'font-semibold text-foreground' : 'text-foreground'), style: typographyStyle(visual, 'label'), children: title }), _jsxs(View, { className: "flex-row items-center gap-2", children: [_jsx(Text, { className: "text-muted-foreground", style: typographyStyle(visual, 'caption'), children: time }), !read && showDot && _jsx(View, { className: "h-2 w-2 rounded-full bg-primary" })] })] }), _jsx(Text, { className: "mt-0.5 text-muted-foreground", style: typographyStyle(visual, 'label'), numberOfLines: 2, children: message })] })] }));
31
31
  }
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React, { useState } from 'react';
3
- import { View } from 'react-native';
3
+ import { Platform, View } from 'react-native';
4
4
  import { Image } from 'expo-image';
5
5
  import { Skeleton } from '../../components/skeleton';
6
6
  import { cn } from '../../lib/cn';
@@ -56,7 +56,9 @@ const SmartImage = React.forwardRef(({ className, source, alt, skeleton = true,
56
56
  onError?.();
57
57
  };
58
58
  const imageSource = phase === 'fallback' ? fallbackSource : source;
59
- return (_jsxs(View, { ref: ref, className: cn(smartImageVariants({ radius, objectFit }), className), style: [{ borderRadius }, style], ...props, children: [skeleton && phase === 'loading' && (_jsx(Skeleton, { className: cn('absolute inset-0', skeletonClassName) })), _jsx(Image, { source: imageSource, alt: alt, transition: transition, onLoadStart: () => setPhase((current) => (current === 'fallback' ? current : 'loading')), onLoad: handleLoad, onError: handleError, className: cn('w-full h-full', objectFit === 'cover' && 'object-cover', objectFit === 'contain' && 'object-contain', objectFit === 'fill' && 'object-fill', objectFit === 'scale-down' && 'object-scale-down'), style: { opacity: phase === 'loading' ? 0 : 1 } })] }));
59
+ return (_jsxs(View, { ref: ref, className: cn(smartImageVariants({ radius, objectFit }), className), style: [{ borderRadius }, style], ...props, children: [skeleton && phase === 'loading' && (_jsx(Skeleton, { className: cn('absolute inset-0', skeletonClassName) })), _jsx(Image, { source: imageSource, ...(Platform.OS === 'web'
60
+ ? null
61
+ : { accessible: Boolean(alt), accessibilityRole: 'image' }), accessibilityLabel: alt, transition: transition, onLoadStart: () => setPhase((current) => (current === 'fallback' ? current : 'loading')), onLoad: handleLoad, onError: handleError, className: cn('w-full h-full', objectFit === 'cover' && 'object-cover', objectFit === 'contain' && 'object-contain', objectFit === 'fill' && 'object-fill', objectFit === 'scale-down' && 'object-scale-down'), style: { opacity: phase === 'loading' ? 0 : 1 } })] }));
60
62
  });
61
63
  SmartImage.displayName = 'SmartImage';
62
64
  export { SmartImage, smartImageVariants };
@@ -12,7 +12,7 @@ function BottomNavItemComponent({ item, isActive, className, style: styleProp, .
12
12
  const interactiveStyle = withInteractivePressableStyle(styleProp, interaction, {
13
13
  pressedVariant: 'default',
14
14
  });
15
- return (_jsxs(Pressable, { accessibilityRole: "tab", accessibilityState: { selected: isActive }, "aria-selected": isActive, className: cn('flex-1 items-center justify-center py-2', className), style: interactiveStyle, ...props, children: [_jsxs(View, { className: "relative", style: !isActive ? { opacity: interaction.inactiveOpacity } : undefined, children: [icon, badge !== undefined && badge > 0 && (_jsx(View, { className: "absolute -right-2 -top-1 h-4 min-w-4 items-center justify-center rounded-full bg-destructive px-1", children: _jsx(Text, { className: "font-bold text-destructive-foreground", style: { fontSize: visual.bottomNavBadgeFontSize }, children: badge > 99 ? '99+' : badge }) }))] }), _jsx(Text, { className: cn('mt-1 font-medium', isActive ? 'text-primary' : 'text-muted-foreground'), style: typographyStyle(visual, 'caption'), children: label })] }));
15
+ return (_jsxs(Pressable, { accessibilityRole: "tab", accessibilityState: { selected: isActive }, "aria-selected": isActive, className: cn('flex-1 items-center justify-center py-2', className), style: interactiveStyle, ...props, children: [_jsxs(View, { className: "relative", style: !isActive ? { opacity: interaction.inactiveOpacity } : undefined, children: [_jsx(View, { "aria-hidden": true, children: icon }), badge !== undefined && badge > 0 && (_jsx(View, { className: "absolute -right-2 -top-1 h-4 min-w-4 items-center justify-center rounded-full bg-destructive px-1", children: _jsx(Text, { className: "font-bold text-destructive-foreground", style: { fontSize: visual.bottomNavBadgeFontSize }, children: badge > 99 ? '99+' : badge }) }))] }), _jsx(Text, { className: cn('mt-1 font-medium', isActive ? 'text-primary' : 'text-muted-foreground'), style: typographyStyle(visual, 'caption'), children: label })] }));
16
16
  }
17
17
  export function BottomNav({ items, activeKey, onSelect, className, ...props }) {
18
18
  return (_jsx(View
@@ -21,7 +21,7 @@ function DrawerItemRow({ item, active, onSelect, interaction, visual }) {
21
21
  paddingVertical: visual.controlPaddingYDefault,
22
22
  minHeight: visual.touchTargetMinSize,
23
23
  }, interaction, { pressedVariant: 'default' }), [interaction, visual]);
24
- return (_jsxs(Pressable, { accessibilityRole: "button", onPress: handlePress, className: cn('flex-row items-center gap-3 mx-1 transition-colors', active && 'bg-primary-soft-subtle'), style: pressStyle, children: [item.icon, _jsx(Text, { className: cn('flex-1 font-semibold tracking-tight', item.destructive ? 'text-destructive' : active ? 'text-primary' : 'text-foreground'), style: { fontSize: visual.drawerItemFontSize }, children: item.label }), item.badge === undefined ? null : (_jsx(View, { className: "bg-primary px-2 py-0.5 items-center", style: { minWidth: visual.drawerBadgeMinWidth, borderRadius: visual.badgeRadius }, children: _jsx(Text, { className: "font-semibold text-primary-foreground", style: { fontSize: visual.navItemBadgeFontSize }, children: item.badge }) }))] }));
24
+ return (_jsxs(Pressable, { accessibilityRole: "button", onPress: handlePress, className: cn('flex-row items-center gap-3 mx-1 transition-colors', active && 'bg-primary-soft-subtle'), style: pressStyle, children: [_jsx(View, { "aria-hidden": true, children: item.icon }), _jsx(Text, { className: cn('flex-1 font-semibold tracking-tight', item.destructive ? 'text-destructive' : active ? 'text-primary' : 'text-foreground'), style: { fontSize: visual.drawerItemFontSize }, children: item.label }), item.badge === undefined ? null : (_jsx(View, { className: "bg-primary px-2 py-0.5 items-center", style: { minWidth: visual.drawerBadgeMinWidth, borderRadius: visual.badgeRadius }, children: _jsx(Text, { className: "font-semibold text-primary-foreground", style: { fontSize: visual.navItemBadgeFontSize }, children: item.badge }) }))] }));
25
25
  }
26
26
  function DrawerSectionGap() {
27
27
  return _jsx(View, { className: "mb-4" });
@@ -125,5 +125,5 @@ export function DrawerMenu({ visible, onClose, sections, activeKey, onSelect, he
125
125
  frostedSurface.surfaceStyle,
126
126
  style,
127
127
  ], className: drawerPanelClassName(frosted, className), children: [frostedSurface.overlay, _jsxs(View, { className: "flex-1 pb-8", style: { paddingTop: contentTopPadding }, children: [header && _jsx(View, { className: "px-5 pb-4 mb-2 border-b border-border", children: header }), _jsx(DrawerSectionList, { sections: sections, activeKey: activeKey, onSelect: handleItemPress, interaction: interaction, visual: visual }), footer && _jsx(View, { className: "px-5 py-4 mt-auto border-t border-border", children: footer })] })] })] }));
128
- return (_jsx(OverlayHost, { open: mounted, animationType: "none", onRequestClose: onClose, children: drawerTree }));
128
+ return (_jsx(OverlayHost, { open: mounted, animationType: "none", onRequestClose: visible ? onClose : undefined, children: drawerTree }));
129
129
  }
@@ -13,7 +13,9 @@ function FloatingNavItem({ item, isActive, className, style: styleProp, ...props
13
13
  paddingHorizontal: visual.controlPaddingXDefault,
14
14
  paddingVertical: visual.controlPaddingYSm,
15
15
  };
16
- return (_jsxs(Pressable, { accessibilityRole: "tab", accessibilityState: { selected: isActive }, "aria-selected": isActive, className: cn('flex-row items-center gap-2 active:scale-95', isActive && 'bg-primary', className), style: (state) => [pillStyle, typeof styleProp === 'function' ? styleProp(state) : styleProp], ...props, children: [_jsx(View, { className: cn(isActive ? 'text-primary-foreground' : 'text-muted-foreground'), children: icon }), isActive && (_jsx(Text, { className: "font-semibold text-primary-foreground", style: typographyStyle(visual, 'label'), children: label }))] }));
16
+ return (_jsxs(Pressable, { accessibilityRole: "tab",
17
+ // Inactive tabs show only an icon, so the label must still name them.
18
+ accessibilityLabel: label, accessibilityState: { selected: isActive }, "aria-selected": isActive, className: cn('flex-row items-center gap-2 active:scale-95', isActive && 'bg-primary', className), style: (state) => [pillStyle, typeof styleProp === 'function' ? styleProp(state) : styleProp], ...props, children: [_jsx(View, { "aria-hidden": true, className: cn(isActive ? 'text-primary-foreground' : 'text-muted-foreground'), children: icon }), isActive && (_jsx(Text, { className: "font-semibold text-primary-foreground", style: typographyStyle(visual, 'label'), children: label }))] }));
17
19
  }
18
20
  export function FloatingBottomNav({ items, activeKey, onSelect, className, ...props }) {
19
21
  const visual = useVisualTokens();
@@ -34,7 +34,7 @@ function TabBarItemComponent({ item, isActive, variant, className, style: styleP
34
34
  return (_jsxs(Pressable, { accessibilityRole: "tab", accessibilityState: { selected: isActive }, "aria-selected": isActive, className: cn(baseStyles, variantStyles[variant], className), style: (state) => [
35
35
  tokenStyle,
36
36
  typeof interactiveStyle === 'function' ? interactiveStyle(state) : interactiveStyle,
37
- ], ...props, children: [icon && (_jsx(View, { className: isActive && variant === 'pills' ? 'text-primary-foreground' : '', children: icon })), _jsx(Text, { className: cn(textStyles[variant]), style: typographyStyle(visual, 'label'), children: label })] }));
37
+ ], ...props, children: [icon && (_jsx(View, { "aria-hidden": true, className: isActive && variant === 'pills' ? 'text-primary-foreground' : '', children: icon })), _jsx(Text, { className: cn(textStyles[variant]), style: typographyStyle(visual, 'label'), children: label })] }));
38
38
  }
39
39
  export function TabBar({ items, activeKey, onSelect, variant = 'default', scrollable = false, className, style, ...props }) {
40
40
  const visual = useVisualTokens();
@@ -27,6 +27,13 @@ declare const alertVariants: import("tailwind-variants").TVReturnType<{
27
27
  };
28
28
  }, undefined>>;
29
29
  export interface AlertProps extends ViewProps, VariantProps<typeof alertVariants> {
30
+ /**
31
+ * Speak the alert when it appears, for alerts shown in response to an action such as a
32
+ * failed submit. Android and the web already read it through the live region; iOS has
33
+ * none, so this announces its text once. Leave it off for alerts that are part of a
34
+ * screen's content, or every visit would read them aloud.
35
+ */
36
+ announce?: boolean;
30
37
  }
31
38
  declare const Alert: ForwardRefComponent<ViewInstance, AlertProps>;
32
39
  export interface AlertTitleProps extends TextProps {
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import React from 'react';
3
- import { View, Text, } from 'react-native';
2
+ import React, { useEffect } from 'react';
3
+ import { AccessibilityInfo, Platform, View, Text, } from 'react-native';
4
4
  import { cn } from '../../lib/cn';
5
5
  import { tv } from '../../lib/tv';
6
6
  import { useVisualTokens } from '../../lib/visual-tokens';
@@ -20,8 +20,27 @@ const alertVariants = tv({
20
20
  variant: 'default',
21
21
  },
22
22
  });
23
- const Alert = React.forwardRef(({ className, variant, style, ...props }, ref) => {
23
+ /** The visible text of an alert's children, for the iOS announcement. */
24
+ function textOf(node) {
25
+ if (typeof node === 'string' || typeof node === 'number')
26
+ return String(node);
27
+ if (Array.isArray(node))
28
+ return node.map(textOf).filter(Boolean).join('. ');
29
+ if (React.isValidElement(node))
30
+ return textOf(node.props.children);
31
+ return '';
32
+ }
33
+ const Alert = React.forwardRef(({ className, variant, style, announce = false, children, ...props }, ref) => {
24
34
  const visual = useVisualTokens();
35
+ useEffect(() => {
36
+ if (!announce || Platform.OS !== 'ios')
37
+ return;
38
+ const message = textOf(children);
39
+ if (message)
40
+ AccessibilityInfo.announceForAccessibility(message);
41
+ // Once, when the alert appears; a re-render must not repeat it.
42
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- once per alert, by design
43
+ }, []);
25
44
  return (_jsx(View, { ref: ref,
26
45
  // A status message that appears is announced without focus moving to it. Set
27
46
  // before the spread so a decorative note can opt out.
@@ -32,7 +51,7 @@ const Alert = React.forwardRef(({ className, variant, style, ...props }, ref) =>
32
51
  padding: surfacePadding(visual, 'sm'),
33
52
  },
34
53
  style,
35
- ], ...props }));
54
+ ], ...props, children: children }));
36
55
  });
37
56
  Alert.displayName = 'Alert';
38
57
  const AlertTitle = React.forwardRef(({ className, style, ...props }, ref) => {
@@ -97,7 +97,9 @@ declare const buttonTextVariants: import("tailwind-variants").TVReturnType<{
97
97
  }, undefined>>;
98
98
  export interface ButtonProps extends Omit<PressableProps, 'children' | 'feedback' | 'loading'>, VariantProps<typeof buttonVariants> {
99
99
  children?: React.ReactNode;
100
+ /** Additional Tailwind classes. */
100
101
  className?: string;
102
+ /** Disable the button. */
101
103
  disabled?: boolean;
102
104
  /** Shows a centered spinner, exposes busy state, and prevents interaction. */
103
105
  loading?: boolean;
@@ -115,6 +117,7 @@ declare function ButtonText({ className, variant: variantProp, size: sizeProp, s
115
117
  declare namespace ButtonText {
116
118
  var displayName: string;
117
119
  }
120
+ /** Decorative: the button is named by its text, so an icon glyph must not join that name. */
118
121
  declare function ButtonIcon({ className, ...props }: ButtonIconProps): React.JSX.Element;
119
122
  declare namespace ButtonIcon {
120
123
  var displayName: string;
@@ -107,8 +107,9 @@ function ButtonText({ className, variant: variantProp, size: sizeProp, style, ..
107
107
  return (_jsx(Text, { className: font ? font.className : resolved, style: font ? [typography, font.style, style] : [typography, style], ...props }));
108
108
  }
109
109
  ButtonText.displayName = 'ButtonText';
110
+ /** Decorative: the button is named by its text, so an icon glyph must not join that name. */
110
111
  function ButtonIcon({ className, ...props }) {
111
- return _jsx(View, { className: cn('items-center justify-center', className), ...props });
112
+ return _jsx(View, { "aria-hidden": true, className: cn('items-center justify-center', className), ...props });
112
113
  }
113
114
  ButtonIcon.displayName = 'ButtonIcon';
114
115
  export { Button, ButtonText, ButtonIcon, buttonVariants, buttonTextVariants };
@@ -51,8 +51,26 @@ export interface FormFieldState {
51
51
  invalid: boolean;
52
52
  disabled: boolean;
53
53
  required: boolean;
54
+ /** Set inside a FormField: `FormLabel` takes it as its id and controls point to it. */
55
+ labelId?: string;
54
56
  }
55
57
  export declare function useFormFieldState(): FormFieldState;
58
+ interface CallerNaming {
59
+ accessibilityLabel?: string;
60
+ 'aria-label'?: string;
61
+ 'aria-labelledby'?: string;
62
+ accessibilityLabelledBy?: string | string[];
63
+ }
64
+ /**
65
+ * Accessibility props that tie a text control to its FormField: named by the FormLabel
66
+ * unless the caller names or links it any of the four ways React Native allows, and
67
+ * required when the field is. Spread before the caller's props. iOS has no labelled-by;
68
+ * VoiceOver reads the visible label just before the field.
69
+ */
70
+ export declare function fieldAccessibilityProps(field: FormFieldState, props: CallerNaming): {
71
+ 'aria-labelledby': string | undefined;
72
+ 'aria-required': true | undefined;
73
+ };
56
74
  export interface FormFieldProps extends ViewProps, VariantProps<typeof formFieldVariants> {
57
75
  invalid?: boolean;
58
76
  disabled?: boolean;