@rdicons/react-native 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/runtime.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ComponentType } from 'react'
1
+ import type { ForwardRefExoticComponent, RefAttributes } from 'react'
2
2
  import type { SvgProps } from 'react-native-svg'
3
3
 
4
4
  export interface RdIconProps extends Omit<SvgProps, 'width' | 'height' | 'color' | 'viewBox'> {
@@ -10,10 +10,29 @@ export interface RdIconProps extends Omit<SvgProps, 'width' | 'height' | 'color'
10
10
  strokeWidth?: number | string
11
11
  /** Accessible name. Without it the icon is hidden from screen readers (decorative) */
12
12
  title?: string
13
+ /**
14
+ * Plays the icon's animation (only the rd icons have one; the rest ignore this). Off by default.
15
+ * `true` or `"play"`: once, when the icon appears and again each time `animate` turns on. `"loop"`: continuously, for loading states.
16
+ * Call `ref.current.play()` to replay it, for example from onPress. Nothing moves when the device asks for reduced motion.
17
+ */
18
+ animate?: boolean | 'play' | 'loop'
19
+ /** Animation speed: 2 is twice as fast, 0.5 twice as slow. Default 1 */
20
+ speed?: number
21
+ /** Fills the closed shapes of an outline icon with a soft second tone. `true`, a strength (0.05 to 0.6), another color, or `{ color, opacity }`. */
22
+ duotone?: boolean | number | string | { color?: string | null; opacity?: number }
23
+ /** The line runs from one color to another: `['#ec4899', '#4f46e5']`, with an angle as a third item, or `{ from, to, angle }`. Replaces `color`. */
24
+ gradient?: [string, string] | [string, string, number] | { from: string; to: string; angle?: number }
13
25
  }
14
26
 
15
- export type RdIcon = ComponentType<RdIconProps>
27
+ /** What `ref` gives you on an icon that can animate: play it again on demand. */
28
+ export interface RdIconHandle {
29
+ /** Plays the animation once more (only icons that have one) */
30
+ play(): void
31
+ stop(): void
32
+ }
33
+
34
+ export type RdIcon = ForwardRefExoticComponent<RdIconProps & RefAttributes<RdIconHandle>>
16
35
 
17
- export declare function createIcon(id: string, body: string, width?: number, height?: number): RdIcon
36
+ export declare function createIcon(id: string, body: string, width?: number, height?: number, anim?: { b: string; m: unknown }): RdIcon
18
37
  /** The inner markup of an icon as a tree (what createIcon draws). */
19
38
  export declare function parseBody(body: string): { tag: string; props: Record<string, string>; children: unknown[] }[]
package/dist/runtime.js CHANGED
@@ -1,5 +1,13 @@
1
- import { createElement } from 'react'
1
+ import * as React from 'react'
2
+ import { AccessibilityInfo, Animated } from 'react-native'
2
3
  import Svg, { ClipPath, Circle, Defs, Ellipse, G, Line, LinearGradient, Mask, Path, Polygon, Polyline, RadialGradient, Rect, Stop, Use } from 'react-native-svg'
4
+ import { applyEffects, parseEffectProps } from './effects.js'
5
+ import { delayOf, trackOf } from './motion.js'
6
+
7
+ const { createElement } = React
8
+ let legacyIds = 0
9
+ // useId needs React 18; on older React a counter kept in a ref gives every icon its own gradient id
10
+ const useUid = React.useId || function useLegacyUid() { const ref = React.useRef(null); if (ref.current === null) ref.current = ':l' + ++legacyIds; return ref.current }
3
11
 
4
12
  // SVG tags that appear in the icons, and the react-native-svg component that draws each
