@thewhileloop/whileui 3.0.0 → 4.1.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 (59) hide show
  1. package/README.md +244 -80
  2. package/dist/blocks/chat/chat-conversation.d.ts +8 -0
  3. package/dist/blocks/chat/chat-conversation.js +36 -0
  4. package/dist/blocks/chat/chat.js +5 -11
  5. package/dist/blocks/chat/index.d.ts +1 -0
  6. package/dist/blocks/chat/index.js +1 -0
  7. package/dist/blocks/datepicker/index.d.ts +1 -0
  8. package/dist/blocks/datepicker/index.js +1 -0
  9. package/dist/blocks/datepicker/time-picker-modal.d.ts +43 -0
  10. package/dist/blocks/datepicker/time-picker-modal.js +187 -0
  11. package/dist/blocks/layout/smart-input.js +2 -1
  12. package/dist/code.d.ts +1 -0
  13. package/dist/code.js +1 -0
  14. package/dist/components/card/card.d.ts +1 -1
  15. package/dist/components/card/card.js +4 -25
  16. package/dist/components/code-block/code-block.d.ts +12 -0
  17. package/dist/components/code-block/code-block.js +13 -0
  18. package/dist/components/code-block/index.d.ts +1 -0
  19. package/dist/components/code-block/index.js +1 -0
  20. package/dist/components/fab/fab.d.ts +87 -0
  21. package/dist/components/fab/fab.js +59 -0
  22. package/dist/components/fab/index.d.ts +1 -0
  23. package/dist/components/fab/index.js +1 -0
  24. package/dist/components/form-field/form-field.d.ts +8 -0
  25. package/dist/components/form-field/form-field.js +3 -0
  26. package/dist/components/form-field/index.d.ts +1 -1
  27. package/dist/components/form-field/index.js +1 -1
  28. package/dist/components/input/input.d.ts +7 -0
  29. package/dist/components/input/input.js +26 -9
  30. package/dist/components/markdown/index.d.ts +1 -0
  31. package/dist/components/markdown/index.js +1 -0
  32. package/dist/components/markdown/markdown.d.ts +14 -0
  33. package/dist/components/markdown/markdown.js +94 -0
  34. package/dist/components/numeric-input/numeric-input.d.ts +1 -0
  35. package/dist/components/numeric-input/numeric-input.js +19 -6
  36. package/dist/components/select/select.d.ts +2 -1
  37. package/dist/components/select/select.js +18 -15
  38. package/dist/components/surface/index.d.ts +1 -0
  39. package/dist/components/surface/index.js +1 -0
  40. package/dist/components/surface/surface.d.ts +14 -0
  41. package/dist/components/surface/surface.js +35 -0
  42. package/dist/components/text/text.d.ts +42 -42
  43. package/dist/components/text/text.js +39 -17
  44. package/dist/components/textarea/textarea.d.ts +1 -0
  45. package/dist/components/textarea/textarea.js +8 -4
  46. package/dist/index.d.ts +4 -2
  47. package/dist/index.js +4 -2
  48. package/dist/lib/focus-ring.d.ts +10 -0
  49. package/dist/lib/focus-ring.js +10 -0
  50. package/dist/lib/interaction-tokens.d.ts +24 -1
  51. package/dist/lib/interaction-tokens.js +26 -44
  52. package/dist/lib/motion-policy.js +2 -2
  53. package/dist/lib/theme-presets.d.ts +26 -0
  54. package/dist/lib/theme-presets.js +28 -0
  55. package/dist/lib/web-autofill.d.ts +9 -0
  56. package/dist/lib/web-autofill.js +9 -0
  57. package/dist/markdown.d.ts +1 -0
  58. package/dist/markdown.js +1 -0
  59. package/package.json +16 -2
