@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.
Files changed (95) hide show
  1. package/README.md +83 -0
  2. package/lib/art.d.ts +8 -0
  3. package/lib/art.js +29 -0
  4. package/lib/assets/art/avatar.png +0 -0
  5. package/lib/assets/art/empty.png +0 -0
  6. package/lib/assets/art/image-placeholder.png +0 -0
  7. package/lib/assets/art/plan-update.png +0 -0
  8. package/lib/assets/brand/app-store.png +0 -0
  9. package/lib/assets/brand/etisalat.png +0 -0
  10. package/lib/assets/brand/facebook.png +0 -0
  11. package/lib/assets/brand/google-play.png +0 -0
  12. package/lib/assets/brand/instagram.png +0 -0
  13. package/lib/assets/brand/orange.png +0 -0
  14. package/lib/assets/brand/snapchat.png +0 -0
  15. package/lib/assets/brand/tiktok.png +0 -0
  16. package/lib/assets/brand/vodafone.png +0 -0
  17. package/lib/assets/brand/we.png +0 -0
  18. package/lib/assets/brand/whatsapp.png +0 -0
  19. package/lib/assets/brand/x.png +0 -0
  20. package/lib/assets/brand/youtube.png +0 -0
  21. package/lib/components/Button.d.ts +28 -0
  22. package/lib/components/Button.js +75 -0
  23. package/lib/components/Disclosure.d.ts +41 -0
  24. package/lib/components/Disclosure.js +98 -0
  25. package/lib/components/Display.d.ts +88 -0
  26. package/lib/components/Display.js +157 -0
  27. package/lib/components/Drawer.d.ts +27 -0
  28. package/lib/components/Drawer.js +58 -0
  29. package/lib/components/Feedback.d.ts +43 -0
  30. package/lib/components/Feedback.js +136 -0
  31. package/lib/components/Form.d.ts +92 -0
  32. package/lib/components/Form.js +196 -0
  33. package/lib/components/Money.d.ts +29 -0
  34. package/lib/components/Money.js +65 -0
  35. package/lib/components/Nav.d.ts +29 -0
  36. package/lib/components/Nav.js +65 -0
  37. package/lib/components/NumberStepper.d.ts +16 -0
  38. package/lib/components/NumberStepper.js +79 -0
  39. package/lib/components/Overlay.d.ts +54 -0
  40. package/lib/components/Overlay.js +147 -0
  41. package/lib/components/Rating.d.ts +18 -0
  42. package/lib/components/Rating.js +32 -0
  43. package/lib/components/Shell.d.ts +54 -0
  44. package/lib/components/Shell.js +99 -0
  45. package/lib/components/Surfaces.d.ts +64 -0
  46. package/lib/components/Surfaces.js +122 -0
  47. package/lib/components/Text.d.ts +14 -0
  48. package/lib/components/Text.js +31 -0
  49. package/lib/components/currency.d.ts +251 -0
  50. package/lib/components/currency.js +48 -0
  51. package/lib/glyphs.d.ts +2 -0
  52. package/lib/glyphs.js +7 -0
  53. package/lib/i18n/content.d.ts +1 -0
  54. package/lib/i18n/content.js +28 -0
  55. package/lib/i18n/en.d.ts +1 -0
  56. package/lib/i18n/en.js +124 -0
  57. package/lib/i18n/index.d.ts +64 -0
  58. package/lib/i18n/index.js +91 -0
  59. package/lib/i18n/screens.d.ts +1 -0
  60. package/lib/i18n/screens.js +2025 -0
  61. package/lib/icons.d.ts +18 -0
  62. package/lib/icons.js +400 -0
  63. package/lib/index.d.ts +25 -0
  64. package/lib/index.js +25 -0
  65. package/lib/lib/a11y.d.ts +4 -0
  66. package/lib/lib/a11y.js +8 -0
  67. package/lib/lib/digits.d.ts +3 -0
  68. package/lib/lib/digits.js +22 -0
  69. package/lib/lib/motion.d.ts +14 -0
  70. package/lib/lib/motion.js +47 -0
  71. package/lib/lib/tone.d.ts +9 -0
  72. package/lib/lib/tone.js +16 -0
  73. package/lib/qumra/NavIcon.d.ts +14 -0
  74. package/lib/qumra/NavIcon.js +17 -0
  75. package/lib/qumra/QumraIcon.d.ts +17 -0
  76. package/lib/qumra/QumraIcon.js +91 -0
  77. package/lib/qumra/data.generated.d.ts +3 -0
  78. package/lib/qumra/data.generated.js +146 -0
  79. package/lib/qumra/types.d.ts +5 -0
  80. package/lib/qumra/types.js +1 -0
  81. package/lib/theme/context.d.ts +31 -0
  82. package/lib/theme/context.js +46 -0
  83. package/lib/theme/fonts.d.ts +9 -0
  84. package/lib/theme/fonts.js +36 -0
  85. package/lib/theme/index.d.ts +4 -0
  86. package/lib/theme/index.js +4 -0
  87. package/lib/theme/shadows.d.ts +2 -0
  88. package/lib/theme/shadows.js +30 -0
  89. package/lib/theme/theme.d.ts +52 -0
  90. package/lib/theme/theme.js +78 -0
  91. package/lib/tokens/generated.d.ts +180 -0
  92. package/lib/tokens/generated.js +173 -0
  93. package/lib/tokens/index.d.ts +1 -0
  94. package/lib/tokens/index.js +1 -0
  95. 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;