Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 0 additions & 58 deletions packages/proicons-react/src/ProIcon.ts

This file was deleted.

39 changes: 39 additions & 0 deletions packages/proicons-react/src/ProIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { getPascalName } from '@proicons/shared'
import * as icons from './icons'
import { ProIconAttributes } from './types'

type IconEnum<T extends string> = T extends `${infer Base}Icon` ? Base : T

interface ProIconComponent extends ProIconAttributes {
/**
* The name of the icon in kebabCase, PascalCase, Friendly Form, or camelCase. Case-insensitive
* @example These are allowed:
* ```jsx
* <ProIcon icon="add square" />
* <ProIcon icon="home" />
* <ProIcon icon="AddIcon" />
* <ProIcon icon="bookmarkMultiple" />
* ```
* [Documentation](https://procode-software.github.io/proicons/docs/react#proicon-component)
*/
icon: IconEnum<keyof typeof icons> | (string & {})
}


/**
* Generic icon component allowing you to import icons by their name
*
* Note: This breaks tree-shaking
* @example ```jsx
* <ProIcon icon="Add Square" />
* ```
*/
export function ProIcon({ icon, ...props }: ProIconComponent) {
if (!icon) throw new TypeError("An 'icon' attribute is required.")

const pascalName = getPascalName(icons, icon)
if (!pascalName) throw new Error(`Icon '${icon}' not found.`)

const Icon = icons[pascalName]
return <Icon {...props} />
}
58 changes: 0 additions & 58 deletions packages/proicons-react/src/createIcon.ts

This file was deleted.

52 changes: 52 additions & 0 deletions packages/proicons-react/src/createIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import * as React from 'react'
import { kebabCase, kebabToCamelCase } from '@proicons/shared'
import { convertNodesWithConfig } from '@proicons/shared'
import { ProIconAttributes } from './types'

export type IconNode = [string, Record<string, string>, IconNode[]]

export const convertNodes = (n: IconNode[]) => {
return !n.length
? null
: n.map(([tag, attrs, children]) => {
return React.createElement(
tag,
Object.fromEntries(
Object.entries(attrs).map(([k, v]) => [kebabToCamelCase(k), v])
),
convertNodes(children)
)
})
}

export type IconComponent = React.FunctionComponent<ProIconAttributes>

export function createIcon(
{
name,
deprecated,
alternative,
}: { name: string; deprecated?: boolean; alternative?: string },
nodes: IconNode[]
): IconComponent {
if (deprecated)
console.warn(`Icon ${name} is deprecated. Use ${alternative} instead.`)

const Icon: IconComponent = ({ ref, ...props }: ProIconAttributes) => (
<svg
ref={ref}
width={props?.size ?? 24}
height={props?.size ?? 24}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
{...props}
className={((props?.className ?? '') + ' proicon').trim()}
data-proicon-id={kebabCase(name)}
>
{convertNodes(convertNodesWithConfig(nodes, props) as IconNode[])}
</svg>
)
Icon.displayName = name
return Icon
}
32 changes: 24 additions & 8 deletions packages/proicons-react/test/App.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import * as React from 'react'
import { createRoot } from 'react-dom/client'
import { ProIcon } from '../src/ProIcon.ts'
import { AddIcon, TvIcon } from '../src/icons.ts'
import { ProIcon } from '../src/ProIcon.tsx'
import { AddIcon, TvIcon, BrainIcon } from '../src/icons.ts'

