react-native-nano-icons 0.1.8 → 0.2.1

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 (116) hide show
  1. package/README.md +64 -2
  2. package/android/src/main/java/com/nanoicons/NanoIconsFontLoaderModule.kt +63 -0
  3. package/android/src/main/java/com/nanoicons/NanoIconsPackage.kt +19 -2
  4. package/ios/NanoIconView.h +5 -0
  5. package/ios/NanoIconView.mm +35 -9
  6. package/ios/NanoIconsFontLoader.h +11 -0
  7. package/ios/NanoIconsFontLoader.mm +110 -0
  8. package/lib/commonjs/cli/build.d.ts +3 -0
  9. package/lib/commonjs/cli/build.js +8 -4
  10. package/lib/commonjs/cli/config.d.ts +1 -0
  11. package/lib/commonjs/cli/config.js +10 -0
  12. package/lib/commonjs/cli/expoConfig.d.ts +8 -0
  13. package/lib/commonjs/cli/expoConfig.js +34 -0
  14. package/lib/commonjs/cli/index.d.ts +2 -1
  15. package/lib/commonjs/cli/index.js +4 -1
  16. package/lib/commonjs/cli/link.js +32 -22
  17. package/lib/commonjs/plugin/src/types.d.ts +9 -0
  18. package/lib/commonjs/plugin/src/withNanoIconsFontLinking.d.ts +5 -0
  19. package/lib/commonjs/plugin/src/withNanoIconsFontLinking.js +24 -10
  20. package/lib/commonjs/scripts/cli.js +23 -11
  21. package/lib/commonjs/src/core/pipeline/config.d.ts +1 -0
  22. package/lib/commonjs/src/core/pipeline/managers.js +1 -2
  23. package/lib/commonjs/src/core/pipeline/run.js +1 -0
  24. package/lib/commonjs/src/core/svg/svg_dom.d.ts +1 -0
  25. package/lib/commonjs/src/core/svg/svg_dom.js +22 -17
  26. package/lib/commonjs/src/core/svg/svg_pathops.js +30 -16
  27. package/lib/commonjs/src/core/types.d.ts +3 -0
  28. package/lib/module/const/codegenPrimitives.js +2 -0
  29. package/lib/module/const/codegenPrimitives.js.map +1 -0
  30. package/lib/module/core/font/compile.js.map +1 -1
  31. package/lib/module/core/pipeline/config.js.map +1 -1
  32. package/lib/module/core/pipeline/managers.js.map +1 -1
  33. package/lib/module/core/pipeline/run.js +4 -1
  34. package/lib/module/core/pipeline/run.js.map +1 -1
  35. package/lib/module/core/svg/layers.js.map +1 -1
  36. package/lib/module/core/svg/svg_dom.js +19 -19
  37. package/lib/module/core/svg/svg_dom.js.map +1 -1
  38. package/lib/module/core/svg/svg_pathops.js.map +1 -1
  39. package/lib/module/createNanoIconsSet.js +2 -2
  40. package/lib/module/createNanoIconsSet.js.map +1 -1
  41. package/lib/module/createNanoIconsSet.native.js +43 -16
  42. package/lib/module/createNanoIconsSet.native.js.map +1 -1
  43. package/lib/module/createNanoIconsSet.shared.js +49 -20
  44. package/lib/module/createNanoIconsSet.shared.js.map +1 -1
  45. package/lib/module/createNanoIconsSet.web.js +78 -0
  46. package/lib/module/createNanoIconsSet.web.js.map +1 -0
  47. package/lib/module/loadDynamicFont.js +118 -0
  48. package/lib/module/loadDynamicFont.js.map +1 -0
  49. package/lib/module/specs/NanoIconViewNativeComponent.ts +9 -8
  50. package/lib/module/specs/NativeNanoIconsFontLoader.js +7 -0
  51. package/lib/module/specs/NativeNanoIconsFontLoader.js.map +1 -0
  52. package/lib/module/utils/glyphRuntime.js +37 -0
  53. package/lib/module/utils/glyphRuntime.js.map +1 -0
  54. package/lib/typescript/__tests__/glyphRuntime.unit.test.d.ts +2 -0
  55. package/lib/typescript/__tests__/glyphRuntime.unit.test.d.ts.map +1 -0
  56. package/lib/typescript/__tests__/link.unit.test.d.ts +3 -0
  57. package/lib/typescript/__tests__/link.unit.test.d.ts.map +1 -0
  58. package/lib/typescript/__tests__/loadDynamicFont.unit.test.d.ts +2 -0
  59. package/lib/typescript/__tests__/loadDynamicFont.unit.test.d.ts.map +1 -0
  60. package/lib/typescript/cli/build.d.ts +3 -0
  61. package/lib/typescript/cli/build.d.ts.map +1 -1
  62. package/lib/typescript/cli/link.d.ts +12 -0
  63. package/lib/typescript/cli/link.d.ts.map +1 -0
  64. package/lib/typescript/src/const/codegenPrimitives.d.ts +3 -0
  65. package/lib/typescript/src/const/codegenPrimitives.d.ts.map +1 -0
  66. package/lib/typescript/src/core/font/compile.d.ts.map +1 -1
  67. package/lib/typescript/src/core/pipeline/config.d.ts +1 -0
  68. package/lib/typescript/src/core/pipeline/config.d.ts.map +1 -1
  69. package/lib/typescript/src/core/pipeline/managers.d.ts.map +1 -1
  70. package/lib/typescript/src/core/pipeline/run.d.ts.map +1 -1
  71. package/lib/typescript/src/core/svg/svg_dom.d.ts +1 -0
  72. package/lib/typescript/src/core/svg/svg_dom.d.ts.map +1 -1
  73. package/lib/typescript/src/core/svg/svg_pathops.d.ts.map +1 -1
  74. package/lib/typescript/src/core/types.d.ts +3 -0
  75. package/lib/typescript/src/core/types.d.ts.map +1 -1
  76. package/lib/typescript/src/createNanoIconsSet.d.ts +1 -0
  77. package/lib/typescript/src/createNanoIconsSet.d.ts.map +1 -1
  78. package/lib/typescript/src/createNanoIconsSet.native.d.ts +1 -0
  79. package/lib/typescript/src/createNanoIconsSet.native.d.ts.map +1 -1
  80. package/lib/typescript/src/createNanoIconsSet.shared.d.ts +11 -0
  81. package/lib/typescript/src/createNanoIconsSet.shared.d.ts.map +1 -1
  82. package/lib/typescript/src/createNanoIconsSet.web.d.ts +7 -0
  83. package/lib/typescript/src/createNanoIconsSet.web.d.ts.map +1 -0
  84. package/lib/typescript/src/loadDynamicFont.d.ts +28 -0
  85. package/lib/typescript/src/loadDynamicFont.d.ts.map +1 -0
  86. package/lib/typescript/src/specs/NanoIconViewNativeComponent.d.ts +9 -8
  87. package/lib/typescript/src/specs/NanoIconViewNativeComponent.d.ts.map +1 -1
  88. package/lib/typescript/src/specs/NativeNanoIconsFontLoader.d.ts +8 -0
  89. package/lib/typescript/src/specs/NativeNanoIconsFontLoader.d.ts.map +1 -0
  90. package/lib/typescript/src/types.d.ts +7 -1
  91. package/lib/typescript/src/types.d.ts.map +1 -1
  92. package/lib/typescript/src/utils/glyphRuntime.d.ts +18 -0
  93. package/lib/typescript/src/utils/glyphRuntime.d.ts.map +1 -0
  94. package/package.json +8 -5
  95. package/plugin/src/types.ts +9 -0
  96. package/plugin/src/withNanoIconsFontLinking.ts +29 -10
  97. package/react-native-nano-icons.podspec +1 -1
  98. package/scripts/cli.ts +31 -11
  99. package/src/const/codegenPrimitives.ts +14 -0
  100. package/src/core/font/compile.ts +1 -2
  101. package/src/core/pipeline/config.ts +1 -0
  102. package/src/core/pipeline/managers.ts +5 -10
  103. package/src/core/pipeline/run.ts +11 -6
  104. package/src/core/svg/layers.ts +4 -4
  105. package/src/core/svg/svg_dom.ts +26 -23
  106. package/src/core/svg/svg_pathops.ts +50 -24
  107. package/src/core/types.ts +10 -2
  108. package/src/createNanoIconsSet.native.tsx +78 -26
  109. package/src/createNanoIconsSet.shared.tsx +93 -40
  110. package/src/createNanoIconsSet.tsx +9 -1
  111. package/src/createNanoIconsSet.web.tsx +109 -0
  112. package/src/loadDynamicFont.ts +162 -0
  113. package/src/specs/NanoIconViewNativeComponent.ts +9 -8
  114. package/src/specs/NativeNanoIconsFontLoader.ts +11 -0
  115. package/src/types.ts +5 -1
  116. package/src/utils/glyphRuntime.ts +46 -0