@@ -0,0 +1,43 @@
1
+ import React from 'react';
2
+ import { type FrostedSurfaceProps } from '../../lib/frosted-surface';
3
+ export type HourCycle = 'h12' | 'h23';
4
+ export interface TimePickerModalProps extends FrostedSurfaceProps {
5
+ /** Controlled: selected time as `HH:MM`, 24-hour, or null for none. */
6
+ value?: string | null;
7
+ /** Controlled: change handler. Always receives `HH:MM` in 24-hour form. */
8
+ onValueChange?: (time: string | null) => void;
9
+ /** Modal open state */
10
+ open: boolean;
11
+ /** Modal open change handler */
12
+ onOpenChange: (open: boolean) => void;
13
+ /** Minutes between options. 1 by default; 5 or 15 for a coarser pick. */
14
+ minuteStep?: number;
15
+ /** 12-hour with AM/PM, or 24-hour. Defaults to the device locale's preference. */
16
+ hourCycle?: HourCycle;
17
+ /** Trigger element. If not provided, no trigger is rendered. */
18
+ trigger?: React.ReactNode;
19
+ /** Title shown in modal header */
20
+ title?: string;
21
+ /** Button label for confirm */
22
+ confirmLabel?: string;
23
+ className?: string;
24
+ }
25
+ declare function parseTime(value: string | null | undefined): {
26
+ hour: number;
27
+ minute: number;
28
+ } | null;
29
+ /** `HH:MM` as the locale writes it — "10:30 PM" or "22:30". */
30
+ declare function formatDisplayTime(value: string | null | undefined, hourCycle?: HourCycle): string;
31
+ /**
32
+ * A time chosen on two wheels — three with AM/PM — in the same bottom sheet as
33
+ * `DatePickerModal`, so a form's date and time fields open the same way.
34
+ */
35
+ export declare function TimePickerModal(props: TimePickerModalProps): React.JSX.Element;
36
+ /** Compact trigger showing the formatted time. Use as TimePickerModal's trigger prop. */
37
+ export declare function TimePickerTrigger({ value, placeholder, hourCycle, className, }: {
38
+ value?: string | null;
39
+ placeholder?: string;
40
+ hourCycle?: HourCycle;
41
+ className?: string;
42
+ }): React.JSX.Element;
43
+ export { formatDisplayTime, parseTime };
@@ -0,0 +1,187 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useRef, useState } from 'react';
3
+ import { Modal, Platform, Pressable, ScrollView, View, } from 'react-native';
4
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
5
+ import { Text } from '../../components/text';
6
+ import { Button, ButtonText } from '../../components/button';
7
+ import { cn } from '../../lib/cn';
8
+ import { focusRingInset } from '../../lib/focus-ring';
9
+ import { useThemeColors } from '../../lib/theme-colors';
10
+ import { useVisualTokens } from '../../lib/visual-tokens';
11
+ import { fieldRecipe, surfacePadding, surfaceRadius, typographyStyle } from '../../lib/recipes';
12
+ import { useFrostedSurface } from '../../lib/frosted-surface';
13
+ const ITEM_HEIGHT = 44;
14
+ /** Rows visible in a wheel. Odd, so one row sits centred under the selection band. */
15
+ const VISIBLE_ROWS = 5;
16
+ const WHEEL_HEIGHT = ITEM_HEIGHT * VISIBLE_ROWS;
17
+ const WHEEL_PADDING = ITEM_HEIGHT * Math.floor(VISIBLE_ROWS / 2);
18
+ const pad2 = (n) => String(n).padStart(2, '0');
19
+ /** The locale's clock, so a US device gets AM/PM and most of the world does not. */
20
+ function localeHourCycle() {
21
+ try {
22
+ const cycle = new Intl.DateTimeFormat(undefined, { hour: 'numeric' }).resolvedOptions()
23
+ .hourCycle;
24
+ return cycle === 'h11' || cycle === 'h12' ? 'h12' : 'h23';
25
+ }
26
+ catch {
27
+ return 'h23';
28
+ }
29
+ }
30
+ function parseTime(value) {
31
+ if (!value)
32
+ return null;
33
+ const match = /^(\d{1,2}):(\d{2})$/.exec(value.trim());
34
+ if (!match)
35
+ return null;
36
+ const hour = Number(match[1]);
37
+ const minute = Number(match[2]);
38
+ if (hour > 23 || minute > 59)
39
+ return null;
40
+ return { hour, minute };
41
+ }
42
+ /** `HH:MM` as the locale writes it — "10:30 PM" or "22:30". */
43
+ function formatDisplayTime(value, hourCycle) {
44
+ const parsed = parseTime(value);
45
+ if (!parsed)
46
+ return 'Select time';
47
+ const date = new Date(2000, 0, 1, parsed.hour, parsed.minute);
48
+ return date.toLocaleTimeString(undefined, {
49
+ hour: 'numeric',
50
+ minute: '2-digit',
51
+ ...(hourCycle ? { hour12: hourCycle === 'h12' } : {}),
52
+ });
53
+ }
54
+ /**
55
+ * One scrolling column. Scrolls snap to a row, a tap selects a row, and the whole
56
+ * column is a single adjustable element for assistive tech and the keyboard — sixty
57
+ * minute rows as sixty tab stops would be nobody's idea of operable.
58
+ */
59
+ function Wheel({ label, options, selected, onSelect }) {
60
+ const visual = useVisualTokens();
61
+ const colors = useThemeColors();
62
+ const ref = useRef(null);
63
+ const index = Math.max(0, options.findIndex((option) => option.value === selected));
64
+ // Keep the band on the selected row when the value changes from outside the wheel
65
+ // (a tap, a keyboard step, the initial value), not after the user's own scroll.
66
+ const lastScrolledIndex = useRef(-1);
67
+ useEffect(() => {
68
+ if (lastScrolledIndex.current === index)
69
+ return;
70
+ lastScrolledIndex.current = index;
71
+ ref.current?.scrollTo({ y: index * ITEM_HEIGHT, animated: Platform.OS !== 'web' });
72
+ }, [index]);
73
+ const settle = useCallback((event) => {
74
+ const next = Math.round(event.nativeEvent.contentOffset.y / ITEM_HEIGHT);
75
+ const clamped = Math.min(options.length - 1, Math.max(0, next));
76
+ lastScrolledIndex.current = clamped;
77
+ const option = options[clamped];
78
+ if (option && option.value !== selected)
79
+ onSelect(option.value);
80
+ }, [onSelect, options, selected]);
81
+ const step = useCallback((delta) => {
82
+ const next = (index + delta + options.length) % options.length;
83
+ onSelect(options[next].value);
84
+ }, [index, onSelect, options]);
85
+ const current = options[index]?.label ?? '';
86
+ return (_jsxs(View, { accessible: true, accessibilityRole: "adjustable", accessibilityLabel: label, accessibilityValue: { text: current }, accessibilityActions: [{ name: 'increment' }, { name: 'decrement' }], onAccessibilityAction: (event) => {
87
+ if (event.nativeEvent.actionName === 'increment')
88
+ step(1);
89
+ if (event.nativeEvent.actionName === 'decrement')
90
+ step(-1);
91
+ }, ...(Platform.OS === 'web'
92
+ ? {
93
+ focusable: true,
94
+ onKeyDown: (event) => {
95
+ if (event.nativeEvent.key === 'ArrowUp') {
96
+ event.preventDefault();
97
+ step(-1);
98
+ }
99
+ else if (event.nativeEvent.key === 'ArrowDown') {
100
+ event.preventDefault();
101
+ step(1);
102
+ }
103
+ },
104
+ }
105
+ : null), className: cn('flex-1 overflow-hidden', focusRingInset), style: { height: WHEEL_HEIGHT, borderRadius: surfaceRadius(visual, 'lg') }, children: [_jsx(View, { pointerEvents: "none", className: "absolute left-0 right-0 bg-muted", style: {
106
+ top: WHEEL_PADDING,
107
+ height: ITEM_HEIGHT,
108
+ borderRadius: surfaceRadius(visual, 'md'),
109
+ } }), _jsx(ScrollView, { ref: ref, showsVerticalScrollIndicator: false, snapToInterval: ITEM_HEIGHT, decelerationRate: "fast", nestedScrollEnabled: true, contentContainerStyle: { paddingVertical: WHEEL_PADDING }, contentOffset: { x: 0, y: index * ITEM_HEIGHT }, onMomentumScrollEnd: settle, onScrollEndDrag: Platform.OS === 'web' ? settle : undefined, importantForAccessibility: "no-hide-descendants", accessibilityElementsHidden: true, children: options.map((option) => {
110
+ const on = option.value === selected;
111
+ return (_jsx(Pressable, { onPress: () => onSelect(option.value), focusable: false, className: "items-center justify-center", style: { height: ITEM_HEIGHT }, children: _jsx(Text, { className: on ? 'font-medium text-foreground' : 'text-muted-foreground', style: {
112
+ ...typographyStyle(visual, on ? 'title' : 'body'),
113
+ color: on ? colors.foreground : colors.mutedForeground,
114
+ }, children: option.label }) }, option.value));
115
+ }) })] }));
116
+ }
117
+ function TimePickerModalSession({ value, onValueChange, open, onOpenChange, minuteStep = 1, hourCycle: hourCycleProp, trigger, title = 'Select time', confirmLabel = 'Done', className, frosted = false, blurIntensity, blurTintToken, }) {
118
+ const insets = useSafeAreaInsets();
119
+ const colors = useThemeColors();
120
+ const visual = useVisualTokens();
121
+ const frostedSurface = useFrostedSurface({
122
+ frosted,
123
+ blurIntensity,
124
+ blurTintToken,
125
+ defaultTintToken: 'surfaceTranslucent',
126
+ defaultBlurPreset: 'medium',
127
+ });
128
+ const hourCycle = hourCycleProp ?? localeHourCycle();
129
+ const step = Math.min(30, Math.max(1, Math.round(minuteStep)));
130
+ // The draft opens on the current value, or on the current clock time rounded to the
131
+ // step, so the first confirm without touching anything records "now".
132
+ const initial = (() => {
133
+ const parsed = parseTime(value);
134
+ if (parsed)
135
+ return parsed;
136
+ const now = new Date();
137
+ return { hour: now.getHours(), minute: Math.floor(now.getMinutes() / step) * step };
138
+ })();
139
+ const [hour, setHour] = useState(initial.hour);
140
+ const [minute, setMinute] = useState(initial.minute);
141
+ const hours = hourCycle === 'h12'
142
+ ? Array.from({ length: 12 }, (_, i) => ({ value: i + 1, label: String(i + 1) }))
143
+ : Array.from({ length: 24 }, (_, i) => ({ value: i, label: pad2(i) }));
144
+ const minutes = Array.from({ length: Math.ceil(60 / step) }, (_, i) => ({
145
+ value: i * step,
146
+ label: pad2(i * step),
147
+ }));
148
+ const periods = [
149
+ { value: 0, label: 'AM' },
150
+ { value: 1, label: 'PM' },
151
+ ];
152
+ const period = hour >= 12 ? 1 : 0;
153
+ const hour12 = hour % 12 === 0 ? 12 : hour % 12;
154
+ const handleConfirm = useCallback(() => {
155
+ onValueChange?.(`${pad2(hour)}:${pad2(minute)}`);
156
+ onOpenChange(false);
157
+ }, [hour, minute, onOpenChange, onValueChange]);
158
+ const handleDismiss = useCallback(() => onOpenChange(false), [onOpenChange]);
159
+ const handleOpen = useCallback(() => onOpenChange(true), [onOpenChange]);
160
+ return (_jsxs(_Fragment, { children: [trigger ? (_jsx(Pressable, { accessibilityRole: "button", onPress: handleOpen, className: cn('w-full flex-row items-center border-border bg-muted', className), style: fieldRecipe(visual, 'default'), children: trigger })) : null, _jsx(Modal, { visible: open, transparent: true, animationType: "slide", statusBarTranslucent: true, navigationBarTranslucent: true, onRequestClose: handleDismiss, children: _jsx(Pressable, { accessible: false, importantForAccessibility: "no", className: "flex-1 justify-end", style: { backgroundColor: colors.overlay }, onPress: handleDismiss, children: _jsxs(Pressable, { accessible: false, className: cn('border border-border relative overflow-hidden', frosted ? 'bg-transparent' : 'bg-background'), style: [
161
+ {
162
+ borderTopLeftRadius: surfaceRadius(visual, 'xl'),
163
+ borderTopRightRadius: surfaceRadius(visual, 'xl'),
164
+ paddingBottom: Math.max(insets.bottom, surfacePadding(visual, 'sm')),
165
+ },
166
+ frostedSurface.surfaceStyle,
167
+ ], onPress: (e) => e.stopPropagation(), children: [frostedSurface.overlay, _jsxs(View, { className: "flex-row items-center justify-between border-b border-border", style: {
168
+ paddingHorizontal: surfacePadding(visual, 'sm'),
169
+ paddingVertical: visual.controlPaddingYDefault,
170
+ }, children: [_jsx(Text, { accessibilityRole: "header", className: "font-medium text-foreground", style: typographyStyle(visual, 'body'), children: title }), _jsx(Button, { size: "sm", onPress: handleConfirm, children: _jsx(ButtonText, { children: confirmLabel }) })] }), _jsxs(View, { className: "flex-row items-center", style: { padding: surfacePadding(visual, 'sm'), gap: visual.controlPaddingYDefault }, children: [_jsx(Wheel, { label: "Hour", options: hours, selected: hourCycle === 'h12' ? hour12 : hour, onSelect: (next) => setHour(hourCycle === 'h12' ? (next % 12) + (period === 1 ? 12 : 0) : next) }), _jsx(Text, { className: "text-muted-foreground", style: typographyStyle(visual, 'title'), accessibilityElementsHidden: true, importantForAccessibility: "no", children: ":" }), _jsx(Wheel, { label: "Minute", options: minutes, selected: minute, onSelect: setMinute }), hourCycle === 'h12' ? (_jsx(Wheel, { label: "AM or PM", options: periods, selected: period, onSelect: (next) => setHour((hour % 12) + (next === 1 ? 12 : 0)) })) : null] })] }) }) })] }));
171
+ }
172
+ /**
173
+ * A time chosen on two wheels — three with AM/PM — in the same bottom sheet as
174
+ * `DatePickerModal`, so a form's date and time fields open the same way.
175
+ */
176
+ export function TimePickerModal(props) {
177
+ // Remount per opening so the draft starts from the value each time, as the date
178
+ // picker does; a draft that outlives a cancel would reopen on the abandoned pick.
179
+ const sessionKey = props.open ? 'open' : 'closed';
180
+ return _jsx(TimePickerModalSession, { ...props }, sessionKey);
181
+ }
182
+ /** Compact trigger showing the formatted time. Use as TimePickerModal's trigger prop. */
183
+ export function TimePickerTrigger({ value, placeholder = 'Select time', hourCycle, className, }) {
184
+ const visual = useVisualTokens();
185
+ return (_jsxs(View, { className: cn('flex-1 flex-row items-center min-w-0', className), children: [_jsx(Text, { className: cn('flex-1', value ? 'text-foreground' : 'text-muted-foreground'), style: typographyStyle(visual, 'body'), numberOfLines: 1, children: value ? formatDisplayTime(value, hourCycle) : placeholder }), _jsx(View, { className: "ml-2 shrink-0 h-2 w-2 rotate-45 border-b-2 border-r-2 border-muted-foreground" })] }));
186
+ }
187
+ export { formatDisplayTime, parseTime };
@@ -7,6 +7,7 @@ import { focusRing } from '../../lib/focus-ring';
7
7
  import { useThemeColors } from '../../lib/theme-colors';
