@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,147 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from 'react';
3
+ import { Animated, Modal as RNModal, PanResponder, Pressable, ScrollView, StyleSheet, useWindowDimensions, View, } from 'react-native';
4
+ import { Text } from './Text';
5
+ import { Button, IconButton } from './Button';
6
+ import { useFanar } from '../theme/context';
7
+ import { EASE_OUT } from '../lib/motion';
8
+ import { DURATION } from '../theme/theme';
9
+ import { TONE_SOFT } from '../lib/tone';
10
+ import { ChevronLeft, ChevronRight, X } from '../icons';
11
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
12
+ export function Sheet({ open, title, onClose, onBack, action, children, maxHeightRatio = 0.82, scroll = true, style, }) {
13
+ const { theme, p, dir } = useFanar();
14
+ const { height } = useWindowDimensions();
15
+ const { bottom } = useSafeAreaInsets();
16
+ const slide = useRef(new Animated.Value(0)).current;
17
+ const drag = useRef(new Animated.Value(0)).current;
18
+ useEffect(() => {
19
+ Animated.timing(slide, {
20
+ toValue: open ? 1 : 0,
21
+ duration: DURATION.sheet,
22
+ easing: EASE_OUT,
23
+ useNativeDriver: true,
24
+ }).start();
25
+ if (open)
26
+ drag.setValue(0);
27
+ }, [open, slide, drag]);
28
+ const pan = useRef(PanResponder.create({
29
+ onMoveShouldSetPanResponder: (_, g) => g.dy > 6 && Math.abs(g.dy) > Math.abs(g.dx),
30
+ onPanResponderMove: (_, g) => {
31
+ if (g.dy > 0)
32
+ drag.setValue(g.dy);
33
+ },
34
+ onPanResponderRelease: (_, g) => {
35
+ if (g.dy > 90 || g.vy > 0.5)
36
+ onClose();
37
+ else
38
+ Animated.timing(drag, { toValue: 0, duration: 140, useNativeDriver: true }).start();
39
+ },
40
+ })).current;
41
+ const translateY = Animated.add(slide.interpolate({ inputRange: [0, 1], outputRange: [height, 0] }), drag);
42
+ const Body = scroll ? ScrollView : View;
43
+ return (_jsx(RNModal, { visible: open, transparent: true, animationType: "none", onRequestClose: onClose, statusBarTranslucent: true, children: _jsxs(View, { style: [styles.sheetRoot, { direction: dir }], children: [_jsx(Animated.View, { style: [StyleSheet.absoluteFill, { opacity: slide }], children: _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: p('close'), onPress: onClose, style: [StyleSheet.absoluteFill, styles.scrim] }) }), _jsxs(Animated.View, { style: [
44
+ styles.sheet,
45
+ theme.shadow.sheet,
46
+ {
47
+ backgroundColor: theme.color.surface,
48
+ borderTopLeftRadius: 20,
49
+ borderTopRightRadius: 20,
50
+ maxHeight: height * maxHeightRatio,
51
+ paddingBottom: bottom,
52
+ transform: [{ translateY }],
53
+ borderTopWidth: 1,
54
+ borderTopColor: theme.dark ? 'rgba(255,255,255,0.08)' : theme.color.line,
55
+ },
56
+ style,
57
+ ], children: [_jsx(View, { ...pan.panHandlers, style: styles.grabArea, children: _jsx(View, { style: [styles.grabber, { backgroundColor: theme.color.border }] }) }), (title || onBack || action) && (_jsxs(View, { style: styles.sheetHead, children: [_jsxs(View, { style: styles.sheetHeadStart, children: [onBack && (_jsx(IconButton, { icon: dir === 'rtl' ? ChevronRight : ChevronLeft, size: 32, iconSize: 18, color: "muted", onPress: onBack, accessibilityLabel: p('back') })), title ? (_jsx(Text, { size: "h4", weight: "black", align: "start", lines: 1, style: styles.flex1, children: title })) : null] }), _jsxs(View, { style: styles.sheetHeadEnd, children: [action, _jsx(IconButton, { icon: X, size: 36, iconSize: 17, color: "muted", background: "bg", onPress: onClose, accessibilityLabel: p('close') })] })] })), _jsx(Body, { style: styles.flex1, ...(scroll
58
+ ? {
59
+ contentContainerStyle: styles.sheetBody,
60
+ showsVerticalScrollIndicator: false,
61
+ keyboardShouldPersistTaps: 'handled',
62
+ }
63
+ : {}), children: children })] })] }) }));
64
+ }
65
+ export function SheetRow({ icon: Icon, label, meta, danger, onPress, style }) {
66
+ const { theme } = useFanar();
67
+ const fg = danger ? theme.color.red : theme.color.ink2;
68
+ return (_jsxs(Pressable, { accessibilityRole: "menuitem", onPress: onPress, style: ({ pressed }) => [
69
+ styles.sheetRow,
70
+ {
71
+ borderRadius: theme.radius.field,
72
+ backgroundColor: pressed ? (danger ? theme.color['red-bg'] : theme.color.hover) : 'transparent',
73
+ },
74
+ style,
75
+ ], children: [Icon && _jsx(Icon, { size: 17, strokeWidth: 2, color: fg }), _jsx(Text, { size: "body", weight: "semiBold", color: danger ? 'red' : 'ink2', align: "start", style: styles.flex1, lines: 1, children: label }), meta ? (_jsx(Text, { size: "caption", color: "muted2", children: meta })) : null] }));
76
+ }
77
+ const MODAL_WIDTH = { sm: 380, md: 520, lg: 720 };
78
+ export function Modal({ open, title, description, onClose, size = 'md', footer, children }) {
79
+ const { theme, p, dir } = useFanar();
80
+ const { width } = useWindowDimensions();
81
+ const fade = useRef(new Animated.Value(0)).current;
82
+ useEffect(() => {
83
+ Animated.timing(fade, {
84
+ toValue: open ? 1 : 0,
85
+ duration: DURATION.fade,
86
+ easing: EASE_OUT,
87
+ useNativeDriver: true,
88
+ }).start();
89
+ }, [open, fade]);
90
+ return (_jsx(RNModal, { visible: open, transparent: true, animationType: "none", onRequestClose: onClose, statusBarTranslucent: true, children: _jsxs(Animated.View, { style: [styles.modalRoot, { opacity: fade, direction: dir }], children: [_jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: p('close'), onPress: onClose, style: [StyleSheet.absoluteFill, styles.scrim] }), _jsxs(View, { accessibilityViewIsModal: true, style: [
91
+ styles.modal,
92
+ theme.shadow.modal,
93
+ {
94
+ backgroundColor: theme.color.surface,
95
+ borderRadius: theme.radius.section,
96
+ width: Math.min(MODAL_WIDTH[size], width - 32),
97
+ },
98
+ ], children: [(title || description) && (_jsxs(View, { style: styles.modalHead, children: [_jsxs(View, { style: styles.flex1, children: [title ? (_jsx(Text, { size: "h4", weight: "black", align: "start", children: title })) : null, description ? (_jsx(Text, { size: "ui", color: "muted", align: "start", style: styles.modalDesc, children: description })) : null] }), _jsx(IconButton, { icon: X, size: 36, iconSize: 17, color: "muted", background: "bg", onPress: onClose, accessibilityLabel: p('close') })] })), children ? _jsx(View, { style: styles.modalBody, children: children }) : null, footer ? _jsx(View, { style: styles.modalFoot, children: footer }) : null] })] }) }));
99
+ }
100
+ export function Alert({ tone = 'amber', title, children, icon: Icon, action, style }) {
101
+ const { theme } = useFanar();
102
+ const c = TONE_SOFT[tone];
103
+ return (_jsxs(View, { accessibilityRole: "alert", style: [
104
+ styles.alert,
105
+ {
106
+ backgroundColor: theme.colorOf(c.bg, theme.color.hover),
107
+ borderColor: theme.colorOf(c.line, theme.color.line),
108
+ borderRadius: theme.radius.field,
109
+ },
110
+ style,
111
+ ], children: [Icon && _jsx(Icon, { size: 16, strokeWidth: 2, color: theme.colorOf(c.fg, theme.color.ink) }), _jsxs(View, { style: styles.flex1, children: [title ? (_jsx(Text, { size: "ui", weight: "extraBold", color: c.fg, align: "start", children: title })) : null, typeof children === 'string' ? (_jsx(Text, { size: "caption", color: c.fg, align: "start", leading: "relaxed", children: children })) : (children)] }), action] }));
112
+ }
113
+ export function EmptyState({ icon: Icon, title, description, actionLabel, onAction, style }) {
114
+ const { theme } = useFanar();
115
+ return (_jsxs(View, { style: [styles.empty, style], children: [Icon && (_jsx(View, { style: [styles.emptyIcon, { backgroundColor: theme.color.soft, borderRadius: theme.radius.section }], children: _jsx(Icon, { size: 26, strokeWidth: 1.8, color: theme.color.brand }) })), _jsx(Text, { size: "lead", weight: "extraBold", align: "center", children: title }), description ? (_jsx(Text, { size: "ui", color: "muted", align: "center", leading: "relaxed", style: styles.emptyDesc, children: description })) : null, actionLabel && onAction ? (_jsx(Button, { size: "sm", variant: "soft", onPress: onAction, style: styles.emptyAction, children: actionLabel })) : null] }));
116
+ }
117
+ const styles = StyleSheet.create({
118
+ sheetRoot: { flex: 1, justifyContent: 'flex-end' },
119
+ scrim: { backgroundColor: 'rgba(0,0,0,0.5)' },
120
+ sheet: { overflow: 'hidden' },
121
+ grabArea: { paddingTop: 12, paddingBottom: 6, alignItems: 'center' },
122
+ grabber: { width: 44, height: 6, borderRadius: 3 },
123
+ sheetHead: {
124
+ flexDirection: 'row',
125
+ alignItems: 'center',
126
+ justifyContent: 'space-between',
127
+ gap: 12,
128
+ paddingHorizontal: 20,
129
+ paddingBottom: 12,
130
+ },
131
+ sheetHeadStart: { flexDirection: 'row', alignItems: 'center', gap: 8, flex: 1, minWidth: 0 },
132
+ sheetHeadEnd: { flexDirection: 'row', alignItems: 'center', gap: 12 },
133
+ sheetBody: { paddingHorizontal: 12, paddingBottom: 16 },
134
+ sheetRow: { flexDirection: 'row', alignItems: 'center', gap: 12, height: 48, paddingHorizontal: 12 },
135
+ flex1: { flex: 1 },
136
+ modalRoot: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 16 },
137
+ modal: { overflow: 'hidden' },
138
+ modalHead: { flexDirection: 'row', alignItems: 'flex-start', gap: 12, padding: 20, paddingBottom: 12 },
139
+ modalDesc: { marginTop: 4 },
140
+ modalBody: { paddingHorizontal: 20, paddingBottom: 20 },
141
+ modalFoot: { flexDirection: 'row', justifyContent: 'flex-end', gap: 10, padding: 16 },
142
+ alert: { flexDirection: 'row', alignItems: 'flex-start', gap: 10, padding: 12, borderWidth: 1 },
143
+ empty: { alignItems: 'center', gap: 8, paddingVertical: 40, paddingHorizontal: 24 },
144
+ emptyIcon: { width: 56, height: 56, alignItems: 'center', justifyContent: 'center', marginBottom: 4 },
145
+ emptyDesc: { maxWidth: 340 },
146
+ emptyAction: { marginTop: 8 },
147
+ });
@@ -0,0 +1,18 @@
1
+ import { type StyleProp, type ViewStyle } from 'react-native';
2
+ export declare const MAX = 5;
3
+ export interface StarsProps {
4
+ value: number;
5
+ size?: number;
6
+ showValue?: boolean;
7
+ count?: number;
8
+ style?: StyleProp<ViewStyle>;
9
+ }
10
+ export declare function Stars({ value, size, showValue, count, style }: StarsProps): import("react").JSX.Element;
11
+ export interface RatingInputProps {
12
+ value: number;
13
+ onChange: (next: number) => void;
14
+ disabled?: boolean;
15
+ label?: string;
16
+ style?: StyleProp<ViewStyle>;
17
+ }
18
+ export declare function RatingInput({ value, onChange, disabled, label, style }: RatingInputProps): import("react").JSX.Element;
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Pressable, StyleSheet, View } from 'react-native';
3
+ import { Text } from './Text';
4
+ import { useFanar } from '../theme/context';
5
+ import { Star } from '../icons';
6
+ export const MAX = 5;
7
+ export function Stars({ value, size = 14, showValue, count, style }) {
8
+ const { theme } = useFanar();
9
+ const filled = Math.round(value);
10
+ return (_jsxs(View, { accessible: true, accessibilityRole: "image", accessibilityLabel: `${value} / ${MAX}`, style: [styles.row, style], children: [Array.from({ length: MAX }, (_, i) => (_jsx(Star, { size: size, strokeWidth: 2, color: i < filled ? theme.color.amber : theme.color.muted2, fill: i < filled ? theme.color.amber : 'transparent' }, i))), showValue && (_jsx(Text, { size: "caption", weight: "bold", color: "muted", numeric: true, style: styles.value, children: value.toFixed(1) })), count !== undefined && (_jsxs(Text, { size: "caption", color: "muted2", numeric: true, children: ["(", count, ")"] }))] }));
11
+ }
12
+ export function RatingInput({ value, onChange, disabled, label, style }) {
13
+ const { theme } = useFanar();
14
+ return (_jsx(View, { accessibilityRole: "radiogroup", accessibilityLabel: label, style: [styles.inputRow, style], children: Array.from({ length: MAX }, (_, i) => {
15
+ const n = i + 1;
16
+ const on = n <= value;
17
+ return (_jsx(Pressable, { accessibilityRole: "radio", accessibilityState: { checked: n === value, disabled: !!disabled }, accessibilityLabel: `${n} / ${MAX}`, disabled: disabled, onPress: () => onChange(n), style: ({ pressed }) => [
18
+ styles.starBtn,
19
+ {
20
+ borderRadius: theme.radius.sm,
21
+ backgroundColor: pressed && !disabled ? theme.color.hover : 'transparent',
22
+ opacity: disabled ? 0.6 : 1,
23
+ },
24
+ ], children: _jsx(Star, { size: 22, strokeWidth: 2, color: on ? theme.color.amber : theme.color.muted2, fill: on ? theme.color.amber : 'transparent' }) }, n));
25
+ }) }));
26
+ }
27
+ const styles = StyleSheet.create({
28
+ row: { flexDirection: 'row', alignItems: 'center', gap: 2 },
29
+ value: { marginStart: 8 },
30
+ inputRow: { flexDirection: 'row', alignItems: 'center', gap: 4 },
31
+ starBtn: { width: 36, height: 36, alignItems: 'center', justifyContent: 'center' },
32
+ });
@@ -0,0 +1,54 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type ImageSourcePropType, type StyleProp, type ViewStyle } from 'react-native';
3
+ import type { LucideIcon } from '../icons';
4
+ export interface ScreenProps {
5
+ children: ReactNode;
6
+ scroll?: boolean;
7
+ padded?: boolean;
8
+ gap?: number;
9
+ tabBarSpace?: boolean;
10
+ refreshing?: boolean;
11
+ onRefresh?: () => void;
12
+ background?: 'bg' | 'surface';
13
+ header?: ReactNode;
14
+ footer?: ReactNode;
15
+ style?: StyleProp<ViewStyle>;
16
+ contentStyle?: StyleProp<ViewStyle>;
17
+ }
18
+ export declare function Screen({ children, scroll, padded, gap, tabBarSpace, refreshing, onRefresh, background, header, footer, style, contentStyle, }: ScreenProps): import("react").JSX.Element;
19
+ export interface HeaderAction {
20
+ key: string;
21
+ icon: LucideIcon;
22
+ label: string;
23
+ onPress: () => void;
24
+ active?: boolean;
25
+ badge?: number;
26
+ }
27
+ export type HeaderMark = ImageSourcePropType | string | LucideIcon;
28
+ export interface AppHeaderProps {
29
+ logo?: HeaderMark;
30
+ onPressLogo?: () => void;
31
+ logoActive?: boolean;
32
+ avatar?: HeaderMark;
33
+ onPressAvatar?: () => void;
34
+ avatarActive?: boolean;
35
+ actions?: HeaderAction[];
36
+ title?: string;
37
+ leading?: ReactNode;
38
+ safeTop?: boolean;
39
+ style?: StyleProp<ViewStyle>;
40
+ }
41
+ export declare function AppHeader({ logo, onPressLogo, logoActive, avatar, onPressAvatar, avatarActive, actions, title, leading, safeTop, style, }: AppHeaderProps): import("react").JSX.Element;
42
+ export interface TabBarItem {
43
+ key: string;
44
+ label: string;
45
+ icon: LucideIcon | string;
46
+ badge?: number;
47
+ }
48
+ export interface BottomTabBarProps {
49
+ items: TabBarItem[];
50
+ value: string;
51
+ onChange: (key: string) => void;
52
+ style?: StyleProp<ViewStyle>;
53
+ }
54
+ export declare function BottomTabBar({ items, value, onChange, style }: BottomTabBarProps): import("react").JSX.Element;
@@ -0,0 +1,99 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Image, Pressable, RefreshControl, ScrollView, StatusBar, StyleSheet, View, } from 'react-native';
3
+ import { Text } from './Text';
4
+ import { Counter } from './Display';
5
+ import { useFanar } from '../theme/context';
6
+ import { NavIcon } from '../qumra/NavIcon';
7
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
8
+ export function Screen({ children, scroll = true, padded = true, gap = 16, tabBarSpace = false, refreshing, onRefresh, background = 'bg', header, footer, style, contentStyle, }) {
9
+ const { theme } = useFanar();
10
+ const insets = useSafeAreaInsets();
11
+ const content = [
12
+ {
13
+ paddingHorizontal: padded ? 16 : 0,
14
+ paddingTop: 16,
15
+ paddingBottom: (tabBarSpace ? 76 : 16) + insets.bottom,
16
+ gap,
17
+ },
18
+ contentStyle,
19
+ ];
20
+ return (_jsxs(View, { style: [styles.flex1, { backgroundColor: theme.color[background] }, style], children: [_jsx(StatusBar, { barStyle: theme.dark ? 'light-content' : 'dark-content', backgroundColor: theme.color.surface }), header, scroll ? (_jsx(ScrollView, { style: styles.flex1, contentContainerStyle: content, showsVerticalScrollIndicator: false, keyboardShouldPersistTaps: "handled", refreshControl: onRefresh ? (_jsx(RefreshControl, { refreshing: !!refreshing, onRefresh: onRefresh, tintColor: theme.color.brand, colors: [theme.color.brand], progressBackgroundColor: theme.color.surface })) : undefined, children: children })) : (_jsx(View, { style: [styles.flex1, content], children: children })), footer] }));
21
+ }
22
+ function isIconMark(v) {
23
+ return (typeof v === 'function' ||
24
+ (typeof v === 'object' && v !== null && '$$typeof' in v));
25
+ }
26
+ function Mark({ src, size, color, style, }) {
27
+ if (isIconMark(src)) {
28
+ const Glyph = src;
29
+ return _jsx(Glyph, { size: size, color: color });
30
+ }
31
+ return (_jsx(Image, { accessibilityIgnoresInvertColors: true, source: typeof src === 'string' ? { uri: src } : src, style: style, resizeMode: "cover" }));
32
+ }
33
+ export function AppHeader({ logo, onPressLogo, logoActive, avatar, onPressAvatar, avatarActive, actions = [], title, leading, safeTop = true, style, }) {
34
+ const { theme, p } = useFanar();
35
+ const insets = useSafeAreaInsets();
36
+ return (_jsx(View, { style: [
37
+ styles.header,
38
+ {
39
+ paddingTop: safeTop ? 12 : 0,
40
+ backgroundColor: theme.color.surface,
41
+ borderBottomWidth: 1,
42
+ borderBottomColor: theme.color.line,
43
+ },
44
+ style,
45
+ ], children: _jsxs(View, { style: styles.headerRow, children: [logo ? (_jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: p('switchStore'), accessibilityState: { expanded: logoActive }, onPress: onPressLogo, style: [
46
+ styles.logo,
47
+ {
48
+ borderRadius: theme.radius.field,
49
+ borderColor: logoActive ? theme.color.brand : theme.color.line,
50
+ backgroundColor: theme.color.bg,
51
+ },
52
+ ], children: _jsx(Mark, { src: logo, size: 20, color: theme.color.ink2, style: styles.logoImg }) })) : null, leading ? _jsx(View, { style: styles.leading, children: leading }) : null, title ? (_jsx(Text, { size: "h4", weight: "black", align: "start", lines: 1, style: styles.flex1, children: title })) : (_jsx(View, { style: [styles.actions, !!leading && styles.actionsCompact], children: actions.map((a) => (_jsxs(Pressable, { accessibilityRole: "button", accessibilityLabel: a.label, accessibilityState: { selected: a.active }, onPress: a.onPress, style: ({ pressed }) => [
53
+ styles.iconBtn,
54
+ {
55
+ borderRadius: theme.radius.full,
56
+ backgroundColor: a.active
57
+ ? theme.color.soft
58
+ : pressed
59
+ ? theme.color.hover
60
+ : 'transparent',
61
+ },
62
+ ], children: [_jsx(a.icon, { size: 25, strokeWidth: 2, color: a.active ? theme.color.brand : theme.color.ink2 }), !!a.badge && _jsx(Counter, { value: a.badge, style: styles.badge })] }, a.key))) })), avatar ? (_jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: p('profile'), onPress: onPressAvatar, style: [
63
+ styles.avatar,
64
+ { borderColor: avatarActive ? theme.color.brand : 'transparent' },
65
+ ], children: _jsx(Mark, { src: avatar, size: 20, color: theme.color.ink2, style: styles.avatarImg }) })) : null] }) }));
66
+ }
67
+ export function BottomTabBar({ items, value, onChange, style }) {
68
+ const { theme } = useFanar();
69
+ return (_jsx(View, { accessibilityRole: "tablist", style: [
70
+ styles.tabBar,
71
+ {
72
+ backgroundColor: theme.color.surface,
73
+ borderTopWidth: 1,
74
+ borderTopColor: theme.color.line,
75
+ },
76
+ style,
77
+ ], children: items.map((it) => {
78
+ const on = it.key === value;
79
+ return (_jsxs(Pressable, { accessibilityRole: "tab", accessibilityState: { selected: on }, accessibilityLabel: it.label, onPress: () => onChange(it.key), style: styles.tab, children: [_jsxs(View, { style: styles.tabIcon, children: [_jsx(NavIcon, { icon: it.icon, size: 22, active: on, strokeWidth: on ? 2.4 : 2, color: theme.color.muted2, activeColor: theme.color.brand }), !!it.badge && _jsx(Counter, { value: it.badge, style: styles.tabBadge })] }), _jsx(Text, { size: "micro", weight: on ? 'bold' : 'medium', color: on ? 'brand' : 'muted2', leading: "none", children: it.label })] }, it.key));
80
+ }) }));
81
+ }
82
+ const styles = StyleSheet.create({
83
+ flex1: { flex: 1 },
84
+ header: { paddingBottom: 8 },
85
+ headerRow: { height: 36, flexDirection: 'row', alignItems: 'center', gap: 8, paddingHorizontal: 14 },
86
+ logo: { width: 36, height: 36, borderWidth: 1, overflow: 'hidden', alignItems: 'center', justifyContent: 'center' },
87
+ logoImg: { width: '100%', height: '100%' },
88
+ leading: { flex: 1, flexShrink: 1, minWidth: 0 },
89
+ actions: { flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-around' },
90
+ actionsCompact: { flex: 0, flexShrink: 0, justifyContent: 'flex-end' },
91
+ iconBtn: { height: 36, minWidth: 36, paddingHorizontal: 8, alignItems: 'center', justifyContent: 'center' },
92
+ badge: { position: 'absolute', top: 0, insetInlineEnd: 6, },
93
+ avatar: { width: 36, height: 36, borderRadius: 18, borderWidth: 1, overflow: 'hidden', alignItems: 'center', justifyContent: 'center' },
94
+ avatarImg: { width: '100%', height: '100%' },
95
+ tabBar: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-around', paddingTop: 6, paddingBottom: 6 },
96
+ tab: { flex: 1, alignItems: 'center', gap: 4, paddingVertical: 3 },
97
+ tabIcon: { height: 26, alignItems: 'center', justifyContent: 'center' },
98
+ tabBadge: { position: 'absolute', top: -2, insetInlineEnd: 4 },
99
+ });
@@ -0,0 +1,64 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type StyleProp, type ViewStyle } from 'react-native';
3
+ import { type LucideIcon } from '../icons';
4
+ export declare function RuleGrid({ spacing, opacity, count }: {
5
+ spacing?: number;
6
+ opacity?: number;
7
+ count?: number;
8
+ }): import("react").JSX.Element;
9
+ export declare function Glow({ size, color, top, bottom, start, end, }: {
10
+ size?: number;
11
+ color?: string;
12
+ top?: number;
13
+ bottom?: number;
14
+ start?: number;
15
+ end?: number;
16
+ }): import("react").JSX.Element;
17
+ export interface HeroProps {
18
+ tag?: string;
19
+ tagIcon?: LucideIcon;
20
+ title: string;
21
+ body?: string;
22
+ metric?: string;
23
+ metricNote?: string;
24
+ children?: ReactNode;
25
+ style?: StyleProp<ViewStyle>;
26
+ }
27
+ export declare function Hero({ tag, tagIcon: TagIcon, title, body, metric, metricNote, children, style }: HeroProps): import("react").JSX.Element;
28
+ export interface PromptCardProps {
29
+ icon?: LucideIcon;
30
+ title: string;
31
+ body?: string;
32
+ action?: ReactNode;
33
+ prompts?: {
34
+ key: string;
35
+ text: string;
36
+ icon?: LucideIcon;
37
+ }[];
38
+ onPickPrompt?: (key: string) => void;
39
+ style?: StyleProp<ViewStyle>;
40
+ }
41
+ export declare function PromptCard({ icon: Icon, title, body, action, prompts, onPickPrompt, style }: PromptCardProps): import("react").JSX.Element;
42
+ export interface ListRowProps {
43
+ icon?: LucideIcon | string;
44
+ leading?: ReactNode;
45
+ title: string;
46
+ subtitle?: string;
47
+ meta?: ReactNode;
48
+ trailing?: ReactNode;
49
+ onPress?: () => void;
50
+ chevron?: boolean;
51
+ danger?: boolean;
52
+ style?: StyleProp<ViewStyle>;
53
+ }
54
+ export declare function ListRow({ icon: Icon, leading, title, subtitle, meta, trailing, onPress, chevron, danger, style, }: ListRowProps): import("react").JSX.Element;
55
+ export interface StepRowProps {
56
+ icon?: LucideIcon | string;
57
+ title: string;
58
+ why?: string;
59
+ done?: boolean;
60
+ action?: ReactNode;
61
+ onPress?: () => void;
62
+ style?: StyleProp<ViewStyle>;
63
+ }
64
+ export declare function StepRow({ icon, title, why, done, action, onPress, style }: StepRowProps): import("react").JSX.Element;
@@ -0,0 +1,122 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Pressable, StyleSheet, View } from 'react-native';
3
+ import { Text } from './Text';
4
+ import { useFanar } from '../theme/context';
5
+ import { Check, ChevronLeft, ChevronRight, qumraIconFor } from '../icons';
6
+ import { NavIcon } from '../qumra/NavIcon';
7
+ import { QUMRA_ICONS, QumraIcon } from '../qumra/QumraIcon';
8
+ const brandIconName = (name) => qumraIconFor(name) ?? (name in QUMRA_ICONS ? name : undefined);
9
+ export function RuleGrid({ spacing = 72, opacity = 0.045, count = 8 }) {
10
+ return (_jsx(View, { pointerEvents: "none", style: StyleSheet.absoluteFill, children: Array.from({ length: count }, (_, i) => (_jsx(View, { style: {
11
+ position: 'absolute',
12
+ top: 0,
13
+ bottom: 0,
14
+ insetInlineStart: i * spacing,
15
+ width: 1,
16
+ backgroundColor: `rgba(255,255,255,${opacity})`,
17
+ } }, i))) }));
18
+ }
19
+ export function Glow({ size = 230, color = 'rgba(125,211,192,0.09)', top, bottom, start, end, }) {
20
+ return (_jsx(View, { pointerEvents: "none", style: {
21
+ position: 'absolute',
22
+ width: size,
23
+ height: size,
24
+ borderRadius: size / 2,
25
+ backgroundColor: color,
26
+ top,
27
+ bottom,
28
+ insetInlineStart: start,
29
+ insetInlineEnd: end,
30
+ } }));
31
+ }
32
+ export function Hero({ tag, tagIcon: TagIcon, title, body, metric, metricNote, children, style }) {
33
+ const { theme } = useFanar();
34
+ return (_jsxs(View, { style: [
35
+ styles.hero,
36
+ { backgroundColor: theme.color['brand-deep'], borderRadius: theme.radius.lg },
37
+ style,
38
+ ], children: [_jsx(RuleGrid, {}), _jsx(Glow, { top: -70, end: -50 }), _jsxs(View, { style: styles.heroTop, children: [_jsxs(View, { style: styles.heroText, children: [tag ? (_jsxs(View, { style: [styles.heroTag, { borderRadius: theme.radius.full }], children: [TagIcon && _jsx(TagIcon, { size: 13, strokeWidth: 2, color: theme.color.amber }), _jsx(Text, { style: { textAlign: 'center' }, size: 11.5, weight: "extraBold", color: "amber", leading: "none", children: tag })] })) : null, _jsx(Text, { size: 23, style: { textAlign: 'left' }, weight: "black", color: "white", align: "start", leading: 1.42, children: title }), body ? (_jsx(Text, { size: 13.5, color: "hero-body", align: "start", leading: 1.75, style: styles.heroBody, children: body })) : null] }), metric ? (_jsxs(View, { style: styles.heroMetric, children: [_jsx(Text, { size: 34, weight: "black", color: "mint", leading: "none", numeric: true, children: metric }), metricNote ? (_jsx(Text, { size: 11.5, color: "hero-meta", numeric: true, children: metricNote })) : null] })) : null] }), children ? _jsx(View, { style: styles.heroFoot, children: children }) : null] }));
39
+ }
40
+ export function PromptCard({ icon: Icon, title, body, action, prompts = [], onPickPrompt, style }) {
41
+ const { theme, dir } = useFanar();
42
+ const Chevron = dir === 'rtl' ? ChevronLeft : ChevronRight;
43
+ return (_jsxs(View, { style: [styles.prompt, { backgroundColor: theme.color.night, borderRadius: theme.radius.lg }, style], children: [_jsx(Glow, { size: 190, color: "rgba(125,211,192,0.07)", top: -60, start: -40 }), _jsxs(View, { style: styles.promptHead, children: [Icon && (_jsx(View, { style: [styles.promptIcon, { borderRadius: theme.radius.field }], children: _jsx(Icon, { size: 19, strokeWidth: 2, color: theme.color.mint }) })), _jsxs(View, { style: styles.flex1, children: [_jsx(Text, { size: 15, weight: "extraBold", color: "white", align: "start", children: title }), body ? (_jsx(Text, { size: 12.5, color: "hero-meta", align: "start", children: body })) : null] }), action] }), prompts.length > 0 && (_jsx(View, { style: styles.promptList, children: prompts.map((pr) => (_jsxs(Pressable, { accessibilityRole: "button", onPress: () => onPickPrompt?.(pr.key), style: ({ pressed }) => [
44
+ styles.promptRow,
45
+ {
46
+ borderRadius: theme.radius.field,
47
+ backgroundColor: pressed ? 'rgba(255,255,255,0.09)' : 'rgba(255,255,255,0.05)',
48
+ borderColor: 'rgba(255,255,255,0.08)',
49
+ },
50
+ ], children: [pr.icon && _jsx(pr.icon, { size: 14, strokeWidth: 2, color: theme.color.mint }), _jsx(Text, { size: 12.5, color: "hero-text", align: "start", lines: 1, style: styles.flex1, children: pr.text }), _jsx(Chevron, { size: 13, strokeWidth: 2, color: theme.color['hero-dim'] })] }, pr.key))) }))] }));
51
+ }
52
+ export function ListRow({ icon: Icon, leading, title, subtitle, meta, trailing, onPress, chevron, danger, style, }) {
53
+ const { theme, dir } = useFanar();
54
+ const Chevron = dir === 'rtl' ? ChevronLeft : ChevronRight;
55
+ const showChevron = chevron ?? !!onPress;
56
+ const body = (_jsxs(_Fragment, { children: [leading ??
57
+ (Icon ? (_jsx(View, { style: [styles.rowIcon, { backgroundColor: theme.color.soft, borderRadius: theme.radius.sm }], children: _jsx(NavIcon, { icon: Icon, size: 19, active: true, strokeWidth: 2, color: danger ? theme.color.red : theme.color['brand-ink'], activeColor: danger ? theme.color.red : theme.color['brand-ink'] }) })) : null), _jsxs(View, { style: styles.rowText, children: [_jsx(Text, { size: "body", weight: "bold", color: danger ? 'red' : 'ink', align: "start", lines: 1, children: title }), subtitle ? (_jsx(Text, { size: "caption", color: "muted", align: "start", lines: 2, children: subtitle })) : null] }), typeof meta === 'string' ? (_jsx(Text, { size: "caption", color: "muted2", children: meta })) : (meta), trailing, showChevron && !trailing && _jsx(Chevron, { size: 16, strokeWidth: 2.2, color: theme.color.muted2 })] }));
58
+ if (!onPress)
59
+ return _jsx(View, { style: [styles.row, style], children: body });
60
+ return (_jsx(Pressable, { accessibilityRole: "button", onPress: onPress, style: ({ pressed }) => [
61
+ styles.row,
62
+ pressed && { backgroundColor: danger ? theme.color['red-bg'] : theme.color.hover },
63
+ style,
64
+ ], children: body }));
65
+ }
66
+ export function StepRow({ icon, title, why, done, action, onPress, style }) {
67
+ const { theme } = useFanar();
68
+ const brand = typeof icon === 'string' ? brandIconName(icon) : undefined;
69
+ const Lucide = typeof icon === 'string' ? undefined : icon;
70
+ const inner = (_jsxs(_Fragment, { children: [done ? (_jsx(View, { style: [
71
+ styles.stepIcon,
72
+ { borderRadius: theme.radius.field, backgroundColor: theme.color.brand },
73
+ ], children: _jsx(Check, { size: 17, strokeWidth: 3, color: "#ffffff" }) })) : brand ? (_jsx(QumraIcon, { name: brand, size: 34 })) : (_jsx(View, { style: [
74
+ styles.stepIcon,
75
+ { borderRadius: theme.radius.field, backgroundColor: theme.color.soft },
76
+ ], children: Lucide ? _jsx(Lucide, { size: 18, strokeWidth: 2, color: theme.color.brand }) : null })), _jsxs(View, { style: styles.rowText, children: [_jsx(Text, { size: 14.5, weight: "extraBold", color: done ? 'muted' : 'ink', align: "start", style: done ? styles.struck : { textAlign: 'left' }, children: title }), why ? (_jsx(Text, { size: 12.5, style: { textAlign: 'left' }, color: "muted", align: "start", leading: "relaxed", lines: 1, children: why })) : null] }), action] }));
77
+ if (!onPress)
78
+ return _jsx(View, { style: [styles.stepRow, style], children: inner });
79
+ return (_jsx(Pressable, { accessibilityRole: "button", accessibilityState: { checked: !!done }, onPress: onPress, style: ({ pressed }) => [styles.stepRow, pressed && { backgroundColor: theme.color.hover }, style], children: inner }));
80
+ }
81
+ const styles = StyleSheet.create({
82
+ flex1: { flex: 1 },
83
+ hero: { overflow: 'hidden', paddingVertical: 24, paddingHorizontal: 22, gap: 18 },
84
+ heroTop: { flexDirection: 'row', alignItems: 'flex-start', justifyContent: 'space-between', gap: 20 },
85
+ heroText: { flex: 1, minWidth: 0, gap: 8 },
86
+ heroTag: {
87
+ alignSelf: 'flex-start',
88
+ flexDirection: 'row',
89
+ alignItems: 'center',
90
+ gap: 7,
91
+ paddingVertical: 5,
92
+ paddingHorizontal: 12,
93
+ backgroundColor: 'rgba(248,172,51,0.18)',
94
+ },
95
+ heroBody: { maxWidth: 520, textAlign: 'left' },
96
+ heroMetric: { alignItems: 'center', gap: 2 },
97
+ heroFoot: { flexDirection: 'row', alignItems: 'center', gap: 14, flexWrap: 'wrap' },
98
+ prompt: { overflow: 'hidden', padding: 20, gap: 14 },
99
+ promptHead: { flexDirection: 'row', alignItems: 'center', gap: 12 },
100
+ promptIcon: {
101
+ width: 40,
102
+ height: 40,
103
+ alignItems: 'center',
104
+ justifyContent: 'center',
105
+ backgroundColor: 'rgba(125,211,192,0.14)',
106
+ },
107
+ promptList: { gap: 7 },
108
+ promptRow: {
109
+ flexDirection: 'row',
110
+ alignItems: 'center',
111
+ gap: 10,
112
+ paddingVertical: 11,
113
+ paddingHorizontal: 13,
114
+ borderWidth: 1,
115
+ },
116
+ row: { flexDirection: 'row', alignItems: 'center', gap: 12, minHeight: 56, paddingHorizontal: 16, paddingVertical: 10 },
117
+ rowIcon: { width: 34, height: 34, alignItems: 'center', justifyContent: 'center' },
118
+ rowText: { flex: 1, minWidth: 0, gap: 2 },
119
+ stepRow: { flexDirection: 'row', alignItems: 'center', gap: 15, paddingVertical: 15, paddingHorizontal: 20 },
120
+ stepIcon: { width: 38, height: 38, alignItems: 'center', justifyContent: 'center' },
121
+ struck: { textDecorationLine: 'line-through', textAlign: 'left' },
122
+ });
@@ -0,0 +1,14 @@
1
+ import { Text as RNText, type TextProps as RNTextProps } from 'react-native';
2
+ import { type Leading } from '../theme/theme';
3
+ import type { Weight } from '../theme/fonts';
4
+ import type { ColorToken, TextSize } from '../tokens';
5
+ export interface TextProps extends RNTextProps {
6
+ size?: TextSize | number;
7
+ weight?: Weight;
8
+ color?: ColorToken | 'white' | (string & {});
9
+ align?: 'start' | 'end' | 'center' | 'auto';
10
+ leading?: Leading | number;
11
+ numeric?: boolean;
12
+ lines?: number;
13
+ }
14
+ export declare const Text: import("react").ForwardRefExoticComponent<TextProps & import("react").RefAttributes<RNText>>;
@@ -0,0 +1,31 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { forwardRef } from 'react';
3
+ import { Text as RNText } from 'react-native';
4
+ import { useFanar } from '../theme/context';
5
+ import { LEADING } from '../theme/theme';
6
+ export const Text = forwardRef(function Text({ size = 'body', weight = 'medium', color = 'ink', align, leading, numeric, lines, style, numberOfLines, ...rest }, ref) {
7
+ const { theme } = useFanar();
8
+ const fontSize = typeof size === 'number' ? size : theme.text[size];
9
+ const asked = typeof leading === 'number' ? leading : LEADING[leading ?? 'normal'];
10
+ const ratio = Math.max(asked, theme.minLeading);
11
+ const resolved = {
12
+ ...theme.font(weight),
13
+ fontSize,
14
+ lineHeight: Math.round(fontSize * ratio),
15
+ color: theme.colorOf(color, color === 'white' ? '#ffffff' : theme.color.ink),
16
+ };
17
+ const resolvedAlign = align ?? 'start';
18
+ resolved.textAlign =
19
+ resolvedAlign === 'auto'
20
+ ? 'auto'
21
+ : resolvedAlign === 'center'
22
+ ? 'center'
23
+ : resolvedAlign === 'start'
24
+ ? 'left'
25
+ : 'right';
26
+ if (numeric) {
27
+ resolved.fontVariant = ['tabular-nums'];
28
+ resolved.writingDirection = 'ltr';
29
+ }
30
+ return (_jsx(RNText, { ref: ref, numberOfLines: numberOfLines ?? lines, style: [resolved, style], ...rest }));
31
+ });