@qumra/fanar-native 0.1.1 → 0.1.4

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.
@@ -70,7 +70,7 @@ export function StepRow({ icon, title, why, done, action, onPress, style }) {
70
70
  const inner = (_jsxs(_Fragment, { children: [done ? (_jsx(View, { style: [
71
71
  styles.stepIcon,
72
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: [
73
+ ], children: _jsx(Check, { size: 17, strokeWidth: 3, color: "#ffffff" }) })) : brand ? (_jsx(View, { style: styles.stepIcon, children: _jsx(QumraIcon, { name: brand, size: 34 }) })) : (_jsx(View, { style: [
74
74
  styles.stepIcon,
75
75
  { borderRadius: theme.radius.field, backgroundColor: theme.color.soft },
76
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] }));
package/lib/icons.d.ts CHANGED
@@ -2,6 +2,7 @@ import * as React from 'react';
2
2
  import { type StyleProp, type ViewStyle } from 'react-native';
3
3
  import { AlertTriangle, ArrowLeft, ArrowRight, BarChart3, Bell, Bot, Check, CheckCheck, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, Copy, ExternalLink, Globe, Home, Info, Lock, Menu, Minus, Plus, Search, Share2, Sparkles, Star, Undo2, X, type LucideIcon } from 'lucide-react-native';
4
4
  export type { LucideIcon };
5
+ export declare function uiIconFor(name: string): string | undefined;
5
6
  export declare function qumraIconFor(name: string): string | undefined;
6
7
  export declare function icon(name: string): LucideIcon;
7
8
  export declare function hasIcon(name: string): boolean;
@@ -14,5 +15,17 @@ export type IconProps = {
14
15
  flip?: boolean;
15
16
  onPress?: () => void;
16
17
  };
18
+ export declare const ICON_SIZE: {
19
+ readonly xs: 12;
20
+ readonly sm: 14;
21
+ readonly md: 16;
22
+ readonly lg: 18;
23
+ readonly xl: 20;
24
+ readonly '2xl': 24;
25
+ readonly '3xl': 32;
26
+ };
27
+ export type IconSize = keyof typeof ICON_SIZE;
28
+ export declare const ICON_STROKE = 2;
29
+ export declare function snapIconSize(size: number): number;
17
30
  export declare function Icon({ name, size, color, strokeWidth, style, flip, onPress }: IconProps): React.FunctionComponentElement<import("lucide-react-native").LucideProps>;
18
31
  export { AlertTriangle, ArrowLeft, ArrowRight, BarChart3, Bell, Bot, Check, CheckCheck, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, Copy, ExternalLink, Globe, Home, Info, Lock, Menu, Minus, Plus, Search, Share2, Sparkles, Star, Undo2, X, };
package/lib/icons.js CHANGED
@@ -374,6 +374,59 @@ function qumraAs(qumraName) {
374
374
  wrapped[qumraName] = Wrapped;
375
375
  return wrapped[qumraName];
376
376
  }
377
+ const stemOf = (name) => name.replace(/-(outline|sharp)$/, '');
378
+ const UI_FOR = {
379
+ add: 'plus', 'add-circle': 'plus', plus: 'plus', 'folder-plus': 'plus', 'package-plus': 'plus',
380
+ 'person-add': 'user-plus', remove: 'close', close: 'close', 'close-circle': 'close', x: 'close',
381
+ checkmark: 'check', 'checkmark-circle': 'check', 'checkmark-done': 'check', check: 'check',
382
+ checkbox: 'check', trash: 'trash', 'trash-2': 'trash', create: 'pencil', pencil: 'pencil',
383
+ brush: 'pencil', 'color-wand': 'sparkle', copy: 'copy', save: 'save', download: 'save',
384
+ 'cloud-download': 'save', 'cloud-upload': 'publish', print: 'print', send: 'send',
385
+ 'share-social': 'share', 'share-2': 'share', search: 'search', filter: 'filter',
386
+ funnel: 'filter', options: 'sliders', sync: 'repeat', reload: 'repeat', refresh: 'reset',
387
+ 'refresh-circle': 'reset', scan: 'search', stop: 'stop', 'pause-circle': 'stop', play: 'live',
388
+ 'play-circle': 'live', 'log-out': 'logout', exit: 'logout', open: 'external',
389
+ 'external-link': 'external', link: 'link', unlink: 'unlink', expand: 'expand',
390
+ contract: 'collapse', resize: 'expand',
391
+ 'alert-circle': 'alert', 'alert-triangle': 'alert', warning: 'alert', 'help-circle': 'alert',
392
+ 'information-circle': 'alert', 'life-buoy': 'alert', ban: 'ban', 'shield-checkmark': 'shield-check',
393
+ 'shield-half': 'shield', 'lock-closed': 'lock', key: 'key', 'finger-print': 'key',
394
+ eye: 'eye', 'eye-off': 'eye-off', star: 'star', crown: 'crown', diamond: 'crown',
395
+ sparkles: 'sparkle', rocket: 'rocket', flash: 'bolt', flame: 'flame', bulb: 'bulb',
396
+ storefront: 'store', business: 'building', cube: 'box', 'package-open': 'box',
397
+ 'bag-handle': 'bag', bag: 'bag', cart: 'cart', shoppingcart: 'cart', pricetag: 'tag',
398
+ pricetags: 'tag', gift: 'tag', ticket: 'tag', receipt: 'receipt', card: 'card',
399
+ cash: 'money', payments: 'money', wallet: 'wallet', calculator: 'sigma',
400
+ person: 'user', 'person-circle': 'user', user: 'user', people: 'users', customers: 'users',
401
+ partners: 'users', car: 'truck', barcode: 'hash', keypad: 'hash',
402
+ document: 'file', documents: 'file', 'document-text': 'file', reader: 'file', sheet: 'sheet',
403
+ folder: 'folder', library: 'layers', albums: 'layers', layers: 'layers', book: 'note',
404
+ journal: 'note', chatbubble: 'note', chatbubbles: 'note', text: 'text', 'code-slash': 'code',
405
+ terminal: 'code', image: 'image-plus', images: 'image-plus', camera: 'image-plus',
406
+ 'camera-off': 'image-off', videocam: 'film', film: 'film', shapes: 'template',
407
+ 'layout-grid': 'grid', grid: 'grid', apps: 'grid', widgets: 'template', list: 'rows',
408
+ 'reorder-three': 'rows', 'drag-indicator': 'grip', archive: 'archive',
409
+ 'bar-chart': 'chart', 'stats-chart': 'chart', 'pie-chart': 'chart', analytics: 'chart',
410
+ pulse: 'chart', speedometer: 'chart', 'trending-up': 'trend-up', 'trending-down': 'trend-down',
411
+ settings: 'gear', construct: 'gear', build: 'gear', hammer: 'gear', 'color-palette': 'palette',
412
+ globe: 'globe', earth: 'globe', language: 'language', server: 'database', cloud: 'database',
413
+ 'cloud-offline': 'database', desktop: 'desktop', laptop: 'desktop', 'phone-portrait': 'mobile',
414
+ 'tablet-portrait': 'tablet', radio: 'plug', bot: 'sparkle',
415
+ mail: 'mail', 'mail-open': 'mail', 'mail-unread': 'mail', call: 'phone', phone: 'phone',
416
+ 'phone-call': 'phone', headset: 'phone', megaphone: 'megaphone', notifications: 'bell',
417
+ 'notifications-off': 'bell', bell: 'bell', time: 'clock', clock: 'clock',
418
+ calendar: 'calendar', 'calendar-clear': 'calendar', location: 'pin', 'map-marker': 'pin',
419
+ map: 'pin', navigate: 'cursor', pin: 'pin', flag: 'thumbtack', menu: 'menu',
420
+ 'ellipsis-vertical': 'more', 'ellipsis-horizontal': 'more', more: 'more',
421
+ 'swap-vertical': 'sort', sun: 'live', moon: 'live',
422
+ 'arrow-back': 'arrow-left', 'arrow-forward': 'arrow-right',
423
+ 'arrow-undo': 'undo', 'arrow-redo': 'redo',
424
+ 'chevron-down': 'chevron-down', 'chevron-small-down': 'chevron-down', 'chevron-up': 'chevron-up',
425
+ };
426
+ export function uiIconFor(name) {
427
+ const tool = UI_FOR[stemOf(name)];
428
+ return tool ? `ui-${tool}` : undefined;
429
+ }
377
430
  export function qumraIconFor(name) {
378
431
  return QUMRA_FOR[name];
379
432
  }
@@ -381,18 +434,48 @@ export function icon(name) {
381
434
  const brand = QUMRA_FOR[name];
382
435
  if (brand)
383
436
  return qumraAs(brand);
437
+ const tool = uiIconFor(name);
438
+ if (tool)
439
+ return qumraAs(tool);
384
440
  return MAP[name] ?? ALIAS[name] ?? Home;
385
441
  }
386
442
  export function hasIcon(name) {
387
- return name in QUMRA_FOR || name in MAP || name in ALIAS;
443
+ return name in QUMRA_FOR || !!uiIconFor(name) || name in MAP || name in ALIAS;
444
+ }
445
+ export const ICON_SIZE = {
446
+ xs: 12,
447
+ sm: 14,
448
+ md: 16,
449
+ lg: 18,
450
+ xl: 20,
451
+ '2xl': 24,
452
+ '3xl': 32,
453
+ };
454
+ const ICON_SCALE = [12, 14, 16, 18, 20, 22, 24, 28, 32, 36, 40, 44, 48, 56];
455
+ export const ICON_STROKE = 2;
456
+ export function snapIconSize(size) {
457
+ if (!Number.isFinite(size))
458
+ return ICON_SIZE.md;
459
+ if (size <= ICON_SCALE[0])
460
+ return ICON_SCALE[0];
461
+ const top = ICON_SCALE[ICON_SCALE.length - 1];
462
+ if (size >= top)
463
+ return size;
464
+ for (let i = 0; i < ICON_SCALE.length - 1; i++) {
465
+ const low = ICON_SCALE[i];
466
+ const high = ICON_SCALE[i + 1];
467
+ if (size <= high)
468
+ return size - low < high - size ? low : high;
469
+ }
470
+ return top;
388
471
  }
389
- export function Icon({ name, size = 16, color, strokeWidth, style, flip, onPress }) {
472
+ export function Icon({ name, size = ICON_SIZE.md, color, strokeWidth, style, flip, onPress }) {
390
473
  const Cmp = icon(name);
391
474
  const { dir } = useFanar();
392
475
  return React.createElement(Cmp, {
393
- size,
476
+ size: snapIconSize(size),
394
477
  color,
395
- strokeWidth,
478
+ strokeWidth: strokeWidth ?? ICON_STROKE,
396
479
  onPress,
397
480
  style: flip && dir === 'rtl' ? [style, { transform: [{ scaleX: -1 }] }] : style,
398
481
  });
package/lib/index.d.ts CHANGED
@@ -5,7 +5,7 @@ export { TONE_SOFT, TONE_SOLID, type Tone, type ToneColors } from './lib/tone';
5
5
  export { toLatinDigits, toArabicDigits, groupDigits } from './lib/digits';
6
6
  export { usePressScale, useEnter, useSkeletonPulse, EASE_OUT, EASE_IN_OUT } from './lib/motion';
7
7
  export { hitSlopFor, HIT_SLOP_SM, TOUCH_MIN } from './lib/a11y';
8
- export { icon, hasIcon, Icon, qumraIconFor, type LucideIcon, type IconProps } from './icons';
8
+ export { icon, hasIcon, Icon, qumraIconFor, uiIconFor, snapIconSize, ICON_SIZE, ICON_STROKE, type LucideIcon, type IconProps, type IconSize, } from './icons';
9
9
  export { QumraIcon, hasQumraIcon, QUMRA_ICONS, type QumraIconProps, type QumraIconName, type QumraIconSpec, } from './qumra/QumraIcon';
10
10
  export { NavIcon, type NavIconProps } from './qumra/NavIcon';
11
11
  export { art, hasArt, BRAND, ART, type BrandName, type ArtName } from './art';
package/lib/index.js CHANGED
@@ -5,7 +5,7 @@ export { TONE_SOFT, TONE_SOLID } from './lib/tone';
5
5
  export { toLatinDigits, toArabicDigits, groupDigits } from './lib/digits';
6
6
  export { usePressScale, useEnter, useSkeletonPulse, EASE_OUT, EASE_IN_OUT } from './lib/motion';
7
7
  export { hitSlopFor, HIT_SLOP_SM, TOUCH_MIN } from './lib/a11y';
8
- export { icon, hasIcon, Icon, qumraIconFor } from './icons';
8
+ export { icon, hasIcon, Icon, qumraIconFor, uiIconFor, snapIconSize, ICON_SIZE, ICON_STROKE, } from './icons';
9
9
  export { QumraIcon, hasQumraIcon, QUMRA_ICONS, } from './qumra/QumraIcon';
10
10
  export { NavIcon } from './qumra/NavIcon';
11
11
  export { art, hasArt, BRAND, ART } from './art';
@@ -1,17 +1,19 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { icon as resolveLucide, qumraIconFor } from '../icons';
2
+ import { ICON_SIZE, ICON_STROKE, icon as resolveLucide, qumraIconFor, snapIconSize } from '../icons';
3
3
  import { QUMRA_ICONS, QumraIcon } from './QumraIcon';
4
4
  const brandName = (name) => qumraIconFor(name) ?? (name in QUMRA_ICONS ? name : undefined);
5
- export function NavIcon({ icon, size = 20, active, color, activeColor, strokeWidth, style, }) {
5
+ export function NavIcon({ icon, size = ICON_SIZE.xl, active, color, activeColor, strokeWidth, style, }) {
6
+ const px = snapIconSize(size);
7
+ const stroke = strokeWidth ?? ICON_STROKE;
6
8
  if (typeof icon === 'string') {
7
9
  const brand = brandName(icon);
8
10
  if (brand) {
9
- return _jsx(QumraIcon, { name: brand, size: size, mono: true, active: active });
11
+ return _jsx(QumraIcon, { name: brand, size: px, mono: true, active: active });
10
12
  }
11
13
  const Lucide = resolveLucide(icon);
12
- return _jsx(Lucide, { size: size, strokeWidth: strokeWidth, color: active ? activeColor : color, style: style });
14
+ return _jsx(Lucide, { size: px, strokeWidth: stroke, color: active ? activeColor : color, style: style });
13
15
  }
14
16
  const Lucide = icon;
15
- return _jsx(Lucide, { size: size, strokeWidth: strokeWidth, color: active ? activeColor : color, style: style });
17
+ return _jsx(Lucide, { size: px, strokeWidth: stroke, color: active ? activeColor : color, style: style });
16
18
  }
17
19
  export default NavIcon;
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as React from 'react';
3
3
  import Svg, { Circle, ClipPath, Defs, Ellipse, G, LinearGradient, Path, RadialGradient, Rect, Stop, Text as SvgText, } from 'react-native-svg';
4
4
  import { QUMRA_ICONS } from './data.generated';
5
+ import { useFanar } from '../theme/context';
5
6
  const TAGS = {
6
7
  svg: Svg,
7
8
  g: G,
@@ -34,7 +35,8 @@ function grayscale(value) {
34
35
  const c = l.toString(16).padStart(2, '0');
35
36
  return `#${c}${c}${c}`;
36
37
  }
37
- function build(node, uid, key, tint) {
38
+ const CURRENT = 'currentColor';
39
+ function build(node, uid, key, tint, current) {
38
40
  const [tag, props, kids] = node;
39
41
  const Cmp = TAGS[tag];
40
42
  if (!Cmp)
@@ -49,7 +51,9 @@ function build(node, uid, key, tint) {
49
51
  v = `${v}-${uid}`;
50
52
  else if (typeof v === 'string' && v.indexOf('url(#') !== -1)
51
53
  v = uniquify(v, uid);
52
- if (tint && isPaint(k) && v !== 'none')
54
+ if (isPaint(k) && v === CURRENT)
55
+ v = current ?? v;
56
+ if (tint && isPaint(k) && v !== 'none' && v !== CURRENT)
53
57
  v = flat ?? grayscale(v);
54
58
  p[k] = v;
55
59
  }
@@ -59,7 +63,7 @@ function build(node, uid, key, tint) {
59
63
  if (p.strokeOpacity !== undefined)
60
64
  p.strokeOpacity = '1';
61
65
  }
62
- const children = kids?.map((c, i) => build(c, uid, i, tint)).filter(Boolean);
66
+ const children = kids?.map((c, i) => build(c, uid, i, tint, current)).filter(Boolean);
63
67
  return React.createElement(Cmp, { key, ...p }, children?.length ? children : undefined);
64
68
  }
65
69
  function badge(count) {
@@ -74,11 +78,13 @@ function badge(count) {
74
78
  return (_jsxs(G, { children: [_jsx(Rect, { x: x, y: 2.2, width: w, height: 12.4, rx: 6.2, fill: "#E8A94F", stroke: "#F2F7F8", strokeWidth: 1.4 }), _jsx(SvgText, { x: x + w / 2, y: 8.4, dy: fs * 0.35, textAnchor: "middle", fill: "#FFFFFF", fontSize: fs, fontWeight: "700", children: s })] }, "qi-count"));
75
79
  }
76
80
  export function QumraIcon({ name, size = 32, mono, active, color, count, label, onPress, }) {
81
+ const { theme } = useFanar();
77
82
  const spec = QUMRA_ICONS[name];
78
83
  const uid = React.useId().replace(/[^a-zA-Z0-9]/g, '');
79
84
  const dimmed = !!mono && !active;
80
85
  const tint = dimmed ? color ?? 'gray' : undefined;
81
- const children = React.useMemo(() => (spec ? spec.tree.map((n, i) => build(n, uid, i, tint)) : []), [spec, uid, tint]);
86
+ const current = color ?? theme.color.ink;
87
+ const children = React.useMemo(() => (spec ? spec.tree.map((n, i) => build(n, uid, i, tint, current)) : []), [spec, uid, tint, current]);
82
88
  if (!spec) {
83
89
  if (__DEV__)
84
90
  console.warn(`[qumra-icons] لا توجد أيقونة بالاسم «${name}»`);