8
8
  import { useInteractionTokens } from '../../lib/interaction-tokens';
9
9
  import { useVisualTokens } from '../../lib/visual-tokens';
10
+ import { webAutofillField } from '../../lib/web-autofill';
10
11
  import { controlRecipe, shadowStyle, surfaceRadius, typographyStyle } from '../../lib/recipes';
11
12
  // The examples list is joined into one string so it can be an effect dependency without
12
13
  // re-running on every render. A NUL byte cannot appear in a placeholder.
@@ -143,5 +144,5 @@ export const SmartInput = forwardRef(function SmartInput({ leftSlot, centerSlot,
143
144
  { maxHeight: visual.smartInputMaxHeight },
144
145
  !editable ? { opacity: interaction.disabledOpacity } : null,
145
146
  style,
146
- ], className: cn('flex-1 text-foreground placeholder:text-muted-foreground', focusRing, variant === 'bar' && 'border-border bg-muted', variant === 'card' && 'bg-transparent', inputClassName), multiline: true, submitBehavior: submitBehavior, textAlignVertical: "top", placeholderTextColor: placeholderTextColor ?? colors.placeholder, placeholder: placeholderState.placeholder, value: value, defaultValue: defaultValue, onChangeText: placeholderState.onChangeText, editable: editable, ...props }), _jsx(SmartInputSlot, { children: rightSlot })] }));
147
+ ], className: cn('flex-1 text-foreground placeholder:text-muted-foreground', focusRing, variant === 'bar' && 'border-border bg-muted', variant === 'bar' && webAutofillField, variant === 'card' && 'bg-transparent', inputClassName), multiline: true, submitBehavior: submitBehavior, textAlignVertical: "top", placeholderTextColor: placeholderTextColor ?? colors.placeholder, placeholder: placeholderState.placeholder, value: value, defaultValue: defaultValue, onChangeText: placeholderState.onChangeText, editable: editable, ...props }), _jsx(SmartInputSlot, { children: rightSlot })] }));
147
148
  });