5
13
  const TAGS = {
@@ -22,7 +30,7 @@ const TAGS = {
22
30
 
23
31
  const camel = (name) => (name === 'xlink:href' ? 'href' : name.replace(/-([a-z])/g, (_m, c) => c.toUpperCase()))
24
32
 
25
- /** Reads the inner markup of an icon into a small tree: [{ tag, props, children }]. Done once per icon. */
33
+ /** Reads the inner markup of an icon into a small tree: [{ tag, props, cls, vars, children }]. Done once per icon. */
26
34
  export function parseBody(body) {
27
35
  const root = { children: [] }
28
36
  const stack = [root]
@@ -34,11 +42,14 @@ export function parseBody(body) {
34
42
  continue
35
43
  }
36
44
  const props = {}
37
- for (const a of rawAttrs.matchAll(/([a-zA-Z:-]+)\s*=\s*"([^"]*)"/g)) {
38
- if (a[1] === 'class' || a[1] === 'style') continue // CSS does not exist in React Native
39
- props[camel(a[1])] = a[2]
45
+ const node = { tag, props, cls: undefined, vars: {}, children: [] }
46
+ for (const a of rawAttrs.matchAll(/([a-zA-Z][a-zA-Z0-9:-]*)\s*=\s*"([^"]*)"/g)) {
47
+ if (a[1] === 'class') node.cls = a[2]
48
+ else if (a[1] === 'style') {
49
+ // CSS does not exist in React Native; only the custom properties the animations read (--k: stagger index, --s: direction) are kept
50
+ for (const v of a[2].matchAll(/--([ks]):\s*(-?[\d.]+)/g)) node.vars[v[1]] = parseFloat(v[2])
51
+ } else props[camel(a[1])] = a[2]
40
52
  }
41
- const node = { tag, props, children: [] }
42
53
  stack[stack.length - 1].children.push(node)
43
54
  if (!selfClosing) stack.push(node)
44
55
  }
@@ -48,30 +59,169 @@ export function parseBody(body) {
48
59
  const draw = (node, key, strokeWidth) => {
49
60
  const Comp = TAGS[node.tag]
50
61
  if (!Comp) return null
51
- const props = { ...node.props, key }
62
+ const { len, ...rest } = node.props // `len` is the real path length, only used by animations
63
+ void len
64
+ const props = { ...rest, key }
52
65
  if (strokeWidth != null && props.strokeWidth != null) props.strokeWidth = String(strokeWidth)
53
66
  return createElement(Comp, props, ...node.children.map((c, i) => draw(c, i, strokeWidth)))
54
67
  }
55
68
 
69
+ // ---------------------------------------------------------------- animation (rd icons)
70
+ const animatedTags = {}
71
+ const animatedTag = (tag) => (animatedTags[tag] ??= Animated.createAnimatedComponent(TAGS[tag]))
72
+
73
+ const linear = (t) => t
74
+
75
+ /** One Animated.Value per animation of every animated element, and the props each one drives. Built once per speed. */
76
+ function planOf(tree, motion, centerX, centerY) {
77
+ const items = []
78
+ const walk = (nodes) => {
79
+ for (const node of nodes) {
80
+ const part = node.cls ? motion.p[node.cls] : undefined
81
+ if (part && (part.a.length || part.da)) items.push({ node, part })
82
+ walk(node.children)
83
+ }
84
+ }
85
+ walk(tree)
86
+ return items.map((it) => ({ ...it, origin: it.part.o ?? [centerX, centerY], values: it.part.a.map(() => new Animated.Value(0)) }))
87
+ }
88
+
89
+ /** Props that make a node follow its animations: transform props on every element, dash props on the shapes (the real length scales the dash). */
90
+ function animatedProps(item, ctxDash) {
91
+ const { part, node, values, origin } = item
92
+ const vars = node.vars
93
+ const props = { originX: origin[0], originY: origin[1] }
94
+ const own = {}
95
+ part.a.forEach((spec, i) => {
96
+ const track = (key) => {
97
+ const t = trackOf(spec.f, key, spec.e, vars)
98
+ return t ? { v: values[i], t } : null
99
+ }
100
+ for (const [key, name] of [['tx', 'translateX'], ['ty', 'translateY'], ['r', 'rotation'], ['sx', 'scaleX'], ['sy', 'scaleY'], ['o', 'opacity']]) {
101
+ const t = track(key)
102
+ if (t) props[name] = t.v.interpolate({ inputRange: t.t.inputRange, outputRange: t.t.outputRange })
103
+ }
104
+ const d = track('d')
105
+ if (d) own.dash = d
106
+ })
107
+ const dash = own.dash ? { da: part.da, track: own.dash } : part.da ? { da: part.da, track: null, rest: part.ds ?? 0 } : ctxDash
108
+ return { props, dash: dash ?? ctxDash }
109
+ }
110
+
111
+ function drawAnimated(node, key, plan, ctx) {
112
+ const Base = TAGS[node.tag]
113
+ if (!Base) return null
114
+ const { len, ...rest } = node.props
115
+ const item = plan.get(node)
116
+ let props = { ...rest, key }
117
+ if (ctx.strokeWidth != null && props.strokeWidth != null) props.strokeWidth = String(ctx.strokeWidth)
118
+ let dash = ctx.dash
119
+ if (item) {
120
+ const a = animatedProps(item, ctx.dash)
121
+ props = { ...props, ...a.props }
122
+ dash = a.dash
123
+ }
124
+ if (len && dash) {
125
+ const L = parseFloat(len)
126
+ const [d0, d1] = dash.da ?? [1, 2]
127
+ props.strokeDasharray = `${d0 * L} ${d1 * L}`
128
+ if (dash.track) props.strokeDashoffset = dash.track.v.interpolate({ inputRange: dash.track.t.inputRange, outputRange: dash.track.t.outputRange.map((x) => x * L) })
129
+ else props.strokeDashoffset = (dash.rest ?? 0) * L
130
+ }
131
+ const Comp = item || (props.strokeDashoffset !== undefined && typeof props.strokeDashoffset === 'object') ? animatedTag(node.tag) : Base
132
+ return createElement(Comp, props, ...node.children.map((c, i) => drawAnimated(c, i, plan, { dash, strokeWidth: ctx.strokeWidth })))
133
+ }
134
+
135
+ /** Starts every animation of the plan; returns a function that stops them. */
136
+ function run(items, mode, speed) {
137
+ const anims = []
138
+ for (const item of items) {
139
+ item.part.a.forEach((spec, i) => {
140
+ const v = item.values[i]
141
+ v.setValue(0)
142
+ const timing = Animated.timing(v, { toValue: 1, duration: (spec.d * 1000) / speed, easing: linear, useNativeDriver: false })
143
+ const delay = (delayOf(spec, item.node.vars) * 1000) / speed
144
+ const body = mode === 'loop' ? Animated.loop(timing) : timing
145
+ anims.push(delay > 0 ? Animated.sequence([Animated.delay(delay), body]) : body)
146
+ })
147
+ }
148
+ const all = Animated.parallel(anims)
149
+ all.start()
150
+ return () => {
151
+ all.stop()
152
+ for (const item of items) for (const v of item.values) v.setValue(0)
153
+ }
154
+ }
155
+
156
+ function useReducedMotion() {
157
+ // null until the device has answered, so an icon never starts to move before it is known that motion is wanted
158
+ const [reduced, setReduced] = React.useState(null)
159
+ React.useEffect(() => {
160
+ let live = true
161
+ if (!AccessibilityInfo.isReduceMotionEnabled) setReduced(false)
162
+ else AccessibilityInfo.isReduceMotionEnabled().then((v) => live && setReduced(!!v)).catch(() => live && setReduced(false))
163
+ const sub = AccessibilityInfo.addEventListener?.('reduceMotionChanged', (v) => setReduced(!!v))
164
+ return () => {
165
+ live = false
166
+ sub?.remove?.()
167
+ }
168
+ }, [])
169
+ return reduced
170
+ }
171
+
56
172
  /**
57
173
  * Turns an icon (SVG inner markup + viewBox size) into a React Native component drawn with react-native-svg.
58
- * Every generated icon in this package is created with this function.
174
+ * Every generated icon in this package is created with this function. `anim` = { b: animated drawing, m: motion } for icons that move.
59
175
  */
60
- export function createIcon(id, body, width = 24, height = 24) {
176
+ export function createIcon(id, body, width = 24, height = 24, anim) {
61
177
  let tree = null
62
- const Icon = ({ size = 24, color = '#000000', strokeWidth, title, ...rest }) => {
63
- tree ??= parseBody(body)
178
+ const cache = new Map() // the drawing of an effect combination, so a re-render does not parse again
179
+ const Icon = React.forwardRef(function RdIcon({ size = 24, color = '#000000', strokeWidth, title, duotone, gradient, animate, speed = 1, ...rest }, ref) {
180
+ const uid = useUid()
181
+ const reduced = useReducedMotion()
182
+ const mode = anim && animate ? (animate === 'loop' ? 'loop' : 'play') : null
183
+ const [runs, setRuns] = React.useState(0)
184
+ const fx = duotone || gradient ? parseEffectProps({ duotone, gradient }, 'rdg' + uid) : null
185
+ const fxKey = fx ? JSON.stringify([fx.duotone, fx.gradient, fx.gradientId]) : ''
186
+
187
+ // the animated drawing: one Animated.Value per animation, kept while the icon is mounted
188
+ const plan = React.useMemo(() => {
189
+ if (!mode) return null
190
+ const drawing = fx ? applyEffects(anim.b, width, height, 'currentColor', fx) : anim.b
191
+ const animTree = parseBody(drawing)
192
+ const items = planOf(animTree, anim.m, width / 2, height / 2)
193
+ return { animTree, items, byNode: new Map(items.map((it) => [it.node, it])) }
194
+ }, [mode, fxKey])
195
+ React.useImperativeHandle(ref, () => ({ play: () => setRuns((n) => n + 1), stop: () => {} }), [])
196
+ React.useEffect(() => {
197
+ if (!plan || reduced !== false) return undefined
198
+ return run(plan.items, mode, speed > 0 ? speed : 1)
199
+ }, [plan, mode, speed, reduced, runs])
200
+
201
+ let nodes = null
202
+ if (!plan) {
203
+ if (fx) {
204
+ if (!cache.has(fxKey)) { if (cache.size > 20) cache.clear(); cache.set(fxKey, parseBody(applyEffects(body, width, height, 'currentColor', fx))) }
205
+ nodes = cache.get(fxKey)
206
+ } else {
207
+ tree ??= parseBody(body)
208
+ nodes = tree
209
+ }
210
+ }
64
211
  // fit the longest side into the `size` box, like the rdicons website does
65
212
  const n = typeof size === 'number' ? size : Number.parseFloat(size)
66
213
  const long = Math.max(width, height)
67
214
  const w = width !== height ? (n * width) / long : size
68
215
  const h = width !== height ? (n * height) / long : size
216
+ const children = plan
217
+ ? plan.animTree.map((node, i) => drawAnimated(node, i, plan.byNode, { dash: null, strokeWidth }))
218
+ : nodes.map((node, i) => draw(node, i, strokeWidth))
69
219
  return createElement(
70
220
  Svg,
71
221
  { width: w, height: h, viewBox: '0 0 ' + width + ' ' + height, color, accessibilityRole: title ? 'image' : undefined, accessibilityLabel: title, accessible: title ? true : undefined, ...rest },
72
- ...tree.map((node, i) => draw(node, i, strokeWidth)),
222
+ ...children,
73
223
  )
74
- }
224
+ })
75
225
  Icon.displayName = id
76
226
  return Icon
77
227
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rdicons/react-native",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "79,000+ open-source icons as React Native components, from 19 libraries, drawn with react-native-svg. Import one library at a time, e.g. import { House } from '@rdicons/react-native/lucide'.",
5
5
  "keywords": ["icons", "react-native", "expo", "svg", "react-native-svg", "lucide", "tabler", "phosphor", "heroicons", "material-icons", "rdicons"],
6
6
  "license": "MIT AND ISC AND Apache-2.0 AND CC-BY-4.0",