@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,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;