@@ -457,7 +457,6 @@ function bestStartMinYMinX(
457
457
  return best;
458
458
  }
459
459
 
460
-
461
460
  // ---------------------------------------------------------------------------
462
461
  // Containment helpers (module-level for reuse by fixPathWinding)
463
462
  // ---------------------------------------------------------------------------
@@ -510,7 +509,10 @@ function contourToPolyline(
510
509
  for (let i = 1; i <= steps; i++) {
511
510
  const t = i / steps;
512
511
  const mt = 1 - t;
513
- pts.push([mt * mt * x0 + 2 * mt * t * x1 + t * t * x2, mt * mt * y0 + 2 * mt * t * y1 + t * t * y2]);
512
+ pts.push([
513
+ mt * mt * x0 + 2 * mt * t * x1 + t * t * x2,
514
+ mt * mt * y0 + 2 * mt * t * y1 + t * t * y2,
515
+ ]);
514
516
  }
515
517
  cx = x2;
516
518
  cy = y2;
@@ -527,8 +529,14 @@ function contourToPolyline(
527
529
  const t = i / steps;
528
530
  const mt = 1 - t;
529
531
  pts.push([
530
- mt * mt * mt * x0 + 3 * mt * mt * t * x1 + 3 * mt * t * t * x2 + t * t * t * x3,
531
- mt * mt * mt * y0 + 3 * mt * mt * t * y1 + 3 * mt * t * t * y2 + t * t * t * y3,
532
+ mt * mt * mt * x0 +
533
+ 3 * mt * mt * t * x1 +
534
+ 3 * mt * t * t * x2 +
535
+ t * t * t * x3,
536
+ mt * mt * mt * y0 +
537
+ 3 * mt * mt * t * y1 +
538
+ 3 * mt * t * t * y2 +
539
+ t * t * t * y3,
532
540
  ]);
533
541
  }
534
542
  cx = x3;
@@ -550,10 +558,7 @@ function pointInPolygon(px: number, py: number, poly: Point[]): boolean {
550
558
  for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
551
559
  const [xi, yi] = poly[i]!;
552
560
  const [xj, yj] = poly[j]!;
553
- if (
554
- yi > py !== yj > py &&
555
- px < ((xj - xi) * (py - yi)) / (yj - yi) + xi
556
- ) {
561
+ if (yi > py !== yj > py && px < ((xj - xi) * (py - yi)) / (yj - yi) + xi) {
557
562
  inside = !inside;
558
563
  }
559
564
  }
@@ -563,7 +568,10 @@ function pointInPolygon(px: number, py: number, poly: Point[]): boolean {
563
568
  /**
564
569
  * Get a representative point on the contour boundary (midpoint of first segment).
565
570
  */
566
- function getContourSamplePoint(contourCmds: readonly Cmd[], V: VerbMap): Point | null {
571
+ function getContourSamplePoint(
572
+ contourCmds: readonly Cmd[],
573
+ V: VerbMap
574
+ ): Point | null {
567
575
  let cx = 0,
568
576
  cy = 0;
569
577
  for (const cmd of contourCmds) {
@@ -584,8 +592,14 @@ function getContourSamplePoint(contourCmds: readonly Cmd[], V: VerbMap): Point |
584
592
  const t = 0.5,
585
593
  mt = 0.5;
586
594
  return [
587
- mt ** 3 * cx + 3 * mt ** 2 * t * cmd[1]! + 3 * mt * t ** 2 * cmd[3]! + t ** 3 * cmd[5]!,
588
- mt ** 3 * cy + 3 * mt ** 2 * t * cmd[2]! + 3 * mt * t ** 2 * cmd[4]! + t ** 3 * cmd[6]!,
595
+ mt ** 3 * cx +
596
+ 3 * mt ** 2 * t * cmd[1]! +
597
+ 3 * mt * t ** 2 * cmd[3]! +
598
+ t ** 3 * cmd[5]!,
599
+ mt ** 3 * cy +
600
+ 3 * mt ** 2 * t * cmd[2]! +
601
+ 3 * mt * t ** 2 * cmd[4]! +
602
+ t ** 3 * cmd[6]!,
589
603
  ];
590
604
  }
591
605
  }
@@ -597,14 +611,20 @@ function getContourSamplePoint(contourCmds: readonly Cmd[], V: VerbMap): Point |
597
611
  * Even nesting depth = CCW (outer), odd = CW (hole).
598
612
  */
599
613
  function applyContainmentWinding(
600
- contourObjs: Array<{ cmds: Cmd[]; explicitCloseWanted: boolean; absA: number }>,
614
+ contourObjs: Array<{
615
+ cmds: Cmd[];
616
+ explicitCloseWanted: boolean;
617
+ absA: number;
618
+ }>,
601
619
  V: VerbMap
602
620
  ): void {
603
621
  const n = contourObjs.length;
604
622
  if (n === 0) return;
605
623
 
606
624
  const polylines = contourObjs.map((obj) => contourToPolyline(obj.cmds, V));
607
- const samplePts = contourObjs.map((obj) => getContourSamplePoint(obj.cmds, V));
625
+ const samplePts = contourObjs.map((obj) =>
626
+ getContourSamplePoint(obj.cmds, V)
627
+ );
608
628
  const depths = new Array<number>(n).fill(0);
609
629
 
610
630
  for (let i = 0; i < n; i++) {
@@ -625,7 +645,11 @@ function applyContainmentWinding(
625
645
  const a = approxSignedAreaFromContourCmds(obj.cmds, V);
626
646
  const isCCW = a > 0;
627
647
  if (wantCCW !== isCCW) {
628
- obj.cmds = reverseClosedContourKeepStart(obj.cmds, obj.explicitCloseWanted, V);
648
+ obj.cmds = reverseClosedContourKeepStart(
649
+ obj.cmds,
650
+ obj.explicitCloseWanted,
651
+ V
652
+ );
629
653
  }
630
654
  };
631
655
 
@@ -692,7 +716,9 @@ export function convertEvenoddToWinding(
692
716
  if (v === V.MOVE) parts.push(`M${roundN(cmd[1]!)} ${roundN(cmd[2]!)}`);
693
717
  else if (v === V.LINE) parts.push(`L${roundN(cmd[1]!)} ${roundN(cmd[2]!)}`);
694
718
  else if (v === V.QUAD)
695
- parts.push(`Q${roundN(cmd[1]!)} ${roundN(cmd[2]!)} ${roundN(cmd[3]!)} ${roundN(cmd[4]!)}`);
719
+ parts.push(
720
+ `Q${roundN(cmd[1]!)} ${roundN(cmd[2]!)} ${roundN(cmd[3]!)} ${roundN(cmd[4]!)}`
721
+ );
696
722
  else if (v === V.CUBIC)
697
723
  parts.push(
698
724
  `C${roundN(cmd[1]!)} ${roundN(cmd[2]!)} ${roundN(cmd[3]!)} ${roundN(cmd[4]!)} ${roundN(cmd[5]!)} ${roundN(cmd[6]!)}`
@@ -882,17 +908,17 @@ export function buildPathopsBackend(PathKit: PathKitModule) {
882
908
 
883
909
  const cap =
884
910
  capInt === 1
885
- ? Caps.ROUND ?? 1
911
+ ? (Caps.ROUND ?? 1)
886
912
  : capInt === 2
887
- ? Caps.SQUARE ?? 2
888
- : Caps.BUTT ?? 0;
913
+ ? (Caps.SQUARE ?? 2)
914
+ : (Caps.BUTT ?? 0);
889
915
 
890
916
  const join =
891
917
  joinInt === 1
892
- ? Joins.ROUND ?? 1
918
+ ? (Joins.ROUND ?? 1)
893
919
  : joinInt === 2
894
- ? Joins.BEVEL ?? 2
895
- : Joins.MITER ?? 0;
920
+ ? (Joins.BEVEL ?? 2)
921
+ : (Joins.MITER ?? 0);
896
922
 
897
923
  const work = PathKit.NewPath(h.p);
898
924
 
@@ -961,10 +987,10 @@ export function buildPathopsBackend(PathKit: PathKitModule) {
961
987
  const Ops = PathKit.PathOp ?? {};
962
988
  const op =
963
989
  opInt === 1
964
- ? Ops.INTERSECT ?? 1
990
+ ? (Ops.INTERSECT ?? 1)
965
991
  : opInt === 2
966
- ? Ops.DIFFERENCE ?? 2
967
- : Ops.UNION ?? 0;
992
+ ? (Ops.DIFFERENCE ?? 2)
993
+ : (Ops.UNION ?? 0);
968
994
 
969
995
  const out = PathKit.MakeFromOp(aHandle.p, bHandle.p, op);
970
996
  if (!out) return null;
package/src/core/types.ts CHANGED
@@ -134,16 +134,24 @@ export type IconsMap = Record<string, GlyphEntry>;
134
134
  * z - safe zone,
135
135
  * s - start unicode,
136
136
  * h - hash,
137
+ * l - linking mode: 's' (static, bundled — default when absent) or 'd' (dynamic, OTA-delivered),
137
138
  * i - icons,
138
139
  * adv - advance width,
139
140
  */
140
141
  export type NanoGlyphMap = {
141
- m: { f: string; u: number; z: number; s: number; h?: string };
142
+ m: { f: string; u: number; z: number; s: number; h?: string; l?: 's' | 'd' };
142
143
  i: IconsMap;
143
144
  };
144
145
 
145
146
  /** Accepts JSON-inferred types where arrays aren't tuples. */
146
147
  export type NanoGlyphMapInput = {
147
- m: { f: string; u: number; z: number; s: number; h?: string };
148
+ m: {
149
+ f: string;
150
+ u: number;
151
+ z: number;
152
+ s: number;
153
+ h?: string;
154
+ l?: 's' | 'd' | (string & {});
155
+ };
148
156
  i: Record<string, readonly unknown[]>;
149
157
  };
@@ -1,16 +1,23 @@
1
1
  import { memo, useMemo } from 'react';
2
- import { PixelRatio, UIManager, processColor } from 'react-native';
2
+ import { PixelRatio, UIManager, View, processColor } from 'react-native';
3
3
  import type { NanoGlyphMapInput, GlyphEntry } from './core/types';
4
4
  import type { IconComponent, IconProps } from './types';
5
5
  import { shallowEqualColor } from './utils/shallowEqualColor';
6
+ import {
7
+ DEFAULT_ICON_SIZE,
8
+ resolveGlyphEntry,
9
+ createLayerColorResolver,
10
+ } from './utils/glyphRuntime';
6
11
  import NanoIconViewNative from './specs/NanoIconViewNativeComponent';
7
- import { createJSIconSet } from './createNanoIconsSet.shared';
12
+ import {
13
+ createJSIconSet,
14
+ warnIfLinkingMismatch,
15
+ } from './createNanoIconsSet.shared';
16
+ import { loadDynamicFont, useDynamicFontPending } from './loadDynamicFont';
8
17
 
9
18
  export type { IconComponent, IconProps };
10
19
  export { shallowEqualColor };
11
20
 
12
- const DEFAULT_ICON_SIZE = 12;
13
-
14
21
  const HAS_NATIVE_IMPL = UIManager.hasViewManagerConfig('NanoIconView');
15
22
 
16
23
  // Shared processColor cache — avoids redundant color parsing for repeated
@@ -27,27 +34,44 @@ function cachedProcessColor(color: string): number {
27
34
 
28
35
  export function createIconSet<GM extends NanoGlyphMapInput>(
29
36
  glyphMap: GM
37
+ ): IconComponent<GM>;
38
+ export function createIconSet<GM extends NanoGlyphMapInput>(
39
+ glyphMap: GM,
40
+ font: unknown
41
+ ): IconComponent<GM>;
42
+ export function createIconSet<GM extends NanoGlyphMapInput>(
43
+ glyphMap: GM,
44
+ font?: unknown
30
45
  ): IconComponent<GM> {
31
46
  if (!HAS_NATIVE_IMPL) {
32
- return createJSIconSet(glyphMap);
47
+ return createJSIconSet(glyphMap, font);
33
48
  }
34
49
 
35
50
  const fontFamilyBasename = glyphMap.m.f;
36
51
  const unitsPerEm = glyphMap.m.u;
37
-
38
- const resolveEntry = (name: keyof GM['i']): GlyphEntry => {
39
- return (glyphMap.i[name as string] ?? [
40
- unitsPerEm,
41
- [[63, 'black']],
42
- ]) as GlyphEntry;
43
- };
52
+ warnIfLinkingMismatch(fontFamilyBasename, glyphMap.m.l, font);
53
+
54
+ // dynamically linked font - register and hide icons until ready
55
+ const managed = glyphMap.m.l === 'd' && font != null;
56
+ if (managed) {
57
+ void loadDynamicFont(fontFamilyBasename, font).catch((err) => {
58
+ if (__DEV__)
59
+ console.warn(
60
+ `[react-native-nano-icons] Failed to load dynamic font "${fontFamilyBasename}".`,
61
+ err
62
+ );
63
+ });
64
+ }
44
65
 
45
66
  // Pre-compute per-icon static data (codepoints, default colors) once at set creation
46
67
  // Avoids layers.map() + processColor per icon mount
47
68
  const codepointsCache = new Map<string, readonly number[]>();
48
69
  const defaultColorsCache = new Map<string, readonly number[]>();
49
70
 
50
- function getCodepoints(name: string, layers: GlyphEntry[1]): readonly number[] {
71
+ function getCodepoints(
72
+ name: string,
73
+ layers: GlyphEntry[1]
74
+ ): readonly number[] {
51
75
  let cp = codepointsCache.get(name);
52
76
  if (!cp) {
53
77
  cp = layers.map(([c]) => c);
@@ -56,10 +80,15 @@ export function createIconSet<GM extends NanoGlyphMapInput>(
56
80
  return cp;
57
81
  }
58
82
 
59
- function getDefaultColors(name: string, layers: GlyphEntry[1]): readonly number[] {
83
+ function getDefaultColors(
84
+ name: string,
85
+ layers: GlyphEntry[1]
86
+ ): readonly number[] {
60
87
  let colors = defaultColorsCache.get(name);
61
88
  if (!colors) {
62
- colors = layers.map(([, srcColor]) => cachedProcessColor(srcColor ?? 'black'));
89
+ colors = layers.map(([, srcColor]) =>
90
+ cachedProcessColor(srcColor ?? 'black')
91
+ );
63
92
  defaultColorsCache.set(name, colors);
64
93
  }
65
94
  return colors;
@@ -75,14 +104,18 @@ export function createIconSet<GM extends NanoGlyphMapInput>(
75
104
  accessible,
76
105
  accessibilityLabel,
77
106
  accessibilityRole = 'image',
107
+ accessibilityElementsHidden,
108
+ importantForAccessibility,
78
109
  testID,
79
110
  ref,
80
111
  }: IconProps<keyof GM['i']>) => {
81
112
  const fontScale = allowFontScaling ? PixelRatio.getFontScale() : 1;
82
- const [adv, layers] = resolveEntry(name);
113
+ const [adv, layers] = resolveGlyphEntry(glyphMap, name);
83
114
  const scaledSize = size * fontScale;
84
115
  const width = (adv / unitsPerEm) * scaledSize;
85
116
 
117
+ const pending = useDynamicFontPending(managed, fontFamilyBasename);
118
+
86
119
  const nameStr = name as string;
87
120
  const codepoints = getCodepoints(nameStr, layers);
88
121
 
@@ -91,15 +124,10 @@ export function createIconSet<GM extends NanoGlyphMapInput>(
91
124
  if (color === undefined || color === null) {
92
125
  return getDefaultColors(nameStr, layers);
93
126
  }
94
- const colorArray = Array.isArray(color) ? color : [color];
95
- const lastPaletteColor = colorArray.length
96
- ? colorArray[colorArray.length - 1]
97
- : undefined;
98
- return layers.map(([, srcColor], i) => {
99
- const layerColor =
100
- colorArray[i] ?? lastPaletteColor ?? srcColor ?? 'black';
101
- return cachedProcessColor(layerColor as string);
102
- });
127
+ const resolveColor = createLayerColorResolver(color);
128
+ return layers.map(([, srcColor], i) =>
129
+ cachedProcessColor(resolveColor(i, srcColor) as string)
130
+ );
103
131
  }, [nameStr, color]);
104
132
 
105
133
  const nativeStyle = useMemo(
@@ -107,6 +135,23 @@ export function createIconSet<GM extends NanoGlyphMapInput>(
107
135
  [scaledSize, width, style]
108
136
  );
109
137
 
138
+ // Hide-until-ready: while the dynamic font is registering, render a
139
+ // placeholder. The native view mounts only once the font is registered.
140
+ if (pending) {
141
+ return (
142
+ <View
143
+ ref={ref}
144
+ style={nativeStyle}
145
+ accessible={accessible}
146
+ accessibilityRole={accessibilityRole}
147
+ accessibilityLabel={accessibilityLabel ?? (name as string)}
148
+ accessibilityElementsHidden={accessibilityElementsHidden}
149
+ importantForAccessibility={importantForAccessibility}
150
+ testID={testID}
151
+ />
152
+ );
153
+ }
154
+
110
155
  return (
111
156
  <NanoIconViewNative
112
157
  ref={ref}
@@ -122,6 +167,8 @@ export function createIconSet<GM extends NanoGlyphMapInput>(
122
167
  accessible={accessible}
123
168
  accessibilityRole={accessibilityRole}
124
169
  accessibilityLabel={accessibilityLabel ?? (name as string)}
170
+ accessibilityElementsHidden={accessibilityElementsHidden}
171
+ importantForAccessibility={importantForAccessibility}
125
172
  testID={testID}
126
173
  />
127
174
  );
@@ -136,5 +183,10 @@ export function createIconSet<GM extends NanoGlyphMapInput>(
136
183
 
137
184
  Icon.displayName = `NanoIcon(${fontFamilyBasename})`;
138
185
 
139
- return Icon;
186
+ const IconComp = Icon as unknown as IconComponent<GM>;
187
+ IconComp.loadFont = (override) =>
188
+ glyphMap.m.l === 'd'
189
+ ? loadDynamicFont(fontFamilyBasename, override ?? font, { force: true })
190
+ : Promise.resolve();
191
+ return IconComp;
140
192
  }
@@ -1,13 +1,52 @@
1
1
  import { memo, useMemo } from 'react';
2
2
  import { PixelRatio, Platform, Text, View, type TextProps } from 'react-native';
3
- import type { NanoGlyphMapInput, GlyphEntry } from './core/types';
3
+ import type { NanoGlyphMapInput } from './core/types';
4
4
  import type { IconComponent, IconProps } from './types';
5
5
  import { shallowEqualColor } from './utils/shallowEqualColor';
6
+ import {
7
+ DEFAULT_ICON_SIZE,
8
+ resolveGlyphEntry,
9
+ createCharCache,
10
+ createLayerColorResolver,
11
+ } from './utils/glyphRuntime';
12
+ import { loadDynamicFont, useDynamicFontPending } from './loadDynamicFont';
6
13
 
7
14
  export type { IconComponent, IconProps };
8
15
  export { shallowEqualColor };
9
16
 
10
- const DEFAULT_ICON_SIZE = 12;
17
+ /**
18
+ * Warn when the `font` argument and the glyphmap's linking mode are inconsistent.
19
+ *
20
+ * - Dynamic glyphmap without a `font` arg: caller probably forgot to pass it; icons
21
+ * will render as tofu until the host app loads a font under family `fontFamily`.
22
+ * - Static glyphmap with a `font` arg: argument is ignored, font is bundled natively.
23
+ *
24
+ * Fires once per `createIconSet` call (not per render).
25
+ */
26
+ export function warnIfLinkingMismatch(
27
+ fontFamily: string,
28
+ linking: string | undefined,
29
+ font: unknown
30
+ ): void {
31
+ if (!__DEV__) return;
32
+
33
+ const isDynamic = linking === 'd';
34
+ if (isDynamic && font == null) {
35
+ console.warn(
36
+ `[react-native-nano-icons] "${fontFamily}" is built with dynamic linking ` +
37
+ `but no font was passed to createIconSet. Icons will render as tofu ` +
38
+ `until a font is loaded and registered under family "${fontFamily}".`
39
+ );
40
+ return;
41
+ }
42
+ if (!isDynamic && font != null) {
43
+ console.warn(
44
+ `[react-native-nano-icons] "${fontFamily}" is built with static linking; ` +
45
+ `the font argument passed to createIconSet is ignored. ` +
46
+ `Set linking: 'dynamic' in your config to opt into OTA delivery.`
47
+ );
48
+ }
49
+ }
11
50
 
12
51
  /**
13
52
  * JS implementation using <View> + <Text> layers.
@@ -15,8 +54,29 @@ const DEFAULT_ICON_SIZE = 12;
15
54
  */
16
55
  export function createJSIconSet<GM extends NanoGlyphMapInput>(
17
56
  glyphMap: GM
57
+ ): IconComponent<GM>;
58
+ export function createJSIconSet<GM extends NanoGlyphMapInput>(
59
+ glyphMap: GM,
60
+ font: unknown
61
+ ): IconComponent<GM>;
62
+ export function createJSIconSet<GM extends NanoGlyphMapInput>(
63
+ glyphMap: GM,
64
+ font?: unknown
18
65
  ): IconComponent<GM> {
19
66
  const fontBasename = glyphMap.m.f;
67
+ warnIfLinkingMismatch(fontBasename, glyphMap.m.l, font);
68
+
69
+ // dynamically linked font - register and hide icons until ready
70
+ const managed = glyphMap.m.l === 'd' && font != null;
71
+ if (managed) {
72
+ void loadDynamicFont(fontBasename, font).catch((err) => {
73
+ if (__DEV__)
74
+ console.warn(
75
+ `[react-native-nano-icons] Failed to load dynamic font "${fontBasename}".`,
76
+ err
77
+ );
78
+ });
79
+ }
20
80
 
21
81
  const fontReference = Platform.select({
22
82
  windows: `/Assets/${fontBasename}`,
@@ -33,23 +93,7 @@ export function createJSIconSet<GM extends NanoGlyphMapInput>(
33
93
  };
34
94
 
35
95
  const unitsPerEm = glyphMap.m.u;
36
-
37
- const resolveEntry = (name: keyof GM['i']): GlyphEntry => {
38
- return (glyphMap.i[name as string] ?? [
39
- unitsPerEm,
40
- [[63, 'black']],
41
- ]) as GlyphEntry;
42
- };
43
-
44
- const codepointCache = new Map<number, string>();
45
- const getChar = (codepoint: number): string => {
46
- let ch = codepointCache.get(codepoint);
47
- if (ch === undefined) {
48
- ch = String.fromCodePoint(codepoint);
49
- codepointCache.set(codepoint, ch);
50
- }
51
- return ch;
52
- };
96
+ const getChar = createCharCache();
53
97
 
54
98
  const Icon = memo(
55
99
  ({
@@ -61,18 +105,19 @@ export function createJSIconSet<GM extends NanoGlyphMapInput>(
61
105
  accessible,
62
106
  accessibilityLabel,
63
107
  accessibilityRole = 'image',
108
+ accessibilityElementsHidden,
109
+ importantForAccessibility,
64
110
  testID,
65
111
  ref,
66
112
  }: IconProps<keyof GM['i']>) => {
67
113
  const fontScale = allowFontScaling ? PixelRatio.getFontScale() : 1;
68
- const [adv, layers] = resolveEntry(name);
114
+ const [adv, layers] = resolveGlyphEntry(glyphMap, name);
69
115
  const scaledSize = size * fontScale;
70
116
  const width = (adv / unitsPerEm) * scaledSize;
71
117
 
72
- const colorArray = Array.isArray(color) ? color : [color];
73
- const lastPaletteColor = colorArray?.length
74
- ? colorArray[colorArray.length - 1]
75
- : undefined;
118
+ const pending = useDynamicFontPending(managed, fontBasename);
119
+
120
+ const resolveColor = createLayerColorResolver(color);
76
121
 
77
122
  const containerStyle = useMemo(
78
123
  () => [{ height: scaledSize, width, bottom: 0 as const }, style],
@@ -88,22 +133,25 @@ export function createJSIconSet<GM extends NanoGlyphMapInput>(
88
133
  accessible={accessible}
89
134
  accessibilityRole={accessibilityRole}
90
135
  accessibilityLabel={accessibilityLabel ?? (name as string)}
136
+ accessibilityElementsHidden={accessibilityElementsHidden}
137
+ importantForAccessibility={importantForAccessibility}
91
138
  testID={testID}>
92
- {layers.map(([codepoint, srcColor], i) => {
93
- const layerColor =
94
- colorArray?.[i] ?? lastPaletteColor ?? srcColor ?? 'black';
95
-
96
- return (
97
- <Text
98
- key={i}
99
- selectable={false}
100
- accessible={false}
101
- allowFontScaling={allowFontScaling}
102
- style={[styleOverrides, sizeStyle, { color: layerColor }]}>
103
- {getChar(codepoint)}
104
- </Text>
105
- );
106
- })}
139
+ {pending
140
+ ? null
141
+ : layers.map(([codepoint, srcColor], i) => {
142
+ const layerColor = resolveColor(i, srcColor);
143
+
144
+ return (
145
+ <Text
146
+ key={i}
147
+ selectable={false}
148
+ accessible={false}
149
+ allowFontScaling={allowFontScaling}
150
+ style={[styleOverrides, sizeStyle, { color: layerColor }]}>
151
+ {getChar(codepoint)}
152
+ </Text>
153
+ );
154
+ })}
107
155
  </View>
108
156
  );
109
157
  },
@@ -117,5 +165,10 @@ export function createJSIconSet<GM extends NanoGlyphMapInput>(
117
165
 
118
166
  Icon.displayName = `NanoIcon(${fontBasename})`;
119
167
 
120
- return Icon;
168
+ const IconComp = Icon as unknown as IconComponent<GM>;
169
+ IconComp.loadFont = (override) =>
170
+ glyphMap.m.l === 'd'
171
+ ? loadDynamicFont(fontBasename, override ?? font, { force: true })
172
+ : Promise.resolve();
173
+ return IconComp;
121
174
  }
@@ -7,6 +7,14 @@ export { shallowEqualColor } from './utils/shallowEqualColor';
7
7
 
8
8
  export function createIconSet<GM extends NanoGlyphMapInput>(
9
9
  glyphMap: GM
10
+ ): IconComponent<GM>;
11
+ export function createIconSet<GM extends NanoGlyphMapInput>(
12
+ glyphMap: GM,
13
+ font: unknown
14
+ ): IconComponent<GM>;
15
+ export function createIconSet<GM extends NanoGlyphMapInput>(
16
+ glyphMap: GM,
17
+ font?: unknown
10
18
  ): IconComponent<GM> {
11
- return createJSIconSet(glyphMap);
19
+ return createJSIconSet(glyphMap, font);
12
20
  }
@@ -0,0 +1,109 @@
1
+ import { memo, useMemo, type CSSProperties } from 'react';
2
+ import type { NanoGlyphMapInput } from './core/types';
3
+ import type { IconComponent, IconProps } from './types';
4
+ import { shallowEqualColor } from './utils/shallowEqualColor';
5
+ import {
6
+ DEFAULT_ICON_SIZE,
7
+ resolveGlyphEntry,
8
+ createCharCache,
9
+ createLayerColorResolver,
10
+ } from './utils/glyphRuntime';
11
+
12
+ export type { IconComponent, IconProps };
13
+ export { shallowEqualColor };
14
+
15
+ // Web renderer: uses inline <span> elements so icons flow out of the box (display: inline-block keeps width/height)
16
+ export function createIconSet<GM extends NanoGlyphMapInput>(
17
+ glyphMap: GM
18
+ ): IconComponent<GM> {
19
+ const fontBasename = glyphMap.m.f;
20
+ const unitsPerEm = glyphMap.m.u;
21
+ const getChar = createCharCache();
22
+
23
+ const Icon = memo(
24
+ ({
25
+ name,
26
+ size = DEFAULT_ICON_SIZE,
27
+ color,
28
+ style,
29
+ accessible,
30
+ accessibilityLabel,
31
+ accessibilityRole = 'image',
32
+ accessibilityElementsHidden,
33
+ testID,
34
+ ref,
35
+ }: IconProps<keyof GM['i']>) => {
36
+ const [adv, layers] = resolveGlyphEntry(glyphMap, name);
37
+ const width = (adv / unitsPerEm) * size;
38
+
39
+ const resolveColor = createLayerColorResolver(color);
40
+
41
+ const containerStyle = useMemo<CSSProperties>(
42
+ () => ({
43
+ display: 'inline-block',
44
+ position: 'relative',
45
+ width,
46
+ height: size,
47
+ lineHeight: 0,
48
+ verticalAlign: 'middle',
49
+ ...(style as CSSProperties | undefined),
50
+ }),
51
+ [size, width, style]
52
+ );
53
+
54
+ const layerBaseStyle = useMemo<CSSProperties>(
55
+ () => ({
56
+ position: 'absolute',
57
+ left: 0,
58
+ bottom: 0,
59
+ fontFamily: fontBasename,
60
+ fontWeight: 'normal',
61
+ fontStyle: 'normal',
62
+ fontSize: size,
63
+ lineHeight: 1,
64
+ whiteSpace: 'pre',
65
+ }),
66
+ [size]
67
+ );
68
+
69
+ const isHidden = accessible === false || accessibilityElementsHidden;
70
+ const role = accessibilityRole === 'image' ? 'img' : accessibilityRole;
71
+
72
+ return (
73
+ <span
74
+ ref={ref as React.Ref<HTMLSpanElement>}
75
+ style={containerStyle}
76
+ role={isHidden ? undefined : role}
77
+ aria-label={
78
+ isHidden ? undefined : (accessibilityLabel ?? (name as string))
79
+ }
80
+ aria-hidden={isHidden || undefined}
81
+ data-testid={testID}>
82
+ {layers.map(([codepoint, srcColor], i) => {
83
+ const layerColor = resolveColor(i, srcColor);
84
+ return (
85
+ <span
86
+ key={i}
87
+ aria-hidden
88
+ style={{ ...layerBaseStyle, color: layerColor as string }}>
89
+ {getChar(codepoint)}
90
+ </span>
91
+ );
92
+ })}
93
+ </span>
94
+ );
95
+ },
96
+ (prev, next) =>
97
+ prev.name === next.name &&
98
+ prev.size === next.size &&
99
+ prev.style === next.style &&
100
+ shallowEqualColor(prev.color, next.color)
101
+ );
102
+
103
+ Icon.displayName = `NanoIcon(${fontBasename})`;
104
+
105
+ // No-op on web: fonts come from CSS @font-face, nothing to load at runtime.
106
+ const IconComp = Icon as unknown as IconComponent<GM>;
107
+ IconComp.loadFont = () => Promise.resolve();
108
+ return IconComp;
109
+ }