function App() {
const [size, setSize] = React.useState(48)
const randomIcons = ['Photo', 'Search', 'Cheese', 'Toast', 'Save']
const getRandomIcon = () =>
randomIcons[Math.floor(Math.random() * randomIcons.length)]
const [icon, setIcon] = React.useState(getRandomIcon())

return (
<>
<h1>ProIcons React Test</h1>
<ProIcon icon="Add Square Multiple" size={32} className="myClass" />
<AddIcon
size={32}
color="red"
strokeWidth={2}
className="myIconClassAdd"
/>
<AddIcon size={32} color="red" strokeWidth={2} className="myIconClassAdd" />
{/* Test for https://github.com/ProCode-Software/proicons/issues/5 */}
<div style={{ color: 'green' }}>
<AddIcon color="red" /> {/* red ✓ */}
Expand All @@ -23,6 +24,21 @@ function App() {
<AddIcon /> {/* red x */}
<TvIcon /> {/* blue ✓ */}
</div>
<h2>Dynamic icons</h2>
Size:{' '}
<input
type="range"
value={size}
onChange={e => setSize(+e.target.value)}
min="24"
max="72"
/>
<br />
<ProIcon icon="Search Cancel" size={size} />
<BrainIcon size={size} />
<br />
<ProIcon icon={icon} size={size} />
<button onClick={() => setIcon(getRandomIcon())}>Random Icon</button>
</>
)
}
Expand Down
33 changes: 8 additions & 25 deletions packages/proicons-svelte/src/ProIcon.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,40 +10,23 @@ Note: This breaks tree-shaking
[Documentation](https://procode-software.github.io/proicons/docs/svelte#proicon-component)
-->
<script lang="ts">
import type { ProIconAttributes } from './types'
import * as icons from './icons'
import { kebabCase, pascalCase } from './utils'
import type { ProIconAttributes } from './types'
import { getPascalName } from './utils'

type IconEnum<T extends string> = T extends `${infer Base}Icon` ? Base : T
type IconProp = IconEnum<keyof typeof icons> | (string & {})

type Props = ProIconAttributes & { icon: IconProp }
const { icon, ...props }: Props = $props()

if (!icon) throw new Error("An 'icon' attribute is required.")

function getPascalName(name: string) {
const lowerName = name.toLowerCase()
const iconEntries = Object.keys(icons)

return iconEntries.find(pascalName => {
const lowerIconName = pascalName.replace(/Icon$/, '').toLowerCase()

return (
lowerIconName == lowerName ||
lowerIconName + 'icon' == lowerName ||
kebabCase(lowerIconName) == lowerName ||
lowerIconName == pascalCase(lowerName)
)
})
}
const name = getPascalName(icon)

if (!name) {
throw new Error(`Icon '${name}' not found.`)
}
const Icon = $derived.by(() => {
if (!icon) throw new TypeError("An 'icon' attribute is required.")

const Icon = icons[name]
const name = getPascalName(icons, icon)
if (!name) throw new Error(`Icon '${icon}' not found.`)
return icons[name]
})
</script>

<Icon {...props} />
26 changes: 13 additions & 13 deletions packages/proicons-svelte/test/App.svelte
Original file line number Diff line number Diff line change
@@ -1,24 +1,16 @@
<script lang="ts">
import { AddSquareMultipleIcon, AddIcon, TvIcon } from '../src/icons'
import { AddSquareMultipleIcon, AddIcon, TvIcon, BrainIcon } from '../src/icons'
import { ProIcon } from '../src/proicons-svelte'

let count: number = $state(0)

const increment = () => {
count += 1
}
let size = $state(48)
const randomIcons = ['Photo', 'Search', 'Cheese', 'Toast', 'Save']
const getRandomIcon = () => randomIcons[Math.floor(Math.random() * randomIcons.length)]
let iconName = $state(getRandomIcon())
</script>

<main>
<h1>ProIcons Svelte Test</h1>

<div class="card">
<button onclick={increment}>
count is {count}
</button>
</div>

<p></p>
<AddSquareMultipleIcon size={48} strokeWidth={2} class="myClass" />
<ProIcon icon="Add" strokeWidth={2} color="red" />

Expand All @@ -31,4 +23,12 @@ const increment = () => {
<AddIcon />
<TvIcon />
</div>
<h2>Dynamic icons</h2>
Size:<input type="range" bind:value={size} min="24" max="72" />
<br />
<ProIcon icon="Search Cancel" {size} />
<BrainIcon {size} />
<br>
<ProIcon icon={iconName} {size} />
<button onclick={() => iconName = getRandomIcon()}>Random Icon</button>
</main>
Loading
Loading