@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.
- package/LICENSE +22 -0
- package/NOTICE.md +12 -0
- package/README.md +104 -3
- package/dist/amount.d.ts +3 -0
- package/dist/calendar.d.ts +7 -0
- package/dist/components/action-chip.d.ts +9 -0
- package/dist/components/amount-field.d.ts +12 -0
- package/dist/components/avatar.d.ts +17 -0
- package/dist/components/back-button.d.ts +12 -0
- package/dist/components/back-icon.d.ts +3 -0
- package/dist/components/badge.d.ts +13 -0
- package/dist/components/banner.d.ts +14 -0
- package/dist/components/bottom-sheet-native.d.ts +11 -0
- package/dist/components/bottom-sheet-web.d.ts +18 -0
- package/dist/components/bottom-sheet.d.ts +3 -0
- package/dist/components/button-group.d.ts +26 -0
- package/dist/components/button.d.ts +48 -0
- package/dist/components/checkbox.d.ts +12 -0
- package/dist/components/color-picker.d.ts +21 -0
- package/dist/components/context-menu.d.ts +26 -0
- package/dist/components/date-picker.d.ts +64 -0
- package/dist/components/dialog-context.d.ts +2 -0
- package/dist/components/dialog.d.ts +20 -0
- package/dist/components/empty-state.d.ts +15 -0
- package/dist/components/expand-indicator.d.ts +27 -0
- package/dist/components/expressive-shape.d.ts +18 -0
- package/dist/components/fab-menu-legacy.d.ts +31 -0
- package/dist/components/fab-menu.d.ts +21 -0
- package/dist/components/filter-chip.d.ts +69 -0
- package/dist/components/floating-action-button.d.ts +15 -0
- package/dist/components/icon-button.d.ts +40 -0
- package/dist/components/icons.d.ts +18 -0
- package/dist/components/input.d.ts +9 -0
- package/dist/components/keyboard-inset-view.d.ts +15 -0
- package/dist/components/list-controls.d.ts +38 -0
- package/dist/components/loading-indicator.d.ts +9 -0
- package/dist/components/menu-sheet.d.ts +36 -0
- package/dist/components/panel-sheet.d.ts +8 -0
- package/dist/components/password-visibility.d.ts +5 -0
- package/dist/components/popover-layout.d.ts +25 -0
- package/dist/components/popover.d.ts +20 -0
- package/dist/components/pressable.d.ts +12 -0
- package/dist/components/radio-navigation.d.ts +11 -0
- package/dist/components/refresh-control.d.ts +16 -0
- package/dist/components/row-gap.d.ts +9 -0
- package/dist/components/search-field.d.ts +22 -0
- package/dist/components/select-field.d.ts +16 -0
- package/dist/components/settings-list.d.ts +98 -0
- package/dist/components/settings-row.d.ts +13 -0
- package/dist/components/sheet-frame.d.ts +27 -0
- package/dist/components/sheet-lifecycle.d.ts +36 -0
- package/dist/components/sheet-transition.d.ts +36 -0
- package/dist/components/slider.d.ts +21 -0
- package/dist/components/snackbar.d.ts +10 -0
- package/dist/components/split-button.d.ts +38 -0
- package/dist/components/squish.d.ts +20 -0
- package/dist/components/swipeable-row.d.ts +20 -0
- package/dist/components/switch.d.ts +8 -0
- package/dist/components/tab-bar.d.ts +20 -0
- package/dist/components/text-field.d.ts +31 -0
- package/dist/components/text.d.ts +9 -0
- package/dist/components/time-picker.d.ts +40 -0
- package/dist/components/toolbar.d.ts +77 -0
- package/dist/components/top-bar.d.ts +27 -0
- package/dist/components/undo-bar.d.ts +23 -0
- package/dist/components/wavy-progress.d.ts +21 -0
- package/dist/components/zoomable-image.d.ts +18 -0
- package/dist/expressive-shapes.d.ts +8 -0
- package/dist/index.d.ts +62 -0
- package/dist/keyboard.d.ts +2 -0
- package/dist/material.d.ts +11 -0
- package/dist/media.d.ts +11 -0
- package/dist/motion.d.ts +96 -0
- package/dist/platform.d.ts +3 -0
- package/dist/preset-tokens.d.ts +264 -0
- package/dist/provider.d.ts +59 -0
- package/dist/reduced-motion.d.ts +45 -0
- package/dist/selection.d.ts +2 -0
- package/dist/shape.d.ts +99 -0
- package/dist/sheet-physics.d.ts +37 -0
- package/dist/theme.d.ts +103 -0
- package/dist/tokens.d.ts +2 -0
- package/dist/use-keyboard-height.d.ts +10 -0
- package/dist/utils.d.ts +3 -0
- package/nativewind.cjs +68 -0
- package/package.json +63 -6
- package/src/amount.ts +53 -0
- package/src/calendar.ts +42 -0
- package/src/components/action-chip.tsx +64 -0
- package/src/components/amount-field.tsx +49 -0
- package/src/components/avatar.tsx +91 -0
- package/src/components/back-button.tsx +39 -0
- package/src/components/back-icon.tsx +25 -0
- package/src/components/badge.tsx +41 -0
- package/src/components/banner.tsx +63 -0
- package/src/components/bottom-sheet-native.tsx +52 -0
- package/src/components/bottom-sheet-web.tsx +193 -0
- package/src/components/bottom-sheet.tsx +5 -0
- package/src/components/button-group.tsx +326 -0
- package/src/components/button.tsx +337 -0
- package/src/components/checkbox.tsx +71 -0
- package/src/components/color-picker.tsx +115 -0
- package/src/components/context-menu.tsx +149 -0
- package/src/components/date-picker.tsx +466 -0
- package/src/components/dialog-context.ts +3 -0
- package/src/components/dialog.tsx +210 -0
- package/src/components/empty-state.tsx +49 -0
- package/src/components/expand-indicator.tsx +60 -0
- package/src/components/expressive-shape.tsx +37 -0
- package/src/components/fab-menu-legacy.tsx +181 -0
- package/src/components/fab-menu.tsx +90 -0
- package/src/components/filter-chip.tsx +136 -0
- package/src/components/floating-action-button.tsx +79 -0
- package/src/components/icon-button.tsx +127 -0
- package/src/components/icons.tsx +43 -0
- package/src/components/input.tsx +80 -0
- package/src/components/keyboard-inset-view.tsx +28 -0
- package/src/components/list-controls.tsx +134 -0
- package/src/components/loading-indicator.tsx +183 -0
- package/src/components/menu-sheet.tsx +133 -0
- package/src/components/panel-sheet.tsx +90 -0
- package/src/components/password-visibility.tsx +39 -0
- package/src/components/popover-layout.ts +47 -0
- package/src/components/popover.tsx +139 -0
- package/src/components/pressable.tsx +108 -0
- package/src/components/radio-navigation.ts +52 -0
- package/src/components/refresh-control.tsx +39 -0
- package/src/components/row-gap.tsx +13 -0
- package/src/components/search-field.tsx +92 -0
- package/src/components/select-field.tsx +105 -0
- package/src/components/settings-list.tsx +302 -0
- package/src/components/settings-row.tsx +61 -0
- package/src/components/sheet-frame.tsx +250 -0
- package/src/components/sheet-lifecycle.ts +233 -0
- package/src/components/sheet-transition.ts +105 -0
- package/src/components/slider.tsx +185 -0
- package/src/components/snackbar.tsx +97 -0
- package/src/components/split-button.tsx +212 -0
- package/src/components/squish.ts +41 -0
- package/src/components/swipeable-row.tsx +109 -0
- package/src/components/switch.tsx +77 -0
- package/src/components/tab-bar.tsx +195 -0
- package/src/components/text-field.tsx +264 -0
- package/src/components/text.tsx +58 -0
- package/src/components/time-picker.tsx +277 -0
- package/src/components/toolbar.tsx +406 -0
- package/src/components/top-bar.tsx +91 -0
- package/src/components/undo-bar.tsx +137 -0
- package/src/components/wavy-progress.tsx +150 -0
- package/src/components/zoomable-image.tsx +240 -0
- package/src/expressive-shapes.ts +25 -0
- package/src/index.ts +62 -0
- package/src/keyboard.ts +5 -0
- package/src/material.ts +16 -0
- package/src/media.ts +51 -0
- package/src/motion.ts +96 -0
- package/src/platform.ts +13 -0
- package/src/preset-tokens.ts +265 -0
- package/src/provider.tsx +201 -0
- package/src/reduced-motion.ts +50 -0
- package/src/selection.ts +11 -0
- package/src/shape.ts +217 -0
- package/src/sheet-physics.ts +73 -0
- package/src/theme.ts +180 -0
- package/src/tokens.ts +2 -0
- package/src/use-keyboard-height.ts +64 -0
- 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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|
package/src/calendar.ts
ADDED
|
@@ -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
|
+
}
|