@isoline-ui/react-native 0.0.0-stage → 0.1.0-alpha.3

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 (167) hide show
  1. package/LICENSE +22 -0
  2. package/NOTICE.md +12 -0
  3. package/README.md +104 -3
  4. package/dist/amount.d.ts +3 -0
  5. package/dist/calendar.d.ts +7 -0
  6. package/dist/components/action-chip.d.ts +9 -0
  7. package/dist/components/amount-field.d.ts +12 -0
  8. package/dist/components/avatar.d.ts +17 -0
  9. package/dist/components/back-button.d.ts +12 -0
  10. package/dist/components/back-icon.d.ts +3 -0
  11. package/dist/components/badge.d.ts +13 -0
  12. package/dist/components/banner.d.ts +14 -0
  13. package/dist/components/bottom-sheet-native.d.ts +11 -0
  14. package/dist/components/bottom-sheet-web.d.ts +18 -0
  15. package/dist/components/bottom-sheet.d.ts +3 -0
  16. package/dist/components/button-group.d.ts +26 -0
  17. package/dist/components/button.d.ts +48 -0
  18. package/dist/components/checkbox.d.ts +12 -0
  19. package/dist/components/color-picker.d.ts +21 -0
  20. package/dist/components/context-menu.d.ts +26 -0
  21. package/dist/components/date-picker.d.ts +64 -0
  22. package/dist/components/dialog-context.d.ts +2 -0
  23. package/dist/components/dialog.d.ts +20 -0
  24. package/dist/components/empty-state.d.ts +15 -0
  25. package/dist/components/expand-indicator.d.ts +27 -0
  26. package/dist/components/expressive-shape.d.ts +18 -0
  27. package/dist/components/fab-menu-legacy.d.ts +31 -0
  28. package/dist/components/fab-menu.d.ts +21 -0
  29. package/dist/components/filter-chip.d.ts +69 -0
  30. package/dist/components/floating-action-button.d.ts +15 -0
  31. package/dist/components/icon-button.d.ts +40 -0
  32. package/dist/components/icons.d.ts +18 -0
  33. package/dist/components/input.d.ts +9 -0
  34. package/dist/components/keyboard-inset-view.d.ts +15 -0
  35. package/dist/components/list-controls.d.ts +38 -0
  36. package/dist/components/loading-indicator.d.ts +9 -0
  37. package/dist/components/menu-sheet.d.ts +36 -0
  38. package/dist/components/panel-sheet.d.ts +8 -0
  39. package/dist/components/password-visibility.d.ts +5 -0
  40. package/dist/components/popover-layout.d.ts +25 -0
  41. package/dist/components/popover.d.ts +20 -0
  42. package/dist/components/pressable.d.ts +12 -0
  43. package/dist/components/radio-navigation.d.ts +11 -0
  44. package/dist/components/refresh-control.d.ts +16 -0
  45. package/dist/components/row-gap.d.ts +9 -0
  46. package/dist/components/search-field.d.ts +22 -0
  47. package/dist/components/select-field.d.ts +16 -0
  48. package/dist/components/settings-list.d.ts +98 -0
  49. package/dist/components/settings-row.d.ts +13 -0
  50. package/dist/components/sheet-frame.d.ts +27 -0
  51. package/dist/components/sheet-lifecycle.d.ts +36 -0
  52. package/dist/components/sheet-transition.d.ts +36 -0
  53. package/dist/components/slider.d.ts +21 -0
  54. package/dist/components/snackbar.d.ts +10 -0
  55. package/dist/components/split-button.d.ts +38 -0
  56. package/dist/components/squish.d.ts +20 -0
  57. package/dist/components/swipeable-row.d.ts +20 -0
  58. package/dist/components/switch.d.ts +8 -0
  59. package/dist/components/tab-bar.d.ts +20 -0
  60. package/dist/components/text-field.d.ts +31 -0
  61. package/dist/components/text.d.ts +9 -0
  62. package/dist/components/time-picker.d.ts +40 -0
  63. package/dist/components/toolbar.d.ts +77 -0
  64. package/dist/components/top-bar.d.ts +27 -0
  65. package/dist/components/undo-bar.d.ts +23 -0
  66. package/dist/components/wavy-progress.d.ts +21 -0
  67. package/dist/components/zoomable-image.d.ts +18 -0
  68. package/dist/expressive-shapes.d.ts +8 -0
  69. package/dist/index.d.ts +62 -0
  70. package/dist/keyboard.d.ts +2 -0
  71. package/dist/material.d.ts +11 -0
  72. package/dist/media.d.ts +11 -0
  73. package/dist/motion.d.ts +96 -0
  74. package/dist/platform.d.ts +3 -0
  75. package/dist/preset-tokens.d.ts +264 -0
  76. package/dist/provider.d.ts +59 -0
  77. package/dist/reduced-motion.d.ts +45 -0
  78. package/dist/selection.d.ts +2 -0
  79. package/dist/shape.d.ts +99 -0
  80. package/dist/sheet-physics.d.ts +37 -0
  81. package/dist/theme.d.ts +103 -0
  82. package/dist/tokens.d.ts +2 -0
  83. package/dist/use-keyboard-height.d.ts +10 -0
  84. package/dist/utils.d.ts +3 -0
  85. package/nativewind.cjs +68 -0
  86. package/package.json +63 -6
  87. package/src/amount.ts +53 -0
  88. package/src/calendar.ts +42 -0
  89. package/src/components/action-chip.tsx +64 -0
  90. package/src/components/amount-field.tsx +49 -0
  91. package/src/components/avatar.tsx +91 -0
  92. package/src/components/back-button.tsx +39 -0
  93. package/src/components/back-icon.tsx +25 -0
  94. package/src/components/badge.tsx +41 -0
  95. package/src/components/banner.tsx +63 -0
  96. package/src/components/bottom-sheet-native.tsx +52 -0
  97. package/src/components/bottom-sheet-web.tsx +193 -0
  98. package/src/components/bottom-sheet.tsx +5 -0
  99. package/src/components/button-group.tsx +326 -0
  100. package/src/components/button.tsx +337 -0
  101. package/src/components/checkbox.tsx +71 -0
  102. package/src/components/color-picker.tsx +115 -0
  103. package/src/components/context-menu.tsx +149 -0
  104. package/src/components/date-picker.tsx +466 -0
  105. package/src/components/dialog-context.ts +3 -0
  106. package/src/components/dialog.tsx +210 -0
  107. package/src/components/empty-state.tsx +49 -0
  108. package/src/components/expand-indicator.tsx +60 -0
  109. package/src/components/expressive-shape.tsx +37 -0
  110. package/src/components/fab-menu-legacy.tsx +181 -0
  111. package/src/components/fab-menu.tsx +90 -0
  112. package/src/components/filter-chip.tsx +136 -0
  113. package/src/components/floating-action-button.tsx +79 -0
  114. package/src/components/icon-button.tsx +127 -0
  115. package/src/components/icons.tsx +43 -0
  116. package/src/components/input.tsx +80 -0
  117. package/src/components/keyboard-inset-view.tsx +28 -0
  118. package/src/components/list-controls.tsx +134 -0
  119. package/src/components/loading-indicator.tsx +183 -0
  120. package/src/components/menu-sheet.tsx +133 -0
  121. package/src/components/panel-sheet.tsx +90 -0
  122. package/src/components/password-visibility.tsx +39 -0
  123. package/src/components/popover-layout.ts +47 -0
  124. package/src/components/popover.tsx +139 -0
  125. package/src/components/pressable.tsx +108 -0
  126. package/src/components/radio-navigation.ts +52 -0
  127. package/src/components/refresh-control.tsx +39 -0
  128. package/src/components/row-gap.tsx +13 -0
  129. package/src/components/search-field.tsx +92 -0
  130. package/src/components/select-field.tsx +105 -0
  131. package/src/components/settings-list.tsx +302 -0
  132. package/src/components/settings-row.tsx +61 -0
  133. package/src/components/sheet-frame.tsx +250 -0
  134. package/src/components/sheet-lifecycle.ts +233 -0
  135. package/src/components/sheet-transition.ts +105 -0
  136. package/src/components/slider.tsx +185 -0
  137. package/src/components/snackbar.tsx +97 -0
  138. package/src/components/split-button.tsx +212 -0
  139. package/src/components/squish.ts +41 -0
  140. package/src/components/swipeable-row.tsx +109 -0
  141. package/src/components/switch.tsx +77 -0
  142. package/src/components/tab-bar.tsx +195 -0
  143. package/src/components/text-field.tsx +264 -0
  144. package/src/components/text.tsx +58 -0
  145. package/src/components/time-picker.tsx +277 -0
  146. package/src/components/toolbar.tsx +406 -0
  147. package/src/components/top-bar.tsx +91 -0
  148. package/src/components/undo-bar.tsx +137 -0
  149. package/src/components/wavy-progress.tsx +150 -0
  150. package/src/components/zoomable-image.tsx +240 -0
  151. package/src/expressive-shapes.ts +25 -0
  152. package/src/index.ts +62 -0
  153. package/src/keyboard.ts +5 -0
  154. package/src/material.ts +16 -0
  155. package/src/media.ts +51 -0
  156. package/src/motion.ts +96 -0
  157. package/src/platform.ts +13 -0
  158. package/src/preset-tokens.ts +265 -0
  159. package/src/provider.tsx +201 -0
  160. package/src/reduced-motion.ts +50 -0
  161. package/src/selection.ts +11 -0
  162. package/src/shape.ts +217 -0
  163. package/src/sheet-physics.ts +73 -0
  164. package/src/theme.ts +180 -0
  165. package/src/tokens.ts +2 -0
  166. package/src/use-keyboard-height.ts +64 -0
  167. package/src/utils.ts +7 -0
