@caidentity/testicon 0.3.0 → 1.0.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 +88 -5
- package/package.json +1 -1
- package/src/Icon.tsx +82 -4
- package/src/icons/manifest.json +4 -1
- package/src/icons/manifest.ts +9 -1
- package/src/index.ts +1 -0
package/README.md
CHANGED
|
@@ -1,21 +1,104 @@
|
|
|
1
1
|
# @caidentity/testicon
|
|
2
2
|
|
|
3
|
-
React icon
|
|
3
|
+
React icon components generated from SVG sources.
|
|
4
4
|
|
|
5
5
|
- Version: 0.0.3
|
|
6
|
-
-
|
|
6
|
+
- Icons: 2
|
|
7
|
+
- Manifest: `iconsManifest` (id, name, component, viewBox, tags)
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @caidentity/testicon
|
|
13
|
+
```
|
|
7
14
|
|
|
8
15
|
## Usage
|
|
9
16
|
|
|
10
17
|
```tsx
|
|
11
|
-
import {
|
|
12
|
-
import { Home } from '@caidentity/testicon'
|
|
18
|
+
import { Icon } from '@caidentity/testicon'
|
|
13
19
|
|
|
14
20
|
export function Example() {
|
|
15
21
|
return (
|
|
16
22
|
<div style={{ display: 'flex', gap: 12 }}>
|
|
17
|
-
<
|
|
23
|
+
<Icon name="home" size={24} />
|
|
24
|
+
<Icon name="search" size={20} />
|
|
25
|
+
<Icon name="user" size={16} className="user-icon" />
|
|
18
26
|
</div>
|
|
19
27
|
)
|
|
20
28
|
}
|
|
21
29
|
```
|
|
30
|
+
|
|
31
|
+
## Component Props
|
|
32
|
+
|
|
33
|
+
The Icon component extends `SVGProps<SVGSVGElement>` with these additional props:
|
|
34
|
+
|
|
35
|
+
- `name: string` - **Required.** Name of the icon to render (case-insensitive)
|
|
36
|
+
- `size?: number | string` - Icon size in pixels (default: 24)
|
|
37
|
+
- `title?: string` - Accessible title for screen readers
|
|
38
|
+
|
|
39
|
+
### Animation Props
|
|
40
|
+
|
|
41
|
+
- `animated?: boolean` - Enable or disable animation when available
|
|
42
|
+
- `animationDuration?: number` - Override animation duration in milliseconds
|
|
43
|
+
- `animationDelay?: number` - Delay before animation starts in milliseconds
|
|
44
|
+
- `animationLoop?: boolean | number` - `true` for infinite, `false` for one-time, or a number of iterations
|
|
45
|
+
- `animationDirection?: 'normal' | 'reverse' | 'alternate'` - Playback direction
|
|
46
|
+
- `onAnimationStart?: () => void` and `onAnimationEnd?: () => void`
|
|
47
|
+
|
|
48
|
+
All standard SVG props are supported (className, style, onClick, etc.).
|
|
49
|
+
|
|
50
|
+
### Duotone Props
|
|
51
|
+
|
|
52
|
+
- `duotone1?: string`, `duotone2?: string`, `duotone3?: string`
|
|
53
|
+
- Apply when the icon declares layered content via `duotoneLayers` in the registry.
|
|
54
|
+
- Mapped to CSS variables `--duotone1/2/3` for layer-targeted colors.
|
|
55
|
+
- When any duotone color is provided and the icon reports layered content, `fill` is omitted so layered colors determine appearance.
|
|
56
|
+
- Otherwise, `fill` falls back to `currentColor` or the explicit `fill` prop.
|
|
57
|
+
|
|
58
|
+
Example:
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
<Icon
|
|
62
|
+
name="logo"
|
|
63
|
+
duotone1="#4F46E5" // primary layer
|
|
64
|
+
duotone2="#A78BFA" // secondary layer
|
|
65
|
+
size={32}
|
|
66
|
+
/>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Theming
|
|
70
|
+
|
|
71
|
+
Icons use `fill="currentColor"` by default, so they inherit the text color of their container:
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
<div style={{ color: 'blue' }}>
|
|
75
|
+
<Icon name="home" /> {/* Will be blue */}
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<Icon name="home" style={{ color: 'red' }} /> {/* Override with style */}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Available Icons
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import { iconsManifest } from '@caidentity/testicon'
|
|
85
|
+
|
|
86
|
+
// Get all available icons
|
|
87
|
+
console.log(iconsManifest)
|
|
88
|
+
|
|
89
|
+
// Each icon has: { id, name, component, width, height, viewBox, tags? }
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## TypeScript Support
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
import { Icon, IconName } from '@caidentity/testicon'
|
|
96
|
+
|
|
97
|
+
// Type-safe icon names
|
|
98
|
+
function MyComponent() {
|
|
99
|
+
return <Icon name="home" size={24} />
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// IconName is a union of all available icon names
|
|
103
|
+
const iconName: IconName = 'home'
|
|
104
|
+
```
|
package/package.json
CHANGED
package/src/Icon.tsx
CHANGED
|
@@ -1,14 +1,32 @@
|
|
|
1
|
-
import React, { SVGProps } from 'react'
|
|
1
|
+
import React, { SVGProps, useEffect } from 'react'
|
|
2
2
|
|
|
3
3
|
export interface IconProps extends SVGProps<SVGSVGElement> {
|
|
4
4
|
name: string
|
|
5
5
|
size?: number | string
|
|
6
6
|
title?: string
|
|
7
|
+
|
|
8
|
+
// Single color (backward compatible)
|
|
9
|
+
fill?: string
|
|
10
|
+
|
|
11
|
+
// Duotone colors (new)
|
|
12
|
+
duotone1?: string // Primary duotone layer color
|
|
13
|
+
duotone2?: string // Secondary duotone layer color
|
|
14
|
+
duotone3?: string // Tertiary duotone layer color (optional)
|
|
15
|
+
|
|
16
|
+
// Animation controls (new). When an icon has animation defaults in the registry,
|
|
17
|
+
// these props can override or toggle playback intent; consumers can pair with CSS.
|
|
18
|
+
animated?: boolean
|
|
19
|
+
animationDuration?: number
|
|
20
|
+
animationDelay?: number
|
|
21
|
+
animationLoop?: boolean | number
|
|
22
|
+
animationDirection?: 'normal' | 'reverse' | 'alternate'
|
|
23
|
+
onAnimationStart?: () => void
|
|
24
|
+
onAnimationEnd?: () => void
|
|
7
25
|
}
|
|
8
26
|
|
|
9
27
|
const iconRegistry = {
|
|
10
28
|
"frame": {
|
|
11
|
-
"svg": "
|
|
29
|
+
"svg": "<g transform=\"translate(-1490, -237)\"><path d=\"M 8 -90 L 422 -90 Q 430 -90 430 -82 L 430 172 Q 430 180 422 180 L 8 180 Q 0 180 0 172 L 0 -82 Q 0 -90 8 -90 Z\" fill=\"none\" fill-opacity=\"0\" stroke=\"#000000\" stroke-opacity=\"1\" strokeWidth=\"2\" strokeLinecap=\"butt\" strokeLinejoin=\"miter\" transform=\"translate(746,441)\" />\n<path d=\"M 4 0 L 4 0 Q 8 0 8 4 L 8 4 Q 8 8 4 8 L 4 8 Q 0 8 0 4 L 0 4 Q 0 0 4 0 Z\" fill=\"none\" fill-opacity=\"0\" stroke=\"#ffcfcfff\" stroke-opacity=\"1\" strokeWidth=\"2\" strokeLinecap=\"butt\" strokeLinejoin=\"miter\" transform=\"translate(1031,672)\" />\n<path d=\"M 0 0 L 0 153 L 186 153 L 186 -7\" fill=\"none\" fill-opacity=\"0\" stroke=\"#000000\" stroke-opacity=\"1\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" transform=\"translate(1765,554)\" /></g>",
|
|
12
30
|
"viewBox": "0 0 800 600"
|
|
13
31
|
}
|
|
14
32
|
}
|
|
@@ -18,6 +36,17 @@ export const Icon: React.FC<IconProps> = ({
|
|
|
18
36
|
size = 24,
|
|
19
37
|
title,
|
|
20
38
|
className,
|
|
39
|
+
fill,
|
|
40
|
+
duotone1,
|
|
41
|
+
duotone2,
|
|
42
|
+
duotone3,
|
|
43
|
+
animated,
|
|
44
|
+
animationDuration,
|
|
45
|
+
animationDelay,
|
|
46
|
+
animationLoop,
|
|
47
|
+
animationDirection,
|
|
48
|
+
onAnimationStart,
|
|
49
|
+
onAnimationEnd,
|
|
21
50
|
...props
|
|
22
51
|
}) => {
|
|
23
52
|
const iconData = iconRegistry[name.toLowerCase()]
|
|
@@ -27,19 +56,68 @@ export const Icon: React.FC<IconProps> = ({
|
|
|
27
56
|
return null
|
|
28
57
|
}
|
|
29
58
|
|
|
59
|
+
// Apply duotone colors to CSS variables for layer targeting
|
|
60
|
+
const duotoneVars: Record<string, string> = {}
|
|
61
|
+
if (iconData.duotoneLayers && iconData.duotoneLayers >= 2 && duotone1) duotoneVars['--duotone1'] = duotone1
|
|
62
|
+
if (iconData.duotoneLayers && iconData.duotoneLayers >= 2 && duotone2) duotoneVars['--duotone2'] = duotone2
|
|
63
|
+
if (iconData.duotoneLayers && iconData.duotoneLayers >= 3 && duotone3) duotoneVars['--duotone3'] = duotone3
|
|
64
|
+
|
|
65
|
+
const styleWithDuotone = {
|
|
66
|
+
...props.style,
|
|
67
|
+
...duotoneVars
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Use duotone colors if available, otherwise fall back to single color
|
|
71
|
+
// Note: when duotone is present, we intentionally omit 'fill' to let layered colors drive
|
|
72
|
+
// the appearance; otherwise we use 'currentColor' or 'fill' prop.
|
|
73
|
+
const hasDuotone = iconData.duotoneLayers && (duotone1 || duotone2 || duotone3)
|
|
74
|
+
const svgFill = hasDuotone ? undefined : (fill || 'currentColor')
|
|
75
|
+
|
|
76
|
+
// Derive animation intent from registry defaults and props
|
|
77
|
+
const meta = iconData.animation || null
|
|
78
|
+
const isAnimated = typeof animated === 'boolean' ? animated : !!(meta && meta.autoplay)
|
|
79
|
+
const durationMs = typeof animationDuration === 'number' ? animationDuration : (meta && typeof meta.duration === 'number' ? meta.duration : undefined)
|
|
80
|
+
const delayMs = typeof animationDelay === 'number' ? animationDelay : undefined
|
|
81
|
+
const iterCount = typeof animationLoop === 'number' ? String(animationLoop) : (animationLoop === true ? 'infinite' : (animationLoop === false ? '1' : (meta && meta.loop ? 'infinite' : undefined)))
|
|
82
|
+
const dir = animationDirection ?? (meta && meta.direction ? meta.direction : undefined)
|
|
83
|
+
|
|
84
|
+
// Merge animation style on top of duotone variables and incoming style.
|
|
85
|
+
// Consumers can add 'animationName' via props.style or class to bind keyframes.
|
|
86
|
+
const styleWithAnimation: React.CSSProperties = {
|
|
87
|
+
...styleWithDuotone,
|
|
88
|
+
...(durationMs !== undefined ? { animationDuration: String(durationMs) + 'ms' } : {}),
|
|
89
|
+
...(delayMs !== undefined ? { animationDelay: String(delayMs) + 'ms' } : {}),
|
|
90
|
+
...(iterCount !== undefined ? { animationIterationCount: iterCount } : {}),
|
|
91
|
+
...(dir !== undefined ? { animationDirection: dir } : {}),
|
|
92
|
+
animationPlayState: isAnimated ? 'running' : 'paused'
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (isAnimated) onAnimationStart?.()
|
|
97
|
+
else onAnimationEnd?.()
|
|
98
|
+
}, [isAnimated])
|
|
99
|
+
|
|
30
100
|
return (
|
|
31
101
|
<svg
|
|
32
102
|
width={size}
|
|
33
103
|
height={size}
|
|
34
104
|
viewBox={iconData.viewBox}
|
|
35
|
-
fill=
|
|
105
|
+
fill={svgFill}
|
|
36
106
|
className={className}
|
|
107
|
+
style={styleWithAnimation}
|
|
108
|
+
data-animated={isAnimated ? '1' : undefined}
|
|
109
|
+
data-animation-duration={durationMs}
|
|
110
|
+
data-animation-delay={delayMs}
|
|
111
|
+
data-animation-iter={iterCount}
|
|
112
|
+
data-animation-direction={dir}
|
|
37
113
|
aria-hidden={title ? undefined : true}
|
|
38
114
|
aria-labelledby={title ? `${name}-title` : undefined}
|
|
39
115
|
role="img"
|
|
40
116
|
{...props}
|
|
41
117
|
>
|
|
42
|
-
{title && <title id={
|
|
118
|
+
{title && <title id={
|
|
119
|
+
|
|
120
|
+
}>{title}</title>}
|
|
43
121
|
<g dangerouslySetInnerHTML={{ __html: iconData.svg }} />
|
|
44
122
|
</svg>
|
|
45
123
|
)
|
package/src/icons/manifest.json
CHANGED
package/src/icons/manifest.ts
CHANGED
|
@@ -7,6 +7,11 @@ export type IconManifestEntry = {
|
|
|
7
7
|
height: number
|
|
8
8
|
viewBox: string
|
|
9
9
|
tags?: string[]
|
|
10
|
+
animation?: {
|
|
11
|
+
duration?: number
|
|
12
|
+
loop?: boolean | number
|
|
13
|
+
autoplay?: boolean
|
|
14
|
+
}
|
|
10
15
|
}
|
|
11
16
|
|
|
12
17
|
export const iconsManifest: IconManifestEntry[] = [
|
|
@@ -16,7 +21,10 @@ export const iconsManifest: IconManifestEntry[] = [
|
|
|
16
21
|
"component": "frame",
|
|
17
22
|
"width": 800,
|
|
18
23
|
"height": 600,
|
|
19
|
-
"viewBox": "0 0 800 600"
|
|
24
|
+
"viewBox": "0 0 800 600",
|
|
25
|
+
"animation": {
|
|
26
|
+
"autoplay": false
|
|
27
|
+
}
|
|
20
28
|
},
|
|
21
29
|
{
|
|
22
30
|
"id": "frame-t8zz5m1-1765658197783",
|
package/src/index.ts
CHANGED