react-native-nano-icons 0.1.1 → 0.1.3

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 (99) hide show
  1. package/README.md +20 -164
  2. package/android/build.gradle +28 -0
  3. package/android/src/main/java/com/nanoicons/NanoIconView.kt +78 -0
  4. package/android/src/main/java/com/nanoicons/NanoIconViewManager.kt +84 -0
  5. package/android/src/main/java/com/nanoicons/NanoIconsPackage.kt +22 -0
  6. package/ios/NanoIconView.h +4 -0
  7. package/ios/NanoIconView.mm +246 -0
  8. package/lib/commonjs/cli/build.js +1 -1
  9. package/lib/commonjs/cli/config.d.ts +2 -2
  10. package/lib/commonjs/cli/config.js +7 -6
  11. package/lib/commonjs/plugin/src/buildFonts.d.ts +1 -0
  12. package/lib/commonjs/plugin/src/buildFonts.js +9 -0
  13. package/lib/commonjs/plugin/src/index.js +1 -34
  14. package/lib/commonjs/plugin/src/withNanoIconsFontLinking.d.ts +6 -6
  15. package/lib/commonjs/plugin/src/withNanoIconsFontLinking.js +11 -15
  16. package/lib/commonjs/scripts/cli.js +15 -5
  17. package/lib/commonjs/src/core/font/compile.d.ts +13 -2
  18. package/lib/commonjs/src/core/font/compile.js +49 -46
  19. package/lib/commonjs/src/core/pipeline/managers.js +19 -3
  20. package/lib/commonjs/src/core/pipeline/run.js +121 -32
  21. package/lib/commonjs/src/core/svg/layers.d.ts +16 -0
  22. package/lib/commonjs/src/core/svg/layers.js +27 -0
  23. package/lib/commonjs/src/core/svg/svg_dom.d.ts +29 -1
  24. package/lib/commonjs/src/core/svg/svg_dom.js +78 -2
  25. package/lib/commonjs/src/core/svg/svg_pathops.d.ts +11 -0
  26. package/lib/commonjs/src/core/svg/svg_pathops.js +209 -19
  27. package/lib/commonjs/src/core/types.d.ts +30 -15
  28. package/lib/module/core/font/compile.js +52 -41
  29. package/lib/module/core/font/compile.js.map +1 -1
  30. package/lib/module/core/pipeline/managers.js +17 -3
  31. package/lib/module/core/pipeline/managers.js.map +1 -1
  32. package/lib/module/core/pipeline/run.js +131 -44
  33. package/lib/module/core/pipeline/run.js.map +1 -1
  34. package/lib/module/core/shims/picosvg-0.22.3-py3-none-any.whl +0 -0
  35. package/lib/module/core/svg/layers.js +34 -0
  36. package/lib/module/core/svg/layers.js.map +1 -1
  37. package/lib/module/core/svg/svg_dom.js +91 -4
  38. package/lib/module/core/svg/svg_dom.js.map +1 -1
  39. package/lib/module/core/svg/svg_pathops.js +203 -19
  40. package/lib/module/core/svg/svg_pathops.js.map +1 -1
  41. package/lib/module/createNanoIconsSet.js +3 -79
  42. package/lib/module/createNanoIconsSet.js.map +1 -1
  43. package/lib/module/createNanoIconsSet.native.js +68 -0
  44. package/lib/module/createNanoIconsSet.native.js.map +1 -0
  45. package/lib/module/createNanoIconsSet.shared.js +91 -0
  46. package/lib/module/createNanoIconsSet.shared.js.map +1 -0
  47. package/lib/module/index.js +1 -2
  48. package/lib/module/index.js.map +1 -1
  49. package/lib/module/specs/NanoIconViewNativeComponent.ts +15 -0
  50. package/lib/module/types.js +4 -0
  51. package/lib/module/types.js.map +1 -0
  52. package/lib/module/utils/shallowEqualColor.js +15 -0
  53. package/lib/module/utils/shallowEqualColor.js.map +1 -0
  54. package/lib/typescript/src/core/font/compile.d.ts +13 -2
  55. package/lib/typescript/src/core/font/compile.d.ts.map +1 -1
  56. package/lib/typescript/src/core/pipeline/managers.d.ts.map +1 -1
  57. package/lib/typescript/src/core/pipeline/run.d.ts.map +1 -1
  58. package/lib/typescript/src/core/svg/layers.d.ts +16 -0
  59. package/lib/typescript/src/core/svg/layers.d.ts.map +1 -1
  60. package/lib/typescript/src/core/svg/svg_dom.d.ts +29 -1
  61. package/lib/typescript/src/core/svg/svg_dom.d.ts.map +1 -1
  62. package/lib/typescript/src/core/svg/svg_pathops.d.ts +11 -0
  63. package/lib/typescript/src/core/svg/svg_pathops.d.ts.map +1 -1
  64. package/lib/typescript/src/core/types.d.ts +30 -15
  65. package/lib/typescript/src/core/types.d.ts.map +1 -1
  66. package/lib/typescript/src/createNanoIconsSet.d.ts +5 -18
  67. package/lib/typescript/src/createNanoIconsSet.d.ts.map +1 -1
  68. package/lib/typescript/src/createNanoIconsSet.native.d.ts +7 -0
  69. package/lib/typescript/src/createNanoIconsSet.native.d.ts.map +1 -0
  70. package/lib/typescript/src/createNanoIconsSet.shared.d.ts +11 -0
  71. package/lib/typescript/src/createNanoIconsSet.shared.d.ts.map +1 -0
  72. package/lib/typescript/src/index.d.ts.map +1 -1
  73. package/lib/typescript/src/specs/NanoIconViewNativeComponent.d.ts +14 -0
  74. package/lib/typescript/src/specs/NanoIconViewNativeComponent.d.ts.map +1 -0
  75. package/lib/typescript/src/types.d.ts +19 -0
  76. package/lib/typescript/src/types.d.ts.map +1 -0
  77. package/lib/typescript/src/utils/shallowEqualColor.d.ts +4 -0
  78. package/lib/typescript/src/utils/shallowEqualColor.d.ts.map +1 -0
  79. package/package.json +22 -5
  80. package/plugin/src/buildFonts.ts +13 -0
  81. package/plugin/src/index.ts +3 -50
  82. package/plugin/src/withNanoIconsFontLinking.ts +22 -24
  83. package/react-native-nano-icons.podspec +18 -0
  84. package/scripts/cli.ts +14 -5
  85. package/src/core/font/compile.ts +65 -61
  86. package/src/core/pipeline/managers.ts +26 -3
  87. package/src/core/pipeline/run.ts +156 -38
  88. package/src/core/shims/picosvg-0.22.3-py3-none-any.whl +0 -0
  89. package/src/core/svg/layers.ts +44 -0
  90. package/src/core/svg/svg_dom.ts +96 -4
  91. package/src/core/svg/svg_pathops.ts +245 -27
  92. package/src/core/types.ts +21 -10
  93. package/src/createNanoIconsSet.native.tsx +108 -0
  94. package/src/createNanoIconsSet.shared.tsx +121 -0
  95. package/src/createNanoIconsSet.tsx +7 -126
  96. package/src/index.ts +1 -2
  97. package/src/specs/NanoIconViewNativeComponent.ts +15 -0
  98. package/src/types.ts +27 -0
  99. package/src/utils/shallowEqualColor.ts +17 -0
