@qumra/fanar-native 0.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.
- package/README.md +83 -0
- package/lib/art.d.ts +8 -0
- package/lib/art.js +29 -0
- package/lib/assets/art/avatar.png +0 -0
- package/lib/assets/art/empty.png +0 -0
- package/lib/assets/art/image-placeholder.png +0 -0
- package/lib/assets/art/plan-update.png +0 -0
- package/lib/assets/brand/app-store.png +0 -0
- package/lib/assets/brand/etisalat.png +0 -0
- package/lib/assets/brand/facebook.png +0 -0
- package/lib/assets/brand/google-play.png +0 -0
- package/lib/assets/brand/instagram.png +0 -0
- package/lib/assets/brand/orange.png +0 -0
- package/lib/assets/brand/snapchat.png +0 -0
- package/lib/assets/brand/tiktok.png +0 -0
- package/lib/assets/brand/vodafone.png +0 -0
- package/lib/assets/brand/we.png +0 -0
- package/lib/assets/brand/whatsapp.png +0 -0
- package/lib/assets/brand/x.png +0 -0
- package/lib/assets/brand/youtube.png +0 -0
- package/lib/components/Button.d.ts +28 -0
- package/lib/components/Button.js +75 -0
- package/lib/components/Disclosure.d.ts +41 -0
- package/lib/components/Disclosure.js +98 -0
- package/lib/components/Display.d.ts +88 -0
- package/lib/components/Display.js +157 -0
- package/lib/components/Drawer.d.ts +27 -0
- package/lib/components/Drawer.js +58 -0
- package/lib/components/Feedback.d.ts +43 -0
- package/lib/components/Feedback.js +136 -0
- package/lib/components/Form.d.ts +92 -0
- package/lib/components/Form.js +196 -0
- package/lib/components/Money.d.ts +29 -0
- package/lib/components/Money.js +65 -0
- package/lib/components/Nav.d.ts +29 -0
- package/lib/components/Nav.js +65 -0
- package/lib/components/NumberStepper.d.ts +16 -0
- package/lib/components/NumberStepper.js +79 -0
- package/lib/components/Overlay.d.ts +54 -0
- package/lib/components/Overlay.js +147 -0
- package/lib/components/Rating.d.ts +18 -0
- package/lib/components/Rating.js +32 -0
- package/lib/components/Shell.d.ts +54 -0
- package/lib/components/Shell.js +99 -0
- package/lib/components/Surfaces.d.ts +64 -0
- package/lib/components/Surfaces.js +122 -0
- package/lib/components/Text.d.ts +14 -0
- package/lib/components/Text.js +31 -0
- package/lib/components/currency.d.ts +251 -0
- package/lib/components/currency.js +48 -0
- package/lib/glyphs.d.ts +2 -0
- package/lib/glyphs.js +7 -0
- package/lib/i18n/content.d.ts +1 -0
- package/lib/i18n/content.js +28 -0
- package/lib/i18n/en.d.ts +1 -0
- package/lib/i18n/en.js +124 -0
- package/lib/i18n/index.d.ts +64 -0
- package/lib/i18n/index.js +91 -0
- package/lib/i18n/screens.d.ts +1 -0
- package/lib/i18n/screens.js +2025 -0
- package/lib/icons.d.ts +18 -0
- package/lib/icons.js +400 -0
- package/lib/index.d.ts +25 -0
- package/lib/index.js +25 -0
- package/lib/lib/a11y.d.ts +4 -0
- package/lib/lib/a11y.js +8 -0
- package/lib/lib/digits.d.ts +3 -0
- package/lib/lib/digits.js +22 -0
- package/lib/lib/motion.d.ts +14 -0
- package/lib/lib/motion.js +47 -0
- package/lib/lib/tone.d.ts +9 -0
- package/lib/lib/tone.js +16 -0
- package/lib/qumra/NavIcon.d.ts +14 -0
- package/lib/qumra/NavIcon.js +17 -0
- package/lib/qumra/QumraIcon.d.ts +17 -0
- package/lib/qumra/QumraIcon.js +91 -0
- package/lib/qumra/data.generated.d.ts +3 -0
- package/lib/qumra/data.generated.js +146 -0
- package/lib/qumra/types.d.ts +5 -0
- package/lib/qumra/types.js +1 -0
- package/lib/theme/context.d.ts +31 -0
- package/lib/theme/context.js +46 -0
- package/lib/theme/fonts.d.ts +9 -0
- package/lib/theme/fonts.js +36 -0
- package/lib/theme/index.d.ts +4 -0
- package/lib/theme/index.js +4 -0
- package/lib/theme/shadows.d.ts +2 -0
- package/lib/theme/shadows.js +30 -0
- package/lib/theme/theme.d.ts +52 -0
- package/lib/theme/theme.js +78 -0
- package/lib/tokens/generated.d.ts +180 -0
- package/lib/tokens/generated.js +173 -0
- package/lib/tokens/index.d.ts +1 -0
- package/lib/tokens/index.js +1 -0
- package/package.json +59 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { Animated, Pressable, StyleSheet, TextInput, View, } from 'react-native';
|
|
4
|
+
import { Text } from './Text';
|
|
5
|
+
import { Sheet } from './Overlay';
|
|
6
|
+
import { useFanar } from '../theme/context';
|
|
7
|
+
import { HIT_SLOP_SM } from '../lib/a11y';
|
|
8
|
+
import { EASE_IN_OUT } from '../lib/motion';
|
|
9
|
+
import { Check, ChevronDown, Search, X } from '../icons';
|
|
10
|
+
const HEIGHT = { sm: 36, md: 44, lg: 56 };
|
|
11
|
+
const FONT = { sm: 13, md: 14, lg: 16 };
|
|
12
|
+
const PAD = { sm: 14, md: 14, lg: 18 };
|
|
13
|
+
const BORDER = { sm: 1, md: 1, lg: 1.5 };
|
|
14
|
+
export function Field({ label, required, meta, error, hint, size = 'md', children, style }) {
|
|
15
|
+
return (_jsxs(View, { style: [styles.shell, style], children: [(label || meta) && (_jsxs(View, { style: styles.labelRow, children: [label ? (_jsxs(Text, { size: size === 'lg' ? 'body' : 'ui', weight: "bold", color: "ink2", align: "start", children: [label, required ? (_jsx(Text, { size: size === 'lg' ? 'body' : 'ui', weight: "bold", color: "danger", children: ' *' })) : null] })) : (_jsx(View, {})), meta ? (_jsx(Text, { size: "caption", color: "muted", children: meta })) : null] })), children, error ? (_jsx(Text, { size: "caption", color: "red", align: "start", accessibilityLiveRegion: "polite", children: error })) : hint ? (_jsx(Text, { size: "caption", color: "muted", align: "start", children: hint })) : null] }));
|
|
16
|
+
}
|
|
17
|
+
export function Input({ label, required, meta, error, invalid: invalidProp, hint, size = 'md', icon: Icon, suffix, action, numeric, editable = true, style, inputStyle, onFocus, onBlur, accessibilityLabel, ...rest }) {
|
|
18
|
+
const { theme, dir } = useFanar();
|
|
19
|
+
const [focused, setFocused] = useState(false);
|
|
20
|
+
const invalid = !!error || !!invalidProp;
|
|
21
|
+
const borderColor = invalid
|
|
22
|
+
? theme.color.danger
|
|
23
|
+
: focused
|
|
24
|
+
? theme.color.brand
|
|
25
|
+
: theme.color.border;
|
|
26
|
+
return (_jsx(Field, { label: label, required: required, meta: meta, error: error, hint: hint, size: size, style: style, children: _jsxs(View, { style: [
|
|
27
|
+
styles.row,
|
|
28
|
+
{
|
|
29
|
+
height: HEIGHT[size],
|
|
30
|
+
borderRadius: theme.radius.field,
|
|
31
|
+
borderWidth: BORDER[size],
|
|
32
|
+
borderColor,
|
|
33
|
+
backgroundColor: editable ? theme.color.surface : theme.color.bg,
|
|
34
|
+
paddingHorizontal: PAD[size],
|
|
35
|
+
gap: 10,
|
|
36
|
+
},
|
|
37
|
+
inputStyle,
|
|
38
|
+
], children: [Icon && _jsx(Icon, { size: 17, strokeWidth: 2, color: theme.color.muted }), _jsx(TextInput, { accessibilityLabel: accessibilityLabel ?? label, editable: editable, placeholderTextColor: theme.color.muted2, selectionColor: theme.color.brand, onFocus: (e) => {
|
|
39
|
+
setFocused(true);
|
|
40
|
+
onFocus?.(e);
|
|
41
|
+
}, onBlur: (e) => {
|
|
42
|
+
setFocused(false);
|
|
43
|
+
onBlur?.(e);
|
|
44
|
+
}, style: [
|
|
45
|
+
styles.input,
|
|
46
|
+
theme.font('medium'),
|
|
47
|
+
{
|
|
48
|
+
fontSize: FONT[size],
|
|
49
|
+
color: editable ? theme.color.ink : theme.color.muted,
|
|
50
|
+
writingDirection: numeric ? 'ltr' : dir,
|
|
51
|
+
textAlign: 'left',
|
|
52
|
+
},
|
|
53
|
+
numeric && { fontVariant: ['tabular-nums'] },
|
|
54
|
+
], ...rest }), suffix ? (_jsx(Text, { size: "ui", color: "muted", children: suffix })) : null, action] }) }));
|
|
55
|
+
}
|
|
56
|
+
export function SearchInput({ value, onClear, onChangeText, ...rest }) {
|
|
57
|
+
const { theme, p } = useFanar();
|
|
58
|
+
const show = !!value;
|
|
59
|
+
return (_jsx(Input, { icon: Search, value: value, onChangeText: onChangeText, placeholder: rest.placeholder ?? p('search'), returnKeyType: "search", clearButtonMode: "never", accessibilityRole: "search", action: show ? (_jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: p('clear'), hitSlop: HIT_SLOP_SM, onPress: () => {
|
|
60
|
+
onClear?.();
|
|
61
|
+
onChangeText?.('');
|
|
62
|
+
}, children: _jsx(X, { size: 16, strokeWidth: 2.4, color: theme.color.muted2 }) })) : undefined, ...rest }));
|
|
63
|
+
}
|
|
64
|
+
export function Textarea({ label, required, meta, error, hint, minHeight = 80, numeric, editable = true, style, onFocus, onBlur, accessibilityLabel, ...rest }) {
|
|
65
|
+
const { theme, dir } = useFanar();
|
|
66
|
+
const [focused, setFocused] = useState(false);
|
|
67
|
+
return (_jsx(Field, { label: label, required: required, meta: meta, error: error, hint: hint, style: style, children: _jsx(TextInput, { multiline: true, textAlignVertical: "top", accessibilityLabel: accessibilityLabel ?? label, editable: editable, placeholderTextColor: theme.color.muted2, selectionColor: theme.color.brand, onFocus: (e) => {
|
|
68
|
+
setFocused(true);
|
|
69
|
+
onFocus?.(e);
|
|
70
|
+
}, onBlur: (e) => {
|
|
71
|
+
setFocused(false);
|
|
72
|
+
onBlur?.(e);
|
|
73
|
+
}, style: [
|
|
74
|
+
theme.font('medium'),
|
|
75
|
+
{
|
|
76
|
+
minHeight,
|
|
77
|
+
borderRadius: theme.radius.field,
|
|
78
|
+
borderWidth: 1,
|
|
79
|
+
borderColor: error ? theme.color.danger : focused ? theme.color.brand : theme.color.border,
|
|
80
|
+
backgroundColor: editable ? theme.color.surface : theme.color.bg,
|
|
81
|
+
paddingHorizontal: 14,
|
|
82
|
+
paddingVertical: 12,
|
|
83
|
+
fontSize: theme.text.body,
|
|
84
|
+
lineHeight: Math.round(theme.text.body * theme.leading.input),
|
|
85
|
+
color: editable ? theme.color.ink : theme.color.muted,
|
|
86
|
+
writingDirection: dir,
|
|
87
|
+
textAlign: 'left',
|
|
88
|
+
},
|
|
89
|
+
], ...rest }) }));
|
|
90
|
+
}
|
|
91
|
+
function Control({ checked, round, disabled }) {
|
|
92
|
+
const { theme } = useFanar();
|
|
93
|
+
return (_jsx(View, { style: {
|
|
94
|
+
width: 19,
|
|
95
|
+
height: 19,
|
|
96
|
+
borderRadius: round ? theme.radius.full : theme.radius.xs,
|
|
97
|
+
borderWidth: 1.5,
|
|
98
|
+
alignItems: 'center',
|
|
99
|
+
justifyContent: 'center',
|
|
100
|
+
borderColor: checked ? theme.color.brand : theme.color.border,
|
|
101
|
+
backgroundColor: checked ? theme.color.brand : disabled ? theme.color.bg : theme.color.surface,
|
|
102
|
+
}, children: checked &&
|
|
103
|
+
(round ? (_jsx(View, { style: { width: 6, height: 6, borderRadius: 3, backgroundColor: '#ffffff' } })) : (_jsx(Check, { size: 13, strokeWidth: 3.2, color: "#ffffff" }))) }));
|
|
104
|
+
}
|
|
105
|
+
function ChoiceRow({ round, label, description, checked, onChange, disabled, style }) {
|
|
106
|
+
return (_jsxs(Pressable, { accessibilityRole: round ? 'radio' : 'checkbox', accessibilityState: { checked, disabled: !!disabled }, accessibilityLabel: label, disabled: disabled, onPress: () => onChange(!checked), hitSlop: HIT_SLOP_SM, style: [styles.choiceRow, disabled && styles.dim, style], children: [_jsx(View, { style: styles.choiceBox, children: _jsx(Control, { checked: checked, round: round, disabled: disabled }) }), (label || description) && (_jsxs(View, { style: styles.choiceText, children: [label ? (_jsx(Text, { size: "body", color: "ink", align: "start", children: label })) : null, description ? (_jsx(Text, { size: "caption", color: "muted", align: "start", children: description })) : null] }))] }));
|
|
107
|
+
}
|
|
108
|
+
export function Checkbox(props) {
|
|
109
|
+
return _jsx(ChoiceRow, { round: false, ...props });
|
|
110
|
+
}
|
|
111
|
+
export function Radio(props) {
|
|
112
|
+
return _jsx(ChoiceRow, { round: true, ...props });
|
|
113
|
+
}
|
|
114
|
+
export function RadioGroup({ legend, value, options, onChange, inline, style }) {
|
|
115
|
+
return (_jsxs(View, { accessibilityRole: "radiogroup", style: [styles.group, style], children: [legend ? (_jsx(Text, { size: "ui", weight: "bold", color: "ink2", align: "start", style: styles.legend, children: legend })) : null, _jsx(View, { style: inline ? styles.groupInline : styles.groupStacked, children: options.map((o) => (_jsx(Radio, { label: o.label, description: o.description, disabled: o.disabled, checked: o.value === value, onChange: () => onChange(o.value) }, o.value))) })] }));
|
|
116
|
+
}
|
|
117
|
+
export function Switch({ value, onValueChange, label, description, disabled, activeColor, style }) {
|
|
118
|
+
const { theme } = useFanar();
|
|
119
|
+
const pos = useRef(new Animated.Value(value ? 1 : 0)).current;
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
Animated.timing(pos, {
|
|
122
|
+
toValue: value ? 1 : 0,
|
|
123
|
+
duration: 200,
|
|
124
|
+
easing: EASE_IN_OUT,
|
|
125
|
+
useNativeDriver: false,
|
|
126
|
+
}).start();
|
|
127
|
+
}, [value, pos]);
|
|
128
|
+
const start = pos.interpolate({ inputRange: [0, 1], outputRange: [2, 22] });
|
|
129
|
+
return (_jsxs(Pressable, { accessibilityRole: "switch", accessibilityState: { checked: value, disabled: !!disabled }, accessibilityLabel: label, disabled: disabled, onPress: () => onValueChange(!value), style: [styles.switchRow, disabled && styles.dim, style], children: [(label || description) && (_jsxs(View, { style: styles.switchText, children: [label ? (_jsx(Text, { size: "body", color: "ink", align: "start", children: label })) : null, description ? (_jsx(Text, { size: "caption", color: "muted", align: "start", children: description })) : null] })), _jsx(View, { style: {
|
|
130
|
+
width: 44,
|
|
131
|
+
height: 24,
|
|
132
|
+
borderRadius: theme.radius.full,
|
|
133
|
+
backgroundColor: value ? (activeColor ?? theme.color.brand) : theme.color.border,
|
|
134
|
+
justifyContent: 'center',
|
|
135
|
+
}, children: _jsx(Animated.View, { style: [
|
|
136
|
+
{
|
|
137
|
+
width: 20,
|
|
138
|
+
height: 20,
|
|
139
|
+
borderRadius: 10,
|
|
140
|
+
backgroundColor: '#ffffff',
|
|
141
|
+
insetInlineStart: start,
|
|
142
|
+
},
|
|
143
|
+
theme.shadow.raise,
|
|
144
|
+
] }) })] }));
|
|
145
|
+
}
|
|
146
|
+
export function Select({ label, required, error, hint, size = 'md', value, placeholder, options, onChange, disabled, style, }) {
|
|
147
|
+
const { theme, p } = useFanar();
|
|
148
|
+
const [open, setOpen] = useState(false);
|
|
149
|
+
const current = options.find((o) => o.value === value);
|
|
150
|
+
return (_jsxs(_Fragment, { children: [_jsx(Field, { label: label, required: required, error: error, hint: hint, size: size, style: style, children: _jsxs(Pressable, { accessibilityRole: "button", accessibilityState: { disabled: !!disabled, expanded: open }, accessibilityLabel: label, accessibilityValue: { text: current?.label }, disabled: disabled, onPress: () => setOpen(true), style: ({ pressed }) => [
|
|
151
|
+
styles.row,
|
|
152
|
+
{
|
|
153
|
+
height: HEIGHT[size],
|
|
154
|
+
borderRadius: theme.radius.field,
|
|
155
|
+
borderWidth: BORDER[size],
|
|
156
|
+
borderColor: error ? theme.color.danger : theme.color.border,
|
|
157
|
+
backgroundColor: disabled ? theme.color.bg : pressed ? theme.color.hover : theme.color.surface,
|
|
158
|
+
paddingHorizontal: PAD[size],
|
|
159
|
+
gap: 10,
|
|
160
|
+
},
|
|
161
|
+
], children: [_jsx(Text, { size: FONT[size], color: current ? 'ink' : 'muted2', align: "start", lines: 1, style: styles.input, children: current?.label ?? placeholder ?? p('search') }), _jsx(ChevronDown, { size: 17, strokeWidth: 2, color: theme.color.muted2 })] }) }), open && (_jsx(SelectSheet, { title: label, options: options, value: value, onPick: (next) => {
|
|
162
|
+
onChange(next);
|
|
163
|
+
setOpen(false);
|
|
164
|
+
}, onClose: () => setOpen(false) }))] }));
|
|
165
|
+
}
|
|
166
|
+
function SelectSheet({ title, options, value, onPick, onClose, }) {
|
|
167
|
+
const { theme, p } = useFanar();
|
|
168
|
+
return (_jsx(Sheet, { open: true, title: title ?? p('selected'), onClose: onClose, children: options.map((o) => {
|
|
169
|
+
const on = o.value === value;
|
|
170
|
+
return (_jsxs(Pressable, { accessibilityRole: "menuitem", accessibilityState: { selected: on, disabled: !!o.disabled }, disabled: o.disabled, onPress: () => onPick(o.value), style: ({ pressed }) => [
|
|
171
|
+
styles.selectRow,
|
|
172
|
+
{
|
|
173
|
+
borderRadius: theme.radius.field,
|
|
174
|
+
backgroundColor: on ? theme.color.bg : pressed ? theme.color.hover : 'transparent',
|
|
175
|
+
opacity: o.disabled ? 0.5 : 1,
|
|
176
|
+
},
|
|
177
|
+
], children: [_jsx(Text, { size: "body", weight: on ? 'extraBold' : 'medium', align: "start", style: styles.input, lines: 1, children: o.label }), on && _jsx(Check, { size: 17, strokeWidth: 2.4, color: theme.color.brand })] }, o.value));
|
|
178
|
+
}) }));
|
|
179
|
+
}
|
|
180
|
+
const styles = StyleSheet.create({
|
|
181
|
+
shell: { gap: 6 },
|
|
182
|
+
labelRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 12 },
|
|
183
|
+
row: { flexDirection: 'row', alignItems: 'center' },
|
|
184
|
+
input: { flex: 1, minWidth: 0, padding: 0 },
|
|
185
|
+
choiceRow: { flexDirection: 'row', alignItems: 'flex-start', gap: 10 },
|
|
186
|
+
choiceBox: { marginTop: 1 },
|
|
187
|
+
choiceText: { flex: 1, gap: 2 },
|
|
188
|
+
dim: { opacity: 0.5 },
|
|
189
|
+
group: { gap: 10 },
|
|
190
|
+
legend: { marginBottom: 4 },
|
|
191
|
+
groupInline: { flexDirection: 'row', flexWrap: 'wrap', gap: 20 },
|
|
192
|
+
groupStacked: { gap: 12 },
|
|
193
|
+
switchRow: { flexDirection: 'row', alignItems: 'flex-start', gap: 12 },
|
|
194
|
+
switchText: { flex: 1, gap: 2 },
|
|
195
|
+
selectRow: { flexDirection: 'row', alignItems: 'center', gap: 10, height: 48, paddingHorizontal: 12 },
|
|
196
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type StyleProp, type TextStyle, type ViewStyle } from 'react-native';
|
|
3
|
+
import { CURRENCY, currencyName, isCurrency, type Currency, type SymbolStyle } from './currency';
|
|
4
|
+
export type { Currency, SymbolStyle };
|
|
5
|
+
export { CURRENCY, currencyName, isCurrency };
|
|
6
|
+
export declare function MoneyProvider({ currency, locale, children, }: {
|
|
7
|
+
currency: Currency;
|
|
8
|
+
locale?: string;
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
}): import("react").JSX.Element;
|
|
11
|
+
export declare function useCurrency(): Currency;
|
|
12
|
+
export declare function useMoneyLocale(): string;
|
|
13
|
+
export declare function useCurrencySymbol(): string;
|
|
14
|
+
export type MoneySize = 'inherit' | 'sm' | 'md' | 'lg' | 'xl';
|
|
15
|
+
export interface MoneyProps {
|
|
16
|
+
value: number;
|
|
17
|
+
currency?: Currency;
|
|
18
|
+
symbol?: SymbolStyle;
|
|
19
|
+
size?: MoneySize;
|
|
20
|
+
fontSize?: number;
|
|
21
|
+
was?: number;
|
|
22
|
+
compact?: boolean;
|
|
23
|
+
signed?: boolean;
|
|
24
|
+
tone?: boolean;
|
|
25
|
+
color?: string;
|
|
26
|
+
style?: StyleProp<ViewStyle>;
|
|
27
|
+
textStyle?: StyleProp<TextStyle>;
|
|
28
|
+
}
|
|
29
|
+
export declare function Money({ value, currency, symbol, was, size, fontSize, compact, signed, tone, color, style, textStyle, }: MoneyProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext } from 'react';
|
|
3
|
+
import { StyleSheet, View } from 'react-native';
|
|
4
|
+
import { Text } from './Text';
|
|
5
|
+
import { useFanar } from '../theme/context';
|
|
6
|
+
import { groupDigits } from '../lib/digits';
|
|
7
|
+
import { CURRENCY, currencyName, isCurrency, resolveStyle, UNIT_RATIO, unitOf } from './currency';
|
|
8
|
+
export { CURRENCY, currencyName, isCurrency };
|
|
9
|
+
const CurrencyCtx = createContext('SAR');
|
|
10
|
+
const LocaleCtx = createContext(null);
|
|
11
|
+
export function MoneyProvider({ currency, locale, children, }) {
|
|
12
|
+
return (_jsx(CurrencyCtx.Provider, { value: currency, children: _jsx(LocaleCtx.Provider, { value: locale ?? null, children: children }) }));
|
|
13
|
+
}
|
|
14
|
+
export function useCurrency() {
|
|
15
|
+
return useContext(CurrencyCtx);
|
|
16
|
+
}
|
|
17
|
+
export function useMoneyLocale() {
|
|
18
|
+
const fromProvider = useContext(LocaleCtx);
|
|
19
|
+
const { en } = useFanar();
|
|
20
|
+
return fromProvider ?? (en ? 'en-US' : 'ar-EG');
|
|
21
|
+
}
|
|
22
|
+
export function useCurrencySymbol() {
|
|
23
|
+
return CURRENCY[useCurrency()].short;
|
|
24
|
+
}
|
|
25
|
+
function format(v, decimals, locale, compact, signed) {
|
|
26
|
+
const sign = v < 0 ? '−' : signed && v > 0 ? '+' : '';
|
|
27
|
+
const abs = Math.abs(v);
|
|
28
|
+
try {
|
|
29
|
+
const nf = new Intl.NumberFormat(locale, compact
|
|
30
|
+
? { notation: 'compact', minimumFractionDigits: 0, maximumFractionDigits: 1 }
|
|
31
|
+
: { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
|
|
32
|
+
return sign + nf.format(abs);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return sign + groupDigits(abs, compact ? 0 : decimals);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const SIZE = {
|
|
39
|
+
sm: { size: 'caption', weight: 'medium' },
|
|
40
|
+
md: { size: 'ui', weight: 'medium' },
|
|
41
|
+
lg: { size: 'body', weight: 'extraBold' },
|
|
42
|
+
xl: { size: 'h2', weight: 'black' },
|
|
43
|
+
};
|
|
44
|
+
export function Money({ value, currency, symbol = 'best', was, size = 'md', fontSize, compact, signed, tone, color, style, textStyle, }) {
|
|
45
|
+
const { theme, en } = useFanar();
|
|
46
|
+
const locale = useMoneyLocale();
|
|
47
|
+
const fallback = useCurrency();
|
|
48
|
+
const code = currency ?? fallback;
|
|
49
|
+
const c = CURRENCY[code];
|
|
50
|
+
const shape = resolveStyle(symbol, c, en);
|
|
51
|
+
const unit = unitOf(c, shape);
|
|
52
|
+
const text = format(value, c.decimals, locale, compact, signed);
|
|
53
|
+
const spec = size === 'inherit' ? null : SIZE[size];
|
|
54
|
+
const px = fontSize ?? (spec ? theme.text[spec.size] : theme.text.ui);
|
|
55
|
+
const showWas = typeof was === 'number' && was > value;
|
|
56
|
+
const wasText = showWas ? format(was, c.decimals, locale, compact) : null;
|
|
57
|
+
const wasPx = Math.round(px * 0.82);
|
|
58
|
+
const fg = color ?? (tone ? (value > 0 ? theme.color.green : value < 0 ? theme.color.red : theme.color.ink) : theme.color.ink);
|
|
59
|
+
return (_jsxs(View, { accessible: true, accessibilityLabel: `${text} ${currencyName(code, en)}`, style: [styles.row, style], children: [_jsx(Text, { size: px, weight: spec?.weight ?? 'extraBold', color: fg, numeric: true, leading: "none", style: textStyle, accessibilityElementsHidden: true, importantForAccessibility: "no", children: text }), _jsx(Text, { size: Math.round(px * UNIT_RATIO[shape]), weight: shape === 'word' ? 'regular' : 'bold', color: fg, leading: "none", accessibilityElementsHidden: true, importantForAccessibility: "no", children: unit }), wasText && (_jsxs(View, { style: styles.was, children: [_jsx(Text, { size: wasPx, weight: "medium", color: theme.color.muted, numeric: true, leading: "none", style: styles.strike, accessibilityElementsHidden: true, importantForAccessibility: "no", children: wasText }), _jsx(Text, { size: Math.round(wasPx * UNIT_RATIO[shape]), weight: "regular", color: theme.color.muted, leading: "none", style: styles.strike, accessibilityElementsHidden: true, importantForAccessibility: "no", children: unit })] }))] }));
|
|
60
|
+
}
|
|
61
|
+
const styles = StyleSheet.create({
|
|
62
|
+
row: { flexDirection: 'row', alignItems: 'baseline', gap: 4 },
|
|
63
|
+
was: { flexDirection: 'row', alignItems: 'baseline', gap: 3 },
|
|
64
|
+
strike: { textDecorationLine: 'line-through' },
|
|
65
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
2
|
+
import { type LucideIcon } from '../icons';
|
|
3
|
+
export interface TabItem<T extends string = string> {
|
|
4
|
+
key: T;
|
|
5
|
+
label: string;
|
|
6
|
+
count?: number;
|
|
7
|
+
icon?: LucideIcon;
|
|
8
|
+
}
|
|
9
|
+
export interface TabsProps<T extends string> {
|
|
10
|
+
items: TabItem<T>[];
|
|
11
|
+
value: T;
|
|
12
|
+
onChange: (next: T) => void;
|
|
13
|
+
variant?: 'segmented' | 'pill';
|
|
14
|
+
fill?: boolean;
|
|
15
|
+
style?: StyleProp<ViewStyle>;
|
|
16
|
+
}
|
|
17
|
+
export declare function Tabs<T extends string>({ items, value, onChange, variant, fill, style }: TabsProps<T>): import("react").JSX.Element;
|
|
18
|
+
export interface FilterChip {
|
|
19
|
+
key: string;
|
|
20
|
+
label: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function FilterChips({ label, chips, onRemove, onClearAll, clearAllLabel, style, }: {
|
|
23
|
+
label?: string;
|
|
24
|
+
chips: FilterChip[];
|
|
25
|
+
onRemove: (key: string) => void;
|
|
26
|
+
onClearAll?: () => void;
|
|
27
|
+
clearAllLabel?: string;
|
|
28
|
+
style?: StyleProp<ViewStyle>;
|
|
29
|
+
}): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { StyleSheet, ScrollView, Pressable, View } from 'react-native';
|
|
3
|
+
import { Text } from './Text';
|
|
4
|
+
import { useFanar } from '../theme/context';
|
|
5
|
+
import { HIT_SLOP_SM } from '../lib/a11y';
|
|
6
|
+
import { X } from '../icons';
|
|
7
|
+
export function Tabs({ items, value, onChange, variant = 'segmented', fill, style }) {
|
|
8
|
+
const { theme } = useFanar();
|
|
9
|
+
const seg = variant === 'segmented';
|
|
10
|
+
const list = (_jsx(View, { style: [
|
|
11
|
+
seg
|
|
12
|
+
? [
|
|
13
|
+
styles.segList,
|
|
14
|
+
{ backgroundColor: theme.color.bg, borderRadius: theme.radius.field },
|
|
15
|
+
fill && styles.fill,
|
|
16
|
+
]
|
|
17
|
+
: styles.pillList,
|
|
18
|
+
], children: items.map((it) => {
|
|
19
|
+
const on = it.key === value;
|
|
20
|
+
const Icon = it.icon;
|
|
21
|
+
const fg = seg ? (on ? 'ink' : 'muted') : on ? 'brand-ink' : 'ink2';
|
|
22
|
+
return (_jsxs(Pressable, { accessibilityRole: "tab", accessibilityState: { selected: on }, accessibilityLabel: it.label, onPress: () => onChange(it.key), style: ({ pressed }) => [
|
|
23
|
+
seg ? styles.segTab : styles.pillTab,
|
|
24
|
+
fill && styles.fill,
|
|
25
|
+
seg
|
|
26
|
+
? {
|
|
27
|
+
borderRadius: theme.radius.sm,
|
|
28
|
+
backgroundColor: on ? theme.color.surface : pressed ? theme.color.hover : 'transparent',
|
|
29
|
+
...(on ? theme.shadow.raise : null),
|
|
30
|
+
}
|
|
31
|
+
: {
|
|
32
|
+
borderRadius: theme.radius.field,
|
|
33
|
+
borderWidth: 1,
|
|
34
|
+
borderColor: on ? theme.color.brand : theme.color.border,
|
|
35
|
+
backgroundColor: on ? theme.color.soft : pressed ? theme.color.hover : theme.color.surface,
|
|
36
|
+
},
|
|
37
|
+
], children: [Icon && _jsx(Icon, { size: 15, strokeWidth: 2.2, color: theme.colorOf(fg, theme.color.ink) }), _jsx(Text, { size: "ui", weight: "bold", color: fg, lines: 1, leading: "tight", children: it.label }), it.count !== undefined && (_jsx(Text, { size: seg ? 'ui' : 'micro', weight: "bold", color: fg, numeric: true, style: styles.count, children: it.count }))] }, it.key));
|
|
38
|
+
}) }));
|
|
39
|
+
if (fill)
|
|
40
|
+
return _jsx(View, { style: style, children: list });
|
|
41
|
+
return (_jsx(ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, contentContainerStyle: styles.scroller, style: style, children: list }));
|
|
42
|
+
}
|
|
43
|
+
export function FilterChips({ label, chips, onRemove, onClearAll, clearAllLabel, style, }) {
|
|
44
|
+
const { theme, p } = useFanar();
|
|
45
|
+
if (!chips.length)
|
|
46
|
+
return null;
|
|
47
|
+
return (_jsxs(View, { style: [styles.chipsRow, style], children: [label ? (_jsx(Text, { size: "caption", color: "muted", children: label })) : null, chips.map((c) => (_jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: `${p('clear')} — ${c.label}`, hitSlop: HIT_SLOP_SM, onPress: () => onRemove(c.key), style: ({ pressed }) => [
|
|
48
|
+
styles.chip,
|
|
49
|
+
{
|
|
50
|
+
borderRadius: theme.radius.full,
|
|
51
|
+
backgroundColor: pressed ? theme.color.soft2 : theme.color.soft,
|
|
52
|
+
},
|
|
53
|
+
], children: [_jsx(Text, { size: "caption", weight: "bold", color: "brand-ink", lines: 1, children: c.label }), _jsx(X, { size: 13, strokeWidth: 2.6, color: theme.color['brand-ink'] })] }, c.key))), onClearAll && chips.length > 1 && (_jsx(Pressable, { accessibilityRole: "button", hitSlop: HIT_SLOP_SM, onPress: onClearAll, children: _jsx(Text, { size: "caption", weight: "bold", color: "muted", children: clearAllLabel ?? p('clear') }) }))] }));
|
|
54
|
+
}
|
|
55
|
+
const styles = StyleSheet.create({
|
|
56
|
+
scroller: { paddingHorizontal: 0 },
|
|
57
|
+
segList: { flexDirection: 'row', gap: 2, padding: 2, alignSelf: 'flex-start' },
|
|
58
|
+
segTab: { height: 36, paddingHorizontal: 14, flexDirection: 'row', alignItems: 'center', gap: 8 },
|
|
59
|
+
pillList: { flexDirection: 'row', alignItems: 'center', gap: 10 },
|
|
60
|
+
pillTab: { height: 40, paddingHorizontal: 14, flexDirection: 'row', alignItems: 'center', gap: 6 },
|
|
61
|
+
fill: { flex: 1, justifyContent: 'center' },
|
|
62
|
+
count: { opacity: 0.65 },
|
|
63
|
+
chipsRow: { flexDirection: 'row', alignItems: 'center', gap: 8, flexWrap: 'wrap' },
|
|
64
|
+
chip: { flexDirection: 'row', alignItems: 'center', gap: 6, height: 28, paddingStart: 12, paddingEnd: 8 },
|
|
65
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
2
|
+
export type StepperSize = 'sm' | 'md' | 'lg';
|
|
3
|
+
export interface NumberStepperProps {
|
|
4
|
+
value: number;
|
|
5
|
+
onChange: (next: number) => void;
|
|
6
|
+
min?: number;
|
|
7
|
+
max?: number;
|
|
8
|
+
step?: number;
|
|
9
|
+
size?: StepperSize;
|
|
10
|
+
unit?: string;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
readOnlyField?: boolean;
|
|
13
|
+
label?: string;
|
|
14
|
+
style?: StyleProp<ViewStyle>;
|
|
15
|
+
}
|
|
16
|
+
export declare function NumberStepper({ value, onChange, min, max, step, size, unit, disabled, readOnlyField, label, style, }: NumberStepperProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { Pressable, StyleSheet, TextInput, View } from 'react-native';
|
|
4
|
+
import { Text } from './Text';
|
|
5
|
+
import { useFanar } from '../theme/context';
|
|
6
|
+
import { toLatinDigits } from '../lib/digits';
|
|
7
|
+
import { Minus, Plus } from '../icons';
|
|
8
|
+
const BOX = { sm: 32, md: 44, lg: 56 };
|
|
9
|
+
const NUM_W = { sm: 40, md: 56, lg: 80 };
|
|
10
|
+
const NUM_T = { sm: 13, md: 14, lg: 20 };
|
|
11
|
+
const ICON = { sm: 14, md: 16, lg: 20 };
|
|
12
|
+
const HOLD_START_MS = 400;
|
|
13
|
+
const HOLD_MIN_MS = 60;
|
|
14
|
+
const HOLD_DECAY = 0.72;
|
|
15
|
+
export function NumberStepper({ value, onChange, min = 0, max = Number.MAX_SAFE_INTEGER, step = 1, size = 'md', unit, disabled, readOnlyField, label, style, }) {
|
|
16
|
+
const { theme, p } = useFanar();
|
|
17
|
+
const [draft, setDraft] = useState(null);
|
|
18
|
+
const timer = useRef(null);
|
|
19
|
+
const clamp = useCallback((n) => Math.min(max, Math.max(min, n)), [min, max]);
|
|
20
|
+
const stop = useCallback(() => {
|
|
21
|
+
if (timer.current)
|
|
22
|
+
clearTimeout(timer.current);
|
|
23
|
+
timer.current = null;
|
|
24
|
+
}, []);
|
|
25
|
+
useEffect(() => stop, [stop]);
|
|
26
|
+
const latest = useRef(value);
|
|
27
|
+
latest.current = value;
|
|
28
|
+
const hold = useCallback((delta, delay) => {
|
|
29
|
+
timer.current = setTimeout(() => {
|
|
30
|
+
const next = clamp(latest.current + delta);
|
|
31
|
+
if (next === latest.current)
|
|
32
|
+
return stop();
|
|
33
|
+
onChange(next);
|
|
34
|
+
hold(delta, Math.max(HOLD_MIN_MS, delay * HOLD_DECAY));
|
|
35
|
+
}, delay);
|
|
36
|
+
}, [clamp, onChange, stop]);
|
|
37
|
+
const bump = (delta) => {
|
|
38
|
+
const next = clamp(value + delta);
|
|
39
|
+
if (next !== value)
|
|
40
|
+
onChange(next);
|
|
41
|
+
};
|
|
42
|
+
const box = BOX[size];
|
|
43
|
+
const canDown = !disabled && value > min;
|
|
44
|
+
const canUp = !disabled && value < max;
|
|
45
|
+
const StepBtn = ({ dir, enabled }) => {
|
|
46
|
+
const Icon = dir > 0 ? Plus : Minus;
|
|
47
|
+
return (_jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: p(dir > 0 ? 'increase' : 'decrease'), accessibilityState: { disabled: !enabled }, disabled: !enabled, onPress: () => bump(dir * step), onPressIn: () => enabled && hold(dir * step, HOLD_START_MS), onPressOut: stop, style: ({ pressed }) => [
|
|
48
|
+
styles.btn,
|
|
49
|
+
{ width: box, height: box, backgroundColor: pressed && enabled ? theme.color.hover : 'transparent' },
|
|
50
|
+
], children: _jsx(Icon, { size: ICON[size], strokeWidth: 2.6, color: enabled ? theme.color.ink2 : theme.color.muted2, opacity: enabled ? 1 : 0.45 }) }));
|
|
51
|
+
};
|
|
52
|
+
return (_jsxs(View, { accessibilityLabel: label, style: [
|
|
53
|
+
styles.shell,
|
|
54
|
+
{
|
|
55
|
+
height: box,
|
|
56
|
+
borderRadius: theme.radius.field,
|
|
57
|
+
borderWidth: 1,
|
|
58
|
+
borderColor: theme.color.line,
|
|
59
|
+
backgroundColor: theme.color.surface,
|
|
60
|
+
opacity: disabled ? 0.6 : 1,
|
|
61
|
+
},
|
|
62
|
+
style,
|
|
63
|
+
], children: [_jsx(StepBtn, { dir: -1, enabled: canDown }), _jsx(View, { style: [styles.divider, { backgroundColor: theme.color.line }] }), _jsxs(View, { style: [styles.numWrap, { width: NUM_W[size] }], children: [readOnlyField ? (_jsx(Text, { size: NUM_T[size], weight: "extraBold", numeric: true, align: "center", children: value })) : (_jsx(TextInput, { value: draft ?? String(value), editable: !disabled, keyboardType: "number-pad", selectTextOnFocus: true, selectionColor: theme.color.brand, onChangeText: (t) => setDraft(toLatinDigits(t).replace(/[^\d]/g, '')), onBlur: () => {
|
|
64
|
+
if (draft !== null)
|
|
65
|
+
onChange(clamp(Number(draft) || min));
|
|
66
|
+
setDraft(null);
|
|
67
|
+
}, style: [
|
|
68
|
+
theme.font('extraBold'),
|
|
69
|
+
styles.numField,
|
|
70
|
+
{ fontSize: NUM_T[size], color: disabled ? theme.color.muted2 : theme.color.ink },
|
|
71
|
+
] })), unit ? (_jsx(Text, { size: "micro", color: "muted2", children: unit })) : null] }), _jsx(View, { style: [styles.divider, { backgroundColor: theme.color.line }] }), _jsx(StepBtn, { dir: 1, enabled: canUp })] }));
|
|
72
|
+
}
|
|
73
|
+
const styles = StyleSheet.create({
|
|
74
|
+
shell: { flexDirection: 'row', alignItems: 'center', alignSelf: 'flex-start', overflow: 'hidden' },
|
|
75
|
+
btn: { alignItems: 'center', justifyContent: 'center' },
|
|
76
|
+
divider: { width: 1, alignSelf: 'stretch' },
|
|
77
|
+
numWrap: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 4, paddingHorizontal: 4 },
|
|
78
|
+
numField: { flex: 1, textAlign: 'center', padding: 0 },
|
|
79
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import { type Tone } from '../lib/tone';
|
|
4
|
+
import { type LucideIcon } from '../icons';
|
|
5
|
+
export interface SheetProps {
|
|
6
|
+
open: boolean;
|
|
7
|
+
title?: string;
|
|
8
|
+
onClose: () => void;
|
|
9
|
+
onBack?: () => void;
|
|
10
|
+
action?: ReactNode;
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
maxHeightRatio?: number;
|
|
13
|
+
scroll?: boolean;
|
|
14
|
+
style?: StyleProp<ViewStyle>;
|
|
15
|
+
}
|
|
16
|
+
export declare function Sheet({ open, title, onClose, onBack, action, children, maxHeightRatio, scroll, style, }: SheetProps): import("react").JSX.Element;
|
|
17
|
+
export interface SheetRowProps {
|
|
18
|
+
icon?: LucideIcon;
|
|
19
|
+
label: string;
|
|
20
|
+
meta?: string;
|
|
21
|
+
danger?: boolean;
|
|
22
|
+
onPress: () => void;
|
|
23
|
+
style?: StyleProp<ViewStyle>;
|
|
24
|
+
}
|
|
25
|
+
export declare function SheetRow({ icon: Icon, label, meta, danger, onPress, style }: SheetRowProps): import("react").JSX.Element;
|
|
26
|
+
export type ModalSize = 'sm' | 'md' | 'lg';
|
|
27
|
+
export interface ModalProps {
|
|
28
|
+
open: boolean;
|
|
29
|
+
title?: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
onClose: () => void;
|
|
32
|
+
size?: ModalSize;
|
|
33
|
+
footer?: ReactNode;
|
|
34
|
+
children?: ReactNode;
|
|
35
|
+
}
|
|
36
|
+
export declare function Modal({ open, title, description, onClose, size, footer, children }: ModalProps): import("react").JSX.Element;
|
|
37
|
+
export interface AlertProps {
|
|
38
|
+
tone?: Tone;
|
|
39
|
+
title?: string;
|
|
40
|
+
children?: ReactNode;
|
|
41
|
+
icon?: LucideIcon;
|
|
42
|
+
action?: ReactNode;
|
|
43
|
+
style?: StyleProp<ViewStyle>;
|
|
44
|
+
}
|
|
45
|
+
export declare function Alert({ tone, title, children, icon: Icon, action, style }: AlertProps): import("react").JSX.Element;
|
|
46
|
+
export interface EmptyStateProps {
|
|
47
|
+
icon?: LucideIcon;
|
|
48
|
+
title: string;
|
|
49
|
+
description?: string;
|
|
50
|
+
actionLabel?: string;
|
|
51
|
+
onAction?: () => void;
|
|
52
|
+
style?: StyleProp<ViewStyle>;
|
|
53
|
+
}
|
|
54
|
+
export declare function EmptyState({ icon: Icon, title, description, actionLabel, onAction, style }: EmptyStateProps): import("react").JSX.Element;
|