package/dist/code.d.ts ADDED
@@ -0,0 +1 @@
1
+ export { CodeBlock, type CodeBlockProps } from './components/code-block';
package/dist/code.js ADDED
@@ -0,0 +1 @@
1
+ export { CodeBlock } from './components/code-block';
@@ -1,6 +1,6 @@
1
1
  import { type ViewInstance, type ViewProps, type TextInstance, type TextProps } from 'react-native';
2
2
  import { type VariantProps } from '../../lib/tv';
3
- import { type FrostedSurfaceProps } from '../../lib/frosted-surface';
3
+ import type { FrostedSurfaceProps } from '../../lib/frosted-surface';
4
4
  import { type ShadowTier } from '../../lib/recipes';
5
5
  import type { ForwardRefComponent } from '../../lib/ref-types';
6
6
  declare const cardVariants: import("tailwind-variants").TVReturnType<{
@@ -1,13 +1,12 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import React from 'react';
3
3
  import { View, } from 'react-native';
4
4
  import { Text } from '../text';
5
+ import { Surface } from '../surface';
5
6
  import { cn } from '../../lib/cn';
6
7
  import { tv } from '../../lib/tv';
7
- import { useFrostedSurface } from '../../lib/frosted-surface';
8
8
  import { useVisualTokens } from '../../lib/visual-tokens';
9
- import { useThemeColors } from '../../lib/theme-colors';
10
- import { shadowStyle, surfacePadding, typographyStyle } from '../../lib/recipes';
9
+ import { typographyStyle } from '../../lib/recipes';
11
10
  const cardVariants = tv({
12
11
  base: 'border border-border bg-card',
13
12
  variants: {
@@ -28,27 +27,7 @@ const cardVariants = tv({
28
27
  },
29
28
  });
30
29
  const Card = React.forwardRef(({ className, padding, unstyled, shadow = 'sm', frosted = false, blurIntensity, blurTintToken, style, children, ...props }, ref) => {
31
- const visual = useVisualTokens();
32
- const colors = useThemeColors();
33
- const frostedSurface = useFrostedSurface({
34
- frosted,
35
- blurIntensity,
36
- blurTintToken,
37
- defaultTintToken: 'card',
38
- defaultBlurPreset: 'subtle',
39
- });
40
- const effectivePadding = surfacePadding(visual, padding ?? 'default');
41
- const cardStyle = [
42
- {
43
- borderRadius: unstyled ? 0 : visual.radiusXl,
44
- padding: unstyled ? 0 : effectivePadding,
45
- borderWidth: unstyled ? 0 : visual.borderWidthHairline,
46
- },
47
- unstyled ? null : shadowStyle(visual, colors, shadow),
48
- frostedSurface.surfaceStyle,
49
- style,
50
- ];
51
- return (_jsxs(View, { ref: ref, className: cn(cardVariants({ padding, unstyled }), frosted && 'relative overflow-hidden bg-transparent', className), style: cardStyle, ...props, children: [frostedSurface.overlay, children] }));
30
+ return (_jsx(Surface, { ref: ref, level: "base", padding: unstyled ? 'none' : (padding ?? 'default'), bordered: !unstyled, shadow: unstyled ? 'none' : shadow, frosted: frosted, blurIntensity: blurIntensity, blurTintToken: blurTintToken, className: cn(cardVariants({ padding, unstyled }), className), style: [unstyled ? { borderRadius: 0 } : null, style], ...props, children: children }));
52
31
  });
53
32
  Card.displayName = 'Card';
54
33
  const CardHeader = React.forwardRef(({ className, ...props }, ref) => {
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ import { type ViewProps } from 'react-native';
3
+ export interface CodeBlockProps extends Omit<ViewProps, 'children'> {
4
+ code: string;
5
+ language?: string;
6
+ actions?: React.ReactNode;
7
+ }
8
+ declare function CodeBlock({ code, language, actions, className, ...props }: CodeBlockProps): React.JSX.Element;
9
+ declare namespace CodeBlock {
10
+ var displayName: string;
11
+ }
12
+ export { CodeBlock };
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ScrollView, View } from 'react-native';
3
+ import { Surface } from '../surface';
4
+ import { Text } from '../text';
5
+ import { cn } from '../../lib/cn';
6
+ import { useVisualTokens } from '../../lib/visual-tokens';
7
+ function CodeBlock({ code, language, actions, className, ...props }) {
8
+ const label = language ? `${language} code` : 'Code';
9
+ const visual = useVisualTokens();
10
+ return (_jsxs(Surface, { level: "base", padding: "none", radius: "lg", shadow: "none", className: cn('overflow-hidden bg-muted', className), ...props, children: [language || actions ? (_jsxs(View, { className: "flex-row items-center justify-between border-b border-border px-3 py-2", children: [_jsx(Text, { variant: "caption", children: language ?? 'Code' }), actions] })) : null, _jsx(ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, contentContainerStyle: { padding: visual.surfacePaddingSm }, children: _jsx(Text, { variant: "code", className: "bg-transparent px-0 py-0 text-foreground", accessibilityLabel: label, selectable: true, children: code }) })] }));
11
+ }
12
+ CodeBlock.displayName = 'CodeBlock';
13
+ export { CodeBlock };
@@ -0,0 +1 @@
1
+ export { CodeBlock, type CodeBlockProps } from './code-block';
@@ -0,0 +1 @@
1
+ export { CodeBlock } from './code-block';
@@ -0,0 +1,87 @@
1
+ import React from 'react';
2
+ import { View, type PressableInstance, type ViewProps } from 'react-native';
3
+ import { type PressableProps } from '../pressable';
4
+ import { type VariantProps } from '../../lib/tv';
5
+ import type { ForwardRefComponent } from '../../lib/ref-types';
6
+ declare const fabVariants: import("tailwind-variants").TVReturnType<{
7
+ variant: {
8
+ default: "bg-primary";
9
+ secondary: "bg-secondary";
10
+ destructive: "bg-destructive";
11
+ surface: "border border-border bg-card";
12
+ };
13
+ size: {
14
+ default: "h-14 w-14";
15
+ sm: "h-12 w-12";
16
+ lg: "h-16 w-16";
17
+ };
18
+ placement: {
19
+ /** Positioned by the caller — inside a toolbar, a row, anywhere. */
20
+ inline: "";
21
+ /** Floating over the nearest positioned ancestor, bottom-right. */
22
+ floating: "absolute bottom-6 right-6";
23
+ 'floating-left': "absolute bottom-6 left-6";
24
+ };
25
+ }, undefined, "items-center justify-center rounded-full", {
26
+ variant: {
27
+ default: "bg-primary";
28
+ secondary: "bg-secondary";
29
+ destructive: "bg-destructive";
30
+ surface: "border border-border bg-card";
31
+ };
32
+ size: {
33
+ default: "h-14 w-14";
34
+ sm: "h-12 w-12";
35
+ lg: "h-16 w-16";
36
+ };
37
+ placement: {
38
+ /** Positioned by the caller — inside a toolbar, a row, anywhere. */
39
+ inline: "";
40
+ /** Floating over the nearest positioned ancestor, bottom-right. */
41
+ floating: "absolute bottom-6 right-6";
42
+ 'floating-left': "absolute bottom-6 left-6";
43
+ };
44
+ }, undefined, import("tailwind-variants").TVReturnTypeLike<{
45
+ variant: {
46
+ default: "bg-primary";
47
+ secondary: "bg-secondary";
48
+ destructive: "bg-destructive";
49
+ surface: "border border-border bg-card";
50
+ };
51
+ size: {
52
+ default: "h-14 w-14";
53
+ sm: "h-12 w-12";
54
+ lg: "h-16 w-16";
55
+ };
56
+ placement: {
57
+ /** Positioned by the caller — inside a toolbar, a row, anywhere. */
58
+ inline: "";
59
+ /** Floating over the nearest positioned ancestor, bottom-right. */
60
+ floating: "absolute bottom-6 right-6";
61
+ 'floating-left': "absolute bottom-6 left-6";
62
+ };
63
+ }, undefined>>;
64
+ export interface FabProps extends Omit<PressableProps, 'children'>, VariantProps<typeof fabVariants> {
65
+ /**
66
+ * What the action does, for screen readers. Required: a FAB is an icon with no
67
+ * text, so without this it announces as an unlabelled button.
68
+ */
69
+ label: string;
70
+ /** The glyph. Icon libraries need a concrete colour, so read it from `useIconColors`. */
71
+ children?: React.ReactNode;
72
+ className?: string;
73
+ }
74
+ /**
75
+ * A floating action button: one primary action, raised above the content.
76
+ *
77
+ * It owns no icon dependency. `@expo/vector-icons` and friends need hex colours
78
+ * rather than classes, so the caller passes the glyph and colours it from
79
+ * `useIconColors()` or `useThemeColors()`.
80
+ */
81
+ declare const FAB: ForwardRefComponent<PressableInstance, FabProps>;
82
+ export interface FabGroupProps extends ViewProps {
83
+ className?: string;
84
+ }
85
+ /** Stacks secondary actions above a FAB, bottom-aligned. */
86
+ declare const FABGroup: ForwardRefComponent<React.ComponentRef<typeof View>, FabGroupProps>;
87
+ export { FAB, FABGroup, fabVariants };
@@ -0,0 +1,59 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import { View } from 'react-native';
4
+ import { Pressable } from '../pressable';
5
+ import { cn } from '../../lib/cn';
6
+ import { tv } from '../../lib/tv';
7
+ import { useVisualTokens } from '../../lib/visual-tokens';
8
+ import { shadowStyle } from '../../lib/recipes';
9
+ import { useThemeColors } from '../../lib/theme-colors';
10
+ const fabVariants = tv({
11
+ base: 'items-center justify-center rounded-full',
12
+ variants: {
13
+ variant: {
14
+ default: 'bg-primary',
15
+ secondary: 'bg-secondary',
16
+ destructive: 'bg-destructive',
17
+ surface: 'border border-border bg-card',
18
+ },
19
+ size: {
20
+ // 56 and 40 are Material's FAB and small-FAB sizes. Both clear the iOS 44pt
21
+ // and Android 48dp minimums on their own, so a FAB never needs hitSlop.
22
+ default: 'h-14 w-14',
23
+ sm: 'h-12 w-12',
24
+ lg: 'h-16 w-16',
25
+ },
26
+ placement: {
27
+ /** Positioned by the caller — inside a toolbar, a row, anywhere. */
28
+ inline: '',
29
+ /** Floating over the nearest positioned ancestor, bottom-right. */
30
+ floating: 'absolute bottom-6 right-6',
31
+ 'floating-left': 'absolute bottom-6 left-6',
32
+ },
33
+ },
34
+ defaultVariants: {
35
+ variant: 'default',
36
+ size: 'default',
37
+ placement: 'floating',
38
+ },
39
+ });
40
+ /**
41
+ * A floating action button: one primary action, raised above the content.
42
+ *
43
+ * It owns no icon dependency. `@expo/vector-icons` and friends need hex colours
44
+ * rather than classes, so the caller passes the glyph and colours it from
45
+ * `useIconColors()` or `useThemeColors()`.
46
+ */
47
+ const FAB = React.forwardRef(({ className, variant, size, placement, label, children, style, ...props }, ref) => {
48
+ const visual = useVisualTokens();
49
+ const colors = useThemeColors();
50
+ return (_jsx(Pressable, { ref: ref, accessibilityRole: "button", accessibilityLabel: label, className: cn(fabVariants({ variant, size, placement }), className), style: (state) => [
51
+ shadowStyle(visual, colors, 'lg'),
52
+ typeof style === 'function' ? style(state) : style,
53
+ ], ...props, children: children }));
54
+ });
55
+ FAB.displayName = 'FAB';
56
+ /** Stacks secondary actions above a FAB, bottom-aligned. */
57
+ const FABGroup = React.forwardRef(({ className, ...props }, ref) => (_jsx(View, { ref: ref, className: cn('absolute bottom-6 right-6 items-end gap-3', className), ...props })));
58
+ FABGroup.displayName = 'FABGroup';
59
+ export { FAB, FABGroup, fabVariants };
@@ -0,0 +1 @@
1
+ export { FAB, FABGroup, fabVariants, type FabProps, type FabGroupProps } from './fab';
@@ -0,0 +1 @@
1
+ export { FAB, FABGroup, fabVariants } from './fab';
@@ -45,6 +45,14 @@ declare const formControlVariants: import("tailwind-variants").TVReturnType<{
45
45
  false: "";
46
46
  };
47
47
  }, undefined>>;
48
+ type FormFieldDensity = NonNullable<VariantProps<typeof formFieldVariants>['density']>;
49
+ export interface FormFieldState {
50
+ density: FormFieldDensity;
51
+ invalid: boolean;
52
+ disabled: boolean;
53
+ required: boolean;
54
+ }
55
+ export declare function useFormFieldState(): FormFieldState;
48
56
  export interface FormFieldProps extends ViewProps, VariantProps<typeof formFieldVariants> {
49
57
  invalid?: boolean;
50
58
  disabled?: boolean;
@@ -41,6 +41,9 @@ const FormFieldContext = createContext({
41
41
  disabled: false,
42
42
  required: false,
43
43
  });
44
+ export function useFormFieldState() {
45
+ return useContext(FormFieldContext);
46
+ }
44
47
  const FormField = React.forwardRef(({ className, density = 'default', invalid = false, disabled = false, required = false, ...props }, ref) => {
45
48
  const interaction = useInteractionTokens();
46
49
  const contextValue = useMemo(() => ({ density, invalid, disabled, required }), [density, disabled, invalid, required]);
@@ -1 +1 @@
1
- export { FormField, FormLabel, FormControl, FormHint, FormMessage, formFieldVariants, formControlVariants, type FormFieldProps, type FormLabelProps, type FormControlProps, type FormHintProps, type FormMessageProps, } from './form-field';
1
+ export { FormField, FormLabel, FormControl, FormHint, FormMessage, useFormFieldState, formFieldVariants, formControlVariants, type FormFieldProps, type FormLabelProps, type FormControlProps, type FormHintProps, type FormMessageProps, type FormFieldState, } from './form-field';
@@ -1 +1 @@
1
- export { FormField, FormLabel, FormControl, FormHint, FormMessage, formFieldVariants, formControlVariants, } from './form-field';
1
+ export { FormField, FormLabel, FormControl, FormHint, FormMessage, useFormFieldState, formFieldVariants, formControlVariants, } from './form-field';
@@ -6,19 +6,26 @@ declare const inputVariants: import("tailwind-variants").TVReturnType<{
6
6
  variant: {
7
7
  default: "border-border";
8
8
  error: "border-destructive";
9
+ /** No border, no fill — for one large figure that is the screen's subject. */
10
+ plain: "border-transparent bg-transparent";
9
11
  };
10
12
  }, undefined, "w-full border-border bg-muted text-foreground", {
11
13
  variant: {
12
14
  default: "border-border";
13
15
  error: "border-destructive";
16
+ /** No border, no fill — for one large figure that is the screen's subject. */
17
+ plain: "border-transparent bg-transparent";
14
18
  };
15
19
  }, undefined, import("tailwind-variants").TVReturnTypeLike<{
16
20
  variant: {
17
21
  default: "border-border";
18
22
  error: "border-destructive";
23
+ /** No border, no fill — for one large figure that is the screen's subject. */
24
+ plain: "border-transparent bg-transparent";
19
25
  };
20
26
  }, undefined>>;
21
27
  export interface InputProps extends TextInputProps, VariantProps<typeof inputVariants> {
28
+ 'aria-invalid'?: boolean;
22
29
  className?: string;
23
30
  prefix?: React.ReactNode;
24
31
  suffix?: React.ReactNode;
@@ -1,42 +1,59 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React from 'react';
2
+ import React, { useState } from 'react';
3
3
  import { View, TextInput } from 'react-native';
4
4
  import { cn } from '../../lib/cn';
5
- import { focusRing } from '../../lib/focus-ring';
5
+ import { focusFieldWrapper } from '../../lib/focus-ring';
6
6
  import { tv } from '../../lib/tv';
7
7
  import { useThemeColors } from '../../lib/theme-colors';
8
8
  import { useInteractionTokens } from '../../lib/interaction-tokens';
9
9
  import { useVisualTokens } from '../../lib/visual-tokens';
10
10
  import { typographyStyle } from '../../lib/recipes';
11
+ import { webAutofillField } from '../../lib/web-autofill';
12
+ import { useFormFieldState } from '../form-field/form-field';
11
13
  const inputVariants = tv({
12
14
  base: 'w-full border-border bg-muted text-foreground',
13
15
  variants: {
14
16
  variant: {
15
17
  default: 'border-border',
16
18
  error: 'border-destructive',
19
+ /** No border, no fill — for one large figure that is the screen's subject. */
20
+ plain: 'border-transparent bg-transparent',
17
21
  },
18
22
  },
19
23
  defaultVariants: {
20
24
  variant: 'default',
21
25
  },
22
26
  });
23
- const Input = React.forwardRef(({ className, prefix, suffix, inputClassName, variant, editable = true, placeholderTextColor, ...props }, ref) => {
27
+ const Input = React.forwardRef(({ className, prefix, suffix, inputClassName, variant, editable, 'aria-invalid': ariaInvalid, placeholderTextColor, onFocus, onBlur, ...props }, ref) => {
24
28
  const colors = useThemeColors();
25
29
  const interaction = useInteractionTokens();
26
30
  const visual = useVisualTokens();
27
- return (_jsxs(View, { className: cn('flex-row items-center border-border bg-muted', inputVariants({ variant }), className), style: [
31
+ const field = useFormFieldState();
32
+ const effectiveEditable = editable ?? !field.disabled;
33
+ const effectiveVariant = variant ?? (field.invalid ? 'error' : 'default');
34
+ // The border is on this wrapper and the focus is on the TextInput inside it, so the
35
+ // wrapper learns about focus from the events rather than a pseudo-class.
36
+ const [focused, setFocused] = useState(false);
37
+ const plain = effectiveVariant === 'plain';
38
+ return (_jsxs(View, { className: cn('flex-row items-center border-border bg-muted', inputVariants({ variant: effectiveVariant }), focused && !plain && focusFieldWrapper, className), style: [
28
39
  {
29
40
  minHeight: visual.controlHeightDefault,
30
- borderWidth: visual.borderWidthControl,
41
+ borderWidth: plain ? 0 : visual.borderWidthControl,
31
42
  borderRadius: visual.radiusLg,
32
- paddingHorizontal: visual.controlPaddingXDefault,
43
+ paddingHorizontal: plain ? 0 : visual.controlPaddingXDefault,
33
44
  },
34
- !editable ? { opacity: interaction.disabledOpacity } : undefined,
35
- ], children: [prefix && (_jsx(View, { style: { paddingRight: Math.max(8, Math.round(visual.controlPaddingXDefault * 0.5)) }, children: prefix })), _jsx(TextInput, { ref: ref, className: cn('flex-1 text-foreground placeholder:text-muted-foreground', focusRing, inputClassName), style: {
45
+ !effectiveEditable ? { opacity: interaction.disabledOpacity } : undefined,
46
+ ], children: [prefix && (_jsx(View, { style: { paddingRight: Math.max(8, Math.round(visual.controlPaddingXDefault * 0.5)) }, children: prefix })), _jsx(TextInput, { ref: ref, className: cn('flex-1 text-foreground placeholder:text-muted-foreground web:outline-none', !plain && webAutofillField, inputClassName), style: {
36
47
  ...typographyStyle(visual, 'label'),
37
48
  minHeight: visual.controlHeightDefault - 2,
38
49
  paddingVertical: visual.controlPaddingYDefault,
39
- }, editable: editable, placeholderTextColor: placeholderTextColor ?? colors.placeholder, ...props }), suffix && (_jsx(View, { style: { paddingLeft: Math.max(8, Math.round(visual.controlPaddingXDefault * 0.5)) }, children: suffix }))] }));
50
+ }, editable: effectiveEditable, "aria-invalid": ariaInvalid ?? field.invalid, placeholderTextColor: placeholderTextColor ?? colors.placeholder, onFocus: (event) => {
51
+ setFocused(true);
52
+ onFocus?.(event);
53
+ }, onBlur: (event) => {
54
+ setFocused(false);
55
+ onBlur?.(event);
56
+ }, ...props }), suffix && (_jsx(View, { style: { paddingLeft: Math.max(8, Math.round(visual.controlPaddingXDefault * 0.5)) }, children: suffix }))] }));
40
57
  });
41
58
  Input.displayName = 'Input';
42
59
  export { Input, inputVariants };
@@ -0,0 +1 @@
1
+ export { Markdown, type MarkdownProps } from './markdown';
@@ -0,0 +1 @@
1
+ export { Markdown } from './markdown';