@thewhileloop/whileui 4.2.3 → 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.
- package/README.md +1235 -663
- package/dist/agent/catalog.json +1737 -0
- package/dist/agent/llms-components.txt +2787 -0
- package/dist/agent/llms-full.txt +2962 -0
- package/dist/agent/llms.txt +152 -0
- package/dist/blocks/chat/chat.d.ts +10 -2
- package/dist/blocks/chat/chat.js +1 -1
- package/dist/blocks/chat/index.d.ts +1 -1
- package/dist/blocks/commerce/feature-gate.js +2 -2
- package/dist/blocks/commerce/metric-card.js +1 -1
- package/dist/blocks/commerce/subscription-card.d.ts +7 -0
- package/dist/blocks/commerce/usage-bar.d.ts +4 -0
- package/dist/blocks/layout/app-shell.d.ts +4 -0
- package/dist/blocks/layout/empty-state.js +1 -1
- package/dist/blocks/layout/error-state.js +1 -1
- package/dist/blocks/layout/page-skeleton.d.ts +1 -0
- package/dist/blocks/layout/pull-to-refresh-scroll-view.d.ts +1 -0
- package/dist/blocks/layout/screen-skeleton.d.ts +5 -0
- package/dist/blocks/lists/list-item.js +1 -1
- package/dist/blocks/lists/notification-item.js +1 -1
- package/dist/blocks/media/smart-image.js +4 -2
- package/dist/blocks/navigation/bottom-nav.js +1 -1
- package/dist/blocks/navigation/drawer-menu.js +1 -1
- package/dist/blocks/navigation/floating-bottom-nav.js +3 -1
- package/dist/blocks/navigation/tab-bar.js +1 -1
- package/dist/components/alert/alert.d.ts +7 -0
- package/dist/components/alert/alert.js +23 -4
- package/dist/components/button/button.d.ts +3 -0
- package/dist/components/button/button.js +2 -1
- package/dist/components/form-field/form-field.d.ts +18 -0
- package/dist/components/form-field/form-field.js +25 -4
- package/dist/components/input/input.js +2 -2
- package/dist/components/numeric-input/numeric-input.d.ts +3 -0
- package/dist/components/numeric-input/numeric-input.js +2 -2
- package/dist/components/otp-input/otp-input.d.ts +8 -0
- package/dist/components/pressable/pressable.d.ts +1 -0
- package/dist/components/segmented-control/segmented-control.d.ts +1 -0
- package/dist/components/surface/surface.d.ts +5 -0
- package/dist/components/textarea/textarea.js +2 -2
- package/dist/components/toast/toast.d.ts +10 -10
- package/dist/components/toast/toast.js +53 -35
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/frosted-surface.d.ts +3 -0
- package/dist/lib/theme-contract.js +12 -12
- package/dist/lib/theme-presets.js +9 -9
- package/dist/lib/theme-seeds.d.ts +40 -0
- package/dist/lib/theme-seeds.js +143 -0
- package/dist/metro.cjs +5 -4
- package/dist/tokens.d.ts +1 -0
- package/dist/tokens.js +1 -0
- 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
|
|
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
|
-
}
|
|
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;
|
package/dist/blocks/chat/chat.js
CHANGED
|
@@ -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
|
}
|
|
@@ -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,
|
|
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" });
|
|
@@ -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",
|
|
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
|
-
|
|
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;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React, { createContext, useContext, useMemo } from 'react';
|
|
2
|
+
import React, { createContext, useContext, useId, useMemo } from 'react';
|
|
3
3
|
import { Text, View, } from 'react-native';
|
|
4
4
|
import { cn } from '../../lib/cn';
|
|
5
5
|
import { tv } from '../../lib/tv';
|
|
@@ -44,16 +44,37 @@ const FormFieldContext = createContext({
|
|
|
44
44
|
export function useFormFieldState() {
|
|
45
45
|
return useContext(FormFieldContext);
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Accessibility props that tie a text control to its FormField: named by the FormLabel
|
|
49
|
+
* unless the caller names or links it any of the four ways React Native allows, and
|
|
50
|
+
* required when the field is. Spread before the caller's props. iOS has no labelled-by;
|
|
51
|
+
* VoiceOver reads the visible label just before the field.
|
|
52
|
+
*/
|
|
53
|
+
export function fieldAccessibilityProps(field, props) {
|
|
54
|
+
const named = props.accessibilityLabel ??
|
|
55
|
+
props['aria-label'] ??
|
|
56
|
+
props['aria-labelledby'] ??
|
|
57
|
+
props.accessibilityLabelledBy;
|
|
58
|
+
return {
|
|
59
|
+
'aria-labelledby': named ? undefined : field.labelId,
|
|
60
|
+
'aria-required': field.required || undefined,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
47
63
|
const FormField = React.forwardRef(({ className, density = 'default', invalid = false, disabled = false, required = false, ...props }, ref) => {
|
|
48
64
|
const interaction = useInteractionTokens();
|
|
49
|
-
const
|
|
65
|
+
const labelId = useId();
|
|
66
|
+
const contextValue = useMemo(() => ({ density, invalid, disabled, required, labelId }), [density, disabled, invalid, labelId, required]);
|
|
50
67
|
return (_jsx(FormFieldContext.Provider, { value: contextValue, children: _jsx(View, { ref: ref, className: cn(formFieldVariants({ density }), className), style: disabled ? { opacity: interaction.disabledOpacitySoft } : undefined, ...props }) }));
|
|
51
68
|
});
|
|
52
69
|
FormField.displayName = 'FormField';
|
|
53
70
|
const FormLabel = React.forwardRef(({ className, showRequiredIndicator = true, children, style, ...props }, ref) => {
|
|
54
|
-
const { invalid, disabled, required } = useContext(FormFieldContext);
|
|
71
|
+
const { invalid, disabled, required, labelId } = useContext(FormFieldContext);
|
|
55
72
|
const visual = useVisualTokens();
|
|
56
|
-
|
|
73
|
+
const indicator = required && showRequiredIndicator;
|
|
74
|
+
return (_jsxs(Text, { ref: ref, nativeID: labelId,
|
|
75
|
+
// iOS reads nested Text as one string, so hiding the asterisk alone still says
|
|
76
|
+
// "star"; a plain-text label names it once. The control announces "required".
|
|
77
|
+
accessibilityLabel: indicator && typeof children === 'string' ? children : undefined, className: cn('font-medium text-foreground', invalid && 'text-destructive', disabled && 'text-muted-foreground', className), style: [typographyStyle(visual, 'label'), style], ...props, children: [children, indicator ? (_jsx(Text, { "aria-hidden": true, className: "text-destructive", children: ' *' })) : null] }));
|
|
57
78
|
});
|
|
58
79
|
FormLabel.displayName = 'FormLabel';
|
|
59
80
|
const FormControl = React.forwardRef(({ className, style, ...props }, ref) => {
|