@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,157 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Animated, Image, Pressable, StyleSheet, View, } from 'react-native';
|
|
3
|
+
import { Text } from './Text';
|
|
4
|
+
import { useFanar } from '../theme/context';
|
|
5
|
+
import { useSkeletonPulse } from '../lib/motion';
|
|
6
|
+
import { TONE_SOFT, TONE_SOLID } from '../lib/tone';
|
|
7
|
+
export function Badge({ children, tone = 'neutral', solid = false, size = 'md', dot, icon: Icon, style }) {
|
|
8
|
+
const { theme } = useFanar();
|
|
9
|
+
const c = (solid ? TONE_SOLID : TONE_SOFT)[tone];
|
|
10
|
+
const fg = theme.colorOf(c.fg, theme.color.ink);
|
|
11
|
+
const text = size === 'sm' ? theme.text.micro : theme.text.caption;
|
|
12
|
+
return (_jsxs(View, { style: [
|
|
13
|
+
styles.badge,
|
|
14
|
+
{
|
|
15
|
+
backgroundColor: theme.colorOf(c.bg, 'transparent'),
|
|
16
|
+
borderRadius: theme.radius.full,
|
|
17
|
+
paddingHorizontal: size === 'sm' ? 8 : 10,
|
|
18
|
+
paddingVertical: size === 'sm' ? 2 : 4,
|
|
19
|
+
},
|
|
20
|
+
style,
|
|
21
|
+
], children: [dot && _jsx(View, { style: [styles.badgeDot, { backgroundColor: fg }] }), Icon && _jsx(Icon, { size: text, strokeWidth: 2.2, color: fg }), _jsx(Text, { size: text, weight: "bold", color: c.fg, leading: "tight", lines: 1, children: children })] }));
|
|
22
|
+
}
|
|
23
|
+
export function Counter({ value, style }) {
|
|
24
|
+
const { theme } = useFanar();
|
|
25
|
+
return (_jsx(View, { style: [
|
|
26
|
+
styles.counter,
|
|
27
|
+
{ backgroundColor: theme.color.danger, borderRadius: theme.radius.full },
|
|
28
|
+
style,
|
|
29
|
+
], children: _jsx(Text, { size: "micro", style: { textAlign: 'center' }, weight: "extraBold", color: "white", leading: "none", numeric: true, children: value }) }));
|
|
30
|
+
}
|
|
31
|
+
const AVATAR = {
|
|
32
|
+
xs: { box: 28, text: 11 },
|
|
33
|
+
sm: { box: 36, text: 13 },
|
|
34
|
+
md: { box: 44, text: 14 },
|
|
35
|
+
lg: { box: 56, text: 17 },
|
|
36
|
+
};
|
|
37
|
+
export function initials(name) {
|
|
38
|
+
return name
|
|
39
|
+
.trim()
|
|
40
|
+
.split(/\s+/)
|
|
41
|
+
.slice(0, 2)
|
|
42
|
+
.map((w) => w[0])
|
|
43
|
+
.join('');
|
|
44
|
+
}
|
|
45
|
+
export function Avatar({ name = '', source, size = 'md', square = false, style }) {
|
|
46
|
+
const { theme } = useFanar();
|
|
47
|
+
const { box, text } = AVATAR[size];
|
|
48
|
+
const radius = square ? theme.radius.field : theme.radius.full;
|
|
49
|
+
const shape = {
|
|
50
|
+
width: box,
|
|
51
|
+
height: box,
|
|
52
|
+
borderRadius: radius,
|
|
53
|
+
borderWidth: 1,
|
|
54
|
+
borderColor: theme.color.line,
|
|
55
|
+
};
|
|
56
|
+
if (source)
|
|
57
|
+
return (_jsx(Image, { accessibilityIgnoresInvertColors: true, source: typeof source === 'string' ? { uri: source } : source, style: [shape, style], resizeMode: "cover" }));
|
|
58
|
+
return (_jsx(View, { style: [shape, styles.center, { backgroundColor: theme.color.soft, borderColor: theme.color.soft2 }, style], children: _jsx(Text, { size: text, weight: "extraBold", color: "brand-ink", leading: "none", children: initials(name) }) }));
|
|
59
|
+
}
|
|
60
|
+
export function Card({ children, padded = false, flat = false, onPress, style }) {
|
|
61
|
+
const { theme } = useFanar();
|
|
62
|
+
const box = {
|
|
63
|
+
backgroundColor: flat ? 'transparent' : theme.color.surface,
|
|
64
|
+
borderWidth: flat ? 0 : 1,
|
|
65
|
+
borderColor: theme.color.line,
|
|
66
|
+
borderRadius: theme.radius.card,
|
|
67
|
+
padding: padded ? 20 : 0,
|
|
68
|
+
overflow: 'hidden',
|
|
69
|
+
};
|
|
70
|
+
if (!onPress)
|
|
71
|
+
return _jsx(View, { style: [box, style], children: children });
|
|
72
|
+
return (_jsx(Pressable, { accessibilityRole: "button", onPress: onPress, style: ({ pressed }) => [box, pressed && { backgroundColor: theme.color.hover }, style], children: children }));
|
|
73
|
+
}
|
|
74
|
+
export function CardHeader({ title, description, actions, style }) {
|
|
75
|
+
return (_jsxs(View, { style: [styles.headRow, style], children: [_jsxs(View, { style: styles.headText, children: [_jsx(Text, { size: "lead", weight: "extraBold", color: "ink", align: "start", children: title }), description ? (_jsx(Text, { size: "ui", color: "muted", align: "start", children: description })) : null] }), actions ? _jsx(View, { style: styles.headActions, children: actions }) : null] }));
|
|
76
|
+
}
|
|
77
|
+
export function PageHeader({ title, hint, actions, style }) {
|
|
78
|
+
return (_jsxs(View, { style: [styles.headRow, style], children: [_jsxs(View, { style: styles.headText, children: [_jsx(Text, { size: "h3", weight: "black", color: "ink", align: "start", children: title }), hint ? (_jsx(Text, { size: "ui", color: "muted", align: "start", children: hint })) : null] }), actions ? _jsx(View, { style: styles.pageActions, children: actions }) : null] }));
|
|
79
|
+
}
|
|
80
|
+
export function StatCard({ label, value, hint, icon: Icon, tone = 'brand', onPress, style }) {
|
|
81
|
+
const { theme } = useFanar();
|
|
82
|
+
const c = TONE_SOFT[tone];
|
|
83
|
+
return (_jsx(Card, { onPress: onPress, style: [{ padding: 16 }, style], children: _jsxs(View, { style: styles.statGap, children: [_jsxs(View, { style: styles.statRow, children: [Icon && (_jsx(View, { style: [
|
|
84
|
+
styles.statIcon,
|
|
85
|
+
{ backgroundColor: theme.colorOf(c.bg, theme.color.hover), borderRadius: theme.radius.sm },
|
|
86
|
+
], children: _jsx(Icon, { size: 16, strokeWidth: 2.2, color: theme.colorOf(c.fg, theme.color.ink) }) })), _jsx(Text, { size: "ui", color: "muted", align: "start", style: styles.flex1, lines: 1, children: label })] }), _jsx(Text, { size: "h3", weight: "extraBold", leading: "none", numeric: true, align: "start", children: value }), hint ? (_jsx(Text, { size: "micro", color: "muted2", align: "start", children: hint })) : null] }) }));
|
|
87
|
+
}
|
|
88
|
+
const PROGRESS_FILL = {
|
|
89
|
+
neutral: 'muted2',
|
|
90
|
+
brand: 'brand',
|
|
91
|
+
amber: 'amber',
|
|
92
|
+
green: 'green-solid',
|
|
93
|
+
red: 'danger',
|
|
94
|
+
violet: 'violet-solid',
|
|
95
|
+
};
|
|
96
|
+
export function Progress({ value, max = 100, label, meta, tone = 'brand', style }) {
|
|
97
|
+
const { theme } = useFanar();
|
|
98
|
+
const safeMax = max > 0 ? max : 100;
|
|
99
|
+
const pct = Math.max(0, Math.min(100, (value / safeMax) * 100));
|
|
100
|
+
return (_jsxs(View, { style: [styles.progressWrap, style], children: [(label || meta) && (_jsxs(View, { style: styles.progressLabels, children: [label ? (_jsx(Text, { size: "caption", color: "muted", align: "start", style: styles.flex1, lines: 1, children: label })) : null, meta ? (_jsx(Text, { size: "caption", color: "muted", numeric: true, children: meta })) : null] })), _jsx(View, { accessibilityRole: "progressbar", accessibilityValue: { now: Math.round(pct), min: 0, max: 100 }, style: [styles.progressTrack, { backgroundColor: theme.color.hover, borderRadius: theme.radius.full }], children: _jsx(View, { style: {
|
|
101
|
+
width: `${pct}%`,
|
|
102
|
+
height: '100%',
|
|
103
|
+
borderRadius: theme.radius.full,
|
|
104
|
+
backgroundColor: theme.color[PROGRESS_FILL[tone]],
|
|
105
|
+
} }) })] }));
|
|
106
|
+
}
|
|
107
|
+
export function ProgressSteps({ total, done, fill = 'mint', track, style, }) {
|
|
108
|
+
const { theme } = useFanar();
|
|
109
|
+
return (_jsx(View, { style: [styles.stepsRow, style], children: Array.from({ length: total }, (_, i) => (_jsx(View, { style: {
|
|
110
|
+
flex: 1,
|
|
111
|
+
height: 7,
|
|
112
|
+
borderRadius: theme.radius.full,
|
|
113
|
+
backgroundColor: i < done ? theme.color[fill] : (track ?? 'rgba(255,255,255,0.14)'),
|
|
114
|
+
} }, i))) }));
|
|
115
|
+
}
|
|
116
|
+
export function Skeleton({ variant = 'text', width, height, style }) {
|
|
117
|
+
const { theme } = useFanar();
|
|
118
|
+
const opacity = useSkeletonPulse();
|
|
119
|
+
const shape = variant === 'circle'
|
|
120
|
+
? { width: width ?? 40, height: height ?? 40, borderRadius: theme.radius.full }
|
|
121
|
+
: variant === 'block'
|
|
122
|
+
? { width: width ?? '100%', height: height ?? 80, borderRadius: theme.radius.field }
|
|
123
|
+
: { width: width ?? '100%', height: height ?? 14, borderRadius: theme.radius.xs };
|
|
124
|
+
return _jsx(Animated.View, { style: [shape, { backgroundColor: theme.color.skeleton, opacity }, style] });
|
|
125
|
+
}
|
|
126
|
+
export function Divider({ inset = 0, style }) {
|
|
127
|
+
const { theme } = useFanar();
|
|
128
|
+
return (_jsx(View, { style: [
|
|
129
|
+
{ height: 1, backgroundColor: theme.color.line, marginHorizontal: inset },
|
|
130
|
+
style,
|
|
131
|
+
] }));
|
|
132
|
+
}
|
|
133
|
+
const styles = StyleSheet.create({
|
|
134
|
+
badge: { flexDirection: 'row', alignItems: 'center', gap: 6, alignSelf: 'flex-start' },
|
|
135
|
+
badgeDot: { width: 6, height: 6, borderRadius: 3 },
|
|
136
|
+
counter: {
|
|
137
|
+
minWidth: 18,
|
|
138
|
+
height: 18,
|
|
139
|
+
paddingHorizontal: 6,
|
|
140
|
+
alignItems: 'center',
|
|
141
|
+
justifyContent: 'center',
|
|
142
|
+
textAlign: 'center',
|
|
143
|
+
},
|
|
144
|
+
center: { alignItems: 'center', justifyContent: 'center' },
|
|
145
|
+
headRow: { flexDirection: 'row', alignItems: 'flex-start', justifyContent: 'space-between', gap: 16 },
|
|
146
|
+
headText: { flex: 1, minWidth: 0, gap: 4 },
|
|
147
|
+
headActions: { flexDirection: 'row', alignItems: 'center', gap: 6 },
|
|
148
|
+
pageActions: { flexDirection: 'row', alignItems: 'center', gap: 10 },
|
|
149
|
+
statGap: { gap: 8 },
|
|
150
|
+
statRow: { flexDirection: 'row', alignItems: 'center', gap: 10 },
|
|
151
|
+
statIcon: { width: 30, height: 30, alignItems: 'center', justifyContent: 'center' },
|
|
152
|
+
flex1: { flex: 1 },
|
|
153
|
+
progressWrap: { gap: 6 },
|
|
154
|
+
progressLabels: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 12 },
|
|
155
|
+
progressTrack: { height: 6, overflow: 'hidden' },
|
|
156
|
+
stepsRow: { flexDirection: 'row', gap: 5, flex: 1, minWidth: 180 },
|
|
157
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import { type LucideIcon } from '../icons';
|
|
4
|
+
export interface DrawerItem {
|
|
5
|
+
key: string;
|
|
6
|
+
label: string;
|
|
7
|
+
isNew?: boolean;
|
|
8
|
+
danger?: boolean;
|
|
9
|
+
chip?: string | number;
|
|
10
|
+
}
|
|
11
|
+
export interface DrawerSection {
|
|
12
|
+
key: string;
|
|
13
|
+
label: string;
|
|
14
|
+
icon: LucideIcon | string;
|
|
15
|
+
badge?: number;
|
|
16
|
+
items: DrawerItem[];
|
|
17
|
+
}
|
|
18
|
+
export interface DrawerMenuProps {
|
|
19
|
+
sections: DrawerSection[];
|
|
20
|
+
onSelect: (sectionKey: string, itemKey: string) => void;
|
|
21
|
+
defaultOpen?: string | null;
|
|
22
|
+
header?: ReactNode;
|
|
23
|
+
footer?: ReactNode;
|
|
24
|
+
newLabel?: string;
|
|
25
|
+
style?: StyleProp<ViewStyle>;
|
|
26
|
+
}
|
|
27
|
+
export declare function DrawerMenu({ sections, onSelect, defaultOpen, header, footer, newLabel, style, }: DrawerMenuProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Pressable, ScrollView, StyleSheet, View } from 'react-native';
|
|
4
|
+
import { Text } from './Text';
|
|
5
|
+
import { Badge, Counter, Divider } from './Display';
|
|
6
|
+
import { useFanar } from '../theme/context';
|
|
7
|
+
import { ChevronDown, ChevronLeft, ChevronRight } from '../icons';
|
|
8
|
+
import { NavIcon } from '../qumra/NavIcon';
|
|
9
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
10
|
+
export function DrawerMenu({ sections, onSelect, defaultOpen = null, header, footer, newLabel, style, }) {
|
|
11
|
+
const { theme, dir } = useFanar();
|
|
12
|
+
const insets = useSafeAreaInsets();
|
|
13
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
14
|
+
const Chevron = dir === 'rtl' ? ChevronLeft : ChevronRight;
|
|
15
|
+
return (_jsxs(View, { style: [styles.root, { backgroundColor: theme.color.surface, paddingTop: insets.top }, style], children: [header ? (_jsxs(_Fragment, { children: [_jsx(View, { style: styles.header, children: header }), _jsx(Divider, {})] })) : null, _jsx(ScrollView, { style: styles.flex1, contentContainerStyle: [styles.body, { paddingBottom: insets.bottom + 16 }], showsVerticalScrollIndicator: false, children: sections.map((section) => {
|
|
16
|
+
const on = section.key === open;
|
|
17
|
+
return (_jsxs(View, { children: [_jsxs(Pressable, { accessibilityRole: "button", accessibilityState: { expanded: on }, accessibilityLabel: section.label, onPress: () => setOpen(on ? null : section.key), style: ({ pressed }) => [
|
|
18
|
+
styles.section,
|
|
19
|
+
{
|
|
20
|
+
borderRadius: theme.radius.field,
|
|
21
|
+
backgroundColor: on
|
|
22
|
+
? theme.color.soft
|
|
23
|
+
: pressed
|
|
24
|
+
? theme.color.hover
|
|
25
|
+
: 'transparent',
|
|
26
|
+
},
|
|
27
|
+
], children: [_jsxs(View, { style: styles.iconWrap, children: [_jsx(NavIcon, { icon: section.icon, size: 21, active: on, strokeWidth: on ? 2.4 : 2, color: theme.color.ink2, activeColor: theme.color.brand }), !!section.badge && _jsx(Counter, { value: section.badge, style: styles.badge })] }), _jsx(Text, { size: "body", weight: on ? 'extraBold' : 'semiBold', color: on ? 'brand-ink' : 'ink2', align: "start", lines: 1, style: styles.flex1, children: section.label }), _jsx(ChevronDown, { size: 16, strokeWidth: 2.2, color: theme.color.muted2, style: on ? styles.flipped : undefined })] }), on && (_jsx(View, { style: [
|
|
28
|
+
styles.items,
|
|
29
|
+
{ borderColor: theme.color.line },
|
|
30
|
+
dir === 'rtl' ? styles.itemsRtl : styles.itemsLtr,
|
|
31
|
+
], children: section.items.map((item) => (_jsxs(Pressable, { accessibilityRole: "menuitem", onPress: () => onSelect(section.key, item.key), style: ({ pressed }) => [
|
|
32
|
+
styles.item,
|
|
33
|
+
{
|
|
34
|
+
borderRadius: theme.radius.sm,
|
|
35
|
+
backgroundColor: pressed
|
|
36
|
+
? item.danger
|
|
37
|
+
? theme.color['red-bg']
|
|
38
|
+
: theme.color.hover
|
|
39
|
+
: 'transparent',
|
|
40
|
+
},
|
|
41
|
+
], children: [_jsx(Text, { size: "ui", weight: "medium", color: item.danger ? 'red' : 'ink2', align: "start", lines: 1, style: styles.flex1, children: item.label }), item.isNew && newLabel ? (_jsx(Badge, { tone: "green", size: "sm", children: newLabel })) : null, item.chip !== undefined ? (_jsx(Text, { size: "micro", color: "muted2", numeric: true, children: item.chip })) : null, _jsx(Chevron, { size: 14, strokeWidth: 2, color: theme.color.muted2 })] }, item.key))) }))] }, section.key));
|
|
42
|
+
}) }), footer ? (_jsxs(_Fragment, { children: [_jsx(Divider, {}), _jsx(View, { style: [styles.footer, { paddingBottom: insets.bottom + 12 }], children: footer })] })) : null] }));
|
|
43
|
+
}
|
|
44
|
+
const styles = StyleSheet.create({
|
|
45
|
+
root: { flex: 1 },
|
|
46
|
+
flex1: { flex: 1 },
|
|
47
|
+
header: { padding: 16 },
|
|
48
|
+
body: { padding: 10, gap: 2 },
|
|
49
|
+
section: { flexDirection: 'row', alignItems: 'center', gap: 12, height: 48, paddingHorizontal: 12 },
|
|
50
|
+
iconWrap: { width: 22, alignItems: 'center', justifyContent: 'center' },
|
|
51
|
+
badge: { position: 'absolute', top: -8, insetInlineEnd: -10 },
|
|
52
|
+
flipped: { transform: [{ rotate: '180deg' }] },
|
|
53
|
+
items: { marginTop: 2, marginBottom: 6, gap: 1, borderStartWidth: 1 },
|
|
54
|
+
itemsRtl: { marginEnd: 23, paddingEnd: 12 },
|
|
55
|
+
itemsLtr: { marginStart: 23, paddingStart: 12 },
|
|
56
|
+
item: { flexDirection: 'row', alignItems: 'center', gap: 8, height: 42, paddingHorizontal: 10 },
|
|
57
|
+
footer: { paddingHorizontal: 16, paddingTop: 12 },
|
|
58
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export type ToastTone = 'success' | 'error' | 'info' | 'warning';
|
|
3
|
+
export interface ToastInput {
|
|
4
|
+
title: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
tone?: ToastTone;
|
|
7
|
+
duration?: number;
|
|
8
|
+
action?: {
|
|
9
|
+
label: string;
|
|
10
|
+
onPress: () => void;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export interface ToastItem extends ToastInput {
|
|
14
|
+
id: number;
|
|
15
|
+
}
|
|
16
|
+
declare function dismiss(id: number): void;
|
|
17
|
+
export declare const toast: {
|
|
18
|
+
show: (input: ToastInput) => number;
|
|
19
|
+
success: (title: string, description?: string) => number;
|
|
20
|
+
error: (title: string, description?: string) => number;
|
|
21
|
+
info: (title: string, description?: string) => number;
|
|
22
|
+
warning: (title: string, description?: string) => number;
|
|
23
|
+
dismiss: typeof dismiss;
|
|
24
|
+
clear: () => void;
|
|
25
|
+
};
|
|
26
|
+
export declare function useToasts(): {
|
|
27
|
+
show: (input: ToastInput) => number;
|
|
28
|
+
success: (title: string, description?: string) => number;
|
|
29
|
+
error: (title: string, description?: string) => number;
|
|
30
|
+
info: (title: string, description?: string) => number;
|
|
31
|
+
warning: (title: string, description?: string) => number;
|
|
32
|
+
dismiss: typeof dismiss;
|
|
33
|
+
clear: () => void;
|
|
34
|
+
toasts: ToastItem[];
|
|
35
|
+
};
|
|
36
|
+
export declare function ToastHost({ bottomOffset }: {
|
|
37
|
+
bottomOffset?: number;
|
|
38
|
+
}): import("react").JSX.Element | null;
|
|
39
|
+
export declare function Tooltip({ text, children }: {
|
|
40
|
+
text: string;
|
|
41
|
+
children: ReactNode;
|
|
42
|
+
}): import("react").JSX.Element;
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { Animated, Platform, Pressable, StyleSheet, View } from 'react-native';
|
|
4
|
+
import { Text } from './Text';
|
|
5
|
+
import { useFanar } from '../theme/context';
|
|
6
|
+
import { EASE_OUT } from '../lib/motion';
|
|
7
|
+
import { DURATION } from '../theme/theme';
|
|
8
|
+
import { AlertTriangle, Check, Info, X } from '../icons';
|
|
9
|
+
let seq = 0;
|
|
10
|
+
let items = [];
|
|
11
|
+
const listeners = new Set();
|
|
12
|
+
let hosts = 0;
|
|
13
|
+
function emit() {
|
|
14
|
+
for (const l of listeners)
|
|
15
|
+
l(items);
|
|
16
|
+
}
|
|
17
|
+
function push(input) {
|
|
18
|
+
const id = ++seq;
|
|
19
|
+
items = [...items, { ...input, id }];
|
|
20
|
+
emit();
|
|
21
|
+
return id;
|
|
22
|
+
}
|
|
23
|
+
function dismiss(id) {
|
|
24
|
+
items = items.filter((t) => t.id !== id);
|
|
25
|
+
emit();
|
|
26
|
+
}
|
|
27
|
+
export const toast = {
|
|
28
|
+
show: (input) => push(input),
|
|
29
|
+
success: (title, description) => push({ title, description, tone: 'success' }),
|
|
30
|
+
error: (title, description) => push({ title, description, tone: 'error', duration: 6000 }),
|
|
31
|
+
info: (title, description) => push({ title, description, tone: 'info' }),
|
|
32
|
+
warning: (title, description) => push({ title, description, tone: 'warning' }),
|
|
33
|
+
dismiss,
|
|
34
|
+
clear: () => {
|
|
35
|
+
items = [];
|
|
36
|
+
emit();
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
export function useToasts() {
|
|
40
|
+
const [list, setList] = useState(items);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
listeners.add(setList);
|
|
43
|
+
return () => {
|
|
44
|
+
listeners.delete(setList);
|
|
45
|
+
};
|
|
46
|
+
}, []);
|
|
47
|
+
return { toasts: list, ...toast };
|
|
48
|
+
}
|
|
49
|
+
const TONE = {
|
|
50
|
+
success: { icon: Check, fg: 'green' },
|
|
51
|
+
error: { icon: AlertTriangle, fg: 'red-100' },
|
|
52
|
+
info: { icon: Info, fg: 'mint' },
|
|
53
|
+
warning: { icon: AlertTriangle, fg: 'amber' },
|
|
54
|
+
};
|
|
55
|
+
function ToastCard({ item }) {
|
|
56
|
+
const { theme, p } = useFanar();
|
|
57
|
+
const enter = useRef(new Animated.Value(0)).current;
|
|
58
|
+
const tone = TONE[item.tone ?? 'info'];
|
|
59
|
+
const Icon = tone.icon;
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
Animated.timing(enter, {
|
|
62
|
+
toValue: 1,
|
|
63
|
+
duration: DURATION.toast,
|
|
64
|
+
easing: EASE_OUT,
|
|
65
|
+
useNativeDriver: true,
|
|
66
|
+
}).start();
|
|
67
|
+
const ms = item.duration ?? 4000;
|
|
68
|
+
if (ms <= 0)
|
|
69
|
+
return;
|
|
70
|
+
const timer = setTimeout(() => dismiss(item.id), ms);
|
|
71
|
+
return () => clearTimeout(timer);
|
|
72
|
+
}, [enter, item.id, item.duration]);
|
|
73
|
+
return (_jsxs(Animated.View, { style: [
|
|
74
|
+
styles.toast,
|
|
75
|
+
theme.shadow.toast,
|
|
76
|
+
{
|
|
77
|
+
backgroundColor: theme.color.night,
|
|
78
|
+
borderRadius: theme.radius.card,
|
|
79
|
+
opacity: enter,
|
|
80
|
+
transform: [{ translateY: enter.interpolate({ inputRange: [0, 1], outputRange: [12, 0] }) }],
|
|
81
|
+
},
|
|
82
|
+
], accessibilityLiveRegion: "polite", accessibilityRole: "alert", children: [_jsx(Icon, { size: 17, strokeWidth: 2.2, color: theme.color[tone.fg] }), _jsxs(View, { style: styles.toastText, children: [_jsx(Text, { size: "ui", weight: "extraBold", color: "white", align: "start", children: item.title }), item.description ? (_jsx(Text, { size: "caption", color: "hero-meta", align: "start", leading: "relaxed", children: item.description })) : null] }), item.action && (_jsx(Pressable, { accessibilityRole: "button", onPress: item.action.onPress, hitSlop: 8, children: _jsx(Text, { size: "caption", weight: "extraBold", color: "mint", children: item.action.label }) })), _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: p('close'), hitSlop: 10, onPress: () => dismiss(item.id), children: _jsx(X, { size: 15, strokeWidth: 2.4, color: theme.color['hero-dim'] }) })] }));
|
|
83
|
+
}
|
|
84
|
+
export function ToastHost({ bottomOffset = 0 }) {
|
|
85
|
+
const { toasts } = useToasts();
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
hosts += 1;
|
|
88
|
+
if (__DEV__ && hosts > 1)
|
|
89
|
+
console.warn('[fanar-native] ToastHost اتركّب أكتر من مرة — كل رسالة هتظهر مرّتين.');
|
|
90
|
+
return () => {
|
|
91
|
+
hosts -= 1;
|
|
92
|
+
};
|
|
93
|
+
}, []);
|
|
94
|
+
if (!toasts.length)
|
|
95
|
+
return null;
|
|
96
|
+
return (_jsx(View, { pointerEvents: "box-none", style: [styles.host, { bottom: bottomOffset + 16 }], children: toasts.map((t) => (_jsx(ToastCard, { item: t }, t.id))) }));
|
|
97
|
+
}
|
|
98
|
+
export function Tooltip({ text, children }) {
|
|
99
|
+
const { theme } = useFanar();
|
|
100
|
+
const [open, setOpen] = useState(false);
|
|
101
|
+
const fade = useRef(new Animated.Value(0)).current;
|
|
102
|
+
const close = useCallback(() => {
|
|
103
|
+
Animated.timing(fade, { toValue: 0, duration: DURATION.fade, useNativeDriver: true }).start(() => setOpen(false));
|
|
104
|
+
}, [fade]);
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (!open)
|
|
107
|
+
return;
|
|
108
|
+
Animated.timing(fade, { toValue: 1, duration: DURATION.fade, useNativeDriver: true }).start();
|
|
109
|
+
const timer = setTimeout(close, 2200);
|
|
110
|
+
return () => clearTimeout(timer);
|
|
111
|
+
}, [open, fade, close]);
|
|
112
|
+
return (_jsxs(View, { children: [_jsx(Pressable, { onLongPress: () => setOpen(true), delayLongPress: 350, accessibilityHint: text, children: children }), open && (_jsx(Animated.View, { pointerEvents: "none", style: [
|
|
113
|
+
styles.tip,
|
|
114
|
+
theme.shadow.tip,
|
|
115
|
+
{ backgroundColor: theme.color.night, borderRadius: theme.radius.sm, opacity: fade },
|
|
116
|
+
], children: _jsx(Text, { size: "caption", weight: "semiBold", color: "white", children: text }) }))] }));
|
|
117
|
+
}
|
|
118
|
+
const styles = StyleSheet.create({
|
|
119
|
+
host: {
|
|
120
|
+
position: 'absolute',
|
|
121
|
+
insetInlineStart: 16,
|
|
122
|
+
insetInlineEnd: 16,
|
|
123
|
+
gap: 8,
|
|
124
|
+
...Platform.select({ android: { elevation: 12 }, default: {} }),
|
|
125
|
+
},
|
|
126
|
+
toast: { flexDirection: 'row', alignItems: 'center', gap: 10, padding: 14 },
|
|
127
|
+
toastText: { flex: 1, gap: 2 },
|
|
128
|
+
tip: {
|
|
129
|
+
position: 'absolute',
|
|
130
|
+
bottom: '100%',
|
|
131
|
+
marginBottom: 6,
|
|
132
|
+
paddingHorizontal: 10,
|
|
133
|
+
paddingVertical: 6,
|
|
134
|
+
alignSelf: 'center',
|
|
135
|
+
},
|
|
136
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type StyleProp, type TextInputProps, type ViewStyle } from 'react-native';
|
|
3
|
+
import { type LucideIcon } from '../icons';
|
|
4
|
+
export type FieldSize = 'sm' | 'md' | 'lg';
|
|
5
|
+
export interface FieldProps {
|
|
6
|
+
label?: string;
|
|
7
|
+
required?: boolean;
|
|
8
|
+
meta?: string;
|
|
9
|
+
error?: string;
|
|
10
|
+
hint?: string;
|
|
11
|
+
size?: FieldSize;
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
style?: StyleProp<ViewStyle>;
|
|
14
|
+
}
|
|
15
|
+
export declare function Field({ label, required, meta, error, hint, size, children, style }: FieldProps): import("react").JSX.Element;
|
|
16
|
+
export interface InputProps extends Omit<TextInputProps, 'style'> {
|
|
17
|
+
label?: string;
|
|
18
|
+
required?: boolean;
|
|
19
|
+
meta?: string;
|
|
20
|
+
error?: string;
|
|
21
|
+
invalid?: boolean;
|
|
22
|
+
hint?: string;
|
|
23
|
+
size?: FieldSize;
|
|
24
|
+
icon?: LucideIcon;
|
|
25
|
+
suffix?: string;
|
|
26
|
+
action?: ReactNode;
|
|
27
|
+
numeric?: boolean;
|
|
28
|
+
style?: StyleProp<ViewStyle>;
|
|
29
|
+
inputStyle?: StyleProp<ViewStyle>;
|
|
30
|
+
}
|
|
31
|
+
export declare function Input({ label, required, meta, error, invalid: invalidProp, hint, size, icon: Icon, suffix, action, numeric, editable, style, inputStyle, onFocus, onBlur, accessibilityLabel, ...rest }: InputProps): import("react").JSX.Element;
|
|
32
|
+
export interface SearchInputProps extends Omit<InputProps, 'icon' | 'action'> {
|
|
33
|
+
onClear?: () => void;
|
|
34
|
+
}
|
|
35
|
+
export declare function SearchInput({ value, onClear, onChangeText, ...rest }: SearchInputProps): import("react").JSX.Element;
|
|
36
|
+
export interface TextareaProps extends Omit<InputProps, 'size' | 'suffix' | 'icon' | 'action'> {
|
|
37
|
+
minHeight?: number;
|
|
38
|
+
}
|
|
39
|
+
export declare function Textarea({ label, required, meta, error, hint, minHeight, numeric, editable, style, onFocus, onBlur, accessibilityLabel, ...rest }: TextareaProps): import("react").JSX.Element;
|
|
40
|
+
export interface ChoiceProps {
|
|
41
|
+
label?: string;
|
|
42
|
+
description?: string;
|
|
43
|
+
checked: boolean;
|
|
44
|
+
onChange: (next: boolean) => void;
|
|
45
|
+
disabled?: boolean;
|
|
46
|
+
style?: StyleProp<ViewStyle>;
|
|
47
|
+
}
|
|
48
|
+
export declare function Checkbox(props: ChoiceProps): import("react").JSX.Element;
|
|
49
|
+
export declare function Radio(props: ChoiceProps): import("react").JSX.Element;
|
|
50
|
+
export interface RadioGroupProps<T extends string> {
|
|
51
|
+
legend?: string;
|
|
52
|
+
value: T;
|
|
53
|
+
options: {
|
|
54
|
+
value: T;
|
|
55
|
+
label: string;
|
|
56
|
+
description?: string;
|
|
57
|
+
disabled?: boolean;
|
|
58
|
+
}[];
|
|
59
|
+
onChange: (next: T) => void;
|
|
60
|
+
inline?: boolean;
|
|
61
|
+
style?: StyleProp<ViewStyle>;
|
|
62
|
+
}
|
|
63
|
+
export declare function RadioGroup<T extends string>({ legend, value, options, onChange, inline, style }: RadioGroupProps<T>): import("react").JSX.Element;
|
|
64
|
+
export interface SwitchProps {
|
|
65
|
+
value: boolean;
|
|
66
|
+
onValueChange: (next: boolean) => void;
|
|
67
|
+
label?: string;
|
|
68
|
+
description?: string;
|
|
69
|
+
disabled?: boolean;
|
|
70
|
+
activeColor?: string;
|
|
71
|
+
style?: StyleProp<ViewStyle>;
|
|
72
|
+
}
|
|
73
|
+
export declare function Switch({ value, onValueChange, label, description, disabled, activeColor, style }: SwitchProps): import("react").JSX.Element;
|
|
74
|
+
export interface SelectOption<T extends string> {
|
|
75
|
+
value: T;
|
|
76
|
+
label: string;
|
|
77
|
+
disabled?: boolean;
|
|
78
|
+
}
|
|
79
|
+
export interface SelectProps<T extends string> {
|
|
80
|
+
label?: string;
|
|
81
|
+
required?: boolean;
|
|
82
|
+
error?: string;
|
|
83
|
+
hint?: string;
|
|
84
|
+
size?: FieldSize;
|
|
85
|
+
value?: T;
|
|
86
|
+
placeholder?: string;
|
|
87
|
+
options: SelectOption<T>[];
|
|
88
|
+
onChange: (next: T) => void;
|
|
89
|
+
disabled?: boolean;
|
|
90
|
+
style?: StyleProp<ViewStyle>;
|
|
91
|
+
}
|
|
92
|
+
export declare function Select<T extends string>({ label, required, error, hint, size, value, placeholder, options, onChange, disabled, style, }: SelectProps<T>): import("react").JSX.Element;
|