package/nativewind.cjs ADDED
@@ -0,0 +1,68 @@
1
+ /** @type {import('tailwindcss').Config} */
2
+ module.exports = {
3
+ darkMode: 'class',
4
+ presets: [require('nativewind/preset')],
5
+ theme: {
6
+ extend: {
7
+ colors: {
8
+ border: 'hsl(var(--border))',
9
+ input: 'hsl(var(--input))',
10
+ ring: 'hsl(var(--ring))',
11
+ background: 'hsl(var(--background))',
12
+ foreground: 'hsl(var(--foreground))',
13
+ outline: 'hsl(var(--outline))',
14
+ outlineVariant: 'hsl(var(--outline-variant))',
15
+ surface: 'hsl(var(--surface))',
16
+ surfaceVariant: 'hsl(var(--surface-variant))',
17
+ onSurface: 'hsl(var(--on-surface))',
18
+ onSurfaceVariant: 'hsl(var(--on-surface-variant))',
19
+ surfaceContainerLowest: 'hsl(var(--surface-container-lowest))',
20
+ surfaceContainerLow: 'hsl(var(--surface-container-low))',
21
+ surfaceContainer: 'hsl(var(--surface-container))',
22
+ surfaceContainerHigh: 'hsl(var(--surface-container-high))',
23
+ surfaceContainerHighest: 'hsl(var(--surface-container-highest))',
24
+ primary: {
25
+ DEFAULT: 'hsl(var(--primary))',
26
+ foreground: 'hsl(var(--primary-foreground))',
27
+ },
28
+ primaryContainer: 'hsl(var(--primary-container))',
29
+ onPrimaryContainer: 'hsl(var(--on-primary-container))',
30
+ secondaryContainer: 'hsl(var(--secondary-container))',
31
+ onSecondaryContainer: 'hsl(var(--on-secondary-container))',
32
+ tertiary: {
33
+ DEFAULT: 'hsl(var(--tertiary))',
34
+ foreground: 'hsl(var(--on-tertiary))',
35
+ },
36
+ tertiaryContainer: 'hsl(var(--tertiary-container))',
37
+ onTertiaryContainer: 'hsl(var(--on-tertiary-container))',
38
+ destructive: {
39
+ DEFAULT: 'hsl(var(--destructive))',
40
+ foreground: 'hsl(var(--destructive-foreground))',
41
+ },
42
+ errorContainer: 'hsl(var(--error-container))',
43
+ onErrorContainer: 'hsl(var(--on-error-container))',
44
+ muted: {
45
+ DEFAULT: 'hsl(var(--muted))',
46
+ foreground: 'hsl(var(--muted-foreground))',
47
+ },
48
+ accent: {
49
+ DEFAULT: 'hsl(var(--accent))',
50
+ foreground: 'hsl(var(--accent-foreground))',
51
+ },
52
+ popover: {
53
+ DEFAULT: 'hsl(var(--popover))',
54
+ foreground: 'hsl(var(--popover-foreground))',
55
+ },
56
+ card: {
57
+ DEFAULT: 'hsl(var(--card))',
58
+ foreground: 'hsl(var(--card-foreground))',
59
+ },
60
+ inverseSurface: 'hsl(var(--inverse-surface))',
61
+ inverseOnSurface: 'hsl(var(--inverse-on-surface))',
62
+ inversePrimary: 'hsl(var(--inverse-primary))',
63
+ success: 'hsl(var(--success))',
64
+ },
65
+ },
66
+ },
67
+ plugins: [],
68
+ };
package/package.json CHANGED
@@ -1,6 +1,63 @@
1
- {
2
- "name": "@isoline-ui/react-native",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
1
+ {
2
+ "name": "@isoline-ui/react-native",
3
+ "version": "0.1.0-alpha.3",
4
+ "description": "Shared expressive React Native components with platform-aware interaction",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/isoline-ui/isoline-ui.git",
9
+ "directory": "packages/react-native"
10
+ },
11
+ "type": "module",
12
+ "main": "./src/index.ts",
13
+ "types": "./dist/index.d.ts",
14
+ "react-native": "./src/index.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "react-native": "./src/index.ts",
19
+ "default": "./src/index.ts"
20
+ },
21
+ "./theme": {
22
+ "types": "./dist/theme.d.ts",
23
+ "default": "./src/theme.ts"
24
+ },
25
+ "./tokens": {
26
+ "types": "./dist/tokens.d.ts",
27
+ "default": "./src/tokens.ts"
28
+ },
29
+ "./nativewind": "./nativewind.cjs",
30
+ "./package.json": "./package.json"
31
+ },
32
+ "files": [
33
+ "src",
34
+ "dist",
35
+ "nativewind.cjs",
36
+ "README.md",
37
+ "LICENSE",
38
+ "NOTICE.md"
39
+ ],
40
+ "sideEffects": false,
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "peerDependencies": {
45
+ "react": ">=19.2.0 <20",
46
+ "react-native": ">=0.86.0 <0.89.0",
47
+ "nativewind": "^4.2.0",
48
+ "tailwindcss": "^3.4.0",
49
+ "react-native-reanimated": ">=4.5.0 <5",
50
+ "react-native-gesture-handler": ">=2.32.0 <4",
51
+ "react-native-safe-area-context": ">=5.7.0 <6",
52
+ "react-native-svg": ">=15.15.0 <16"
53
+ },
54
+ "dependencies": {
55
+ "class-variance-authority": "^0.7.1",
56
+ "clsx": "^2.1.1",
57
+ "tailwind-merge": "^3.6.0"
58
+ },
59
+ "scripts": {
60
+ "build": "tsc --project tsconfig.build.json",
61
+ "typecheck": "tsc --project tsconfig.json"
62
+ }
63
+ }
package/src/amount.ts ADDED
@@ -0,0 +1,53 @@
1
+ /** Parse a locale-formatted editing value; invalid/partial input stays null. */
2
+ export function parseLocalizedAmount(input: string, locale = 'en-US'): number | null {
3
+ const parts = new Intl.NumberFormat(locale).formatToParts(-12345.6);
4
+ const group = parts.find((part) => part.type === 'group')?.value;
5
+ const decimal = parts.find((part) => part.type === 'decimal')?.value ?? '.';
6
+ const minus = parts.find((part) => part.type === 'minusSign')?.value ?? '-';
7
+ let value = input.trim().replace(/[\u200e\u200f\u061c]/g, '');
8
+ for (let digit = 0; digit <= 9; digit++) {
9
+ const local = new Intl.NumberFormat(locale, { useGrouping: false }).format(digit);
10
+ if (local !== String(digit)) value = value.split(local).join(String(digit));
11
+ }
12
+ if (group && /\s/u.test(group)) value = value.replace(/[ \u00a0\u202f]/g, group);
13
+ if (group && value.includes(group)) {
14
+ const [integer, fraction] = value.split(decimal);
15
+ if (fraction?.includes(group)) return null;
16
+ const groups = integer
17
+ .replace(minus, '')
18
+ .replace(/^\u2212/, '')
19
+ .split(group);
20
+ const example = new Intl.NumberFormat(locale)
21
+ .formatToParts(123456789)
22
+ .filter((part) => part.type === 'integer');
23
+ const primary = example.at(-1)?.value.length ?? 3;
24
+ const secondary = example.at(-2)?.value.length ?? primary;
25
+ if (
26
+ groups.some((part) => !/^\d+$/.test(part)) ||
27
+ groups.at(-1)!.length !== primary ||
28
+ groups.slice(1, -1).some((part) => part.length !== secondary) ||
29
+ groups[0].length > secondary
30
+ )
31
+ return null;
32
+ value = value.split(group).join('');
33
+ }
34
+ value = value
35
+ .split(decimal)
36
+ .join('.')
37
+ .split(minus)
38
+ .join('-')
39
+ .replace(/\u2212/g, '-');
40
+ if (!/^-?(?:\d+(?:\.\d+)?|\.\d+)$/.test(value)) return null;
41
+ const result = Number(value);
42
+ return Number.isFinite(result) ? result : null;
43
+ }
44
+ export function formatLocalizedAmount(
45
+ value: number,
46
+ locale = 'en-US',
47
+ fractionDigits = 2,
48
+ ): string {
49
+ return new Intl.NumberFormat(locale, {
50
+ minimumFractionDigits: fractionDigits,
51
+ maximumFractionDigits: fractionDigits,
52
+ }).format(value);
53
+ }
@@ -0,0 +1,42 @@
1
+ /** Gregorian local-date values. UTC arithmetic avoids DST changes and host time zones. */
2
+ export function validDate(value: string): boolean {
3
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
4
+ const date = new Date(`${value}T12:00:00Z`);
5
+ return Number.isFinite(date.valueOf()) && date.toISOString().slice(0, 10) === value;
6
+ }
7
+ export function addDays(value: string, amount: number): string {
8
+ if (!validDate(value)) throw new RangeError('Expected a Gregorian date in YYYY-MM-DD format');
9
+ const date = new Date(`${value}T12:00:00Z`);
10
+ date.setUTCDate(date.getUTCDate() + amount);
11
+ return date.toISOString().slice(0, 10);
12
+ }
13
+ export function addMonths(month: string, amount: number): string {
14
+ const first = month.slice(0, 7) + '-01';
15
+ if (!validDate(first)) throw new RangeError('Expected a Gregorian month');
16
+ const date = new Date(`${first}T12:00:00Z`);
17
+ date.setUTCMonth(date.getUTCMonth() + amount);
18
+ return date.toISOString().slice(0, 7);
19
+ }
20
+ export function monthDays(month: string, firstDayOfWeek = 1): string[] {
21
+ const first = `${month.slice(0, 7)}-01`;
22
+ if (!validDate(first)) throw new RangeError('Expected a Gregorian month');
23
+ const weekday = new Date(`${first}T12:00:00Z`).getUTCDay();
24
+ const offset = (weekday - (((firstDayOfWeek % 7) + 7) % 7) + 7) % 7;
25
+ return Array.from({ length: 42 }, (_, index) => addDays(first, index - offset));
26
+ }
27
+ export function formatCalendarDate(
28
+ date: string,
29
+ locale?: string,
30
+ options: Intl.DateTimeFormatOptions = { dateStyle: 'medium' },
31
+ ): string {
32
+ return validDate(date)
33
+ ? new Intl.DateTimeFormat(locale, {
34
+ ...options,
35
+ calendar: 'gregory',
36
+ timeZone: 'UTC',
37
+ }).format(new Date(`${date}T12:00:00Z`))
38
+ : date;
39
+ }
40
+ export function validTime(value: string): boolean {
41
+ return /^(?:[01]\d|2[0-3]):[0-5]\d$/.test(value);
42
+ }
@@ -0,0 +1,64 @@
1
+ import * as React from 'react';
2
+ import { View } from 'react-native';
3
+ import { useThemeColors, useIsolinePlatform } from '../provider.tsx';
4
+ import { Button, type ButtonProps } from './button.tsx';
5
+ import { Text } from './text.tsx';
6
+ import type { IconProps } from './icons.tsx';
7
+
8
+ export interface ActionChipProps extends Omit<
9
+ ButtonProps,
10
+ 'children' | 'icon' | 'label' | 'variant' | 'size'
11
+ > {
12
+ label: string;
13
+ icon?: React.ComponentType<IconProps>;
14
+ tone?: 'neutral' | 'primary' | 'secondary' | 'tertiary';
15
+ }
16
+ export function ActionChip({
17
+ label,
18
+ icon: Icon,
19
+ tone = 'neutral',
20
+ style,
21
+ ...props
22
+ }: ActionChipProps) {
23
+ const colors = useThemeColors();
24
+ const glass =
25
+ useIsolinePlatform() === 'ios' &&
26
+ props.material !== undefined &&
27
+ props.material !== 'solid';
28
+ const backgroundColor = glass
29
+ ? 'transparent'
30
+ : tone === 'neutral'
31
+ ? colors.surfaceContainerHigh
32
+ : colors[`${tone}Container`];
33
+ const color = glass
34
+ ? colors.primary
35
+ : tone === 'neutral'
36
+ ? colors.onSurface
37
+ : colors[
38
+ tone === 'primary'
39
+ ? 'onPrimaryContainer'
40
+ : tone === 'secondary'
41
+ ? 'onSecondaryContainer'
42
+ : 'onTertiaryContainer'
43
+ ];
44
+ return (
45
+ <Button
46
+ {...props}
47
+ size="s"
48
+ variant="tonal"
49
+ accessibilityLabel={props.accessibilityLabel ?? label}
50
+ // A callback only when the caller passed one: Button has to re-render on
51
+ // press to resolve it, which a plain style avoids.
52
+ style={
53
+ typeof style === 'function'
54
+ ? (state) => [{ backgroundColor, minHeight: 48 }, style(state)]
55
+ : [{ backgroundColor, minHeight: 48 }, style]
56
+ }
57
+ >
58
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
59
+ {Icon ? <Icon size={20} color={color} /> : null}
60
+ <Text style={{ color, fontWeight: '600' }}>{label}</Text>
61
+ </View>
62
+ </Button>
63
+ );
64
+ }
@@ -0,0 +1,49 @@
1
+ import * as React from 'react';
2
+ import { View, TextInput } from 'react-native';
3
+ import { TextField, type TextFieldProps } from './text-field.tsx';
4
+ import { Text } from './text.tsx';
5
+ import { parseLocalizedAmount } from '../amount.ts';
6
+
7
+ export interface AmountFieldProps extends Omit<TextFieldProps, 'value' | 'onChangeText'> {
8
+ value: string;
9
+ onChangeText: (value: string) => void;
10
+ onAmountChange?: (amount: number | null) => void;
11
+ currency?: string | null;
12
+ locale?: string;
13
+ }
14
+ /** Preserve the editing string, including incomplete decimals; report numeric units separately. */
15
+ export const AmountField: React.ForwardRefExoticComponent<
16
+ AmountFieldProps & React.RefAttributes<React.ElementRef<typeof TextInput>>
17
+ > = React.forwardRef<React.ComponentRef<typeof TextInput>, AmountFieldProps>(
18
+ function AmountField(
19
+ { value, onChangeText, onAmountChange, currency, locale = 'en-US', ...props },
20
+ ref,
21
+ ) {
22
+ let symbol = currency ?? '';
23
+ if (currency) {
24
+ try {
25
+ symbol =
26
+ new Intl.NumberFormat(locale, { style: 'currency', currency })
27
+ .formatToParts(0)
28
+ .find((part) => part.type === 'currency')?.value ?? currency;
29
+ } catch {}
30
+ }
31
+ return (
32
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
33
+ <View style={{ flex: 1 }}>
34
+ <TextField
35
+ {...props}
36
+ ref={ref}
37
+ value={value}
38
+ keyboardType="decimal-pad"
39
+ onChangeText={(text) => {
40
+ onChangeText(text);
41
+ onAmountChange?.(parseLocalizedAmount(text, locale));
42
+ }}
43
+ />
44
+ </View>
45
+ {symbol ? <Text accessibilityLabel={currency ?? undefined}>{symbol}</Text> : null}
46
+ </View>
47
+ );
48
+ },
49
+ );
@@ -0,0 +1,91 @@
1
+ import * as React from 'react';
2
+ import {
3
+ Image,
4
+ View,
5
+ type ImageSourcePropType,
6
+ type StyleProp,
7
+ type ViewStyle,
8
+ } from 'react-native';
9
+ import { Text } from './text.tsx';
10
+ import { useThemeColors } from '../provider.tsx';
11
+
12
+ export interface AvatarProps {
13
+ accessibilityLabel: string;
14
+ initials?: string;
15
+ source?: ImageSourcePropType;
16
+ size?: number;
17
+ color?: string;
18
+ foregroundColor?: string;
19
+ accent?: string;
20
+ badge?: React.ReactNode;
21
+ renderImage?: (size: number, onError: () => void) => React.ReactNode;
22
+ style?: StyleProp<ViewStyle>;
23
+ className?: string;
24
+ }
25
+ /** Image rendering and initials only: no address book, network lookup or app preference. */
26
+ export function Avatar({
27
+ accessibilityLabel,
28
+ initials,
29
+ source,
30
+ size = 44,
31
+ color,
32
+ foregroundColor,
33
+ accent,
34
+ badge,
35
+ renderImage,
36
+ style,
37
+ className,
38
+ }: AvatarProps) {
39
+ const colors = useThemeColors();
40
+ const [failed, setFailed] = React.useState(false);
41
+ React.useEffect(() => setFailed(false), [source, renderImage]);
42
+ return (
43
+ <View
44
+ accessible
45
+ accessibilityRole="image"
46
+ accessibilityLabel={accessibilityLabel}
47
+ style={[{ width: size, height: size }, style]}
48
+ >
49
+ <View
50
+ className={className}
51
+ style={{
52
+ width: size,
53
+ height: size,
54
+ borderRadius: size / 2,
55
+ overflow: 'hidden',
56
+ backgroundColor: color ?? colors.secondaryContainer,
57
+ borderWidth: accent ? 2 : 0,
58
+ borderColor: accent,
59
+ alignItems: 'center',
60
+ justifyContent: 'center',
61
+ }}
62
+ >
63
+ <Text
64
+ accessible={false}
65
+ style={{
66
+ fontSize: size * 0.36,
67
+ lineHeight: size * 0.5,
68
+ color: foregroundColor ?? colors.onSecondaryContainer,
69
+ fontWeight: '600',
70
+ }}
71
+ >
72
+ {initials}
73
+ </Text>
74
+ {!failed && renderImage ? (
75
+ renderImage(size, () => setFailed(true))
76
+ ) : !failed && source ? (
77
+ <Image
78
+ source={source}
79
+ accessible={false}
80
+ onError={() => setFailed(true)}
81
+ resizeMode="cover"
82
+ style={{ position: 'absolute', width: size, height: size }}
83
+ />
84
+ ) : null}
85
+ </View>
86
+ {badge ? (
87
+ <View style={{ position: 'absolute', right: -3, bottom: -3 }}>{badge}</View>
88
+ ) : null}
89
+ </View>
90
+ );
91
+ }
@@ -0,0 +1,39 @@
1
+ import { BackIcon } from './back-icon.tsx';
2
+ import { HEADER_ICON_BUTTON } from './icon-button.tsx';
3
+ import { Pressable } from './pressable.tsx';
4
+ import { Text } from './text.tsx';
5
+ import { useLabels } from '../provider.tsx';
6
+ import { useRipple, useThemeColors } from '../provider.tsx';
7
+
8
+ export interface BackButtonProps {
9
+ onPress: () => void;
10
+ /** What the row leads back to, e.g. "Inbox", "Settings". Defaults to "Back". */
11
+ label?: string;
12
+ }
13
+
14
+ /**
15
+ * The `< Back` row every screen opens with.
16
+ *
17
+ * The arrow is drawn at the header icon size, so it matches the bare back
18
+ * arrow of the screens that use a top bar instead (the reader, compose).
19
+ */
20
+ export function BackButton({ onPress, label }: BackButtonProps) {
21
+ const colors = useThemeColors();
22
+ const ripple = useRipple();
23
+ const labels = useLabels();
24
+
25
+ return (
26
+ <Pressable
27
+ onPress={onPress}
28
+ hitSlop={8}
29
+ android_ripple={{ color: ripple.onNeutral }}
30
+ accessibilityRole="button"
31
+ className="h-12 flex-row items-center gap-2 self-start rounded-full pl-2 pr-3"
32
+ >
33
+ <BackIcon size={HEADER_ICON_BUTTON.iconSize} color={colors.onSurfaceVariant} />
34
+ <Text variant="labelLarge" className="text-muted-foreground">
35
+ {label ?? labels.back}
36
+ </Text>
37
+ </Pressable>
38
+ );
39
+ }
@@ -0,0 +1,25 @@
1
+ import { I18nManager, Platform } from 'react-native';
2
+ import Svg, { Path } from 'react-native-svg';
3
+
4
+ import { useIsolinePlatform } from '../provider.tsx';
5
+ import { ArrowLeft, type IconProps } from './icons.tsx';
6
+
7
+ /** Navigation uses the iOS chevron; directional/content arrows stay unchanged. */
8
+ export function BackIcon({ size = 24, color = '#000000', style, ...props }: IconProps) {
9
+ if (useIsolinePlatform() !== 'ios') {
10
+ return <ArrowLeft size={size} color={color} style={style} {...props} />;
11
+ }
12
+
13
+ return (
14
+ <Svg width={size} height={size} viewBox="0 0 24 24" style={style} accessible={false}>
15
+ <Path
16
+ d={I18nManager.isRTL ? 'm9 5 7 7-7 7' : 'm15 5-7 7 7 7'}
17
+ fill="none"
18
+ stroke={color}
19
+ strokeWidth={2.5}
20
+ strokeLinecap="round"
21
+ strokeLinejoin="round"
22
+ />
23
+ </Svg>
24
+ );
25
+ }
@@ -0,0 +1,41 @@
1
+ import * as React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { Text } from './text.tsx';
5
+ import { cn } from '../utils.ts';
6
+
7
+ export interface BadgeProps {
8
+ label: string;
9
+ /**
10
+ * Tints the pill and its text, for a badge that stands for something with a
11
+ * colour of its own — a tag. Left unset, the badge is the neutral container
12
+ * colour a folder name gets.
13
+ */
14
+ color?: string;
15
+ className?: string;
16
+ }
17
+
18
+ /** A small pill label, such as a folder name sitting beside a message count. */
19
+ export function Badge({ label, color, className }: BadgeProps) {
20
+ return (
21
+ <View
22
+ className={cn(
23
+ 'rounded-full px-2 py-0.5',
24
+ color ? undefined : 'bg-surfaceContainerHigh',
25
+ className,
26
+ )}
27
+ // The colour at low alpha, so a tinted badge reads as tinted rather than
28
+ // as a solid swatch competing with the accent.
29
+ style={color ? { backgroundColor: `${color}2A` } : undefined}
30
+ >
31
+ <Text
32
+ variant="caption"
33
+ className={cn(color ? undefined : 'text-onSurfaceVariant')}
34
+ style={color ? { color } : undefined}
35
+ numberOfLines={1}
36
+ >
37
+ {label}
38
+ </Text>
39
+ </View>
40
+ );
41
+ }
@@ -0,0 +1,63 @@
1
+ import * as React from 'react';
2
+ import { View } from 'react-native';
3
+ import { Text } from './text.tsx';
4
+ import { Button } from './button.tsx';
5
+ import { IconButton } from './icon-button.tsx';
6
+ import { X, type IconProps } from './icons.tsx';
7
+ import { useThemeColors } from '../provider.tsx';
8
+
9
+ export interface BannerProps {
10
+ message: string;
11
+ title?: string;
12
+ tone?: 'neutral' | 'info' | 'error';
13
+ icon?: React.ComponentType<IconProps>;
14
+ actionLabel?: string;
15
+ onAction?: () => void;
16
+ onDismiss?: () => void;
17
+ dismissLabel?: string;
18
+ }
19
+ export function Banner({
20
+ message,
21
+ title,
22
+ tone = 'neutral',
23
+ icon: Icon,
24
+ actionLabel,
25
+ onAction,
26
+ onDismiss,
27
+ dismissLabel,
28
+ }: BannerProps) {
29
+ const colors = useThemeColors();
30
+ const backgroundColor =
31
+ tone === 'error'
32
+ ? colors.errorContainer
33
+ : tone === 'info'
34
+ ? colors.primaryContainer
35
+ : colors.surfaceContainerHigh;
36
+ const color =
37
+ tone === 'error'
38
+ ? colors.onErrorContainer
39
+ : tone === 'info'
40
+ ? colors.onPrimaryContainer
41
+ : colors.onSurface;
42
+ return (
43
+ <View
44
+ accessibilityLiveRegion={tone === 'error' ? 'assertive' : 'polite'}
45
+ style={{ borderRadius: 16, padding: 12, gap: 8, backgroundColor }}
46
+ >
47
+ <View style={{ flexDirection: 'row', gap: 10, alignItems: 'center' }}>
48
+ {Icon ? <Icon size={24} color={color} /> : null}
49
+ <View style={{ flex: 1, gap: 4 }}>
50
+ {title ? <Text style={{ color, fontWeight: '600' }}>{title}</Text> : null}
51
+ <Text style={{ color }}>{message}</Text>
52
+ </View>
53
+ {onDismiss && dismissLabel ? (
54
+ <IconButton icon={X} accessibilityLabel={dismissLabel} onPress={onDismiss} />
55
+ ) : null}
56
+ </View>
57
+ {actionLabel && onAction ? (
58
+ <Button variant="ghost" size="s" label={actionLabel} onPress={onAction} />
59
+ ) : null}
60
+ </View>
61
+ );
62
+ }
63
+ export const InlineNotice = Banner;
@@ -0,0 +1,52 @@
1
+ import * as React from 'react';
2
+ import { KeyboardAvoidingView, Modal, type DimensionValue } from 'react-native';
3
+ import { GestureHandlerRootView } from 'react-native-gesture-handler';
4
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
5
+ import { type MaterialOptions, ThemeScope, useIsolinePlatform } from '../provider.tsx';
6
+ import { SheetFrame } from './sheet-frame.tsx';
7
+ import { useSheet } from './sheet-transition.ts';
8
+ export interface BottomSheetProps extends MaterialOptions {
9
+ visible: boolean;
10
+ onClose: () => void;
11
+ maxHeight?: DimensionValue;
12
+ onShow?: () => void;
13
+ children: React.ReactNode;
14
+ }
15
+ export function BottomSheet({
16
+ visible,
17
+ onClose,
18
+ maxHeight = '90%',
19
+ onShow,
20
+ children,
21
+ ...materialOptions
22
+ }: BottomSheetProps) {
23
+ const insets = useSafeAreaInsets();
24
+ const ios = useIsolinePlatform() === 'ios';
25
+ const sheet = useSheet(visible, onClose);
26
+ return (
27
+ <Modal
28
+ visible={sheet.mounted}
29
+ transparent
30
+ presentationStyle="overFullScreen"
31
+ animationType="none"
32
+ onRequestClose={onClose}
33
+ onShow={onShow}
34
+ >
35
+ <ThemeScope style={{ flex: 1 }}>
36
+ <GestureHandlerRootView style={{ flex: 1 }}>
37
+ <KeyboardAvoidingView style={{ flex: 1 }} behavior="padding" enabled={ios}>
38
+ <SheetFrame
39
+ {...materialOptions}
40
+ sheet={sheet}
41
+ visible={visible}
42
+ onClose={onClose}
43
+ panelStyle={{ paddingBottom: insets.bottom + 16, maxHeight }}
44
+ >
45
+ {children}
46
+ </SheetFrame>
47
+ </KeyboardAvoidingView>
48
+ </GestureHandlerRootView>
49
+ </ThemeScope>
50
+ </Modal>
51
+ );
52
+ }