package/README.md CHANGED
@@ -1,174 +1,30 @@
1
- # react-native-nano-icons
1
+ <div align="center">
2
2
 
3
- **High-performance, build-time icon font generation for React Native & Expo.**
3
+ ![Nano Icons (light mode)](./docs/img/logo-nanoicons-default.svg#gh-light-mode-only)
4
+ ![Nano Icons (dark mode)](./docs/img/logo-nanoicons-inverted.svg#gh-dark-mode-only)
4
5
 
5
- `react-native-nano-icons` automates the conversion of SVG directories into optimized, **multi-color-aware** native fonts and strictly typed TypeScript component factories. It leverages a WebAssembly-powered skia pathops binary build pipeline to ensure pixel-perfect geometry and zero runtime overhead.
6
6
 
7
- ---
7
+ <br>
8
+ </div>
8
9
 
9
- ## 🧩 Platforms Supported
10
+ # High-performance, build-time icon font generation for React Native & Expo.
10
11
 
11
- - [x] iOS
12
- - [x] Android
13
- - [x] Web
12
+ Start here ➡️➡️➡️ [react-native-nano-icons README.md](/README.md) ⬅️⬅️⬅️
14
13
 
15
- > [!NOTE]
16
- > `<filter>` and `<mask>` are not yet supported, due to native fonts' glyph limitations.
17
- > In order to leverage those features, use [`react-native-svg`](https://github.com/software-mansion/react-native-svg) or [`expo-image`](https://docs.expo.dev/versions/latest/sdk/image/)
14
+ ## Nano Icons are created by Software Mansion
18
15
 
19
- ## 🚀 Usage
16
+ [![swm](https://logo.swmansion.com/logo?color=white&variant=desktop&width=150&tag=react-native-nano-icons-github 'Software Mansion')](https://swmansion.com)
20
17
 
21
- ### 1. Installation
18
+ Since 2012 [Software Mansion](https://swmansion.com) is a software agency with
19
+ experience in building web and mobile apps. We are Core React Native
20
+ Contributors and experts in dealing with all kinds of React Native issues. We
21
+ can help you build your next dream product –
22
+ [Hire us](https://swmansion.com/contact/projects?utm_source=typegpu&utm_medium=readme).
22
23
 
23
- ```bash
24
- npm install react-native-nano-icons
25
- ```
24
+ <!-- automd:contributors author="software-mansion" -->
26
25
 
27
- ### 2. Configuration
28
-
29
- #### 2.1. Expo
30
-
31
- The library uses an Expo Config Plugin to hook into the prebuild phase. This automatically generates the `.ttf` and corresponding `glyphmap` files and links them to the native iOS/Android project's assets.
32
-
33
- `app.json`
34
-
35
- ```JSON
36
- {
37
- "expo": {
38
- "plugins": [
39
- [
40
- "react-native-nano-icons",
41
- {
42
- "iconSets": [
43
- {
44
- "inputDir": "./assets/icons/user"
45
- }
46
- ]
47
- }
48
- ]
49
- ]
50
- }
51
- }
52
- ```
53
-
54
- <details>
55
- <summary>All iconSets Entry Plugin Options</summary>
56
-
57
- The plugin accepts an object with an `iconSets` array, allowing you to generate multiple distinct fonts in a single build.
58
-
59
- | Property | Type | Required | Default | Description |
60
- | :------------- | :------- | :------- | :------------- | :------------------------------------------------------------------------------------------------------------------------- |
61
- | `inputDir` | `string` | **Yes** | — | Path to the directory containing your `.svg` files (e.g., `./assets/icons/ui`). |
62
- | `fontFamily` | `string` | No | Folder Name | The name of the generated font family and file. If omitted, the name of the `inputDir` folder is used (e.g., `ui`). |
63
- | `outputDir` | `string` | No | `../nanoicons` | Path where the `.ttf` and `.json` artifacts will be saved. Defaults to a sibling `nanoicons` folder relative to the input. |
64
- | `upm` | `number` | No | `1024` | Units Per Em. Defines the resolution of the font grid. |
65
- | `startUnicode` | `string` | No | `0xe900` | The starting Hex Unicode point for the first icon glyph. |
66
-
67
- <details>
68
- <summary>Default Dir Path Behavior</summary>
69
- If you do not specify an `outputDir` or `fontFamily`, the library attempts to keep your project organized by creating a sibling folder.
70
-
71
- - **Input:** `./assets/icons/user`
72
- - **Resulting Output:** `./assets/icons/nanoicons/user.ttf` & `user.glyphmap.json`
73
- </details>
74
- </details>
75
-
76
- #### 2.2 Bare React Native/React Native Web (no Expo)
77
-
78
- Bare apps don’t have a prebuild step, so you run the same pipeline via the CLI and ship the built fonts into the native project yourself:
79
-
80
- 1. **Config** – At the app root, add a `.nanoicons.json` with the same `iconSets` shape as the Expo plugin (see "All iconSets Entry Plugin Options above").
81
- <details>
82
- <summary>.nanoicons.json example</summary>
83
-
84
- ```JSON
85
- {
86
- "iconSets": [
87
- {
88
- "inputDir": "./assets/icons/user"
89
- }
90
- ]
91
- }
92
- ```
93
-
94
- </details>
95
-
96
- 2. **Build and link** – From the app root run:
97
-
98
- ```sh
99
- npx react-native-nano-icons
100
- ```
101
-
102
- This works exactly like the config plugin, removing any necessity for manual Xcode/Android Studio steps.
103
-
104
- ### 3. Creating an Icon Set Component
105
-
106
- Use the factory function to create a fully typed component for your specific icon set. This enables multiple distinct sets (e.g., "Outlined", "Solid", "Brand") within a single app.
107
-
108
- `src/components/UserIcon.tsx`
109
-
110
- ```TypeScript
111
- import { createNanoIconSet } from "react-native-nano-icons";
112
- // auto-generated during build-time in outputDir
113
- import glyphMap from "../../assets/nanoicons/UserIcons.glyphmap.json";
114
-
115
- export const UserIcon = createNanoIconSet(glyphMap);
116
- ```
117
-
118
- ### 4. Component Usage
119
-
120
- The generated component supports standard `Text` props **excluding** `style.color | .fontWeight | .fontFamily`.
121
-
122
- To manipulate the color(s) of the icon you should provide `colorPalette: ColorValue[]`.
123
-
124
- The `name` prop corresponds to **the original name of the svg file** for a given icon.
125
-
126
- ```TypeScript
127
- import { Text } from 'react-native'
128
- import { UserIcon } from './components/UserIcon';
129
-
130
- export default function App() {
131
- return (
132
- <>
133
- // Renders the icon with its original multi-color layers from the SVG
134
- <UserIcon name="avatar-1" size={32} />
135
-
136
- // Overrides all color layers with the provided colors respectively
137
- <UserIcon name="avatar-1" size={24} colorPalette={["blue", "#ffffff", "#fc2930"]} />
138
-
139
- // Renders icon inline within a paragraph
140
- <Text>
141
- User <UserIcon name="avatar-1" size={12} /> liked your photo!
142
- </Text>
143
- </>
144
- );
145
- }
146
- ```
147
-
148
- Your color icons can have as many colors as your original svg has, therefore you should experiment to establish which element of the array corresponds to the layer you aim to change the color of.
149
- If the icon is single-color by design (which results in creating a single glyph during build-time) only the first element is took into consideration, and if the `colorPalette` array is too short - the last color is repeated.
150
-
151
- ### 5. Font Regeneration
152
-
153
- The script detects changes in path and contents of the SVGs in your input directory based on a fingerprint hash. If anything changed, or the output font files are deleted, a given icon-set is regenerated.
154
-
155
- > [!IMPORTANT]
156
- > **You should always verify your icons visually.**
157
-
158
- ---
159
-
160
- ## 💡 Architecture & Pipeline & Examples
161
-
162
- see [MOTIVATION.md](docs/MOTIVATION.md)
163
-
164
- ---
165
-
166
- ## License
167
-
168
- `react-native-nano-icons` is released under the **MIT License**. See [LICENSE](LICENSE) for the full text.
169
-
170
- ---
171
-
172
- Built by [Software Mansion](https://swmansion.com/).
173
-
174
- [<img width="128" height="69" alt="Software Mansion Logo" src="https://github.com/user-attachments/assets/f0e18471-a7aa-4e80-86ac-87686a86fe56" />](https://swmansion.com/)
26
+ Made by [@software-mansion](https://github.com/software-mansion) 💛
27
+ <br><br>
28
+ <a href="https://github.com/software-mansion-labs/react-native-nano-icons/graphs/contributors">
29
+ <img src="https://contrib.rocks/image?repo=software-mansion-labs/react-native-nano-icons" />
30
+ </a>
@@ -0,0 +1,28 @@
1
+ buildscript {
2
+ ext.safeExtGet = { prop, fallback ->
3
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
4
+ }
5
+ }
6
+
7
+ apply plugin: 'com.android.library'
8
+ apply plugin: 'org.jetbrains.kotlin.android'
9
+ apply plugin: 'com.facebook.react'
10
+
11
+ android {
12
+ namespace "com.nanoicons"
13
+ compileSdk safeExtGet('compileSdkVersion', 35)
14
+
15
+ defaultConfig {
16
+ minSdk safeExtGet('minSdkVersion', 24)
17
+ }
18
+
19
+ sourceSets {
20
+ main {
21
+ java.srcDirs += ["src/main/java"]
22
+ }
23
+ }
24
+ }
25
+
26
+ dependencies {
27
+ implementation 'com.facebook.react:react-android'
28
+ }
@@ -0,0 +1,78 @@
1
+ package com.nanoicons
2
+
3
+ import android.content.Context
4
+ import android.graphics.Canvas
5
+ import android.graphics.Paint
6
+ import android.graphics.Typeface
7
+ import android.view.View
8
+ import com.facebook.react.common.assets.ReactFontManager
9
+
10
+ class NanoIconView(context: Context) : View(context) {
11
+
12
+ private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
13
+ private var codepoints: IntArray = intArrayOf()
14
+ private var colors: IntArray = intArrayOf()
15
+ private var cachedFontFamily: String? = null
16
+ private var cachedTypeface: Typeface? = null
17
+ // Cached String objects — rebuilt only when codepoints change
18
+ private var cachedTexts: Array<String> = emptyArray()
19
+ // Cached baseline — rebuilt only when font or size changes
20
+ private var cachedBaseline: Float = 0f
21
+
22
+ init {
23
+ // Transparent background, no default drawing
24
+ setBackgroundColor(0x00000000)
25
+ }
26
+
27
+ fun setFontFamily(fontFamily: String) {
28
+ if (fontFamily != cachedFontFamily) {
29
+ cachedFontFamily = fontFamily
30
+ cachedTypeface = ReactFontManager.getInstance()
31
+ .getTypeface(fontFamily, Typeface.NORMAL, context.assets)
32
+ paint.typeface = cachedTypeface
33
+ updateBaseline()
34
+ invalidate()
35
+ }
36
+ }
37
+
38
+ fun setFontSize(size: Float) {
39
+ val sizeInPx = size * resources.displayMetrics.density
40
+ if (paint.textSize != sizeInPx) {
41
+ paint.textSize = sizeInPx
42
+ updateBaseline()
43
+ invalidate()
44
+ }
45
+ }
46
+
47
+ fun setCodepoints(values: IntArray) {
48
+ codepoints = values
49
+ cachedTexts = Array(values.size) { i -> String(Character.toChars(values[i])) }
50
+ invalidate()
51
+ }
52
+
53
+ fun setColors(values: IntArray) {
54
+ colors = values
55
+ invalidate()
56
+ }
57
+
58
+ private fun updateBaseline() {
59
+ cachedBaseline = -paint.fontMetrics.ascent
60
+ }
61
+
62
+ override fun onDraw(canvas: Canvas) {
63
+ super.onDraw(canvas)
64
+ if (cachedTexts.isEmpty() || cachedTypeface == null) return
65
+
66
+ canvas.save()
67
+ canvas.clipRect(0f, 0f, width.toFloat(), height.toFloat())
68
+
69
+ // All layers drawn at the same position (stacked on each other)
70
+ for (i in cachedTexts.indices) {
71
+ val color = if (i < colors.size) colors[i] else 0xFF000000.toInt()
72
+ paint.color = color
73
+ canvas.drawText(cachedTexts[i], 0f, cachedBaseline, paint)
74
+ }
75
+
76
+ canvas.restore()
77
+ }
78
+ }
@@ -0,0 +1,84 @@
1
+ package com.nanoicons
2
+
3
+ import com.facebook.react.bridge.ReadableArray
4
+ import com.facebook.react.module.annotations.ReactModule
5
+ import com.facebook.react.uimanager.SimpleViewManager
6
+ import com.facebook.react.uimanager.ThemedReactContext
7
+ import com.facebook.react.uimanager.ViewManagerDelegate
8
+ import com.facebook.react.uimanager.annotations.ReactProp
9
+ import com.facebook.react.viewmanagers.NanoIconViewManagerDelegate
10
+ import com.facebook.react.viewmanagers.NanoIconViewManagerInterface
11
+
12
+ @ReactModule(name = NanoIconViewManager.REACT_CLASS)
13
+ class NanoIconViewManager :
14
+ SimpleViewManager<NanoIconView>(),
15
+ NanoIconViewManagerInterface<NanoIconView> {
16
+
17
+ private val delegate: ViewManagerDelegate<NanoIconView> =
18
+ NanoIconViewManagerDelegate(this)
19
+
20
+ companion object {
21
+ const val REACT_CLASS = "NanoIconView"
22
+ }
23
+
24
+ override fun getName(): String = REACT_CLASS
25
+
26
+ override fun createViewInstance(reactContext: ThemedReactContext): NanoIconView =
27
+ NanoIconView(reactContext)
28
+
29
+ override fun getDelegate(): ViewManagerDelegate<NanoIconView> = delegate
30
+
31
+ @ReactProp(name = "fontFamily")
32
+ override fun setFontFamily(view: NanoIconView, value: String?) {
33
+ if (value != null) {
34
+ view.setFontFamily(value)
35
+ }
36
+ }
37
+
38
+ @ReactProp(name = "codepoints")
39
+ override fun setCodepoints(view: NanoIconView, value: ReadableArray?) {
40
+ if (value != null) {
41
+ val arr = IntArray(value.size())
42
+ for (i in 0 until value.size()) {
43
+ arr[i] = value.getInt(i)
44
+ }
45
+ view.setCodepoints(arr)
46
+ }
47
+ }
48
+
49
+ @ReactProp(name = "colors")
50
+ override fun setColors(view: NanoIconView, value: ReadableArray?) {
51
+ if (value != null) {
52
+ val arr = IntArray(value.size())
53
+ for (i in 0 until value.size()) {
54
+ arr[i] = value.getInt(i)
55
+ }
56
+ view.setColors(arr)
57
+ }
58
+ }
59
+
60
+ @ReactProp(name = "fontSize", defaultFloat = 12f)
61
+ override fun setFontSize(view: NanoIconView, value: Float) {
62
+ view.setFontSize(value)
63
+ }
64
+
65
+ @ReactProp(name = "advanceWidth", defaultInt = 0)
66
+ override fun setAdvanceWidth(view: NanoIconView, value: Int) {
67
+ // Used for sizing on JS side; native view uses Canvas layout
68
+ }
69
+
70
+ @ReactProp(name = "unitsPerEm", defaultInt = 0)
71
+ override fun setUnitsPerEm(view: NanoIconView, value: Int) {
72
+ // Used for sizing on JS side; native view uses Canvas layout
73
+ }
74
+
75
+ @ReactProp(name = "iconWidth", defaultFloat = 0f)
76
+ override fun setIconWidth(view: NanoIconView, value: Float) {
77
+ // Width set via style from JS
78
+ }
79
+
80
+ @ReactProp(name = "iconHeight", defaultFloat = 0f)
81
+ override fun setIconHeight(view: NanoIconView, value: Float) {
82
+ // Height set via style from JS
83
+ }
84
+ }
@@ -0,0 +1,22 @@
1
+ package com.nanoicons
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfoProvider
7
+ import com.facebook.react.uimanager.ViewManager
8
+
9
+ class NanoIconsPackage : BaseReactPackage() {
10
+
11
+ override fun createViewManagers(
12
+ reactContext: ReactApplicationContext
13
+ ): List<ViewManager<*, *>> = listOf(NanoIconViewManager())
14
+
15
+ override fun getModule(
16
+ name: String,
17
+ reactContext: ReactApplicationContext
18
+ ): NativeModule? = null
19
+
20
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider =
21
+ ReactModuleInfoProvider { emptyMap() }
22
+ }
@@ -0,0 +1,4 @@
1
+ #import <React/RCTViewComponentView.h>
2
+
3
+ @interface NanoIconView : RCTViewComponentView
4
+ @end
@@ -0,0 +1,246 @@
1
+ #import "NanoIconView.h"
2
+
3
+ #import <CoreText/CoreText.h>
4
+ #import <React/RCTConversions.h>
5
+ #import <React/RCTFabricComponentsPlugins.h>
6
+ #import <react/renderer/components/RNNanoIconsSpec/ComponentDescriptors.h>
7
+ #import <react/renderer/components/RNNanoIconsSpec/Props.h>
8
+
9
+ using namespace facebook::react;
10
+
11
+ @implementation NanoIconView {
12
+ CTFontRef _font;
13
+ NSString *_fontFamily;
14
+ CGFloat _fontSize;
15
+ std::vector<CGGlyph> _glyphs;
16
+ std::vector<uint32_t> _colors;
17
+ // Cached CGColor refs — rebuilt only when colors prop changes
18
+ std::vector<CGColorRef> _cachedCGColors;
19
+ // Cached layout metrics — rebuilt only when font or bounds change
20
+ CGFloat _fitScale;
21
+ CGPoint _baselinePosition;
22
+ BOOL _metricsValid;
23
+ }
24
+
25
+ - (instancetype)initWithFrame:(CGRect)frame
26
+ {
27
+ if (self = [super initWithFrame:frame]) {
28
+ static const auto defaultProps = std::make_shared<const NanoIconViewProps>();
29
+ _props = defaultProps;
30
+ self.opaque = NO;
31
+ self.backgroundColor = [UIColor clearColor];
32
+ _fitScale = 1.0;
33
+ _baselinePosition = CGPointZero;
34
+ _metricsValid = NO;
35
+ }
36
+ return self;
37
+ }
38
+
39
+ + (ComponentDescriptorProvider)componentDescriptorProvider
40
+ {
41
+ return concreteComponentDescriptorProvider<NanoIconViewComponentDescriptor>();
42
+ }
43
+
44
+ - (void)_releaseCachedColors
45
+ {
46
+ for (CGColorRef c : _cachedCGColors) {
47
+ CGColorRelease(c);
48
+ }
49
+ _cachedCGColors.clear();
50
+ }
51
+
52
+ - (void)_rebuildCachedColors
53
+ {
54
+ [self _releaseCachedColors];
55
+ _cachedCGColors.resize(_colors.size());
56
+ for (size_t i = 0; i < _colors.size(); i++) {
57
+ uint32_t colorInt = _colors[i];
58
+ CGFloat a = ((colorInt >> 24) & 0xFF) / 255.0;
59
+ CGFloat r = ((colorInt >> 16) & 0xFF) / 255.0;
60
+ CGFloat g = ((colorInt >> 8) & 0xFF) / 255.0;
61
+ CGFloat b = (colorInt & 0xFF) / 255.0;
62
+ _cachedCGColors[i] = CGColorCreateSRGB(r, g, b, a);
63
+ }
64
+ }
65
+
66
+ - (void)_updateMetrics
67
+ {
68
+ if (!_font) {
69
+ _metricsValid = NO;
70
+ return;
71
+ }
72
+
73
+ CGFloat ascent = CTFontGetAscent(_font);
74
+ CGFloat descent = CTFontGetDescent(_font);
75
+ CGFloat fontTotalHeight = ascent + descent;
76
+ CGFloat viewHeight = self.bounds.size.height;
77
+
78
+ _fitScale = (fontTotalHeight > 0) ? (viewHeight / fontTotalHeight) : 1.0;
79
+ _baselinePosition = CGPointMake(0, descent);
80
+ _metricsValid = YES;
81
+ }
82
+
83
+ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps
84
+ {
85
+ const auto &oldViewProps = static_cast<const NanoIconViewProps &>(*_props);
86
+ const auto &newViewProps = static_cast<const NanoIconViewProps &>(*props);
87
+
88
+ // Recreate font if fontFamily or fontSize changed
89
+ BOOL fontChanged = NO;
90
+ if (oldViewProps.fontFamily != newViewProps.fontFamily || oldViewProps.fontSize != newViewProps.fontSize) {
91
+ if (_font) {
92
+ CFRelease(_font);
93
+ _font = NULL;
94
+ }
95
+ NSString *family = [NSString stringWithUTF8String:newViewProps.fontFamily.c_str()];
96
+ _fontFamily = family;
97
+ _fontSize = newViewProps.fontSize;
98
+ _font = CTFontCreateWithName((__bridge CFStringRef)family, _fontSize, NULL);
99
+ fontChanged = YES;
100
+ _metricsValid = NO;
101
+ }
102
+
103
+ // Update glyphs if codepoints changed or font changed
104
+ BOOL codepointsChanged = fontChanged || (oldViewProps.codepoints != newViewProps.codepoints);
105
+ if (codepointsChanged && _font) {
106
+ const auto &codepoints = newViewProps.codepoints;
107
+ _glyphs.resize(codepoints.size());
108
+
109
+ for (size_t i = 0; i < codepoints.size(); i++) {
110
+ int32_t cp = codepoints[i];
111
+ // Handle BMP and supplementary plane characters
112
+ if (cp <= 0xFFFF) {
113
+ UniChar ch = (UniChar)cp;
114
+ CTFontGetGlyphsForCharacters(_font, &ch, &_glyphs[i], 1);
115
+ } else {
116
+ // Supplementary plane (private use area): use surrogate pair
117
+ UniChar surrogates[2];
118
+ surrogates[0] = (UniChar)(0xD800 + ((cp - 0x10000) >> 10));
119
+ surrogates[1] = (UniChar)(0xDC00 + ((cp - 0x10000) & 0x3FF));
120
+ CGGlyph glyphPair[2] = {0, 0};
121
+ CTFontGetGlyphsForCharacters(_font, surrogates, glyphPair, 2);
122
+ _glyphs[i] = glyphPair[0];
123
+ }
124
+ }
125
+ }
126
+
127
+ // Update colors
128
+ if (oldViewProps.colors != newViewProps.colors) {
129
+ const auto &colors = newViewProps.colors;
130
+ _colors.resize(colors.size());
131
+ for (size_t i = 0; i < colors.size(); i++) {
132
+ _colors[i] = (uint32_t)colors[i];
133
+ }
134
+ [self _rebuildCachedColors];
135
+ }
136
+
137
+ [super updateProps:props oldProps:oldProps];
138
+ [self setNeedsDisplay];
139
+ }
140
+
141
+ - (void)layoutSubviews
142
+ {
143
+ [super layoutSubviews];
144
+ _metricsValid = NO;
145
+ }
146
+
147
+ - (void)drawRect:(CGRect)rect
148
+ {
149
+ if (!_font || _glyphs.empty()) {
150
+ return;
151
+ }
152
+
153
+ CGContextRef context = UIGraphicsGetCurrentContext();
154
+ if (!context) {
155
+ return;
156
+ }
157
+
158
+ if (!_metricsValid) {
159
+ [self _updateMetrics];
160
+ }
161
+
162
+ CGContextSaveGState(context);
163
+ // CoreText draws with y-axis pointing up; UIKit has y-axis pointing down.
164
+ CGContextTranslateCTM(context, 0, self.bounds.size.height);
165
+ CGContextScaleCTM(context, 1.0, -1.0);
166
+ CGContextScaleCTM(context, _fitScale, _fitScale);
167
+
168
+ // Batch consecutive same-color glyphs into a single CTFontDrawGlyphs call
169
+ size_t i = 0;
170
+ while (i < _glyphs.size()) {
171
+ if (_glyphs[i] == 0) {
172
+ i++;
173
+ continue;
174
+ }
175
+
176
+ // Determine the color for this run
177
+ CGColorRef color = (i < _cachedCGColors.size()) ? _cachedCGColors[i] : NULL;
178
+ if (!color) {
179
+ // Fallback: opaque black
180
+ static CGColorRef sBlack = CGColorCreateSRGB(0, 0, 0, 1);
181
+ color = sBlack;
182
+ }
183
+ CGContextSetFillColorWithColor(context, color);
184
+
185
+ // Collect consecutive glyphs with the same color
186
+ size_t batchStart = i;
187
+ size_t batchCount = 0;
188
+ // Use a small stack buffer for positions; heap-allocate only for very large batches
189
+ CGPoint positionsBuf[16];
190
+ CGGlyph glyphsBuf[16];
191
+ CGPoint *positions = positionsBuf;
192
+ CGGlyph *batchGlyphs = glyphsBuf;
193
+
194
+ while (i < _glyphs.size()) {
195
+ if (_glyphs[i] == 0) { i++; continue; }
196
+
197
+ CGColorRef nextColor = (i < _cachedCGColors.size()) ? _cachedCGColors[i] : NULL;
198
+ // Break batch if color changes
199
+ if (i > batchStart && nextColor != color) break;
200
+
201
+ if (batchCount < 16) {
202
+ positions[batchCount] = _baselinePosition;
203
+ batchGlyphs[batchCount] = _glyphs[i];
204
+ }
205
+ batchCount++;
206
+ i++;
207
+ }
208
+
209
+ // If batch exceeded stack buffer, allocate and refill
210
+ if (batchCount > 16) {
211
+ positions = (CGPoint *)malloc(batchCount * sizeof(CGPoint));
212
+ batchGlyphs = (CGGlyph *)malloc(batchCount * sizeof(CGGlyph));
213
+ size_t idx = 0;
214
+ for (size_t j = batchStart; j < i; j++) {
215
+ if (_glyphs[j] == 0) continue;
216
+ positions[idx] = _baselinePosition;
217
+ batchGlyphs[idx] = _glyphs[j];
218
+ idx++;
219
+ }
220
+ }
221
+
222
+ CTFontDrawGlyphs(_font, batchGlyphs, positions, batchCount, context);
223
+
224
+ if (batchCount > 16) {
225
+ free(positions);
226
+ free(batchGlyphs);
227
+ }
228
+ }
229
+
230
+ CGContextRestoreGState(context);
231
+ }
232
+
233
+ - (void)dealloc
234
+ {
235
+ if (_font) {
236
+ CFRelease(_font);
237
+ }
238
+ [self _releaseCachedColors];
239
+ }
240
+
241
+ @end
242
+
243
+ Class<RCTComponentViewProtocol> NanoIconViewCls(void)
244
+ {
245
+ return NanoIconView.class;
246
+ }
@@ -20,7 +20,7 @@ function shouldSkipGeneration(inputHash, outputDir, fontFamily, logger) {
20
20
  return false;
21
21
  }
22
22
  const glyphmap = JSON.parse(fs_1.default.readFileSync(glyphmapPath, 'utf8'));
23
- const storedHash = glyphmap?.meta?.hash;
23
+ const storedHash = glyphmap?.m?.h;
24
24
  if (storedHash && storedHash === inputHash) {
25
25
  logger?.info(`${fontFamily}: SVG fingerprint unchanged, skipping build.`);
26
26
  return true;
@@ -3,7 +3,7 @@ export type NanoIconsConfig = {
3
3
  iconSets: IconSetConfig[];
4
4
  };
5
5
  /**
6
- * Load .nanoicons.json from the project root.
6
+ * Load .nanoicons.json from the given directory.
7
7
  * Throws with a helpful message if the file is missing or malformed.
8
8
  */
9
- export declare function loadNanoIconsConfig(projectRoot: string): NanoIconsConfig;
9
+ export declare function loadNanoIconsConfig(configRoot: string): NanoIconsConfig;