@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/README.md +23 -1
- package/dist/effects.js +210 -0
- package/dist/motion.js +106 -0
- package/dist/rd-static.d.ts +1278 -0
- package/dist/rd-static.js +640 -0
- package/dist/rd.js +638 -638
- package/dist/runtime.d.ts +22 -3
- package/dist/runtime.js +163 -13
- package/package.json +1 -1
package/dist/runtime.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
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
|
|
63
|
-
|
|
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
|
-
...
|
|
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.
|
|
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",
|