From 2bc3b9236d9e42384336f0afc3e1a33edfdd4879 Mon Sep 17 00:00:00 2001 From: Peter Pal Hudak Date: Wed, 26 Nov 2025 15:53:25 +0100 Subject: [PATCH 1/7] feat(ui-avatar): add lucide icons to Avatar --- packages/ui-avatar/package.json | 1 + packages/ui-avatar/src/Avatar/README.md | 192 ++++---- .../src/Avatar/__tests__/Avatar.test.tsx | 257 ++++++++++- packages/ui-avatar/src/Avatar/index.tsx | 23 +- packages/ui-avatar/src/Avatar/props.ts | 28 +- packages/ui-avatar/src/Avatar/styles.ts | 42 +- packages/ui-avatar/tsconfig.build.json | 3 + .../renderLucideIconWithProps.test.tsx | 433 ++++++++++++++++++ packages/ui-react-utils/src/index.ts | 1 + .../src/renderLucideIconWithProps.ts | 111 +++++ pnpm-lock.yaml | 3 + 11 files changed, 966 insertions(+), 128 deletions(-) create mode 100644 packages/ui-react-utils/src/__tests__/renderLucideIconWithProps.test.tsx create mode 100644 packages/ui-react-utils/src/renderLucideIconWithProps.ts diff --git a/packages/ui-avatar/package.json b/packages/ui-avatar/package.json index aa2d09bd9b..02e61c88ae 100644 --- a/packages/ui-avatar/package.json +++ b/packages/ui-avatar/package.json @@ -34,6 +34,7 @@ "@instructure/ui-axe-check": "workspace:*", "@instructure/ui-babel-preset": "workspace:*", "@instructure/ui-color-utils": "workspace:*", + "@instructure/ui-icons-lucide": "workspace:*", "@instructure/ui-themes": "workspace:*", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "15.0.7", diff --git a/packages/ui-avatar/src/Avatar/README.md b/packages/ui-avatar/src/Avatar/README.md index adf861b4a6..9e1383032c 100644 --- a/packages/ui-avatar/src/Avatar/README.md +++ b/packages/ui-avatar/src/Avatar/README.md @@ -16,22 +16,22 @@ readonly: true
- - - - - - - + + + + + + + - - - - - - - + + + + + + +
``` @@ -46,16 +46,47 @@ type: example readonly: true --- - - - - - - - + + + + + + + ``` +### Using Lucide Icons + +Lucide icons in Avatar are automatically sized and colored according to the Avatar's `size` and `color` props, so manual adjustments are not needed on the icon itself. + +```js +--- +type: example +--- +
+ + + + + + + + + + + } /> + } /> + } /> + + + } /> + } /> + } /> + +
+``` + ### Size The `size` prop allows you to select from `xx-small`, `x-small`, `small`, `medium` _(default)_, `large`, `x-large`, and `xx-large`. Each size has predefined dimensions and typography scales. @@ -66,31 +97,31 @@ type: example ---
- - - - - - - + + + + + + + - - - - - - - + + + + + + + - } size="xx-small" /> - } size="x-small" /> - } size="small" /> - } size="medium" /> - } size="large" /> - } size="x-large" /> - } size="xx-large" /> + + + + + + +
``` @@ -105,22 +136,22 @@ type: example ---
- - - - - - - + + + + + + + - } name="Arthur C. Clarke" /> - } name="James Arias" color="accent2" /> - } name="Charles Kimball" color="accent3" /> - } name="Melissa Reed" color="accent4" /> - } name="Heather Wheeler" color="accent5" /> - } name="David Herbert" color="accent6" /> - } name="Isaac Asimov" color="accent1" /> + + + + + + +
``` @@ -135,22 +166,22 @@ type: example ---
- - - - - - - + + + + + + + - } name="Arthur C. Clarke" hasInverseColor /> - } name="James Arias" color="accent2" hasInverseColor /> - } name="Charles Kimball" color="accent3" hasInverseColor /> - } name="Melissa Reed" color="accent4" hasInverseColor /> - } name="Heather Wheeler" color="accent5" hasInverseColor /> - } name="David Herbert" color="accent6" hasInverseColor /> - } name="Isaac Asimov" color="accent1" hasInverseColor /> + + + + + + +
``` @@ -162,10 +193,10 @@ In case you need more control over the color, feel free to use the `themeOverrid type: example ---
- } themeOverride={{ accent1TextColor: '#efb410' }} /> - - } hasInverseColor themeOverride={{ textOnColor: 'lightblue', backgroundColor: 'black' }} /> - + + + +
``` @@ -179,14 +210,14 @@ type: example ---
Inline avatars: - - + + are displayed inline with text.
Block avatars: - - + + stack vertically.
@@ -202,7 +233,7 @@ type: example ---
- } showBorder="never" /> +
``` @@ -218,8 +249,7 @@ type: example } - + renderIcon={UsersInstUIIcon} /> Image takes priority over icon diff --git a/packages/ui-avatar/src/Avatar/__tests__/Avatar.test.tsx b/packages/ui-avatar/src/Avatar/__tests__/Avatar.test.tsx index 52ea7a4bdf..c90ab607f2 100644 --- a/packages/ui-avatar/src/Avatar/__tests__/Avatar.test.tsx +++ b/packages/ui-avatar/src/Avatar/__tests__/Avatar.test.tsx @@ -29,6 +29,10 @@ import { runAxeCheck } from '@instructure/ui-axe-check' import '@testing-library/jest-dom' import Avatar from '../index' import { IconGroupLine } from '@instructure/ui-icons' +import { + UserInstUIIcon, + CircleUserInstUIIcon +} from '@instructure/ui-icons-lucide' describe('', () => { describe('for a11y', () => { @@ -93,25 +97,260 @@ describe('', () => { expect(avatarSvg).toBeInTheDocument() }) - it('should display an InstUI icon passed', async () => { + it('should display correctly when an icon renderer is passed', async () => { const { container } = render( - }> - hello + }> + Hello World ) const avatarSvg = container.querySelector('svg') expect(avatarSvg).toBeInTheDocument() }) - it('should display correctly when an icon renderer is passed', async () => { + it('should pass the correct size and color props to icon based on Avatar size', async () => { + const MockIcon = vi.fn((props: any) => ( + + + + )) + ;(MockIcon as any).displayName = 'wrapLucideIcon(MockIcon)' + const { container } = render( - }> - Hello World - + + ) + + expect(MockIcon).toHaveBeenCalledWith( + expect.objectContaining({ size: 'md', color: expect.any(String) }) + ) + const icon = container.querySelector('[data-testid="mock-icon"]') + expect(icon).toHaveAttribute('data-size', 'md') + expect(icon).toHaveAttribute('data-color') + }) + + it('should map xx-small Avatar to xs icon size', async () => { + const MockIcon = vi.fn(() => ( + + + + )) + ;(MockIcon as any).displayName = 'wrapLucideIcon(MockIcon)' + + render( + + ) + + expect(MockIcon).toHaveBeenCalledWith( + expect.objectContaining({ size: 'xs', color: expect.any(String) }) + ) + }) + + it('should map x-small Avatar to xs icon size', async () => { + const MockIcon = vi.fn(() => ( + + + + )) + ;(MockIcon as any).displayName = 'wrapLucideIcon(MockIcon)' + + render() + + expect(MockIcon).toHaveBeenCalledWith( + expect.objectContaining({ size: 'xs', color: expect.any(String) }) + ) + }) + + it('should work with icons that ignore the size prop (backwards compatibility)', async () => { + const IconWithoutSize = () => ( + + + + ) + + const { container } = render( + + ) + + const icon = container.querySelector('[data-testid="icon-without-size"]') + expect(icon).toBeInTheDocument() + }) + + it('should display a Lucide icon with default size', async () => { + const { container } = render( + + ) + + const avatarSvg = container.querySelector('svg') + expect(avatarSvg).toBeInTheDocument() + }) + + it('should display a Lucide icon with medium Avatar size', async () => { + const { container } = render( + + ) + + const avatarSvg = container.querySelector('svg') + expect(avatarSvg).toBeInTheDocument() + }) + + it('should display a Lucide icon with xx-small Avatar size', async () => { + const { container } = render( + ) + + const avatarSvg = container.querySelector('svg') + expect(avatarSvg).toBeInTheDocument() + }) + + it('should display a Lucide icon with x-small Avatar size', async () => { + const { container } = render( + + ) + const avatarSvg = container.querySelector('svg') expect(avatarSvg).toBeInTheDocument() }) + + it('should accept a JSX element and clone it with size/color props', async () => { + const MockIcon = vi.fn((props: any) => ( + + + + )) + ;(MockIcon as any).displayName = 'wrapLucideIcon(MockIcon)' + + const { container } = render( + } /> + ) + + const icon = container.querySelector('[data-testid="jsx-icon"]') + expect(icon).toBeInTheDocument() + expect(icon).toHaveAttribute('data-size', 'lg') + expect(icon).toHaveAttribute('data-color') + }) + + it('should override props when JSX element is passed', async () => { + const MockIcon = (props: any) => ( + + + + ) + MockIcon.displayName = 'wrapLucideIcon(MockIcon)' + + const { container } = render( + } + /> + ) + + const icon = container.querySelector('[data-testid="override-icon"]') + expect(icon).toBeInTheDocument() + // Avatar should override the size prop + expect(icon).toHaveAttribute('data-size', 'xl') + }) + + it('should work with a render function returning JSX', async () => { + const renderFunc = (props: any) => ( + + + + ) + renderFunc.displayName = 'wrapLucideIcon(renderFunc)' + + const { container } = render( + + ) + + const icon = container.querySelector('[data-testid="function-icon"]') + expect(icon).toBeInTheDocument() + expect(icon).toHaveAttribute('data-size', 'sm') + }) + + it('should work with arrow function returning Lucide icon (user pattern)', async () => { + const MockIcon = vi.fn((props: any) => ( + + + + )) + ;(MockIcon as any).displayName = 'wrapLucideIcon(MockIcon)' + + const { container } = render( + } + /> + ) + + const icon = container.querySelector('[data-testid="arrow-lucide-icon"]') + expect(icon).toBeInTheDocument() + // The icon should have received the correct size prop + expect(icon).toHaveAttribute('data-size', 'sm') + }) + + it('should apply different sizes to arrow function icons', async () => { + const SmallMockIcon = vi.fn((props: any) => ( + + + + )) + ;(SmallMockIcon as any).displayName = 'wrapLucideIcon(SmallIcon)' + + const LargeMockIcon = vi.fn((props: any) => ( + + + + )) + ;(LargeMockIcon as any).displayName = 'wrapLucideIcon(LargeIcon)' + + const { container: smallContainer } = render( + } + /> + ) + + const { container: largeContainer } = render( + } + /> + ) + + const smallIcon = smallContainer.querySelector( + '[data-testid="small-icon"]' + ) + const largeIcon = largeContainer.querySelector( + '[data-testid="large-icon"]' + ) + + expect(smallIcon).toBeInTheDocument() + expect(largeIcon).toBeInTheDocument() + + // Verify different sizes were passed correctly + expect(smallIcon).toHaveAttribute('data-size', 'sm') + expect(largeIcon).toHaveAttribute('data-size', '2xl') + }) }) describe('when an image src url is provided', () => { @@ -126,7 +365,7 @@ describe('', () => { it('should display the image even if an icon is provided', async () => { const { container } = render( - } /> + ) const avatarImg = container.querySelector('img') expect(avatarImg).toHaveAttribute('src', src) @@ -195,7 +434,7 @@ describe('', () => { name="Jessica Jones" color="accent2" hasInverseColor - renderIcon={} + renderIcon={UserInstUIIcon} /> ) const element = container.querySelector('div') diff --git a/packages/ui-avatar/src/Avatar/index.tsx b/packages/ui-avatar/src/Avatar/index.tsx index b13a8d39c0..773359bbf6 100644 --- a/packages/ui-avatar/src/Avatar/index.tsx +++ b/packages/ui-avatar/src/Avatar/index.tsx @@ -22,11 +22,14 @@ * SOFTWARE. */ -import { useStyle, useTheme } from '@instructure/emotion' +import { useStyle } from '@instructure/emotion' import { useState, useEffect, forwardRef, SyntheticEvent } from 'react' -import { callRenderProp, passthroughProps } from '@instructure/ui-react-utils' -import type { AvatarProps } from './props' +import { + passthroughProps, + renderLucideIconWithProps +} from '@instructure/ui-react-utils' +import { AvatarProps, avatarSizeToIconSize } from './props' import generateStyle from './styles' @@ -53,8 +56,6 @@ const Avatar = forwardRef( margin } = props const [loaded, setLoaded] = useState(false) - const theme = useTheme() - const iconTokens = (theme as any).newTheme.components.Icon const styles = useStyle({ generateStyle, @@ -68,8 +69,7 @@ const Avatar = forwardRef( src, showBorder, display, - margin, - iconTokens + margin }, componentId: 'Avatar', displayName: 'Avatar' @@ -136,9 +136,14 @@ const Avatar = forwardRef( } //icon in avatar - //TODO-REWORK make the icon inherit the size prop of the Avatar when the icons have it if (renderIcon) { - return callRenderProp(renderIcon) + const iconSize = avatarSizeToIconSize[size] + const iconColor = styles?.iconColor + + return renderLucideIconWithProps(renderIcon, { + size: iconSize, + color: iconColor + }) } //initials in avatar diff --git a/packages/ui-avatar/src/Avatar/props.ts b/packages/ui-avatar/src/Avatar/props.ts index 4a3f3e8703..16433ee702 100644 --- a/packages/ui-avatar/src/Avatar/props.ts +++ b/packages/ui-avatar/src/Avatar/props.ts @@ -35,6 +35,16 @@ import type { } from '@instructure/shared-types' import { Renderable } from '@instructure/shared-types' +const avatarSizeToIconSize = { + 'xx-small': 'xs', + 'x-small': 'xs', + small: 'sm', + medium: 'md', + large: 'lg', + 'x-large': 'xl', + 'xx-large': '2xl' +} as const + type AvatarOwnProps = { /** * The name to display. It will be automatically converted to initials. @@ -48,14 +58,7 @@ type AvatarOwnProps = { * Accessible label */ alt?: string - size?: - | 'xx-small' - | 'x-small' - | 'small' - | 'medium' - | 'large' - | 'x-large' - | 'xx-large' + size?: keyof typeof avatarSizeToIconSize color?: | 'accent1' | 'accent2' @@ -96,8 +99,9 @@ type AvatarOwnProps = { elementRef?: (element: Element | null) => void /** * An icon, or function that returns an icon that gets displayed. If the `src` prop is provided, `src` will have priority. + * When using Lucide icons, Avatar will automatically pass the appropriate size and color props based on the Avatar's size and color. */ - renderIcon?: Renderable + renderIcon?: Renderable<{ size?: string | number; color?: string }> } export type AvatarState = { @@ -112,7 +116,9 @@ type AvatarProps = AvatarOwnProps & { themeOverride?: ThemeOverrideValue } & OtherHTMLAttributes -type AvatarStyle = ComponentStyle<'avatar' | 'image'> +type AvatarStyle = ComponentStyle<'avatar' | 'image'> & { + iconColor?: string +} const allowedProps: AllowedPropKeys = [ 'name', @@ -130,4 +136,4 @@ const allowedProps: AllowedPropKeys = [ ] export type { AvatarProps, AvatarStyle } -export { allowedProps } +export { allowedProps, avatarSizeToIconSize } diff --git a/packages/ui-avatar/src/Avatar/styles.ts b/packages/ui-avatar/src/Avatar/styles.ts index 4edc75dd65..7bf135e17b 100644 --- a/packages/ui-avatar/src/Avatar/styles.ts +++ b/packages/ui-avatar/src/Avatar/styles.ts @@ -36,7 +36,6 @@ type StyleParams = { showBorder: AvatarProps['showBorder'] display: AvatarProps['display'] margin: AvatarProps['margin'] - iconTokens: NewComponentTypes['Icon'] } /** * --- @@ -61,8 +60,7 @@ const generateStyle = ( shape, showBorder, display, - margin, - iconTokens + margin } = params const sizeStyles = { @@ -113,38 +111,31 @@ const generateStyle = ( const colorVariants = { accent1: { text: componentTheme.blueTextColor, - background: componentTheme.blueBackgroundColor, - icon: iconTokens.accentBlueColor + background: componentTheme.blueBackgroundColor }, accent2: { text: componentTheme.greenTextColor, - background: componentTheme.greenBackgroundColor, - icon: iconTokens.accentGreenColor + background: componentTheme.greenBackgroundColor }, accent3: { text: componentTheme.redTextColor, - background: componentTheme.redBackgroundColor, - icon: iconTokens.accentRedColor + background: componentTheme.redBackgroundColor }, accent4: { text: componentTheme.orangeTextColor, - background: componentTheme.orangeBackgroundColor, - icon: iconTokens.accentOrangeColor + background: componentTheme.orangeBackgroundColor }, accent5: { text: componentTheme.greyTextColor, - background: componentTheme.greyBackgroundColor, - icon: iconTokens.accentGreyColor + background: componentTheme.greyBackgroundColor }, accent6: { text: componentTheme.ashTextColor, - background: componentTheme.ashBackgroundColor, - icon: iconTokens.accentAshColor + background: componentTheme.ashBackgroundColor }, ai: { text: componentTheme.textOnColor, - background: `linear-gradient(135deg, ${componentTheme.aiTopGradientColor} 0%, ${componentTheme.aiBottomGradientColor} 100%)`, - icon: componentTheme.textOnColor + background: `linear-gradient(135deg, ${componentTheme.aiTopGradientColor} 0%, ${componentTheme.aiBottomGradientColor} 100%)` } } @@ -162,6 +153,20 @@ const generateStyle = ( return 'solid' } + const iconColorMap = { + accent1: 'accentBlueColor', + accent2: 'accentGreenColor', + accent3: 'accentRedColor', + accent4: 'accentOrangeColor', + accent5: 'accentGreyColor', + accent6: 'accentAshColor', + ai: componentTheme.textOnColor + } + + const iconColor = hasInverseColor + ? componentTheme.textOnColor + : iconColorMap[color!] + return { avatar: { label: 'avatar', @@ -195,7 +200,8 @@ const generateStyle = ( objectFit: 'cover', objectPosition: 'center', ...(loaded ? {} : { display: 'none' }) - } + }, + iconColor } } diff --git a/packages/ui-avatar/tsconfig.build.json b/packages/ui-avatar/tsconfig.build.json index 73379fbf32..99449e4138 100644 --- a/packages/ui-avatar/tsconfig.build.json +++ b/packages/ui-avatar/tsconfig.build.json @@ -33,6 +33,9 @@ }, { "path": "../ui-icons/tsconfig.build.json" + }, + { + "path": "../ui-icons-lucide/tsconfig.build.json" } ] } diff --git a/packages/ui-react-utils/src/__tests__/renderLucideIconWithProps.test.tsx b/packages/ui-react-utils/src/__tests__/renderLucideIconWithProps.test.tsx new file mode 100644 index 0000000000..3081b0282d --- /dev/null +++ b/packages/ui-react-utils/src/__tests__/renderLucideIconWithProps.test.tsx @@ -0,0 +1,433 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import React from 'react' +import { render } from '@testing-library/react' +import { expect, describe, it, vi } from 'vitest' +import '@testing-library/jest-dom' +import { renderLucideIconWithProps } from '../renderLucideIconWithProps' + +describe('renderLucideIconWithProps', () => { + describe('with undefined/null', () => { + it('should return null for undefined', () => { + const result = renderLucideIconWithProps(undefined, { foo: 'bar' }) + expect(result).toBeNull() + }) + + it('should return null for null', () => { + const result = renderLucideIconWithProps(null as any, { foo: 'bar' }) + expect(result).toBeNull() + }) + }) + + describe('with component reference', () => { + it('should render component with props', () => { + const TestComponent = ({ size, color }: any) => ( +
+ Test +
+ ) + TestComponent.displayName = 'wrapLucideIcon(TestComponent)' + + const result = renderLucideIconWithProps(TestComponent, { + size: 'large', + color: 'blue' + }) + + const { getByTestId } = render(<>{result}) + const element = getByTestId('test') + + expect(element).toHaveAttribute('data-size', 'large') + expect(element).toHaveAttribute('data-color', 'blue') + }) + + it('should call component with props', () => { + const mockComponent = vi.fn((props: any) => ( +
{props.text}
+ )) + ;(mockComponent as any).displayName = 'wrapLucideIcon(MockComponent)' + + renderLucideIconWithProps(mockComponent, { text: 'hello' }) + + expect(mockComponent).toHaveBeenCalledWith( + expect.objectContaining({ text: 'hello' }) + ) + }) + }) + + describe('with JSX element', () => { + it('should clone JSX element with props', () => { + const TestComponent = ({ size, color }: any) => ( +
+ Test +
+ ) + TestComponent.displayName = 'wrapLucideIcon(TestComponent)' + + const result = renderLucideIconWithProps(, { + size: 'medium', + color: 'red' + }) + + const { getByTestId } = render(<>{result}) + const element = getByTestId('test') + + expect(element).toHaveAttribute('data-size', 'medium') + expect(element).toHaveAttribute('data-color', 'red') + }) + + it('should override existing props on JSX element', () => { + const TestComponent = ({ size }: any) => ( +
+ Test +
+ ) + TestComponent.displayName = 'wrapLucideIcon(TestComponent)' + + const result = renderLucideIconWithProps(, { + size: 'large' + }) + + const { getByTestId } = render(<>{result}) + const element = getByTestId('test') + + expect(element).toHaveAttribute('data-size', 'large') + }) + }) + + describe('with render function that accepts props', () => { + it('should call function with props and render result', () => { + const renderFunc = ({ size, color }: any) => ( +
+ Test +
+ ) + renderFunc.displayName = 'wrapLucideIcon(renderFunc)' + + const result = renderLucideIconWithProps(renderFunc, { + size: 'small', + color: 'green' + }) + + const { getByTestId } = render(<>{result}) + const element = getByTestId('test') + + expect(element).toHaveAttribute('data-size', 'small') + expect(element).toHaveAttribute('data-color', 'green') + }) + + it('should merge props if function uses them', () => { + const renderFunc = (props: any) => ( +
+ Test +
+ ) + renderFunc.displayName = 'wrapLucideIcon(renderFunc)' + + const result = renderLucideIconWithProps(renderFunc, { + 'data-size': 'medium', + 'data-color': 'yellow' + } as any) + + const { getByTestId } = render(<>{result}) + const element = getByTestId('test') + + expect(element).toHaveAttribute('data-size', 'medium') + expect(element).toHaveAttribute('data-color', 'yellow') + }) + }) + + describe('with render function that ignores props', () => { + it('should apply props to returned JSX even if function ignores them', () => { + const renderFunc = () => ( +
+ Test +
+ ) + renderFunc.displayName = 'wrapLucideIcon(renderFunc)' + + const result = renderLucideIconWithProps(renderFunc, { + 'data-size': 'overridden', + 'data-color': 'purple' + } as any) + + const { getByTestId } = render(<>{result}) + const element = getByTestId('test') + + // Props should be merged/overridden + expect(element).toHaveAttribute('data-size', 'overridden') + expect(element).toHaveAttribute('data-color', 'purple') + }) + + it('should work with arrow function without parameters', () => { + const TestComponent = ({ size }: any) => ( +
+ Test +
+ ) + TestComponent.displayName = 'wrapLucideIcon(TestComponent)' + + const renderFunc = () => + renderFunc.displayName = 'wrapLucideIcon(renderFunc)' + + const result = renderLucideIconWithProps(renderFunc, { + size: 'x-large' + }) + + const { getByTestId } = render(<>{result}) + const element = getByTestId('test') + + expect(element).toHaveAttribute('data-size', 'x-large') + }) + }) + + describe('with React components', () => { + it('should work with class components', () => { + class TestComponent extends React.Component<{ size?: string }> { + render() { + return ( +
+ Test +
+ ) + } + } + ;(TestComponent as any).displayName = 'wrapLucideIcon(TestComponent)' + + const result = renderLucideIconWithProps(TestComponent, { size: 'huge' }) + + const { getByTestId } = render(<>{result}) + const element = getByTestId('test') + + expect(element).toHaveAttribute('data-size', 'huge') + }) + + it('should work with functional components', () => { + const TestComponent = ({ size }: any) => ( +
+ Test +
+ ) + TestComponent.displayName = 'wrapLucideIcon(TestComponent)' + + const result = renderLucideIconWithProps(TestComponent, { size: 'tiny' }) + + const { getByTestId } = render(<>{result}) + const element = getByTestId('test') + + expect(element).toHaveAttribute('data-size', 'tiny') + }) + }) + + describe('prop precedence', () => { + it('should have passed props override original JSX props', () => { + const TestComponent = ({ a, b, c }: any) => ( +
+ Test +
+ ) + TestComponent.displayName = 'wrapLucideIcon(TestComponent)' + + const result = renderLucideIconWithProps(, { + b: '20', + c: '30' + }) + + const { getByTestId } = render(<>{result}) + const element = getByTestId('test') + + expect(element).toHaveAttribute('data-a', '1') // Original preserved + expect(element).toHaveAttribute('data-b', '20') // Overridden + expect(element).toHaveAttribute('data-c', '30') // Added + }) + + it('should have passed props override function-returned JSX props', () => { + const renderFunc = () => ( +
+ Test +
+ ) + renderFunc.displayName = 'wrapLucideIcon(renderFunc)' + + const result = renderLucideIconWithProps(renderFunc, { + 'data-x': 'new', + 'data-z': 'added' + } as any) + + const { getByTestId } = render(<>{result}) + const element = getByTestId('test') + + expect(element).toHaveAttribute('data-x', 'new') // Overridden + expect(element).toHaveAttribute('data-y', 'original') // Preserved + expect(element).toHaveAttribute('data-z', 'added') // Added + }) + }) + + describe('Lucide icon detection', () => { + it('should apply props to Lucide icons (detected by displayName)', () => { + const LucideIcon = ({ size, color }: any) => ( + + + + ) + LucideIcon.displayName = 'wrapLucideIcon(UserIcon)' + + const result = renderLucideIconWithProps(LucideIcon, { + size: 'lg', + color: 'blue' + }) + + const { getByTestId } = render(<>{result}) + const element = getByTestId('lucide-icon') + + expect(element).toHaveAttribute('data-size', 'lg') + expect(element).toHaveAttribute('data-color', 'blue') + }) + + it('should NOT apply props to non-Lucide icons', () => { + const NonLucideIcon = ({ size }: any) => ( + + + + ) + + const result = renderLucideIconWithProps(NonLucideIcon, { + size: 'lg', + color: 'blue' + }) + + const { getByTestId } = render(<>{result}) + const element = getByTestId('non-lucide-icon') + + // Should render with default value, not the passed prop + expect(element).toHaveAttribute('data-size', 'default') + expect(element).not.toHaveAttribute('data-color') + }) + + it('should detect Lucide icon from JSX element', () => { + const LucideIcon = ({ size }: any) => ( + + + + ) + LucideIcon.displayName = 'wrapLucideIcon(CircleIcon)' + + const result = renderLucideIconWithProps(, { + size: 'md' + }) + + const { getByTestId } = render(<>{result}) + const element = getByTestId('lucide-jsx') + + expect(element).toHaveAttribute('data-size', 'md') + }) + + it('should NOT apply props to JSX element without Lucide displayName', () => { + const NonLucideIcon = ({ size }: any) => ( + + + + ) + + const result = renderLucideIconWithProps(, { + size: 'xl' + }) + + const { getByTestId } = render(<>{result}) + const element = getByTestId('non-lucide-jsx') + + expect(element).toHaveAttribute('data-size', 'default') + }) + + it('should detect Lucide icon from render function', () => { + const renderFunc = ({ size }: any) => ( + + + + ) + renderFunc.displayName = 'wrapLucideIcon(StarIcon)' + + const result = renderLucideIconWithProps(renderFunc, { size: 'sm' }) + + const { getByTestId } = render(<>{result}) + const element = getByTestId('lucide-func') + + expect(element).toHaveAttribute('data-size', 'sm') + }) + + it('should NOT apply props to render function without Lucide displayName', () => { + const renderFunc = ({ size }: any) => ( + + + + ) + + const result = renderLucideIconWithProps(renderFunc, { size: 'lg' }) + + const { getByTestId } = render(<>{result}) + const element = getByTestId('non-lucide-func') + + expect(element).toHaveAttribute('data-size', 'default') + }) + + it('should detect Lucide icon from arrow function returning JSX without displayName', () => { + const LucideIcon = ({ size }: any) => ( + + + + ) + LucideIcon.displayName = 'wrapLucideIcon(ArrowIcon)' + + // Arrow function without displayName that returns a Lucide icon + const renderFunc = () => + + const result = renderLucideIconWithProps(renderFunc, { size: 'lg' }) + + const { getByTestId } = render(<>{result}) + const element = getByTestId('lucide-arrow') + + // Should apply size prop even though arrow function has no displayName + expect(element).toHaveAttribute('data-size', 'lg') + }) + + it('should handle arrow function returning non-Lucide icon', () => { + const NonLucideIcon = ({ size }: any) => ( + + + + ) + + // Arrow function without displayName that returns a non-Lucide icon + const renderFunc = () => + + const result = renderLucideIconWithProps(renderFunc, { size: 'xl' }) + + const { getByTestId } = render(<>{result}) + const element = getByTestId('non-lucide-arrow') + + // Should NOT apply size prop + expect(element).toHaveAttribute('data-size', 'default') + }) + }) +}) diff --git a/packages/ui-react-utils/src/index.ts b/packages/ui-react-utils/src/index.ts index 253aa7a5a9..02e42a2ef6 100644 --- a/packages/ui-react-utils/src/index.ts +++ b/packages/ui-react-utils/src/index.ts @@ -32,6 +32,7 @@ export { matchComponentTypes } from './matchComponentTypes' export { omitProps } from './omitProps' export { passthroughProps } from './passthroughProps' export { pickProps } from './pickProps' +export { renderLucideIconWithProps } from './renderLucideIconWithProps' export { safeCloneElement } from './safeCloneElement' export { windowMessageListener } from './windowMessageListener' export { diff --git a/packages/ui-react-utils/src/renderLucideIconWithProps.ts b/packages/ui-react-utils/src/renderLucideIconWithProps.ts new file mode 100644 index 0000000000..19865d1570 --- /dev/null +++ b/packages/ui-react-utils/src/renderLucideIconWithProps.ts @@ -0,0 +1,111 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import React from 'react' +import type { Renderable } from '@instructure/shared-types' +import { callRenderProp } from './callRenderProp' + +/** + * Check if an element/component is a Lucide icon by checking its displayName + * @param element - Element or component to check + * @returns True if it's a Lucide icon (displayName starts with 'wrapLucideIcon(') + */ +const isLucideIcon = (element: any): boolean => { + if (React.isValidElement(element)) { + const displayName = (element.type as any)?.displayName + return displayName?.startsWith('wrapLucideIcon(') + } + return element?.displayName?.startsWith('wrapLucideIcon(') +} + +/** + * --- + * category: utilities/react + * --- + * Renders a Lucide icon with props, handling multiple input formats. + * Only applies props if the icon is a Lucide icon (detected via displayName). + * For non-Lucide icons, renders without applying props. + * + * Supported use cases: + * 1. Component/function with Lucide displayName: `renderIcon={UserIcon}` - Calls with props + * 2. JSX element: `renderIcon={}` - Clones and overrides props + * 3. Arrow function returning Lucide: `renderIcon={() => }` - Detects Lucide from result + * + * @module renderLucideIconWithProps + * @param elementToRender - The element to render (component/JSX/function) + * @param propsToApply - Props to pass to or override on the element (only for Lucide icons) + * @returns Rendered React element or null + */ +function renderLucideIconWithProps

>( + elementToRender: Renderable

| React.ReactElement | undefined, + propsToApply: P +): React.ReactElement | null { + if (!elementToRender) return null + + // Check once if the input is a Lucide icon + const isInputLucide = isLucideIcon(elementToRender) + + // Use case 2: JSX element like + if (React.isValidElement(elementToRender)) { + return isInputLucide + ? React.cloneElement(elementToRender, propsToApply as any) + : elementToRender + } + + // Use cases 1 & 3: Component/function reference + // callRenderProp "extracts" the JSX element by either: + // 1. Creating it from a component reference: React.createElement(UserIcon, props) + // 2. Calling the function to get the JSX it returns: (() => )() + // The extracted result is then checked below to apply props if needed (use case 3) + const result = callRenderProp( + elementToRender, + isInputLucide ? propsToApply : ({} as P) + ) + + // Check if the result is a React element that needs props applied + if (React.isValidElement(result)) { + // Use case 3: Arrow function returning Lucide icon (e.g., () => ) + // If function wasn't directly Lucide but result is, apply props now + if (!isInputLucide && isLucideIcon(result)) { + return React.cloneElement(result, { + ...(result.props as Record), + ...propsToApply + }) + } + + // Use case 1: Function with Lucide displayName that ignored props + // Force props application by cloning the result + if (isInputLucide) { + return React.cloneElement(result, { + ...(result.props as Record), + ...propsToApply + }) + } + } + + return result +} + +export default renderLucideIconWithProps +export { renderLucideIconWithProps } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 519e25dc51..a00a11b199 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1035,6 +1035,9 @@ importers: '@instructure/ui-color-utils': specifier: workspace:* version: link:../ui-color-utils + '@instructure/ui-icons-lucide': + specifier: workspace:* + version: link:../ui-icons-lucide '@instructure/ui-themes': specifier: workspace:* version: link:../ui-themes From 32c91bac42d2f1350bcc8c53f52b2474bdd1bcd4 Mon Sep 17 00:00:00 2001 From: Peter Pal Hudak Date: Fri, 5 Dec 2025 10:23:01 +0100 Subject: [PATCH 2/7] refactor(ui-react-utils): changes after review --- .../src/renderLucideIconWithProps.ts | 32 ++++++------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/packages/ui-react-utils/src/renderLucideIconWithProps.ts b/packages/ui-react-utils/src/renderLucideIconWithProps.ts index 19865d1570..4207366390 100644 --- a/packages/ui-react-utils/src/renderLucideIconWithProps.ts +++ b/packages/ui-react-utils/src/renderLucideIconWithProps.ts @@ -58,7 +58,7 @@ const isLucideIcon = (element: any): boolean => { * @returns Rendered React element or null */ function renderLucideIconWithProps

>( - elementToRender: Renderable

| React.ReactElement | undefined, + elementToRender: Renderable

, propsToApply: P ): React.ReactElement | null { if (!elementToRender) return null @@ -69,7 +69,7 @@ function renderLucideIconWithProps

>( // Use case 2: JSX element like if (React.isValidElement(elementToRender)) { return isInputLucide - ? React.cloneElement(elementToRender, propsToApply as any) + ? React.cloneElement(elementToRender, propsToApply) : elementToRender } @@ -77,31 +77,19 @@ function renderLucideIconWithProps

>( // callRenderProp "extracts" the JSX element by either: // 1. Creating it from a component reference: React.createElement(UserIcon, props) // 2. Calling the function to get the JSX it returns: (() => )() - // The extracted result is then checked below to apply props if needed (use case 3) const result = callRenderProp( elementToRender, isInputLucide ? propsToApply : ({} as P) ) - // Check if the result is a React element that needs props applied - if (React.isValidElement(result)) { - // Use case 3: Arrow function returning Lucide icon (e.g., () => ) - // If function wasn't directly Lucide but result is, apply props now - if (!isInputLucide && isLucideIcon(result)) { - return React.cloneElement(result, { - ...(result.props as Record), - ...propsToApply - }) - } - - // Use case 1: Function with Lucide displayName that ignored props - // Force props application by cloning the result - if (isInputLucide) { - return React.cloneElement(result, { - ...(result.props as Record), - ...propsToApply - }) - } + // Apply props if result is a valid element and either: + // - Input was Lucide (trust that we should apply props to whatever it returns) + // - Result itself is Lucide (arrow function returned a Lucide icon) + if (React.isValidElement(result) && (isInputLucide || isLucideIcon(result))) { + return React.cloneElement(result, { + ...(result.props as Record), + ...propsToApply + }) } return result From 54f0d86ebe00bf6c89c6360b73ae1ab9a1f6e2c7 Mon Sep 17 00:00:00 2001 From: Matyas Forian-Szabo Date: Tue, 9 Dec 2025 11:34:27 +0100 Subject: [PATCH 3/7] docs(ui-react-utils,ui-icons-lucide,ui-avatar): add code improvement comments --- .../ui-avatar/src/Avatar/__tests__/Avatar.test.tsx | 2 +- packages/ui-avatar/src/Avatar/index.tsx | 1 + packages/ui-avatar/src/Avatar/styles.ts | 2 +- .../ui-icons-lucide/src/wrapLucideIcon/index.tsx | 12 ++++++++---- packages/ui-icons-lucide/src/wrapLucideIcon/props.ts | 5 ++++- .../src/__tests__/renderLucideIconWithProps.test.tsx | 1 + .../ui-react-utils/src/renderLucideIconWithProps.ts | 11 +++++++---- 7 files changed, 23 insertions(+), 11 deletions(-) diff --git a/packages/ui-avatar/src/Avatar/__tests__/Avatar.test.tsx b/packages/ui-avatar/src/Avatar/__tests__/Avatar.test.tsx index c90ab607f2..c394e3e410 100644 --- a/packages/ui-avatar/src/Avatar/__tests__/Avatar.test.tsx +++ b/packages/ui-avatar/src/Avatar/__tests__/Avatar.test.tsx @@ -117,7 +117,7 @@ describe('', () => { )) - ;(MockIcon as any).displayName = 'wrapLucideIcon(MockIcon)' + ;(MockIcon as any).displayName = 'wrapLucideIcon(MockIcon)' // TODO why mock the icon? Why not use a real one? const { container } = render( diff --git a/packages/ui-avatar/src/Avatar/index.tsx b/packages/ui-avatar/src/Avatar/index.tsx index 773359bbf6..95a7a89579 100644 --- a/packages/ui-avatar/src/Avatar/index.tsx +++ b/packages/ui-avatar/src/Avatar/index.tsx @@ -138,6 +138,7 @@ const Avatar = forwardRef( //icon in avatar if (renderIcon) { const iconSize = avatarSizeToIconSize[size] + // TODO we should never do this, do not create a fake style const iconColor = styles?.iconColor return renderLucideIconWithProps(renderIcon, { diff --git a/packages/ui-avatar/src/Avatar/styles.ts b/packages/ui-avatar/src/Avatar/styles.ts index 7bf135e17b..dab916f79c 100644 --- a/packages/ui-avatar/src/Avatar/styles.ts +++ b/packages/ui-avatar/src/Avatar/styles.ts @@ -160,7 +160,7 @@ const generateStyle = ( accent4: 'accentOrangeColor', accent5: 'accentGreyColor', accent6: 'accentAshColor', - ai: componentTheme.textOnColor + ai: componentTheme.textOnColor // TODO why is this an exception? if this is not here it can work nicely } const iconColor = hasInverseColor diff --git a/packages/ui-icons-lucide/src/wrapLucideIcon/index.tsx b/packages/ui-icons-lucide/src/wrapLucideIcon/index.tsx index a877aede31..cc6107b32e 100644 --- a/packages/ui-icons-lucide/src/wrapLucideIcon/index.tsx +++ b/packages/ui-icons-lucide/src/wrapLucideIcon/index.tsx @@ -37,6 +37,7 @@ import generateStyle from './styles' * native Lucide props (size={24}, color="#ff0000"). */ export function wrapLucideIcon(Icon: LucideIcon): LucideIcon { + // TODO why is this returning LucideIcon we should have our own API const WrappedIcon = (props: LucideIconWrapperProps) => { const { size, @@ -53,7 +54,7 @@ export function wrapLucideIcon(Icon: LucideIcon): LucideIcon { style, ...rest } = props - + // TODO we should never do this const theme = useTheme() as Theme const iconTheme = theme?.newTheme?.components?.Icon @@ -67,7 +68,9 @@ export function wrapLucideIcon(Icon: LucideIcon): LucideIcon { let numericSize: number | undefined let semanticSize: string | undefined if (typeof size === 'string' && iconTheme) { + // TODO why is tons of "&& iconTheme"? why not bail at the beginning? // Construct theme property name (e.g., 'xs' -> 'sizeXs') + // TODO why convert here back? Why not use `sizeXs` as prop? const propName = `size${size.charAt(0).toUpperCase()}${size.slice( 1 )}` as keyof typeof iconTheme @@ -108,9 +111,9 @@ export function wrapLucideIcon(Icon: LucideIcon): LucideIcon { } else if ( iconTheme && color in iconTheme && - !color.startsWith('size') && + !color.startsWith('size') && // TODO this would be much simpler if color would be enum !color.startsWith('strokeWidth') && - color !== 'dark' + color !== 'dark' // TODO what is dark?? ) { // Semantic color token from theme (exclude size/strokeWidth/dark properties) colorValue = color @@ -144,6 +147,7 @@ export function wrapLucideIcon(Icon: LucideIcon): LucideIcon { } return ( + // TODO why apply here className and style?? ) diff --git a/packages/ui-icons-lucide/src/wrapLucideIcon/props.ts b/packages/ui-icons-lucide/src/wrapLucideIcon/props.ts index 7b0968e803..9764e0b928 100644 --- a/packages/ui-icons-lucide/src/wrapLucideIcon/props.ts +++ b/packages/ui-icons-lucide/src/wrapLucideIcon/props.ts @@ -32,6 +32,7 @@ import type { OtherHTMLAttributes } from '@instructure/shared-types' * and transform to lowercase literals ('xs', 'sm', etc.) */ type ExtractSizeTokens = { + // TODO why this complexity? hardcoding would be much better [K in keyof T]: K extends `size${infer Size}` ? Lowercase : never }[keyof T] @@ -40,6 +41,7 @@ type ExtractSizeTokens = { * and transform to lowercase literals ('xs', 'sm', etc.) */ type ExtractStrokeWidthTokens = { + // TODO why this complexity? hardcoding would be much better [K in keyof T]: K extends `strokeWidth${infer Size}` ? Lowercase : never }[keyof T] @@ -48,8 +50,9 @@ type ExtractStrokeWidthTokens = { * (all properties except size/strokeWidth and 'dark') */ type ExtractColorTokens = Exclude< + // TODO why this complexity? hardcoding would be much better keyof T, - `size${string}` | `strokeWidth${string}` | 'dark' + `size${string}` | `strokeWidth${string}` | 'dark' // TODO what is 'dark'? > type IconSizeToken = ExtractSizeTokens diff --git a/packages/ui-react-utils/src/__tests__/renderLucideIconWithProps.test.tsx b/packages/ui-react-utils/src/__tests__/renderLucideIconWithProps.test.tsx index 3081b0282d..8946a12f31 100644 --- a/packages/ui-react-utils/src/__tests__/renderLucideIconWithProps.test.tsx +++ b/packages/ui-react-utils/src/__tests__/renderLucideIconWithProps.test.tsx @@ -43,6 +43,7 @@ describe('renderLucideIconWithProps', () => { describe('with component reference', () => { it('should render component with props', () => { + // TODO why fake here an icon? Why not use a real one? const TestComponent = ({ size, color }: any) => (

Test diff --git a/packages/ui-react-utils/src/renderLucideIconWithProps.ts b/packages/ui-react-utils/src/renderLucideIconWithProps.ts index 4207366390..297b07ebab 100644 --- a/packages/ui-react-utils/src/renderLucideIconWithProps.ts +++ b/packages/ui-react-utils/src/renderLucideIconWithProps.ts @@ -33,10 +33,11 @@ import { callRenderProp } from './callRenderProp' */ const isLucideIcon = (element: any): boolean => { if (React.isValidElement(element)) { + // like const displayName = (element.type as any)?.displayName return displayName?.startsWith('wrapLucideIcon(') } - return element?.displayName?.startsWith('wrapLucideIcon(') + return element?.displayName?.startsWith('wrapLucideIcon(') // like `UserIcon` } /** @@ -63,17 +64,17 @@ function renderLucideIconWithProps

>( ): React.ReactElement | null { if (!elementToRender) return null - // Check once if the input is a Lucide icon + // Check once if the input is a Lucide icon e.g. `` or `UserIcon` const isInputLucide = isLucideIcon(elementToRender) - // Use case 2: JSX element like + // Use case 1: JSX element like if (React.isValidElement(elementToRender)) { return isInputLucide ? React.cloneElement(elementToRender, propsToApply) : elementToRender } - // Use cases 1 & 3: Component/function reference + // Use case 2: `UserIcon` or `() => ` // callRenderProp "extracts" the JSX element by either: // 1. Creating it from a component reference: React.createElement(UserIcon, props) // 2. Calling the function to get the JSX it returns: (() => )() @@ -85,8 +86,10 @@ function renderLucideIconWithProps

>( // Apply props if result is a valid element and either: // - Input was Lucide (trust that we should apply props to whatever it returns) // - Result itself is Lucide (arrow function returned a Lucide icon) + // TODO isInputLucide check is needed here? isLucideIcon(result) is not enough? if (React.isValidElement(result) && (isInputLucide || isLucideIcon(result))) { return React.cloneElement(result, { + // TODO didnt callRenderProp already apply the props? ...(result.props as Record), ...propsToApply }) From 543b72a15dc826ffa19944ff870ece265f16dce6 Mon Sep 17 00:00:00 2001 From: Peter Pal Hudak Date: Tue, 9 Dec 2025 19:01:44 +0100 Subject: [PATCH 4/7] refactor(ui-icons-lucide,ui-avatar): review fixes --- .../__docs__/src/Icons/LucideIconsGallery.tsx | 1 - .../src/Avatar/__tests__/Avatar.test.tsx | 255 +--------- packages/ui-avatar/src/Avatar/index.tsx | 28 +- packages/ui-avatar/src/Avatar/props.ts | 10 +- packages/ui-avatar/src/Avatar/styles.ts | 17 +- .../src/wrapLucideIcon/index.tsx | 101 +--- .../src/wrapLucideIcon/props.ts | 114 ++++- .../src/wrapLucideIcon/styles.ts | 109 ++++- .../src/IconPropsProvider/IconPropsContext.ts | 30 ++ .../IconPropsProvider/IconPropsProvider.tsx | 49 ++ .../src/IconPropsProvider/index.ts | 28 ++ .../src/IconPropsProvider/useIconProps.tsx | 32 ++ .../src/__tests__/IconPropsProvider.test.tsx | 161 +++++++ .../renderLucideIconWithProps.test.tsx | 434 ------------------ packages/ui-react-utils/src/index.ts | 7 +- .../src/renderLucideIconWithProps.ts | 102 ---- 16 files changed, 551 insertions(+), 927 deletions(-) create mode 100644 packages/ui-react-utils/src/IconPropsProvider/IconPropsContext.ts create mode 100644 packages/ui-react-utils/src/IconPropsProvider/IconPropsProvider.tsx create mode 100644 packages/ui-react-utils/src/IconPropsProvider/index.ts create mode 100644 packages/ui-react-utils/src/IconPropsProvider/useIconProps.tsx create mode 100644 packages/ui-react-utils/src/__tests__/IconPropsProvider.test.tsx delete mode 100644 packages/ui-react-utils/src/__tests__/renderLucideIconWithProps.test.tsx delete mode 100644 packages/ui-react-utils/src/renderLucideIconWithProps.ts diff --git a/packages/__docs__/src/Icons/LucideIconsGallery.tsx b/packages/__docs__/src/Icons/LucideIconsGallery.tsx index 8dd0f5a335..efad270b36 100644 --- a/packages/__docs__/src/Icons/LucideIconsGallery.tsx +++ b/packages/__docs__/src/Icons/LucideIconsGallery.tsx @@ -299,7 +299,6 @@ const LucideIconsGallery = () => {

  • strokeWidth: Number - e.g., 2
  • -
  • Plus all standard SVG props (className, style, etc.)
  • These icons use the pure Lucide API. See{' '} diff --git a/packages/ui-avatar/src/Avatar/__tests__/Avatar.test.tsx b/packages/ui-avatar/src/Avatar/__tests__/Avatar.test.tsx index c394e3e410..d938661646 100644 --- a/packages/ui-avatar/src/Avatar/__tests__/Avatar.test.tsx +++ b/packages/ui-avatar/src/Avatar/__tests__/Avatar.test.tsx @@ -29,10 +29,7 @@ import { runAxeCheck } from '@instructure/ui-axe-check' import '@testing-library/jest-dom' import Avatar from '../index' import { IconGroupLine } from '@instructure/ui-icons' -import { - UserInstUIIcon, - CircleUserInstUIIcon -} from '@instructure/ui-icons-lucide' +import { HeartInstUIIcon } from '@instructure/ui-icons-lucide' describe('', () => { describe('for a11y', () => { @@ -82,14 +79,9 @@ describe('', () => { }) describe('when the renderIcon prop is provided', () => { - it('should display an svg passed', async () => { - const SomeIcon = () => ( - - - - ) + it('should display a Lucide icon when passed as component reference', async () => { const { container } = render( - + hello ) @@ -107,249 +99,30 @@ describe('', () => { expect(avatarSvg).toBeInTheDocument() }) - it('should pass the correct size and color props to icon based on Avatar size', async () => { - const MockIcon = vi.fn((props: any) => ( - - - - )) - ;(MockIcon as any).displayName = 'wrapLucideIcon(MockIcon)' // TODO why mock the icon? Why not use a real one? - - const { container } = render( - - ) - - expect(MockIcon).toHaveBeenCalledWith( - expect.objectContaining({ size: 'md', color: expect.any(String) }) - ) - const icon = container.querySelector('[data-testid="mock-icon"]') - expect(icon).toHaveAttribute('data-size', 'md') - expect(icon).toHaveAttribute('data-color') - }) - - it('should map xx-small Avatar to xs icon size', async () => { - const MockIcon = vi.fn(() => ( - - - - )) - ;(MockIcon as any).displayName = 'wrapLucideIcon(MockIcon)' - - render( - - ) - - expect(MockIcon).toHaveBeenCalledWith( - expect.objectContaining({ size: 'xs', color: expect.any(String) }) - ) - }) - - it('should map x-small Avatar to xs icon size', async () => { - const MockIcon = vi.fn(() => ( - - - - )) - ;(MockIcon as any).displayName = 'wrapLucideIcon(MockIcon)' - - render() - - expect(MockIcon).toHaveBeenCalledWith( - expect.objectContaining({ size: 'xs', color: expect.any(String) }) - ) - }) - - it('should work with icons that ignore the size prop (backwards compatibility)', async () => { - const IconWithoutSize = () => ( - - - - ) - - const { container } = render( - - ) - - const icon = container.querySelector('[data-testid="icon-without-size"]') - expect(icon).toBeInTheDocument() - }) - - it('should display a Lucide icon with default size', async () => { - const { container } = render( - - ) - - const avatarSvg = container.querySelector('svg') - expect(avatarSvg).toBeInTheDocument() - }) - - it('should display a Lucide icon with medium Avatar size', async () => { - const { container } = render( - - ) - - const avatarSvg = container.querySelector('svg') - expect(avatarSvg).toBeInTheDocument() - }) - - it('should display a Lucide icon with xx-small Avatar size', async () => { + it('should render Lucide icon when passed as JSX element', async () => { const { container } = render( } /> ) - const avatarSvg = container.querySelector('svg') - expect(avatarSvg).toBeInTheDocument() - }) - - it('should display a Lucide icon with x-small Avatar size', async () => { - const { container } = render( - - ) - - const avatarSvg = container.querySelector('svg') - expect(avatarSvg).toBeInTheDocument() + const svg = container.querySelector('svg') + expect(svg).toBeInTheDocument() }) - it('should accept a JSX element and clone it with size/color props', async () => { - const MockIcon = vi.fn((props: any) => ( - - - - )) - ;(MockIcon as any).displayName = 'wrapLucideIcon(MockIcon)' - - const { container } = render( - } /> - ) - - const icon = container.querySelector('[data-testid="jsx-icon"]') - expect(icon).toBeInTheDocument() - expect(icon).toHaveAttribute('data-size', 'lg') - expect(icon).toHaveAttribute('data-color') - }) - - it('should override props when JSX element is passed', async () => { - const MockIcon = (props: any) => ( - - - - ) - MockIcon.displayName = 'wrapLucideIcon(MockIcon)' - + it('should render Lucide icon from render function', async () => { const { container } = render( } - /> - ) - - const icon = container.querySelector('[data-testid="override-icon"]') - expect(icon).toBeInTheDocument() - // Avatar should override the size prop - expect(icon).toHaveAttribute('data-size', 'xl') - }) - - it('should work with a render function returning JSX', async () => { - const renderFunc = (props: any) => ( - - - - ) - renderFunc.displayName = 'wrapLucideIcon(renderFunc)' - - const { container } = render( - - ) - - const icon = container.querySelector('[data-testid="function-icon"]') - expect(icon).toBeInTheDocument() - expect(icon).toHaveAttribute('data-size', 'sm') - }) - - it('should work with arrow function returning Lucide icon (user pattern)', async () => { - const MockIcon = vi.fn((props: any) => ( - - - - )) - ;(MockIcon as any).displayName = 'wrapLucideIcon(MockIcon)' - - const { container } = render( - } - /> - ) - - const icon = container.querySelector('[data-testid="arrow-lucide-icon"]') - expect(icon).toBeInTheDocument() - // The icon should have received the correct size prop - expect(icon).toHaveAttribute('data-size', 'sm') - }) - - it('should apply different sizes to arrow function icons', async () => { - const SmallMockIcon = vi.fn((props: any) => ( - - - - )) - ;(SmallMockIcon as any).displayName = 'wrapLucideIcon(SmallIcon)' - - const LargeMockIcon = vi.fn((props: any) => ( - - - - )) - ;(LargeMockIcon as any).displayName = 'wrapLucideIcon(LargeIcon)' - - const { container: smallContainer } = render( - } + renderIcon={() => } /> ) - const { container: largeContainer } = render( - } - /> - ) - - const smallIcon = smallContainer.querySelector( - '[data-testid="small-icon"]' - ) - const largeIcon = largeContainer.querySelector( - '[data-testid="large-icon"]' - ) - - expect(smallIcon).toBeInTheDocument() - expect(largeIcon).toBeInTheDocument() - - // Verify different sizes were passed correctly - expect(smallIcon).toHaveAttribute('data-size', 'sm') - expect(largeIcon).toHaveAttribute('data-size', '2xl') + const svg = container.querySelector('svg') + expect(svg).toBeInTheDocument() }) }) @@ -365,7 +138,7 @@ describe('', () => { it('should display the image even if an icon is provided', async () => { const { container } = render( - + ) const avatarImg = container.querySelector('img') expect(avatarImg).toHaveAttribute('src', src) @@ -434,7 +207,7 @@ describe('', () => { name="Jessica Jones" color="accent2" hasInverseColor - renderIcon={UserInstUIIcon} + renderIcon={HeartInstUIIcon} /> ) const element = container.querySelector('div') diff --git a/packages/ui-avatar/src/Avatar/index.tsx b/packages/ui-avatar/src/Avatar/index.tsx index 95a7a89579..aa3fd3ebf5 100644 --- a/packages/ui-avatar/src/Avatar/index.tsx +++ b/packages/ui-avatar/src/Avatar/index.tsx @@ -23,16 +23,26 @@ */ import { useStyle } from '@instructure/emotion' -import { useState, useEffect, forwardRef, SyntheticEvent } from 'react' +import React, { useState, useEffect, forwardRef, SyntheticEvent } from 'react' import { passthroughProps, - renderLucideIconWithProps + IconPropsProvider } from '@instructure/ui-react-utils' import { AvatarProps, avatarSizeToIconSize } from './props' import generateStyle from './styles' +const ICON_COLOR_MAP = { + accent1: 'accentBlueColor', + accent2: 'accentGreenColor', + accent3: 'accentRedColor', + accent4: 'accentOrangeColor', + accent5: 'accentGreyColor', + accent6: 'accentAshColor', + ai: 'onColor' +} as const + /** --- category: components @@ -138,13 +148,15 @@ const Avatar = forwardRef( //icon in avatar if (renderIcon) { const iconSize = avatarSizeToIconSize[size] - // TODO we should never do this, do not create a fake style - const iconColor = styles?.iconColor + const iconColor = hasInverseColor ? 'onColor' : ICON_COLOR_MAP[color] - return renderLucideIconWithProps(renderIcon, { - size: iconSize, - color: iconColor - }) + return ( + + {typeof renderIcon === 'function' + ? React.createElement(renderIcon as any) + : (renderIcon as React.ReactElement)} + + ) } //initials in avatar diff --git a/packages/ui-avatar/src/Avatar/props.ts b/packages/ui-avatar/src/Avatar/props.ts index 16433ee702..14b6d01a63 100644 --- a/packages/ui-avatar/src/Avatar/props.ts +++ b/packages/ui-avatar/src/Avatar/props.ts @@ -31,9 +31,9 @@ import type { } from '@instructure/emotion' import type { AsElementType, - OtherHTMLAttributes + OtherHTMLAttributes, + Renderable } from '@instructure/shared-types' -import { Renderable } from '@instructure/shared-types' const avatarSizeToIconSize = { 'xx-small': 'xs', @@ -101,7 +101,7 @@ type AvatarOwnProps = { * An icon, or function that returns an icon that gets displayed. If the `src` prop is provided, `src` will have priority. * When using Lucide icons, Avatar will automatically pass the appropriate size and color props based on the Avatar's size and color. */ - renderIcon?: Renderable<{ size?: string | number; color?: string }> + renderIcon?: Renderable } export type AvatarState = { @@ -116,9 +116,7 @@ type AvatarProps = AvatarOwnProps & { themeOverride?: ThemeOverrideValue } & OtherHTMLAttributes -type AvatarStyle = ComponentStyle<'avatar' | 'image'> & { - iconColor?: string -} +type AvatarStyle = ComponentStyle<'avatar' | 'image'> const allowedProps: AllowedPropKeys = [ 'name', diff --git a/packages/ui-avatar/src/Avatar/styles.ts b/packages/ui-avatar/src/Avatar/styles.ts index dab916f79c..61627a1a42 100644 --- a/packages/ui-avatar/src/Avatar/styles.ts +++ b/packages/ui-avatar/src/Avatar/styles.ts @@ -153,20 +153,6 @@ const generateStyle = ( return 'solid' } - const iconColorMap = { - accent1: 'accentBlueColor', - accent2: 'accentGreenColor', - accent3: 'accentRedColor', - accent4: 'accentOrangeColor', - accent5: 'accentGreyColor', - accent6: 'accentAshColor', - ai: componentTheme.textOnColor // TODO why is this an exception? if this is not here it can work nicely - } - - const iconColor = hasInverseColor - ? componentTheme.textOnColor - : iconColorMap[color!] - return { avatar: { label: 'avatar', @@ -200,8 +186,7 @@ const generateStyle = ( objectFit: 'cover', objectPosition: 'center', ...(loaded ? {} : { display: 'none' }) - }, - iconColor + } } } diff --git a/packages/ui-icons-lucide/src/wrapLucideIcon/index.tsx b/packages/ui-icons-lucide/src/wrapLucideIcon/index.tsx index cc6107b32e..ebf76d860b 100644 --- a/packages/ui-icons-lucide/src/wrapLucideIcon/index.tsx +++ b/packages/ui-icons-lucide/src/wrapLucideIcon/index.tsx @@ -22,10 +22,9 @@ * SOFTWARE. */ -import { useStyle, useTheme } from '@instructure/emotion' -import { px } from '@instructure/ui-utils' -import { passthroughProps } from '@instructure/ui-react-utils' -import type { Theme } from '@instructure/ui-themes' +import React from 'react' +import { useStyle } from '@instructure/emotion' +import { passthroughProps, useIconProps } from '@instructure/ui-react-utils' import type { LucideIcon } from 'lucide-react' import type { LucideIconWrapperProps, InstUIIconOwnProps } from './props' @@ -36,8 +35,9 @@ import generateStyle from './styles' * Supports both InstUI semantic props (size="lg", color="baseColor") and * native Lucide props (size={24}, color="#ff0000"). */ -export function wrapLucideIcon(Icon: LucideIcon): LucideIcon { - // TODO why is this returning LucideIcon we should have our own API +export function wrapLucideIcon( + Icon: LucideIcon +): React.ComponentType { const WrappedIcon = (props: LucideIconWrapperProps) => { const { size, @@ -50,13 +50,15 @@ export function wrapLucideIcon(Icon: LucideIcon): LucideIcon { elementRef, themeOverride, absoluteStrokeWidth, - className, - style, ...rest } = props - // TODO we should never do this - const theme = useTheme() as Theme - const iconTheme = theme?.newTheme?.components?.Icon + + // Get icon props from context (if available) + const contextProps = useIconProps() + + // Merge props: direct props take precedence over context props + const finalSize = size ?? contextProps?.size + const finalColor = color ?? contextProps?.color const handleElementRef = (el: SVGSVGElement | null) => { if (typeof elementRef === 'function') { @@ -64,72 +66,14 @@ export function wrapLucideIcon(Icon: LucideIcon): LucideIcon { } } - // Convert semantic size to pixels for Lucide - let numericSize: number | undefined - let semanticSize: string | undefined - if (typeof size === 'string' && iconTheme) { - // TODO why is tons of "&& iconTheme"? why not bail at the beginning? - // Construct theme property name (e.g., 'xs' -> 'sizeXs') - // TODO why convert here back? Why not use `sizeXs` as prop? - const propName = `size${size.charAt(0).toUpperCase()}${size.slice( - 1 - )}` as keyof typeof iconTheme - if (propName in iconTheme) { - // Semantic size token from theme - semanticSize = size - numericSize = px(iconTheme[propName] as string) - } - } else if (typeof size === 'number') { - numericSize = size - } - - // Convert semantic strokeWidth to pixels for Lucide - let numericStrokeWidth: number | string | undefined - if (typeof strokeWidth === 'string' && iconTheme) { - // Construct theme property name (e.g., 'xs' -> 'strokeWidthXs') - const propName = `strokeWidth${strokeWidth - .charAt(0) - .toUpperCase()}${strokeWidth.slice(1)}` as keyof typeof iconTheme - if (propName in iconTheme) { - // Semantic stroke width token from theme - numericStrokeWidth = px(iconTheme[propName] as string) - } else { - // Not a semantic token, use as-is (custom string value) - numericStrokeWidth = strokeWidth - } - } else { - // Numeric value (custom stroke width) - numericStrokeWidth = strokeWidth - } - - // Determine if color is semantic (theme token) or custom CSS - let colorValue: string | undefined - let customColor: string | undefined - if (color) { - if (color === 'inherit') { - colorValue = color - } else if ( - iconTheme && - color in iconTheme && - !color.startsWith('size') && // TODO this would be much simpler if color would be enum - !color.startsWith('strokeWidth') && - color !== 'dark' // TODO what is dark?? - ) { - // Semantic color token from theme (exclude size/strokeWidth/dark properties) - colorValue = color - } else { - // Custom CSS color (e.g., "#ff0000", "rgb(255, 0, 0)") - customColor = color - } - } - const styles = useStyle({ componentId: 'Icon' as const, generateStyle, themeOverride, params: { - size: semanticSize as InstUIIconOwnProps['size'], - color: colorValue, + size: finalSize as LucideIconWrapperProps['size'], + strokeWidth, + color: finalColor, rotate, bidirectional, inline @@ -147,17 +91,16 @@ export function wrapLucideIcon(Icon: LucideIcon): LucideIcon { } return ( - // TODO why apply here className and style?? - + ) @@ -165,7 +108,7 @@ export function wrapLucideIcon(Icon: LucideIcon): LucideIcon { WrappedIcon.displayName = `wrapLucideIcon(${Icon.displayName || Icon.name})` - return WrappedIcon as LucideIcon + return WrappedIcon } export type { LucideIconWrapperProps, InstUIIconOwnProps } diff --git a/packages/ui-icons-lucide/src/wrapLucideIcon/props.ts b/packages/ui-icons-lucide/src/wrapLucideIcon/props.ts index 9764e0b928..1611f19296 100644 --- a/packages/ui-icons-lucide/src/wrapLucideIcon/props.ts +++ b/packages/ui-icons-lucide/src/wrapLucideIcon/props.ts @@ -24,40 +24,91 @@ import type { LucideProps } from 'lucide-react' import type { ComponentStyle, ThemeOverrideValue } from '@instructure/emotion' -import type { NewComponentTypes } from '@instructure/ui-themes' import type { OtherHTMLAttributes } from '@instructure/shared-types' /** - * Extract size tokens from Icon theme (sizeXs, sizeSm, etc.) - * and transform to lowercase literals ('xs', 'sm', etc.) + * Semantic size tokens for icons */ -type ExtractSizeTokens = { - // TODO why this complexity? hardcoding would be much better - [K in keyof T]: K extends `size${infer Size}` ? Lowercase : never -}[keyof T] +type IconSizeToken = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' /** - * Extract strokeWidth tokens from Icon theme (strokeWidthXs, etc.) - * and transform to lowercase literals ('xs', 'sm', etc.) + * Semantic stroke width tokens for icons */ -type ExtractStrokeWidthTokens = { - // TODO why this complexity? hardcoding would be much better - [K in keyof T]: K extends `strokeWidth${infer Size}` ? Lowercase : never -}[keyof T] +type IconStrokeWidthToken = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' /** - * Extract color tokens from Icon theme - * (all properties except size/strokeWidth and 'dark') + * Semantic color tokens from Icon theme */ -type ExtractColorTokens = Exclude< - // TODO why this complexity? hardcoding would be much better - keyof T, - `size${string}` | `strokeWidth${string}` | 'dark' // TODO what is 'dark'? -> - -type IconSizeToken = ExtractSizeTokens -type IconStrokeWidthToken = ExtractStrokeWidthTokens -type IconColorToken = ExtractColorTokens +type IconColorToken = + | 'baseColor' + | 'mutedColor' + | 'successColor' + | 'errorColor' + | 'warningColor' + | 'infoColor' + | 'onColor' + | 'inverseColor' + | 'disabledBaseColor' + | 'disabledOnColor' + | 'dark' + | 'navigationPrimaryBaseColor' + | 'navigationPrimaryHoverColor' + | 'navigationPrimaryActiveColor' + | 'navigationPrimaryOnColorBaseColor' + | 'navigationPrimaryOnColorHoverColor' + | 'navigationPrimaryOnColorActiveColor' + | 'actionSecondaryBaseColor' + | 'actionSecondaryHoverColor' + | 'actionSecondaryActiveColor' + | 'actionSecondaryDisabledColor' + | 'actionStatusBaseColor' + | 'actionStatusHoverColor' + | 'actionStatusActiveColor' + | 'actionStatusDisabledColor' + | 'actionAiSecondaryTopGradientBaseColor' + | 'actionAiSecondaryBottomGradientBaseColor' + | 'actionAiBaseColor' + | 'actionAiHoverColor' + | 'actionAiActiveColor' + | 'actionAiDisabledColor' + | 'actionPrimaryBaseColor' + | 'actionPrimaryHoverColor' + | 'actionPrimaryActiveColor' + | 'actionPrimaryDisabledColor' + | 'actionPrimaryOnColorBaseColor' + | 'actionPrimaryOnColorHoverColor' + | 'actionPrimaryOnColorActiveColor' + | 'actionPrimaryOnColorDisabledColor' + | 'accentBlueColor' + | 'accentGreenColor' + | 'accentRedColor' + | 'accentOrangeColor' + | 'accentGreyColor' + | 'accentAshColor' + | 'accentPlumColor' + | 'accentVioletColor' + | 'accentStoneColor' + | 'accentSkyColor' + | 'accentHoneyColor' + | 'accentSeaColor' + | 'accentAutoraColor' + | 'actionTertiaryBaseColor' + | 'actionTertiaryHoverColor' + | 'actionTertiaryActiveColor' + | 'actionTertiaryDisabledColor' + | 'actionSuccessSecondaryBaseColor' + | 'actionSuccessSecondaryDisabledColor' + | 'actionDestructiveSecondaryBaseColor' + | 'actionDestructiveSecondaryDisabledColor' + | 'actionAiSecondaryDisabledColor' + | 'actionSecondaryOnColorBaseColor' + | 'actionSecondaryOnColorHoverColor' + | 'actionSecondaryOnColorActiveColor' + | 'actionSecondaryOnColorDisabledColor' + | 'actionSuccessSecondaryHoverColor' + | 'actionSuccessSecondaryActiveColor' + | 'actionDestructiveSecondaryHoverColor' + | 'actionDestructiveSecondaryActiveColor' type InstUIIconOwnProps = { /** @@ -108,6 +159,19 @@ type LucideIconWrapperProps = Omit< themeOverride?: ThemeOverrideValue } & OtherHTMLAttributes -type LucideIconStyle = ComponentStyle<'lucideIcon'> +type LucideIconStyle = ComponentStyle<'lucideIcon'> & { + /** + * Computed numeric size for Lucide icon (in pixels) + */ + numericSize?: number + /** + * Computed numeric stroke width for Lucide icon + */ + numericStrokeWidth?: number | string + /** + * Custom CSS color value (non-semantic) + */ + customColor?: string +} export type { LucideIconWrapperProps, InstUIIconOwnProps, LucideIconStyle } diff --git a/packages/ui-icons-lucide/src/wrapLucideIcon/styles.ts b/packages/ui-icons-lucide/src/wrapLucideIcon/styles.ts index 7fc144bb9d..c2319a3d6e 100644 --- a/packages/ui-icons-lucide/src/wrapLucideIcon/styles.ts +++ b/packages/ui-icons-lucide/src/wrapLucideIcon/styles.ts @@ -22,11 +22,13 @@ * SOFTWARE. */ +import { px } from '@instructure/ui-utils' import type { NewComponentTypes } from '@instructure/ui-themes' import type { LucideIconWrapperProps, LucideIconStyle } from './props' type StyleParams = { size?: LucideIconWrapperProps['size'] + strokeWidth?: LucideIconWrapperProps['strokeWidth'] color?: LucideIconWrapperProps['color'] rotate?: LucideIconWrapperProps['rotate'] bidirectional?: LucideIconWrapperProps['bidirectional'] @@ -34,29 +36,105 @@ type StyleParams = { themeOverride?: LucideIconWrapperProps['themeOverride'] } +/** + * Convert semantic size token to numeric pixels for Lucide + */ +const convertSemanticSize = ( + size: LucideIconWrapperProps['size'], + componentTheme: NewComponentTypes['Icon'] +) => { + if (typeof size === 'string') { + const propName = `size${size.charAt(0).toUpperCase()}${size.slice( + 1 + )}` as keyof typeof componentTheme + if (propName in componentTheme) { + return px(componentTheme[propName]) + } + } else if (typeof size === 'number') { + return size + } + return undefined +} + +/** + * Convert semantic stroke width token to numeric value for Lucide + */ +const convertSemanticStrokeWidth = ( + strokeWidth: LucideIconWrapperProps['strokeWidth'], + componentTheme: NewComponentTypes['Icon'] +) => { + if (typeof strokeWidth === 'string') { + const propName = `strokeWidth${strokeWidth + .charAt(0) + .toUpperCase()}${strokeWidth.slice(1)}` as keyof typeof componentTheme + if (propName in componentTheme) { + return px(componentTheme[propName]) + } + return strokeWidth + } + return strokeWidth +} + +/** + * Determine color values: semantic (for CSS) vs custom (for Lucide) + */ +const determineColorValues = ( + color: LucideIconWrapperProps['color'], + componentTheme: NewComponentTypes['Icon'] +) => { + if (!color) { + return {} + } + + if (color === 'inherit') { + return { colorValue: color } + } + + if ( + color in componentTheme && + !color.startsWith('size') && + !color.startsWith('strokeWidth') + ) { + return { colorValue: color } + } + + return { customColor: color } +} + const generateStyle = ( componentTheme: NewComponentTypes['Icon'], params: StyleParams ): LucideIconStyle => { - const { color, rotate = '0', bidirectional = true, inline = true } = params + const { + size, + strokeWidth, + color, + rotate = '0', + bidirectional = true, + inline = true + } = params - /** - * Determine color value from theme or custom CSS - */ - let colorStyle: { color: string } | undefined + const numericSize = convertSemanticSize(size, componentTheme) + const numericStrokeWidth = convertSemanticStrokeWidth( + strokeWidth, + componentTheme + ) + const { colorValue, customColor } = determineColorValues( + color, + componentTheme + ) - if (color) { - if (color === 'inherit') { + let colorStyle + if (colorValue) { + if (colorValue === 'inherit') { colorStyle = { color: 'inherit' } - } else if (color in componentTheme) { - // Direct theme property access + } else if (colorValue in componentTheme) { colorStyle = { - color: componentTheme[color as keyof typeof componentTheme] as string + color: componentTheme[colorValue as keyof typeof componentTheme] } - } else { - // Custom CSS color (e.g., "#ff0000", "rgb(255, 0, 0)") - colorStyle = { color } } + } else if (customColor) { + colorStyle = { color: customColor } } const rotateVariants = { @@ -85,7 +163,10 @@ const generateStyle = ( ...(bidirectional && { '[dir="rtl"] &': bidirectionalRotateVariants[rotate] }) - } + }, + numericSize, + numericStrokeWidth, + customColor } } diff --git a/packages/ui-react-utils/src/IconPropsProvider/IconPropsContext.ts b/packages/ui-react-utils/src/IconPropsProvider/IconPropsContext.ts new file mode 100644 index 0000000000..3735d21415 --- /dev/null +++ b/packages/ui-react-utils/src/IconPropsProvider/IconPropsContext.ts @@ -0,0 +1,30 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import React from 'react' +import type { IconPropsContextValue } from './IconPropsProvider' + +const IconPropsContext = React.createContext({}) + +export { IconPropsContext } diff --git a/packages/ui-react-utils/src/IconPropsProvider/IconPropsProvider.tsx b/packages/ui-react-utils/src/IconPropsProvider/IconPropsProvider.tsx new file mode 100644 index 0000000000..77edf44134 --- /dev/null +++ b/packages/ui-react-utils/src/IconPropsProvider/IconPropsProvider.tsx @@ -0,0 +1,49 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { IconPropsContext } from './IconPropsContext' + +type IconPropsContextValue = { + size?: string | number + color?: string +} + +type IconPropsProviderProps = React.PropsWithChildren + +const IconPropsProvider: React.FC = ({ + children, + size, + color +}) => { + const value = { size, color } + + return ( + + {children} + + ) +} + +export { IconPropsProvider } +export type { IconPropsContextValue } diff --git a/packages/ui-react-utils/src/IconPropsProvider/index.ts b/packages/ui-react-utils/src/IconPropsProvider/index.ts new file mode 100644 index 0000000000..7449089386 --- /dev/null +++ b/packages/ui-react-utils/src/IconPropsProvider/index.ts @@ -0,0 +1,28 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +export { IconPropsProvider } from './IconPropsProvider' +export { IconPropsContext } from './IconPropsContext' +export { useIconProps } from './useIconProps' +export type { IconPropsContextValue } from './IconPropsProvider' diff --git a/packages/ui-react-utils/src/IconPropsProvider/useIconProps.tsx b/packages/ui-react-utils/src/IconPropsProvider/useIconProps.tsx new file mode 100644 index 0000000000..6e362752bf --- /dev/null +++ b/packages/ui-react-utils/src/IconPropsProvider/useIconProps.tsx @@ -0,0 +1,32 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { useContext } from 'react' +import { IconPropsContext } from './IconPropsContext' + +function useIconProps() { + return useContext(IconPropsContext) +} + +export { useIconProps } diff --git a/packages/ui-react-utils/src/__tests__/IconPropsProvider.test.tsx b/packages/ui-react-utils/src/__tests__/IconPropsProvider.test.tsx new file mode 100644 index 0000000000..04172226dc --- /dev/null +++ b/packages/ui-react-utils/src/__tests__/IconPropsProvider.test.tsx @@ -0,0 +1,161 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { render, screen } from '@testing-library/react' +import '@testing-library/jest-dom' + +import { IconPropsProvider, useIconProps } from '../IconPropsProvider' + +// Test component that exposes context values +const TestComponentWithHook = () => { + const iconProps = useIconProps() + return ( +

    + + {iconProps?.size?.toString() || 'undefined'} + + {iconProps?.color || 'undefined'} +
    + ) +} + +// Mock icon component that uses context +const MockIconComponent = ({ testId = 'mock-icon' }: { testId?: string }) => { + const iconProps = useIconProps() + return ( +
    + Icon +
    + ) +} + +// Mock icon that accepts direct props and merges with context +const MockIconWithProps = ({ + size, + color, + testId = 'mock-icon-with-props' +}: { + size?: string | number + color?: string + testId?: string +}) => { + const contextProps = useIconProps() + const finalSize = size ?? contextProps?.size + const finalColor = color ?? contextProps?.color + + return ( +
    + Icon +
    + ) +} + +describe('IconPropsProvider', () => { + describe('useIconProps hook', () => { + it('should return context values when inside provider', () => { + render( + + + + ) + + expect(screen.getByTestId('size')).toHaveTextContent('lg') + expect(screen.getByTestId('color')).toHaveTextContent('baseColor') + }) + + it('should return empty object when outside provider', () => { + render() + + expect(screen.getByTestId('size')).toHaveTextContent('undefined') + expect(screen.getByTestId('color')).toHaveTextContent('undefined') + }) + + it('should return numeric size values', () => { + render( + + + + ) + + expect(screen.getByTestId('size')).toHaveTextContent('24') + expect(screen.getByTestId('color')).toHaveTextContent('accentRedColor') + }) + }) + + describe('Icon components receive props from context', () => { + it('should pass context values to icon component', () => { + render( + + + + ) + + const icon = screen.getByTestId('mock-icon') + expect(icon).toHaveAttribute('data-size', 'md') + expect(icon).toHaveAttribute('data-color', 'baseColor') + }) + + it('should work without IconPropsProvider', () => { + render() + + const icon = screen.getByTestId('mock-icon') + expect(icon).toHaveAttribute('data-size', 'none') + expect(icon).toHaveAttribute('data-color', 'none') + }) + }) + + describe('Direct props override context props', () => { + it('should allow direct props to override context', () => { + render( + + + + ) + + const icon = screen.getByTestId('mock-icon-with-props') + expect(icon).toHaveAttribute('data-size', '16') + expect(icon).toHaveAttribute('data-color', 'accentRedColor') + }) + + it('should use context when no direct props provided', () => { + render( + + + + ) + + const icon = screen.getByTestId('mock-icon-with-props') + expect(icon).toHaveAttribute('data-size', '24') + expect(icon).toHaveAttribute('data-color', 'baseColor') + }) + }) +}) diff --git a/packages/ui-react-utils/src/__tests__/renderLucideIconWithProps.test.tsx b/packages/ui-react-utils/src/__tests__/renderLucideIconWithProps.test.tsx deleted file mode 100644 index 8946a12f31..0000000000 --- a/packages/ui-react-utils/src/__tests__/renderLucideIconWithProps.test.tsx +++ /dev/null @@ -1,434 +0,0 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2015 - present Instructure, Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -import React from 'react' -import { render } from '@testing-library/react' -import { expect, describe, it, vi } from 'vitest' -import '@testing-library/jest-dom' -import { renderLucideIconWithProps } from '../renderLucideIconWithProps' - -describe('renderLucideIconWithProps', () => { - describe('with undefined/null', () => { - it('should return null for undefined', () => { - const result = renderLucideIconWithProps(undefined, { foo: 'bar' }) - expect(result).toBeNull() - }) - - it('should return null for null', () => { - const result = renderLucideIconWithProps(null as any, { foo: 'bar' }) - expect(result).toBeNull() - }) - }) - - describe('with component reference', () => { - it('should render component with props', () => { - // TODO why fake here an icon? Why not use a real one? - const TestComponent = ({ size, color }: any) => ( -
    - Test -
    - ) - TestComponent.displayName = 'wrapLucideIcon(TestComponent)' - - const result = renderLucideIconWithProps(TestComponent, { - size: 'large', - color: 'blue' - }) - - const { getByTestId } = render(<>{result}) - const element = getByTestId('test') - - expect(element).toHaveAttribute('data-size', 'large') - expect(element).toHaveAttribute('data-color', 'blue') - }) - - it('should call component with props', () => { - const mockComponent = vi.fn((props: any) => ( -
    {props.text}
    - )) - ;(mockComponent as any).displayName = 'wrapLucideIcon(MockComponent)' - - renderLucideIconWithProps(mockComponent, { text: 'hello' }) - - expect(mockComponent).toHaveBeenCalledWith( - expect.objectContaining({ text: 'hello' }) - ) - }) - }) - - describe('with JSX element', () => { - it('should clone JSX element with props', () => { - const TestComponent = ({ size, color }: any) => ( -
    - Test -
    - ) - TestComponent.displayName = 'wrapLucideIcon(TestComponent)' - - const result = renderLucideIconWithProps(, { - size: 'medium', - color: 'red' - }) - - const { getByTestId } = render(<>{result}) - const element = getByTestId('test') - - expect(element).toHaveAttribute('data-size', 'medium') - expect(element).toHaveAttribute('data-color', 'red') - }) - - it('should override existing props on JSX element', () => { - const TestComponent = ({ size }: any) => ( -
    - Test -
    - ) - TestComponent.displayName = 'wrapLucideIcon(TestComponent)' - - const result = renderLucideIconWithProps(, { - size: 'large' - }) - - const { getByTestId } = render(<>{result}) - const element = getByTestId('test') - - expect(element).toHaveAttribute('data-size', 'large') - }) - }) - - describe('with render function that accepts props', () => { - it('should call function with props and render result', () => { - const renderFunc = ({ size, color }: any) => ( -
    - Test -
    - ) - renderFunc.displayName = 'wrapLucideIcon(renderFunc)' - - const result = renderLucideIconWithProps(renderFunc, { - size: 'small', - color: 'green' - }) - - const { getByTestId } = render(<>{result}) - const element = getByTestId('test') - - expect(element).toHaveAttribute('data-size', 'small') - expect(element).toHaveAttribute('data-color', 'green') - }) - - it('should merge props if function uses them', () => { - const renderFunc = (props: any) => ( -
    - Test -
    - ) - renderFunc.displayName = 'wrapLucideIcon(renderFunc)' - - const result = renderLucideIconWithProps(renderFunc, { - 'data-size': 'medium', - 'data-color': 'yellow' - } as any) - - const { getByTestId } = render(<>{result}) - const element = getByTestId('test') - - expect(element).toHaveAttribute('data-size', 'medium') - expect(element).toHaveAttribute('data-color', 'yellow') - }) - }) - - describe('with render function that ignores props', () => { - it('should apply props to returned JSX even if function ignores them', () => { - const renderFunc = () => ( -
    - Test -
    - ) - renderFunc.displayName = 'wrapLucideIcon(renderFunc)' - - const result = renderLucideIconWithProps(renderFunc, { - 'data-size': 'overridden', - 'data-color': 'purple' - } as any) - - const { getByTestId } = render(<>{result}) - const element = getByTestId('test') - - // Props should be merged/overridden - expect(element).toHaveAttribute('data-size', 'overridden') - expect(element).toHaveAttribute('data-color', 'purple') - }) - - it('should work with arrow function without parameters', () => { - const TestComponent = ({ size }: any) => ( -
    - Test -
    - ) - TestComponent.displayName = 'wrapLucideIcon(TestComponent)' - - const renderFunc = () => - renderFunc.displayName = 'wrapLucideIcon(renderFunc)' - - const result = renderLucideIconWithProps(renderFunc, { - size: 'x-large' - }) - - const { getByTestId } = render(<>{result}) - const element = getByTestId('test') - - expect(element).toHaveAttribute('data-size', 'x-large') - }) - }) - - describe('with React components', () => { - it('should work with class components', () => { - class TestComponent extends React.Component<{ size?: string }> { - render() { - return ( -
    - Test -
    - ) - } - } - ;(TestComponent as any).displayName = 'wrapLucideIcon(TestComponent)' - - const result = renderLucideIconWithProps(TestComponent, { size: 'huge' }) - - const { getByTestId } = render(<>{result}) - const element = getByTestId('test') - - expect(element).toHaveAttribute('data-size', 'huge') - }) - - it('should work with functional components', () => { - const TestComponent = ({ size }: any) => ( -
    - Test -
    - ) - TestComponent.displayName = 'wrapLucideIcon(TestComponent)' - - const result = renderLucideIconWithProps(TestComponent, { size: 'tiny' }) - - const { getByTestId } = render(<>{result}) - const element = getByTestId('test') - - expect(element).toHaveAttribute('data-size', 'tiny') - }) - }) - - describe('prop precedence', () => { - it('should have passed props override original JSX props', () => { - const TestComponent = ({ a, b, c }: any) => ( -
    - Test -
    - ) - TestComponent.displayName = 'wrapLucideIcon(TestComponent)' - - const result = renderLucideIconWithProps(, { - b: '20', - c: '30' - }) - - const { getByTestId } = render(<>{result}) - const element = getByTestId('test') - - expect(element).toHaveAttribute('data-a', '1') // Original preserved - expect(element).toHaveAttribute('data-b', '20') // Overridden - expect(element).toHaveAttribute('data-c', '30') // Added - }) - - it('should have passed props override function-returned JSX props', () => { - const renderFunc = () => ( -
    - Test -
    - ) - renderFunc.displayName = 'wrapLucideIcon(renderFunc)' - - const result = renderLucideIconWithProps(renderFunc, { - 'data-x': 'new', - 'data-z': 'added' - } as any) - - const { getByTestId } = render(<>{result}) - const element = getByTestId('test') - - expect(element).toHaveAttribute('data-x', 'new') // Overridden - expect(element).toHaveAttribute('data-y', 'original') // Preserved - expect(element).toHaveAttribute('data-z', 'added') // Added - }) - }) - - describe('Lucide icon detection', () => { - it('should apply props to Lucide icons (detected by displayName)', () => { - const LucideIcon = ({ size, color }: any) => ( - - - - ) - LucideIcon.displayName = 'wrapLucideIcon(UserIcon)' - - const result = renderLucideIconWithProps(LucideIcon, { - size: 'lg', - color: 'blue' - }) - - const { getByTestId } = render(<>{result}) - const element = getByTestId('lucide-icon') - - expect(element).toHaveAttribute('data-size', 'lg') - expect(element).toHaveAttribute('data-color', 'blue') - }) - - it('should NOT apply props to non-Lucide icons', () => { - const NonLucideIcon = ({ size }: any) => ( - - - - ) - - const result = renderLucideIconWithProps(NonLucideIcon, { - size: 'lg', - color: 'blue' - }) - - const { getByTestId } = render(<>{result}) - const element = getByTestId('non-lucide-icon') - - // Should render with default value, not the passed prop - expect(element).toHaveAttribute('data-size', 'default') - expect(element).not.toHaveAttribute('data-color') - }) - - it('should detect Lucide icon from JSX element', () => { - const LucideIcon = ({ size }: any) => ( - - - - ) - LucideIcon.displayName = 'wrapLucideIcon(CircleIcon)' - - const result = renderLucideIconWithProps(, { - size: 'md' - }) - - const { getByTestId } = render(<>{result}) - const element = getByTestId('lucide-jsx') - - expect(element).toHaveAttribute('data-size', 'md') - }) - - it('should NOT apply props to JSX element without Lucide displayName', () => { - const NonLucideIcon = ({ size }: any) => ( - - - - ) - - const result = renderLucideIconWithProps(, { - size: 'xl' - }) - - const { getByTestId } = render(<>{result}) - const element = getByTestId('non-lucide-jsx') - - expect(element).toHaveAttribute('data-size', 'default') - }) - - it('should detect Lucide icon from render function', () => { - const renderFunc = ({ size }: any) => ( - - - - ) - renderFunc.displayName = 'wrapLucideIcon(StarIcon)' - - const result = renderLucideIconWithProps(renderFunc, { size: 'sm' }) - - const { getByTestId } = render(<>{result}) - const element = getByTestId('lucide-func') - - expect(element).toHaveAttribute('data-size', 'sm') - }) - - it('should NOT apply props to render function without Lucide displayName', () => { - const renderFunc = ({ size }: any) => ( - - - - ) - - const result = renderLucideIconWithProps(renderFunc, { size: 'lg' }) - - const { getByTestId } = render(<>{result}) - const element = getByTestId('non-lucide-func') - - expect(element).toHaveAttribute('data-size', 'default') - }) - - it('should detect Lucide icon from arrow function returning JSX without displayName', () => { - const LucideIcon = ({ size }: any) => ( - - - - ) - LucideIcon.displayName = 'wrapLucideIcon(ArrowIcon)' - - // Arrow function without displayName that returns a Lucide icon - const renderFunc = () => - - const result = renderLucideIconWithProps(renderFunc, { size: 'lg' }) - - const { getByTestId } = render(<>{result}) - const element = getByTestId('lucide-arrow') - - // Should apply size prop even though arrow function has no displayName - expect(element).toHaveAttribute('data-size', 'lg') - }) - - it('should handle arrow function returning non-Lucide icon', () => { - const NonLucideIcon = ({ size }: any) => ( - - - - ) - - // Arrow function without displayName that returns a non-Lucide icon - const renderFunc = () => - - const result = renderLucideIconWithProps(renderFunc, { size: 'xl' }) - - const { getByTestId } = render(<>{result}) - const element = getByTestId('non-lucide-arrow') - - // Should NOT apply size prop - expect(element).toHaveAttribute('data-size', 'default') - }) - }) -}) diff --git a/packages/ui-react-utils/src/index.ts b/packages/ui-react-utils/src/index.ts index 02e42a2ef6..272680316a 100644 --- a/packages/ui-react-utils/src/index.ts +++ b/packages/ui-react-utils/src/index.ts @@ -28,11 +28,15 @@ export { ensureSingleChild } from './ensureSingleChild' export { getDisplayName } from './getDisplayName' export { getElementType } from './getElementType' export { getInteraction } from './getInteraction' +export { + IconPropsProvider, + IconPropsContext, + useIconProps +} from './IconPropsProvider' export { matchComponentTypes } from './matchComponentTypes' export { omitProps } from './omitProps' export { passthroughProps } from './passthroughProps' export { pickProps } from './pickProps' -export { renderLucideIconWithProps } from './renderLucideIconWithProps' export { safeCloneElement } from './safeCloneElement' export { windowMessageListener } from './windowMessageListener' export { @@ -44,6 +48,7 @@ export { export type { GetInteractionOptions } from './getInteraction' export type { InteractionType } from './getInteraction' +export type { IconPropsContextValue } from './IconPropsProvider' export type { DeterministicIdProviderValue, WithDeterministicIdProps diff --git a/packages/ui-react-utils/src/renderLucideIconWithProps.ts b/packages/ui-react-utils/src/renderLucideIconWithProps.ts deleted file mode 100644 index 297b07ebab..0000000000 --- a/packages/ui-react-utils/src/renderLucideIconWithProps.ts +++ /dev/null @@ -1,102 +0,0 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2015 - present Instructure, Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -import React from 'react' -import type { Renderable } from '@instructure/shared-types' -import { callRenderProp } from './callRenderProp' - -/** - * Check if an element/component is a Lucide icon by checking its displayName - * @param element - Element or component to check - * @returns True if it's a Lucide icon (displayName starts with 'wrapLucideIcon(') - */ -const isLucideIcon = (element: any): boolean => { - if (React.isValidElement(element)) { - // like - const displayName = (element.type as any)?.displayName - return displayName?.startsWith('wrapLucideIcon(') - } - return element?.displayName?.startsWith('wrapLucideIcon(') // like `UserIcon` -} - -/** - * --- - * category: utilities/react - * --- - * Renders a Lucide icon with props, handling multiple input formats. - * Only applies props if the icon is a Lucide icon (detected via displayName). - * For non-Lucide icons, renders without applying props. - * - * Supported use cases: - * 1. Component/function with Lucide displayName: `renderIcon={UserIcon}` - Calls with props - * 2. JSX element: `renderIcon={}` - Clones and overrides props - * 3. Arrow function returning Lucide: `renderIcon={() => }` - Detects Lucide from result - * - * @module renderLucideIconWithProps - * @param elementToRender - The element to render (component/JSX/function) - * @param propsToApply - Props to pass to or override on the element (only for Lucide icons) - * @returns Rendered React element or null - */ -function renderLucideIconWithProps

    >( - elementToRender: Renderable

    , - propsToApply: P -): React.ReactElement | null { - if (!elementToRender) return null - - // Check once if the input is a Lucide icon e.g. `` or `UserIcon` - const isInputLucide = isLucideIcon(elementToRender) - - // Use case 1: JSX element like - if (React.isValidElement(elementToRender)) { - return isInputLucide - ? React.cloneElement(elementToRender, propsToApply) - : elementToRender - } - - // Use case 2: `UserIcon` or `() => ` - // callRenderProp "extracts" the JSX element by either: - // 1. Creating it from a component reference: React.createElement(UserIcon, props) - // 2. Calling the function to get the JSX it returns: (() => )() - const result = callRenderProp( - elementToRender, - isInputLucide ? propsToApply : ({} as P) - ) - - // Apply props if result is a valid element and either: - // - Input was Lucide (trust that we should apply props to whatever it returns) - // - Result itself is Lucide (arrow function returned a Lucide icon) - // TODO isInputLucide check is needed here? isLucideIcon(result) is not enough? - if (React.isValidElement(result) && (isInputLucide || isLucideIcon(result))) { - return React.cloneElement(result, { - // TODO didnt callRenderProp already apply the props? - ...(result.props as Record), - ...propsToApply - }) - } - - return result -} - -export default renderLucideIconWithProps -export { renderLucideIconWithProps } From 7e5aaeb9f8ca347346c2d2574b2578b33c071cf5 Mon Sep 17 00:00:00 2001 From: Peter Pal Hudak Date: Thu, 11 Dec 2025 15:00:18 +0100 Subject: [PATCH 5/7] refactor(ui-react-utils,ui-icons-lucide,ui-avatar): fixes after review plus add AI gradient coloring --- .../__docs__/src/Icons/LucideIconsGallery.tsx | 16 +- packages/ui-avatar/package.json | 3 +- .../src/Avatar/__tests__/Avatar.test.tsx | 3 +- packages/ui-avatar/src/Avatar/index.tsx | 16 +- packages/ui-avatar/tsconfig.build.json | 3 - packages/ui-icons-lucide/package.json | 2 +- .../ui-icons-lucide/scripts/generateIndex.ts | 2 + .../src/IconPropsProvider/IconPropsContext.ts | 30 +++ .../IconPropsProvider/IconPropsProvider.tsx | 47 ++++ .../src/IconPropsProvider/index.ts | 28 +++ .../IconPropsProvider/renderIconWithProps.tsx | 62 +++++ .../src/__tests__/IconPropsProvider.test.tsx | 162 ++++++++++++ packages/ui-icons-lucide/src/index.ts | 238 ++++++++++++++++++ .../src/wrapLucideIcon/index.tsx | 55 +++- .../src/wrapLucideIcon/props.ts | 38 ++- .../src/wrapLucideIcon/styles.ts | 29 ++- packages/ui-react-utils/src/index.ts | 6 - pnpm-lock.yaml | 17 +- 18 files changed, 699 insertions(+), 58 deletions(-) create mode 100644 packages/ui-icons-lucide/src/IconPropsProvider/IconPropsContext.ts create mode 100644 packages/ui-icons-lucide/src/IconPropsProvider/IconPropsProvider.tsx create mode 100644 packages/ui-icons-lucide/src/IconPropsProvider/index.ts create mode 100644 packages/ui-icons-lucide/src/IconPropsProvider/renderIconWithProps.tsx create mode 100644 packages/ui-icons-lucide/src/__tests__/IconPropsProvider.test.tsx diff --git a/packages/__docs__/src/Icons/LucideIconsGallery.tsx b/packages/__docs__/src/Icons/LucideIconsGallery.tsx index efad270b36..c2a7a71aed 100644 --- a/packages/__docs__/src/Icons/LucideIconsGallery.tsx +++ b/packages/__docs__/src/Icons/LucideIconsGallery.tsx @@ -65,7 +65,7 @@ function getUsageInfo(iconName: string) { const MyIcon = () => { return ( - <${iconName} size={24} /> + <${iconName} size={'2xl'} color='successColor'/> ) }` } @@ -290,14 +290,20 @@ const LucideIconsGallery = () => {

    • - size: Number (pixels) - e.g., 24 + size: Number (pixels) or semantic token - e.g.,{' '} + "sm"
    • - color: CSS color - e.g.,{' '} - "currentColor" + color: CSS color or semantic token- e.g.,{' '} + "successColor"
    • - strokeWidth: Number - e.g., 2 + strokeWidth: Number or semantic token - e.g.,{' '} + "sm" +
    • +
    • + Plus all standard SVG props (except for className, style, css, + children etc.)

    diff --git a/packages/ui-avatar/package.json b/packages/ui-avatar/package.json index 02e61c88ae..46be1388cb 100644 --- a/packages/ui-avatar/package.json +++ b/packages/ui-avatar/package.json @@ -26,7 +26,7 @@ "@babel/runtime": "^7.27.6", "@instructure/emotion": "workspace:*", "@instructure/shared-types": "workspace:*", - "@instructure/ui-icons": "workspace:*", + "@instructure/ui-icons-lucide": "workspace:*", "@instructure/ui-react-utils": "workspace:*", "@instructure/ui-view": "workspace:*" }, @@ -34,7 +34,6 @@ "@instructure/ui-axe-check": "workspace:*", "@instructure/ui-babel-preset": "workspace:*", "@instructure/ui-color-utils": "workspace:*", - "@instructure/ui-icons-lucide": "workspace:*", "@instructure/ui-themes": "workspace:*", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "15.0.7", diff --git a/packages/ui-avatar/src/Avatar/__tests__/Avatar.test.tsx b/packages/ui-avatar/src/Avatar/__tests__/Avatar.test.tsx index d938661646..8b882a5c8d 100644 --- a/packages/ui-avatar/src/Avatar/__tests__/Avatar.test.tsx +++ b/packages/ui-avatar/src/Avatar/__tests__/Avatar.test.tsx @@ -28,7 +28,6 @@ import { runAxeCheck } from '@instructure/ui-axe-check' import '@testing-library/jest-dom' import Avatar from '../index' -import { IconGroupLine } from '@instructure/ui-icons' import { HeartInstUIIcon } from '@instructure/ui-icons-lucide' describe('', () => { @@ -91,7 +90,7 @@ describe('', () => { it('should display correctly when an icon renderer is passed', async () => { const { container } = render( - }> + }> Hello World ) diff --git a/packages/ui-avatar/src/Avatar/index.tsx b/packages/ui-avatar/src/Avatar/index.tsx index aa3fd3ebf5..aec016d032 100644 --- a/packages/ui-avatar/src/Avatar/index.tsx +++ b/packages/ui-avatar/src/Avatar/index.tsx @@ -23,12 +23,10 @@ */ import { useStyle } from '@instructure/emotion' -import React, { useState, useEffect, forwardRef, SyntheticEvent } from 'react' +import { useState, useEffect, forwardRef, SyntheticEvent } from 'react' -import { - passthroughProps, - IconPropsProvider -} from '@instructure/ui-react-utils' +import { passthroughProps } from '@instructure/ui-react-utils' +import { renderIconWithProps } from '@instructure/ui-icons-lucide' import { AvatarProps, avatarSizeToIconSize } from './props' import generateStyle from './styles' @@ -150,13 +148,7 @@ const Avatar = forwardRef( const iconSize = avatarSizeToIconSize[size] const iconColor = hasInverseColor ? 'onColor' : ICON_COLOR_MAP[color] - return ( - - {typeof renderIcon === 'function' - ? React.createElement(renderIcon as any) - : (renderIcon as React.ReactElement)} - - ) + return renderIconWithProps(renderIcon, iconSize, iconColor) } //initials in avatar diff --git a/packages/ui-avatar/tsconfig.build.json b/packages/ui-avatar/tsconfig.build.json index 99449e4138..67312882c9 100644 --- a/packages/ui-avatar/tsconfig.build.json +++ b/packages/ui-avatar/tsconfig.build.json @@ -31,9 +31,6 @@ { "path": "../ui-themes/tsconfig.build.json" }, - { - "path": "../ui-icons/tsconfig.build.json" - }, { "path": "../ui-icons-lucide/tsconfig.build.json" } diff --git a/packages/ui-icons-lucide/package.json b/packages/ui-icons-lucide/package.json index 3609bbd2a7..057d18ec10 100644 --- a/packages/ui-icons-lucide/package.json +++ b/packages/ui-icons-lucide/package.json @@ -28,7 +28,7 @@ "@instructure/ui-react-utils": "workspace:*", "@instructure/ui-themes": "workspace:*", "@instructure/ui-utils": "workspace:*", - "lucide-react": "^0.460.0" + "lucide-react": "^0.559.0" }, "devDependencies": { "@instructure/ui-babel-preset": "workspace:*", diff --git a/packages/ui-icons-lucide/scripts/generateIndex.ts b/packages/ui-icons-lucide/scripts/generateIndex.ts index 432e443505..a76f21245a 100644 --- a/packages/ui-icons-lucide/scripts/generateIndex.ts +++ b/packages/ui-icons-lucide/scripts/generateIndex.ts @@ -109,6 +109,8 @@ import { wrapLucideIcon } from './wrapLucideIcon' export type { LucideProps, LucideIcon } from 'lucide-react' export type { LucideIconWrapperProps, InstUIIconOwnProps } from './wrapLucideIcon' export { wrapLucideIcon } +export { IconPropsProvider, IconPropsContext, renderIconWithProps } from './IconPropsProvider' +export type { IconPropsContextValue } from './IconPropsProvider' ${iconExports} ` diff --git a/packages/ui-icons-lucide/src/IconPropsProvider/IconPropsContext.ts b/packages/ui-icons-lucide/src/IconPropsProvider/IconPropsContext.ts new file mode 100644 index 0000000000..3735d21415 --- /dev/null +++ b/packages/ui-icons-lucide/src/IconPropsProvider/IconPropsContext.ts @@ -0,0 +1,30 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import React from 'react' +import type { IconPropsContextValue } from './IconPropsProvider' + +const IconPropsContext = React.createContext({}) + +export { IconPropsContext } diff --git a/packages/ui-icons-lucide/src/IconPropsProvider/IconPropsProvider.tsx b/packages/ui-icons-lucide/src/IconPropsProvider/IconPropsProvider.tsx new file mode 100644 index 0000000000..65ebd83e1c --- /dev/null +++ b/packages/ui-icons-lucide/src/IconPropsProvider/IconPropsProvider.tsx @@ -0,0 +1,47 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { IconPropsContext } from './IconPropsContext' +import type { InstUIIconOwnProps } from '../wrapLucideIcon/props' + +type IconPropsContextValue = Pick + +type IconPropsProviderProps = React.PropsWithChildren + +const IconPropsProvider: React.FC = ({ + children, + size, + color +}) => { + const value = { size, color } + + return ( + + {children} + + ) +} + +export { IconPropsProvider } +export type { IconPropsContextValue } diff --git a/packages/ui-icons-lucide/src/IconPropsProvider/index.ts b/packages/ui-icons-lucide/src/IconPropsProvider/index.ts new file mode 100644 index 0000000000..7c506573dd --- /dev/null +++ b/packages/ui-icons-lucide/src/IconPropsProvider/index.ts @@ -0,0 +1,28 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +export { IconPropsProvider } from './IconPropsProvider' +export { IconPropsContext } from './IconPropsContext' +export { renderIconWithProps } from './renderIconWithProps' +export type { IconPropsContextValue } from './IconPropsProvider' diff --git a/packages/ui-icons-lucide/src/IconPropsProvider/renderIconWithProps.tsx b/packages/ui-icons-lucide/src/IconPropsProvider/renderIconWithProps.tsx new file mode 100644 index 0000000000..3547a9db09 --- /dev/null +++ b/packages/ui-icons-lucide/src/IconPropsProvider/renderIconWithProps.tsx @@ -0,0 +1,62 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import React from 'react' +import type { Renderable } from '@instructure/shared-types' + +import { IconPropsProvider } from './IconPropsProvider' +import { InstUIIconOwnProps } from '../wrapLucideIcon' + +/** + * Renders an icon wrapped in IconPropsProvider to apply size and color via React context. + * Handles both component references and React elements. + * + * @param icon - The icon to render (component reference or React element) + * @param size - Semantic size token or numeric pixels. + * @param color - Semantic color token or any valid CSS color string. + * @returns Icon element wrapped in IconPropsProvider context + */ +function renderIconWithProps( + icon: Renderable, + size: InstUIIconOwnProps['size'], + color: InstUIIconOwnProps['color'] +): React.ReactElement { + let iconElement: React.ReactNode + + if (typeof icon === 'function') { + // It's a component (class or function) - use createElement + iconElement = React.createElement(icon as React.ComponentType) + } else { + // It's already a React element + iconElement = icon as React.ReactNode + } + + return ( + + {iconElement} + + ) +} + +export { renderIconWithProps } diff --git a/packages/ui-icons-lucide/src/__tests__/IconPropsProvider.test.tsx b/packages/ui-icons-lucide/src/__tests__/IconPropsProvider.test.tsx new file mode 100644 index 0000000000..5d7ae6bdfa --- /dev/null +++ b/packages/ui-icons-lucide/src/__tests__/IconPropsProvider.test.tsx @@ -0,0 +1,162 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { useContext } from 'react' +import { render, screen } from '@testing-library/react' +import '@testing-library/jest-dom' + +import { IconPropsProvider, IconPropsContext } from '../IconPropsProvider' + +// Test component that exposes context values +const TestComponentWithHook = () => { + const iconProps = useContext(IconPropsContext) + return ( +

    + + {iconProps?.size?.toString() || 'undefined'} + + {iconProps?.color || 'undefined'} +
    + ) +} + +// Mock icon component that uses context +const MockIconComponent = ({ testId = 'mock-icon' }: { testId?: string }) => { + const iconProps = useContext(IconPropsContext) + return ( +
    + Icon +
    + ) +} + +// Mock icon that accepts direct props and merges with context +const MockIconWithProps = ({ + size, + color, + testId = 'mock-icon-with-props' +}: { + size?: string | number + color?: string + testId?: string +}) => { + const contextProps = useContext(IconPropsContext) + const finalSize = size ?? contextProps?.size + const finalColor = color ?? contextProps?.color + + return ( +
    + Icon +
    + ) +} + +describe('IconPropsProvider', () => { + describe('useIconProps hook', () => { + it('should return context values when inside provider', () => { + render( + + + + ) + + expect(screen.getByTestId('size')).toHaveTextContent('lg') + expect(screen.getByTestId('color')).toHaveTextContent('baseColor') + }) + + it('should return empty object when outside provider', () => { + render() + + expect(screen.getByTestId('size')).toHaveTextContent('undefined') + expect(screen.getByTestId('color')).toHaveTextContent('undefined') + }) + + it('should return numeric size values', () => { + render( + + + + ) + + expect(screen.getByTestId('size')).toHaveTextContent('24') + expect(screen.getByTestId('color')).toHaveTextContent('accentRedColor') + }) + }) + + describe('Icon components receive props from context', () => { + it('should pass context values to icon component', () => { + render( + + + + ) + + const icon = screen.getByTestId('mock-icon') + expect(icon).toHaveAttribute('data-size', 'md') + expect(icon).toHaveAttribute('data-color', 'baseColor') + }) + + it('should work without IconPropsProvider', () => { + render() + + const icon = screen.getByTestId('mock-icon') + expect(icon).toHaveAttribute('data-size', 'none') + expect(icon).toHaveAttribute('data-color', 'none') + }) + }) + + describe('Direct props override context props', () => { + it('should allow direct props to override context', () => { + render( + + + + ) + + const icon = screen.getByTestId('mock-icon-with-props') + expect(icon).toHaveAttribute('data-size', '16') + expect(icon).toHaveAttribute('data-color', 'accentRedColor') + }) + + it('should use context when no direct props provided', () => { + render( + + + + ) + + const icon = screen.getByTestId('mock-icon-with-props') + expect(icon).toHaveAttribute('data-size', '24') + expect(icon).toHaveAttribute('data-color', 'baseColor') + }) + }) +}) diff --git a/packages/ui-icons-lucide/src/index.ts b/packages/ui-icons-lucide/src/index.ts index d67428413d..9e632ad374 100644 --- a/packages/ui-icons-lucide/src/index.ts +++ b/packages/ui-icons-lucide/src/index.ts @@ -31,6 +31,12 @@ export type { InstUIIconOwnProps } from './wrapLucideIcon' export { wrapLucideIcon } +export { + IconPropsProvider, + IconPropsContext, + renderIconWithProps +} from './IconPropsProvider' +export type { IconPropsContextValue } from './IconPropsProvider' export const AArrowDownInstUIIcon = wrapLucideIcon(Lucide.AArrowDown) export const AArrowUpInstUIIcon = wrapLucideIcon(Lucide.AArrowUp) @@ -292,16 +298,28 @@ export const BadgePlusInstUIIcon = wrapLucideIcon(Lucide.BadgePlus) export const BadgePoundSterlingInstUIIcon = wrapLucideIcon( Lucide.BadgePoundSterling ) +export const BadgeQuestionMarkInstUIIcon = wrapLucideIcon( + Lucide.BadgeQuestionMark +) export const BadgeRussianRubleInstUIIcon = wrapLucideIcon( Lucide.BadgeRussianRuble ) export const BadgeSwissFrancInstUIIcon = wrapLucideIcon(Lucide.BadgeSwissFranc) +export const BadgeTurkishLiraInstUIIcon = wrapLucideIcon( + Lucide.BadgeTurkishLira +) export const BadgeXInstUIIcon = wrapLucideIcon(Lucide.BadgeX) export const BaggageClaimInstUIIcon = wrapLucideIcon(Lucide.BaggageClaim) +export const BalloonInstUIIcon = wrapLucideIcon(Lucide.Balloon) export const BanInstUIIcon = wrapLucideIcon(Lucide.Ban) export const BananaInstUIIcon = wrapLucideIcon(Lucide.Banana) export const BandageInstUIIcon = wrapLucideIcon(Lucide.Bandage) export const BanknoteInstUIIcon = wrapLucideIcon(Lucide.Banknote) +export const BanknoteArrowDownInstUIIcon = wrapLucideIcon( + Lucide.BanknoteArrowDown +) +export const BanknoteArrowUpInstUIIcon = wrapLucideIcon(Lucide.BanknoteArrowUp) +export const BanknoteXInstUIIcon = wrapLucideIcon(Lucide.BanknoteX) export const BarChartInstUIIcon = wrapLucideIcon(Lucide.BarChart) export const BarChart2InstUIIcon = wrapLucideIcon(Lucide.BarChart2) export const BarChart3InstUIIcon = wrapLucideIcon(Lucide.BarChart3) @@ -314,6 +332,7 @@ export const BarChartHorizontalBigInstUIIcon = wrapLucideIcon( Lucide.BarChartHorizontalBig ) export const BarcodeInstUIIcon = wrapLucideIcon(Lucide.Barcode) +export const BarrelInstUIIcon = wrapLucideIcon(Lucide.Barrel) export const BaselineInstUIIcon = wrapLucideIcon(Lucide.Baseline) export const BathInstUIIcon = wrapLucideIcon(Lucide.Bath) export const BatteryInstUIIcon = wrapLucideIcon(Lucide.Battery) @@ -321,6 +340,7 @@ export const BatteryChargingInstUIIcon = wrapLucideIcon(Lucide.BatteryCharging) export const BatteryFullInstUIIcon = wrapLucideIcon(Lucide.BatteryFull) export const BatteryLowInstUIIcon = wrapLucideIcon(Lucide.BatteryLow) export const BatteryMediumInstUIIcon = wrapLucideIcon(Lucide.BatteryMedium) +export const BatteryPlusInstUIIcon = wrapLucideIcon(Lucide.BatteryPlus) export const BatteryWarningInstUIIcon = wrapLucideIcon(Lucide.BatteryWarning) export const BeakerInstUIIcon = wrapLucideIcon(Lucide.Beaker) export const BeanInstUIIcon = wrapLucideIcon(Lucide.Bean) @@ -362,6 +382,7 @@ export const BinaryInstUIIcon = wrapLucideIcon(Lucide.Binary) export const BinocularsInstUIIcon = wrapLucideIcon(Lucide.Binoculars) export const BiohazardInstUIIcon = wrapLucideIcon(Lucide.Biohazard) export const BirdInstUIIcon = wrapLucideIcon(Lucide.Bird) +export const BirdhouseInstUIIcon = wrapLucideIcon(Lucide.Birdhouse) export const BitcoinInstUIIcon = wrapLucideIcon(Lucide.Bitcoin) export const BlendInstUIIcon = wrapLucideIcon(Lucide.Blend) export const BlindsInstUIIcon = wrapLucideIcon(Lucide.Blinds) @@ -380,6 +401,7 @@ export const BombInstUIIcon = wrapLucideIcon(Lucide.Bomb) export const BoneInstUIIcon = wrapLucideIcon(Lucide.Bone) export const BookInstUIIcon = wrapLucideIcon(Lucide.Book) export const BookAInstUIIcon = wrapLucideIcon(Lucide.BookA) +export const BookAlertInstUIIcon = wrapLucideIcon(Lucide.BookAlert) export const BookAudioInstUIIcon = wrapLucideIcon(Lucide.BookAudio) export const BookCheckInstUIIcon = wrapLucideIcon(Lucide.BookCheck) export const BookCopyInstUIIcon = wrapLucideIcon(Lucide.BookCopy) @@ -396,6 +418,7 @@ export const BookOpenInstUIIcon = wrapLucideIcon(Lucide.BookOpen) export const BookOpenCheckInstUIIcon = wrapLucideIcon(Lucide.BookOpenCheck) export const BookOpenTextInstUIIcon = wrapLucideIcon(Lucide.BookOpenText) export const BookPlusInstUIIcon = wrapLucideIcon(Lucide.BookPlus) +export const BookSearchInstUIIcon = wrapLucideIcon(Lucide.BookSearch) export const BookTemplateInstUIIcon = wrapLucideIcon(Lucide.BookTemplate) export const BookTextInstUIIcon = wrapLucideIcon(Lucide.BookText) export const BookTypeInstUIIcon = wrapLucideIcon(Lucide.BookType) @@ -414,6 +437,8 @@ export const BotMessageSquareInstUIIcon = wrapLucideIcon( Lucide.BotMessageSquare ) export const BotOffInstUIIcon = wrapLucideIcon(Lucide.BotOff) +export const BottleWineInstUIIcon = wrapLucideIcon(Lucide.BottleWine) +export const BowArrowInstUIIcon = wrapLucideIcon(Lucide.BowArrow) export const BoxInstUIIcon = wrapLucideIcon(Lucide.Box) export const BoxSelectInstUIIcon = wrapLucideIcon(Lucide.BoxSelect) export const BoxesInstUIIcon = wrapLucideIcon(Lucide.Boxes) @@ -423,6 +448,8 @@ export const BrainInstUIIcon = wrapLucideIcon(Lucide.Brain) export const BrainCircuitInstUIIcon = wrapLucideIcon(Lucide.BrainCircuit) export const BrainCogInstUIIcon = wrapLucideIcon(Lucide.BrainCog) export const BrickWallInstUIIcon = wrapLucideIcon(Lucide.BrickWall) +export const BrickWallFireInstUIIcon = wrapLucideIcon(Lucide.BrickWallFire) +export const BrickWallShieldInstUIIcon = wrapLucideIcon(Lucide.BrickWallShield) export const BriefcaseInstUIIcon = wrapLucideIcon(Lucide.Briefcase) export const BriefcaseBusinessInstUIIcon = wrapLucideIcon( Lucide.BriefcaseBusiness @@ -435,6 +462,8 @@ export const BriefcaseMedicalInstUIIcon = wrapLucideIcon( ) export const BringToFrontInstUIIcon = wrapLucideIcon(Lucide.BringToFront) export const BrushInstUIIcon = wrapLucideIcon(Lucide.Brush) +export const BrushCleaningInstUIIcon = wrapLucideIcon(Lucide.BrushCleaning) +export const BubblesInstUIIcon = wrapLucideIcon(Lucide.Bubbles) export const BugInstUIIcon = wrapLucideIcon(Lucide.Bug) export const BugOffInstUIIcon = wrapLucideIcon(Lucide.BugOff) export const BugPlayInstUIIcon = wrapLucideIcon(Lucide.BugPlay) @@ -467,8 +496,10 @@ export const CalendarPlusInstUIIcon = wrapLucideIcon(Lucide.CalendarPlus) export const CalendarPlus2InstUIIcon = wrapLucideIcon(Lucide.CalendarPlus2) export const CalendarRangeInstUIIcon = wrapLucideIcon(Lucide.CalendarRange) export const CalendarSearchInstUIIcon = wrapLucideIcon(Lucide.CalendarSearch) +export const CalendarSyncInstUIIcon = wrapLucideIcon(Lucide.CalendarSync) export const CalendarXInstUIIcon = wrapLucideIcon(Lucide.CalendarX) export const CalendarX2InstUIIcon = wrapLucideIcon(Lucide.CalendarX2) +export const CalendarsInstUIIcon = wrapLucideIcon(Lucide.Calendars) export const CameraInstUIIcon = wrapLucideIcon(Lucide.Camera) export const CameraOffInstUIIcon = wrapLucideIcon(Lucide.CameraOff) export const CandlestickChartInstUIIcon = wrapLucideIcon( @@ -484,6 +515,7 @@ export const CarInstUIIcon = wrapLucideIcon(Lucide.Car) export const CarFrontInstUIIcon = wrapLucideIcon(Lucide.CarFront) export const CarTaxiFrontInstUIIcon = wrapLucideIcon(Lucide.CarTaxiFront) export const CaravanInstUIIcon = wrapLucideIcon(Lucide.Caravan) +export const CardSimInstUIIcon = wrapLucideIcon(Lucide.CardSim) export const CarrotInstUIIcon = wrapLucideIcon(Lucide.Carrot) export const CaseLowerInstUIIcon = wrapLucideIcon(Lucide.CaseLower) export const CaseSensitiveInstUIIcon = wrapLucideIcon(Lucide.CaseSensitive) @@ -542,10 +574,17 @@ export const CheckInstUIIcon = wrapLucideIcon(Lucide.Check) export const CheckCheckInstUIIcon = wrapLucideIcon(Lucide.CheckCheck) export const CheckCircleInstUIIcon = wrapLucideIcon(Lucide.CheckCircle) export const CheckCircle2InstUIIcon = wrapLucideIcon(Lucide.CheckCircle2) +export const CheckLineInstUIIcon = wrapLucideIcon(Lucide.CheckLine) export const CheckSquareInstUIIcon = wrapLucideIcon(Lucide.CheckSquare) export const CheckSquare2InstUIIcon = wrapLucideIcon(Lucide.CheckSquare2) export const ChefHatInstUIIcon = wrapLucideIcon(Lucide.ChefHat) export const CherryInstUIIcon = wrapLucideIcon(Lucide.Cherry) +export const ChessBishopInstUIIcon = wrapLucideIcon(Lucide.ChessBishop) +export const ChessKingInstUIIcon = wrapLucideIcon(Lucide.ChessKing) +export const ChessKnightInstUIIcon = wrapLucideIcon(Lucide.ChessKnight) +export const ChessPawnInstUIIcon = wrapLucideIcon(Lucide.ChessPawn) +export const ChessQueenInstUIIcon = wrapLucideIcon(Lucide.ChessQueen) +export const ChessRookInstUIIcon = wrapLucideIcon(Lucide.ChessRook) export const ChevronDownInstUIIcon = wrapLucideIcon(Lucide.ChevronDown) export const ChevronDownCircleInstUIIcon = wrapLucideIcon( Lucide.ChevronDownCircle @@ -588,6 +627,7 @@ export const ChevronsRightLeftInstUIIcon = wrapLucideIcon( export const ChevronsUpInstUIIcon = wrapLucideIcon(Lucide.ChevronsUp) export const ChevronsUpDownInstUIIcon = wrapLucideIcon(Lucide.ChevronsUpDown) export const ChromeInstUIIcon = wrapLucideIcon(Lucide.Chrome) +export const ChromiumInstUIIcon = wrapLucideIcon(Lucide.Chromium) export const ChurchInstUIIcon = wrapLucideIcon(Lucide.Church) export const CigaretteInstUIIcon = wrapLucideIcon(Lucide.Cigarette) export const CigaretteOffInstUIIcon = wrapLucideIcon(Lucide.CigaretteOff) @@ -648,12 +688,21 @@ export const CircleParkingOffInstUIIcon = wrapLucideIcon( ) export const CirclePauseInstUIIcon = wrapLucideIcon(Lucide.CirclePause) export const CirclePercentInstUIIcon = wrapLucideIcon(Lucide.CirclePercent) +export const CirclePileInstUIIcon = wrapLucideIcon(Lucide.CirclePile) export const CirclePlayInstUIIcon = wrapLucideIcon(Lucide.CirclePlay) export const CirclePlusInstUIIcon = wrapLucideIcon(Lucide.CirclePlus) +export const CirclePoundSterlingInstUIIcon = wrapLucideIcon( + Lucide.CirclePoundSterling +) export const CirclePowerInstUIIcon = wrapLucideIcon(Lucide.CirclePower) +export const CircleQuestionMarkInstUIIcon = wrapLucideIcon( + Lucide.CircleQuestionMark +) export const CircleSlashInstUIIcon = wrapLucideIcon(Lucide.CircleSlash) export const CircleSlash2InstUIIcon = wrapLucideIcon(Lucide.CircleSlash2) export const CircleSlashedInstUIIcon = wrapLucideIcon(Lucide.CircleSlashed) +export const CircleSmallInstUIIcon = wrapLucideIcon(Lucide.CircleSmall) +export const CircleStarInstUIIcon = wrapLucideIcon(Lucide.CircleStar) export const CircleStopInstUIIcon = wrapLucideIcon(Lucide.CircleStop) export const CircleUserInstUIIcon = wrapLucideIcon(Lucide.CircleUser) export const CircleUserRoundInstUIIcon = wrapLucideIcon(Lucide.CircleUserRound) @@ -663,6 +712,7 @@ export const CitrusInstUIIcon = wrapLucideIcon(Lucide.Citrus) export const ClapperboardInstUIIcon = wrapLucideIcon(Lucide.Clapperboard) export const ClipboardInstUIIcon = wrapLucideIcon(Lucide.Clipboard) export const ClipboardCheckInstUIIcon = wrapLucideIcon(Lucide.ClipboardCheck) +export const ClipboardClockInstUIIcon = wrapLucideIcon(Lucide.ClipboardClock) export const ClipboardCopyInstUIIcon = wrapLucideIcon(Lucide.ClipboardCopy) export const ClipboardEditInstUIIcon = wrapLucideIcon(Lucide.ClipboardEdit) export const ClipboardListInstUIIcon = wrapLucideIcon(Lucide.ClipboardList) @@ -694,8 +744,14 @@ export const Clock9InstUIIcon = wrapLucideIcon(Lucide.Clock9) export const ClockAlertInstUIIcon = wrapLucideIcon(Lucide.ClockAlert) export const ClockArrowDownInstUIIcon = wrapLucideIcon(Lucide.ClockArrowDown) export const ClockArrowUpInstUIIcon = wrapLucideIcon(Lucide.ClockArrowUp) +export const ClockCheckInstUIIcon = wrapLucideIcon(Lucide.ClockCheck) +export const ClockFadingInstUIIcon = wrapLucideIcon(Lucide.ClockFading) +export const ClockPlusInstUIIcon = wrapLucideIcon(Lucide.ClockPlus) +export const ClosedCaptionInstUIIcon = wrapLucideIcon(Lucide.ClosedCaption) export const CloudInstUIIcon = wrapLucideIcon(Lucide.Cloud) export const CloudAlertInstUIIcon = wrapLucideIcon(Lucide.CloudAlert) +export const CloudBackupInstUIIcon = wrapLucideIcon(Lucide.CloudBackup) +export const CloudCheckInstUIIcon = wrapLucideIcon(Lucide.CloudCheck) export const CloudCogInstUIIcon = wrapLucideIcon(Lucide.CloudCog) export const CloudDownloadInstUIIcon = wrapLucideIcon(Lucide.CloudDownload) export const CloudDrizzleInstUIIcon = wrapLucideIcon(Lucide.CloudDrizzle) @@ -710,6 +766,7 @@ export const CloudRainWindInstUIIcon = wrapLucideIcon(Lucide.CloudRainWind) export const CloudSnowInstUIIcon = wrapLucideIcon(Lucide.CloudSnow) export const CloudSunInstUIIcon = wrapLucideIcon(Lucide.CloudSun) export const CloudSunRainInstUIIcon = wrapLucideIcon(Lucide.CloudSunRain) +export const CloudSyncInstUIIcon = wrapLucideIcon(Lucide.CloudSync) export const CloudUploadInstUIIcon = wrapLucideIcon(Lucide.CloudUpload) export const CloudyInstUIIcon = wrapLucideIcon(Lucide.Cloudy) export const CloverInstUIIcon = wrapLucideIcon(Lucide.Clover) @@ -726,7 +783,9 @@ export const CoinsInstUIIcon = wrapLucideIcon(Lucide.Coins) export const ColumnsInstUIIcon = wrapLucideIcon(Lucide.Columns) export const Columns2InstUIIcon = wrapLucideIcon(Lucide.Columns2) export const Columns3InstUIIcon = wrapLucideIcon(Lucide.Columns3) +export const Columns3CogInstUIIcon = wrapLucideIcon(Lucide.Columns3Cog) export const Columns4InstUIIcon = wrapLucideIcon(Lucide.Columns4) +export const ColumnsSettingsInstUIIcon = wrapLucideIcon(Lucide.ColumnsSettings) export const CombineInstUIIcon = wrapLucideIcon(Lucide.Combine) export const CommandInstUIIcon = wrapLucideIcon(Lucide.Command) export const CompassInstUIIcon = wrapLucideIcon(Lucide.Compass) @@ -775,6 +834,12 @@ export const DamInstUIIcon = wrapLucideIcon(Lucide.Dam) export const DatabaseInstUIIcon = wrapLucideIcon(Lucide.Database) export const DatabaseBackupInstUIIcon = wrapLucideIcon(Lucide.DatabaseBackup) export const DatabaseZapInstUIIcon = wrapLucideIcon(Lucide.DatabaseZap) +export const DecimalsArrowLeftInstUIIcon = wrapLucideIcon( + Lucide.DecimalsArrowLeft +) +export const DecimalsArrowRightInstUIIcon = wrapLucideIcon( + Lucide.DecimalsArrowRight +) export const DeleteInstUIIcon = wrapLucideIcon(Lucide.Delete) export const DessertInstUIIcon = wrapLucideIcon(Lucide.Dessert) export const DiameterInstUIIcon = wrapLucideIcon(Lucide.Diameter) @@ -804,6 +869,9 @@ export const DogInstUIIcon = wrapLucideIcon(Lucide.Dog) export const DollarSignInstUIIcon = wrapLucideIcon(Lucide.DollarSign) export const DonutInstUIIcon = wrapLucideIcon(Lucide.Donut) export const DoorClosedInstUIIcon = wrapLucideIcon(Lucide.DoorClosed) +export const DoorClosedLockedInstUIIcon = wrapLucideIcon( + Lucide.DoorClosedLocked +) export const DoorOpenInstUIIcon = wrapLucideIcon(Lucide.DoorOpen) export const DotInstUIIcon = wrapLucideIcon(Lucide.Dot) export const DotSquareInstUIIcon = wrapLucideIcon(Lucide.DotSquare) @@ -813,7 +881,9 @@ export const DraftingCompassInstUIIcon = wrapLucideIcon(Lucide.DraftingCompass) export const DramaInstUIIcon = wrapLucideIcon(Lucide.Drama) export const DribbbleInstUIIcon = wrapLucideIcon(Lucide.Dribbble) export const DrillInstUIIcon = wrapLucideIcon(Lucide.Drill) +export const DroneInstUIIcon = wrapLucideIcon(Lucide.Drone) export const DropletInstUIIcon = wrapLucideIcon(Lucide.Droplet) +export const DropletOffInstUIIcon = wrapLucideIcon(Lucide.DropletOff) export const DropletsInstUIIcon = wrapLucideIcon(Lucide.Droplets) export const DrumInstUIIcon = wrapLucideIcon(Lucide.Drum) export const DrumstickInstUIIcon = wrapLucideIcon(Lucide.Drumstick) @@ -842,6 +912,7 @@ export const EqualSquareInstUIIcon = wrapLucideIcon(Lucide.EqualSquare) export const EraserInstUIIcon = wrapLucideIcon(Lucide.Eraser) export const EthernetPortInstUIIcon = wrapLucideIcon(Lucide.EthernetPort) export const EuroInstUIIcon = wrapLucideIcon(Lucide.Euro) +export const EvChargerInstUIIcon = wrapLucideIcon(Lucide.EvCharger) export const ExpandInstUIIcon = wrapLucideIcon(Lucide.Expand) export const ExternalLinkInstUIIcon = wrapLucideIcon(Lucide.ExternalLink) export const EyeInstUIIcon = wrapLucideIcon(Lucide.Eye) @@ -866,6 +937,10 @@ export const FileBadge2InstUIIcon = wrapLucideIcon(Lucide.FileBadge2) export const FileBarChartInstUIIcon = wrapLucideIcon(Lucide.FileBarChart) export const FileBarChart2InstUIIcon = wrapLucideIcon(Lucide.FileBarChart2) export const FileBoxInstUIIcon = wrapLucideIcon(Lucide.FileBox) +export const FileBracesInstUIIcon = wrapLucideIcon(Lucide.FileBraces) +export const FileBracesCornerInstUIIcon = wrapLucideIcon( + Lucide.FileBracesCorner +) export const FileChartColumnInstUIIcon = wrapLucideIcon(Lucide.FileChartColumn) export const FileChartColumnIncreasingInstUIIcon = wrapLucideIcon( Lucide.FileChartColumnIncreasing @@ -874,15 +949,21 @@ export const FileChartLineInstUIIcon = wrapLucideIcon(Lucide.FileChartLine) export const FileChartPieInstUIIcon = wrapLucideIcon(Lucide.FileChartPie) export const FileCheckInstUIIcon = wrapLucideIcon(Lucide.FileCheck) export const FileCheck2InstUIIcon = wrapLucideIcon(Lucide.FileCheck2) +export const FileCheckCornerInstUIIcon = wrapLucideIcon(Lucide.FileCheckCorner) export const FileClockInstUIIcon = wrapLucideIcon(Lucide.FileClock) export const FileCodeInstUIIcon = wrapLucideIcon(Lucide.FileCode) export const FileCode2InstUIIcon = wrapLucideIcon(Lucide.FileCode2) +export const FileCodeCornerInstUIIcon = wrapLucideIcon(Lucide.FileCodeCorner) export const FileCogInstUIIcon = wrapLucideIcon(Lucide.FileCog) export const FileCog2InstUIIcon = wrapLucideIcon(Lucide.FileCog2) export const FileDiffInstUIIcon = wrapLucideIcon(Lucide.FileDiff) export const FileDigitInstUIIcon = wrapLucideIcon(Lucide.FileDigit) export const FileDownInstUIIcon = wrapLucideIcon(Lucide.FileDown) export const FileEditInstUIIcon = wrapLucideIcon(Lucide.FileEdit) +export const FileExclamationPointInstUIIcon = wrapLucideIcon( + Lucide.FileExclamationPoint +) +export const FileHeadphoneInstUIIcon = wrapLucideIcon(Lucide.FileHeadphone) export const FileHeartInstUIIcon = wrapLucideIcon(Lucide.FileHeart) export const FileImageInstUIIcon = wrapLucideIcon(Lucide.FileImage) export const FileInputInstUIIcon = wrapLucideIcon(Lucide.FileInput) @@ -895,17 +976,27 @@ export const FileLockInstUIIcon = wrapLucideIcon(Lucide.FileLock) export const FileLock2InstUIIcon = wrapLucideIcon(Lucide.FileLock2) export const FileMinusInstUIIcon = wrapLucideIcon(Lucide.FileMinus) export const FileMinus2InstUIIcon = wrapLucideIcon(Lucide.FileMinus2) +export const FileMinusCornerInstUIIcon = wrapLucideIcon(Lucide.FileMinusCorner) export const FileMusicInstUIIcon = wrapLucideIcon(Lucide.FileMusic) export const FileOutputInstUIIcon = wrapLucideIcon(Lucide.FileOutput) export const FilePenInstUIIcon = wrapLucideIcon(Lucide.FilePen) export const FilePenLineInstUIIcon = wrapLucideIcon(Lucide.FilePenLine) export const FilePieChartInstUIIcon = wrapLucideIcon(Lucide.FilePieChart) +export const FilePlayInstUIIcon = wrapLucideIcon(Lucide.FilePlay) export const FilePlusInstUIIcon = wrapLucideIcon(Lucide.FilePlus) export const FilePlus2InstUIIcon = wrapLucideIcon(Lucide.FilePlus2) +export const FilePlusCornerInstUIIcon = wrapLucideIcon(Lucide.FilePlusCorner) export const FileQuestionInstUIIcon = wrapLucideIcon(Lucide.FileQuestion) +export const FileQuestionMarkInstUIIcon = wrapLucideIcon( + Lucide.FileQuestionMark +) export const FileScanInstUIIcon = wrapLucideIcon(Lucide.FileScan) export const FileSearchInstUIIcon = wrapLucideIcon(Lucide.FileSearch) export const FileSearch2InstUIIcon = wrapLucideIcon(Lucide.FileSearch2) +export const FileSearchCornerInstUIIcon = wrapLucideIcon( + Lucide.FileSearchCorner +) +export const FileSignalInstUIIcon = wrapLucideIcon(Lucide.FileSignal) export const FileSignatureInstUIIcon = wrapLucideIcon(Lucide.FileSignature) export const FileSlidersInstUIIcon = wrapLucideIcon(Lucide.FileSliders) export const FileSpreadsheetInstUIIcon = wrapLucideIcon(Lucide.FileSpreadsheet) @@ -915,26 +1006,33 @@ export const FileTerminalInstUIIcon = wrapLucideIcon(Lucide.FileTerminal) export const FileTextInstUIIcon = wrapLucideIcon(Lucide.FileText) export const FileTypeInstUIIcon = wrapLucideIcon(Lucide.FileType) export const FileType2InstUIIcon = wrapLucideIcon(Lucide.FileType2) +export const FileTypeCornerInstUIIcon = wrapLucideIcon(Lucide.FileTypeCorner) export const FileUpInstUIIcon = wrapLucideIcon(Lucide.FileUp) export const FileUserInstUIIcon = wrapLucideIcon(Lucide.FileUser) export const FileVideoInstUIIcon = wrapLucideIcon(Lucide.FileVideo) export const FileVideo2InstUIIcon = wrapLucideIcon(Lucide.FileVideo2) +export const FileVideoCameraInstUIIcon = wrapLucideIcon(Lucide.FileVideoCamera) export const FileVolumeInstUIIcon = wrapLucideIcon(Lucide.FileVolume) export const FileVolume2InstUIIcon = wrapLucideIcon(Lucide.FileVolume2) export const FileWarningInstUIIcon = wrapLucideIcon(Lucide.FileWarning) export const FileXInstUIIcon = wrapLucideIcon(Lucide.FileX) export const FileX2InstUIIcon = wrapLucideIcon(Lucide.FileX2) +export const FileXCornerInstUIIcon = wrapLucideIcon(Lucide.FileXCorner) export const FilesInstUIIcon = wrapLucideIcon(Lucide.Files) export const FilmInstUIIcon = wrapLucideIcon(Lucide.Film) export const FilterInstUIIcon = wrapLucideIcon(Lucide.Filter) export const FilterXInstUIIcon = wrapLucideIcon(Lucide.FilterX) export const FingerprintInstUIIcon = wrapLucideIcon(Lucide.Fingerprint) +export const FingerprintPatternInstUIIcon = wrapLucideIcon( + Lucide.FingerprintPattern +) export const FireExtinguisherInstUIIcon = wrapLucideIcon( Lucide.FireExtinguisher ) export const FishInstUIIcon = wrapLucideIcon(Lucide.Fish) export const FishOffInstUIIcon = wrapLucideIcon(Lucide.FishOff) export const FishSymbolInstUIIcon = wrapLucideIcon(Lucide.FishSymbol) +export const FishingHookInstUIIcon = wrapLucideIcon(Lucide.FishingHook) export const FlagInstUIIcon = wrapLucideIcon(Lucide.Flag) export const FlagOffInstUIIcon = wrapLucideIcon(Lucide.FlagOff) export const FlagTriangleLeftInstUIIcon = wrapLucideIcon( @@ -998,6 +1096,7 @@ export const ForkKnifeCrossedInstUIIcon = wrapLucideIcon( Lucide.ForkKnifeCrossed ) export const ForkliftInstUIIcon = wrapLucideIcon(Lucide.Forklift) +export const FormInstUIIcon = wrapLucideIcon(Lucide.Form) export const FormInputInstUIIcon = wrapLucideIcon(Lucide.FormInput) export const ForwardInstUIIcon = wrapLucideIcon(Lucide.Forward) export const FrameInstUIIcon = wrapLucideIcon(Lucide.Frame) @@ -1006,6 +1105,9 @@ export const FrownInstUIIcon = wrapLucideIcon(Lucide.Frown) export const FuelInstUIIcon = wrapLucideIcon(Lucide.Fuel) export const FullscreenInstUIIcon = wrapLucideIcon(Lucide.Fullscreen) export const FunctionSquareInstUIIcon = wrapLucideIcon(Lucide.FunctionSquare) +export const FunnelInstUIIcon = wrapLucideIcon(Lucide.Funnel) +export const FunnelPlusInstUIIcon = wrapLucideIcon(Lucide.FunnelPlus) +export const FunnelXInstUIIcon = wrapLucideIcon(Lucide.FunnelX) export const GalleryHorizontalInstUIIcon = wrapLucideIcon( Lucide.GalleryHorizontal ) @@ -1021,6 +1123,9 @@ export const GalleryVerticalEndInstUIIcon = wrapLucideIcon( ) export const GamepadInstUIIcon = wrapLucideIcon(Lucide.Gamepad) export const Gamepad2InstUIIcon = wrapLucideIcon(Lucide.Gamepad2) +export const GamepadDirectionalInstUIIcon = wrapLucideIcon( + Lucide.GamepadDirectional +) export const GanttChartInstUIIcon = wrapLucideIcon(Lucide.GanttChart) export const GanttChartSquareInstUIIcon = wrapLucideIcon( Lucide.GanttChartSquare @@ -1029,9 +1134,11 @@ export const GaugeInstUIIcon = wrapLucideIcon(Lucide.Gauge) export const GaugeCircleInstUIIcon = wrapLucideIcon(Lucide.GaugeCircle) export const GavelInstUIIcon = wrapLucideIcon(Lucide.Gavel) export const GemInstUIIcon = wrapLucideIcon(Lucide.Gem) +export const GeorgianLariInstUIIcon = wrapLucideIcon(Lucide.GeorgianLari) export const GhostInstUIIcon = wrapLucideIcon(Lucide.Ghost) export const GiftInstUIIcon = wrapLucideIcon(Lucide.Gift) export const GitBranchInstUIIcon = wrapLucideIcon(Lucide.GitBranch) +export const GitBranchMinusInstUIIcon = wrapLucideIcon(Lucide.GitBranchMinus) export const GitBranchPlusInstUIIcon = wrapLucideIcon(Lucide.GitBranchPlus) export const GitCommitInstUIIcon = wrapLucideIcon(Lucide.GitCommit) export const GitCommitHorizontalInstUIIcon = wrapLucideIcon( @@ -1071,17 +1178,21 @@ export const GlobeInstUIIcon = wrapLucideIcon(Lucide.Globe) export const Globe2InstUIIcon = wrapLucideIcon(Lucide.Globe2) export const GlobeLockInstUIIcon = wrapLucideIcon(Lucide.GlobeLock) export const GoalInstUIIcon = wrapLucideIcon(Lucide.Goal) +export const GpuInstUIIcon = wrapLucideIcon(Lucide.Gpu) export const GrabInstUIIcon = wrapLucideIcon(Lucide.Grab) export const GraduationCapInstUIIcon = wrapLucideIcon(Lucide.GraduationCap) export const GrapeInstUIIcon = wrapLucideIcon(Lucide.Grape) export const GridInstUIIcon = wrapLucideIcon(Lucide.Grid) export const Grid2X2InstUIIcon = wrapLucideIcon(Lucide.Grid2X2) +export const Grid2X2CheckInstUIIcon = wrapLucideIcon(Lucide.Grid2X2Check) export const Grid2X2PlusInstUIIcon = wrapLucideIcon(Lucide.Grid2X2Plus) +export const Grid2X2XInstUIIcon = wrapLucideIcon(Lucide.Grid2X2X) export const Grid2x2InstUIIcon = wrapLucideIcon(Lucide.Grid2x2) export const Grid2x2CheckInstUIIcon = wrapLucideIcon(Lucide.Grid2x2Check) export const Grid2x2PlusInstUIIcon = wrapLucideIcon(Lucide.Grid2x2Plus) export const Grid2x2XInstUIIcon = wrapLucideIcon(Lucide.Grid2x2X) export const Grid3X3InstUIIcon = wrapLucideIcon(Lucide.Grid3X3) +export const Grid3x2InstUIIcon = wrapLucideIcon(Lucide.Grid3x2) export const Grid3x3InstUIIcon = wrapLucideIcon(Lucide.Grid3x3) export const GripInstUIIcon = wrapLucideIcon(Lucide.Grip) export const GripHorizontalInstUIIcon = wrapLucideIcon(Lucide.GripHorizontal) @@ -1089,13 +1200,17 @@ export const GripVerticalInstUIIcon = wrapLucideIcon(Lucide.GripVertical) export const GroupInstUIIcon = wrapLucideIcon(Lucide.Group) export const GuitarInstUIIcon = wrapLucideIcon(Lucide.Guitar) export const HamInstUIIcon = wrapLucideIcon(Lucide.Ham) +export const HamburgerInstUIIcon = wrapLucideIcon(Lucide.Hamburger) export const HammerInstUIIcon = wrapLucideIcon(Lucide.Hammer) export const HandInstUIIcon = wrapLucideIcon(Lucide.Hand) export const HandCoinsInstUIIcon = wrapLucideIcon(Lucide.HandCoins) +export const HandFistInstUIIcon = wrapLucideIcon(Lucide.HandFist) +export const HandGrabInstUIIcon = wrapLucideIcon(Lucide.HandGrab) export const HandHeartInstUIIcon = wrapLucideIcon(Lucide.HandHeart) export const HandHelpingInstUIIcon = wrapLucideIcon(Lucide.HandHelping) export const HandMetalInstUIIcon = wrapLucideIcon(Lucide.HandMetal) export const HandPlatterInstUIIcon = wrapLucideIcon(Lucide.HandPlatter) +export const HandbagInstUIIcon = wrapLucideIcon(Lucide.Handbag) export const HandshakeInstUIIcon = wrapLucideIcon(Lucide.Handshake) export const HardDriveInstUIIcon = wrapLucideIcon(Lucide.HardDrive) export const HardDriveDownloadInstUIIcon = wrapLucideIcon( @@ -1104,7 +1219,9 @@ export const HardDriveDownloadInstUIIcon = wrapLucideIcon( export const HardDriveUploadInstUIIcon = wrapLucideIcon(Lucide.HardDriveUpload) export const HardHatInstUIIcon = wrapLucideIcon(Lucide.HardHat) export const HashInstUIIcon = wrapLucideIcon(Lucide.Hash) +export const HatGlassesInstUIIcon = wrapLucideIcon(Lucide.HatGlasses) export const HazeInstUIIcon = wrapLucideIcon(Lucide.Haze) +export const HdInstUIIcon = wrapLucideIcon(Lucide.Hd) export const HdmiPortInstUIIcon = wrapLucideIcon(Lucide.HdmiPort) export const HeadingInstUIIcon = wrapLucideIcon(Lucide.Heading) export const Heading1InstUIIcon = wrapLucideIcon(Lucide.Heading1) @@ -1119,9 +1236,12 @@ export const HeadsetInstUIIcon = wrapLucideIcon(Lucide.Headset) export const HeartInstUIIcon = wrapLucideIcon(Lucide.Heart) export const HeartCrackInstUIIcon = wrapLucideIcon(Lucide.HeartCrack) export const HeartHandshakeInstUIIcon = wrapLucideIcon(Lucide.HeartHandshake) +export const HeartMinusInstUIIcon = wrapLucideIcon(Lucide.HeartMinus) export const HeartOffInstUIIcon = wrapLucideIcon(Lucide.HeartOff) +export const HeartPlusInstUIIcon = wrapLucideIcon(Lucide.HeartPlus) export const HeartPulseInstUIIcon = wrapLucideIcon(Lucide.HeartPulse) export const HeaterInstUIIcon = wrapLucideIcon(Lucide.Heater) +export const HelicopterInstUIIcon = wrapLucideIcon(Lucide.Helicopter) export const HelpCircleInstUIIcon = wrapLucideIcon(Lucide.HelpCircle) export const HelpingHandInstUIIcon = wrapLucideIcon(Lucide.HelpingHand) export const HexagonInstUIIcon = wrapLucideIcon(Lucide.Hexagon) @@ -1134,13 +1254,16 @@ export const HospitalInstUIIcon = wrapLucideIcon(Lucide.Hospital) export const HotelInstUIIcon = wrapLucideIcon(Lucide.Hotel) export const HourglassInstUIIcon = wrapLucideIcon(Lucide.Hourglass) export const HouseInstUIIcon = wrapLucideIcon(Lucide.House) +export const HouseHeartInstUIIcon = wrapLucideIcon(Lucide.HouseHeart) export const HousePlugInstUIIcon = wrapLucideIcon(Lucide.HousePlug) export const HousePlusInstUIIcon = wrapLucideIcon(Lucide.HousePlus) +export const HouseWifiInstUIIcon = wrapLucideIcon(Lucide.HouseWifi) export const IceCreamInstUIIcon = wrapLucideIcon(Lucide.IceCream) export const IceCream2InstUIIcon = wrapLucideIcon(Lucide.IceCream2) export const IceCreamBowlInstUIIcon = wrapLucideIcon(Lucide.IceCreamBowl) export const IceCreamConeInstUIIcon = wrapLucideIcon(Lucide.IceCreamCone) export const IdCardInstUIIcon = wrapLucideIcon(Lucide.IdCard) +export const IdCardLanyardInstUIIcon = wrapLucideIcon(Lucide.IdCardLanyard) export const ImageInstUIIcon = wrapLucideIcon(Lucide.Image) export const ImageDownInstUIIcon = wrapLucideIcon(Lucide.ImageDown) export const ImageMinusInstUIIcon = wrapLucideIcon(Lucide.ImageMinus) @@ -1148,6 +1271,7 @@ export const ImageOffInstUIIcon = wrapLucideIcon(Lucide.ImageOff) export const ImagePlayInstUIIcon = wrapLucideIcon(Lucide.ImagePlay) export const ImagePlusInstUIIcon = wrapLucideIcon(Lucide.ImagePlus) export const ImageUpInstUIIcon = wrapLucideIcon(Lucide.ImageUp) +export const ImageUpscaleInstUIIcon = wrapLucideIcon(Lucide.ImageUpscale) export const ImagesInstUIIcon = wrapLucideIcon(Lucide.Images) export const ImportInstUIIcon = wrapLucideIcon(Lucide.Import) export const InboxInstUIIcon = wrapLucideIcon(Lucide.Inbox) @@ -1170,6 +1294,7 @@ export const KanbanSquareInstUIIcon = wrapLucideIcon(Lucide.KanbanSquare) export const KanbanSquareDashedInstUIIcon = wrapLucideIcon( Lucide.KanbanSquareDashed ) +export const KayakInstUIIcon = wrapLucideIcon(Lucide.Kayak) export const KeyInstUIIcon = wrapLucideIcon(Lucide.Key) export const KeyRoundInstUIIcon = wrapLucideIcon(Lucide.KeyRound) export const KeySquareInstUIIcon = wrapLucideIcon(Lucide.KeySquare) @@ -1197,6 +1322,7 @@ export const LaughInstUIIcon = wrapLucideIcon(Lucide.Laugh) export const LayersInstUIIcon = wrapLucideIcon(Lucide.Layers) export const Layers2InstUIIcon = wrapLucideIcon(Lucide.Layers2) export const Layers3InstUIIcon = wrapLucideIcon(Lucide.Layers3) +export const LayersPlusInstUIIcon = wrapLucideIcon(Lucide.LayersPlus) export const LayoutInstUIIcon = wrapLucideIcon(Lucide.Layout) export const LayoutDashboardInstUIIcon = wrapLucideIcon(Lucide.LayoutDashboard) export const LayoutGridInstUIIcon = wrapLucideIcon(Lucide.LayoutGrid) @@ -1216,6 +1342,7 @@ export const LigatureInstUIIcon = wrapLucideIcon(Lucide.Ligature) export const LightbulbInstUIIcon = wrapLucideIcon(Lucide.Lightbulb) export const LightbulbOffInstUIIcon = wrapLucideIcon(Lucide.LightbulbOff) export const LineChartInstUIIcon = wrapLucideIcon(Lucide.LineChart) +export const LineSquiggleInstUIIcon = wrapLucideIcon(Lucide.LineSquiggle) export const LinkInstUIIcon = wrapLucideIcon(Lucide.Link) export const Link2InstUIIcon = wrapLucideIcon(Lucide.Link2) export const Link2OffInstUIIcon = wrapLucideIcon(Lucide.Link2Off) @@ -1223,9 +1350,22 @@ export const LinkedinInstUIIcon = wrapLucideIcon(Lucide.Linkedin) export const ListInstUIIcon = wrapLucideIcon(Lucide.List) export const ListCheckInstUIIcon = wrapLucideIcon(Lucide.ListCheck) export const ListChecksInstUIIcon = wrapLucideIcon(Lucide.ListChecks) +export const ListChevronsDownUpInstUIIcon = wrapLucideIcon( + Lucide.ListChevronsDownUp +) +export const ListChevronsUpDownInstUIIcon = wrapLucideIcon( + Lucide.ListChevronsUpDown +) export const ListCollapseInstUIIcon = wrapLucideIcon(Lucide.ListCollapse) export const ListEndInstUIIcon = wrapLucideIcon(Lucide.ListEnd) export const ListFilterInstUIIcon = wrapLucideIcon(Lucide.ListFilter) +export const ListFilterPlusInstUIIcon = wrapLucideIcon(Lucide.ListFilterPlus) +export const ListIndentDecreaseInstUIIcon = wrapLucideIcon( + Lucide.ListIndentDecrease +) +export const ListIndentIncreaseInstUIIcon = wrapLucideIcon( + Lucide.ListIndentIncrease +) export const ListMinusInstUIIcon = wrapLucideIcon(Lucide.ListMinus) export const ListMusicInstUIIcon = wrapLucideIcon(Lucide.ListMusic) export const ListOrderedInstUIIcon = wrapLucideIcon(Lucide.ListOrdered) @@ -1243,6 +1383,7 @@ export const LoaderPinwheelInstUIIcon = wrapLucideIcon(Lucide.LoaderPinwheel) export const LocateInstUIIcon = wrapLucideIcon(Lucide.Locate) export const LocateFixedInstUIIcon = wrapLucideIcon(Lucide.LocateFixed) export const LocateOffInstUIIcon = wrapLucideIcon(Lucide.LocateOff) +export const LocationEditInstUIIcon = wrapLucideIcon(Lucide.LocationEdit) export const LockInstUIIcon = wrapLucideIcon(Lucide.Lock) export const LockKeyholeInstUIIcon = wrapLucideIcon(Lucide.LockKeyhole) export const LockKeyholeOpenInstUIIcon = wrapLucideIcon(Lucide.LockKeyholeOpen) @@ -1260,12 +1401,16 @@ export const MailMinusInstUIIcon = wrapLucideIcon(Lucide.MailMinus) export const MailOpenInstUIIcon = wrapLucideIcon(Lucide.MailOpen) export const MailPlusInstUIIcon = wrapLucideIcon(Lucide.MailPlus) export const MailQuestionInstUIIcon = wrapLucideIcon(Lucide.MailQuestion) +export const MailQuestionMarkInstUIIcon = wrapLucideIcon( + Lucide.MailQuestionMark +) export const MailSearchInstUIIcon = wrapLucideIcon(Lucide.MailSearch) export const MailWarningInstUIIcon = wrapLucideIcon(Lucide.MailWarning) export const MailXInstUIIcon = wrapLucideIcon(Lucide.MailX) export const MailboxInstUIIcon = wrapLucideIcon(Lucide.Mailbox) export const MailsInstUIIcon = wrapLucideIcon(Lucide.Mails) export const MapInstUIIcon = wrapLucideIcon(Lucide.Map) +export const MapMinusInstUIIcon = wrapLucideIcon(Lucide.MapMinus) export const MapPinInstUIIcon = wrapLucideIcon(Lucide.MapPin) export const MapPinCheckInstUIIcon = wrapLucideIcon(Lucide.MapPinCheck) export const MapPinCheckInsideInstUIIcon = wrapLucideIcon( @@ -1277,6 +1422,7 @@ export const MapPinMinusInsideInstUIIcon = wrapLucideIcon( Lucide.MapPinMinusInside ) export const MapPinOffInstUIIcon = wrapLucideIcon(Lucide.MapPinOff) +export const MapPinPenInstUIIcon = wrapLucideIcon(Lucide.MapPinPen) export const MapPinPlusInstUIIcon = wrapLucideIcon(Lucide.MapPinPlus) export const MapPinPlusInsideInstUIIcon = wrapLucideIcon( Lucide.MapPinPlusInside @@ -1284,6 +1430,9 @@ export const MapPinPlusInsideInstUIIcon = wrapLucideIcon( export const MapPinXInstUIIcon = wrapLucideIcon(Lucide.MapPinX) export const MapPinXInsideInstUIIcon = wrapLucideIcon(Lucide.MapPinXInside) export const MapPinnedInstUIIcon = wrapLucideIcon(Lucide.MapPinned) +export const MapPlusInstUIIcon = wrapLucideIcon(Lucide.MapPlus) +export const MarsInstUIIcon = wrapLucideIcon(Lucide.Mars) +export const MarsStrokeInstUIIcon = wrapLucideIcon(Lucide.MarsStroke) export const MartiniInstUIIcon = wrapLucideIcon(Lucide.Martini) export const MaximizeInstUIIcon = wrapLucideIcon(Lucide.Maximize) export const Maximize2InstUIIcon = wrapLucideIcon(Lucide.Maximize2) @@ -1317,6 +1466,9 @@ export const MessageCirclePlusInstUIIcon = wrapLucideIcon( export const MessageCircleQuestionInstUIIcon = wrapLucideIcon( Lucide.MessageCircleQuestion ) +export const MessageCircleQuestionMarkInstUIIcon = wrapLucideIcon( + Lucide.MessageCircleQuestionMark +) export const MessageCircleReplyInstUIIcon = wrapLucideIcon( Lucide.MessageCircleReply ) @@ -1386,6 +1538,7 @@ export const MinusCircleInstUIIcon = wrapLucideIcon(Lucide.MinusCircle) export const MinusSquareInstUIIcon = wrapLucideIcon(Lucide.MinusSquare) export const MonitorInstUIIcon = wrapLucideIcon(Lucide.Monitor) export const MonitorCheckInstUIIcon = wrapLucideIcon(Lucide.MonitorCheck) +export const MonitorCloudInstUIIcon = wrapLucideIcon(Lucide.MonitorCloud) export const MonitorCogInstUIIcon = wrapLucideIcon(Lucide.MonitorCog) export const MonitorDotInstUIIcon = wrapLucideIcon(Lucide.MonitorDot) export const MonitorDownInstUIIcon = wrapLucideIcon(Lucide.MonitorDown) @@ -1403,12 +1556,16 @@ export const MoonInstUIIcon = wrapLucideIcon(Lucide.Moon) export const MoonStarInstUIIcon = wrapLucideIcon(Lucide.MoonStar) export const MoreHorizontalInstUIIcon = wrapLucideIcon(Lucide.MoreHorizontal) export const MoreVerticalInstUIIcon = wrapLucideIcon(Lucide.MoreVertical) +export const MotorbikeInstUIIcon = wrapLucideIcon(Lucide.Motorbike) export const MountainInstUIIcon = wrapLucideIcon(Lucide.Mountain) export const MountainSnowInstUIIcon = wrapLucideIcon(Lucide.MountainSnow) export const MouseInstUIIcon = wrapLucideIcon(Lucide.Mouse) export const MouseOffInstUIIcon = wrapLucideIcon(Lucide.MouseOff) export const MousePointerInstUIIcon = wrapLucideIcon(Lucide.MousePointer) export const MousePointer2InstUIIcon = wrapLucideIcon(Lucide.MousePointer2) +export const MousePointer2OffInstUIIcon = wrapLucideIcon( + Lucide.MousePointer2Off +) export const MousePointerBanInstUIIcon = wrapLucideIcon(Lucide.MousePointerBan) export const MousePointerClickInstUIIcon = wrapLucideIcon( Lucide.MousePointerClick @@ -1442,6 +1599,7 @@ export const NavigationOffInstUIIcon = wrapLucideIcon(Lucide.NavigationOff) export const NetworkInstUIIcon = wrapLucideIcon(Lucide.Network) export const NewspaperInstUIIcon = wrapLucideIcon(Lucide.Newspaper) export const NfcInstUIIcon = wrapLucideIcon(Lucide.Nfc) +export const NonBinaryInstUIIcon = wrapLucideIcon(Lucide.NonBinary) export const NotebookInstUIIcon = wrapLucideIcon(Lucide.Notebook) export const NotebookPenInstUIIcon = wrapLucideIcon(Lucide.NotebookPen) export const NotebookTabsInstUIIcon = wrapLucideIcon(Lucide.NotebookTabs) @@ -1479,6 +1637,7 @@ export const PaintbrushVerticalInstUIIcon = wrapLucideIcon( ) export const PaletteInstUIIcon = wrapLucideIcon(Lucide.Palette) export const PalmtreeInstUIIcon = wrapLucideIcon(Lucide.Palmtree) +export const PandaInstUIIcon = wrapLucideIcon(Lucide.Panda) export const PanelBottomInstUIIcon = wrapLucideIcon(Lucide.PanelBottom) export const PanelBottomCloseInstUIIcon = wrapLucideIcon( Lucide.PanelBottomClose @@ -1497,6 +1656,9 @@ export const PanelLeftInactiveInstUIIcon = wrapLucideIcon( Lucide.PanelLeftInactive ) export const PanelLeftOpenInstUIIcon = wrapLucideIcon(Lucide.PanelLeftOpen) +export const PanelLeftRightDashedInstUIIcon = wrapLucideIcon( + Lucide.PanelLeftRightDashed +) export const PanelRightInstUIIcon = wrapLucideIcon(Lucide.PanelRight) export const PanelRightCloseInstUIIcon = wrapLucideIcon(Lucide.PanelRightClose) export const PanelRightDashedInstUIIcon = wrapLucideIcon( @@ -1507,6 +1669,9 @@ export const PanelRightInactiveInstUIIcon = wrapLucideIcon( ) export const PanelRightOpenInstUIIcon = wrapLucideIcon(Lucide.PanelRightOpen) export const PanelTopInstUIIcon = wrapLucideIcon(Lucide.PanelTop) +export const PanelTopBottomDashedInstUIIcon = wrapLucideIcon( + Lucide.PanelTopBottomDashed +) export const PanelTopCloseInstUIIcon = wrapLucideIcon(Lucide.PanelTopClose) export const PanelTopDashedInstUIIcon = wrapLucideIcon(Lucide.PanelTopDashed) export const PanelTopInactiveInstUIIcon = wrapLucideIcon( @@ -1650,9 +1815,16 @@ export const ReceiptSwissFrancInstUIIcon = wrapLucideIcon( Lucide.ReceiptSwissFranc ) export const ReceiptTextInstUIIcon = wrapLucideIcon(Lucide.ReceiptText) +export const ReceiptTurkishLiraInstUIIcon = wrapLucideIcon( + Lucide.ReceiptTurkishLira +) +export const RectangleCircleInstUIIcon = wrapLucideIcon(Lucide.RectangleCircle) export const RectangleEllipsisInstUIIcon = wrapLucideIcon( Lucide.RectangleEllipsis ) +export const RectangleGogglesInstUIIcon = wrapLucideIcon( + Lucide.RectangleGoggles +) export const RectangleHorizontalInstUIIcon = wrapLucideIcon( Lucide.RectangleHorizontal ) @@ -1684,9 +1856,11 @@ export const RibbonInstUIIcon = wrapLucideIcon(Lucide.Ribbon) export const RocketInstUIIcon = wrapLucideIcon(Lucide.Rocket) export const RockingChairInstUIIcon = wrapLucideIcon(Lucide.RockingChair) export const RollerCoasterInstUIIcon = wrapLucideIcon(Lucide.RollerCoaster) +export const RoseInstUIIcon = wrapLucideIcon(Lucide.Rose) export const Rotate3DInstUIIcon = wrapLucideIcon(Lucide.Rotate3D) export const Rotate3dInstUIIcon = wrapLucideIcon(Lucide.Rotate3d) export const RotateCcwInstUIIcon = wrapLucideIcon(Lucide.RotateCcw) +export const RotateCcwKeyInstUIIcon = wrapLucideIcon(Lucide.RotateCcwKey) export const RotateCcwSquareInstUIIcon = wrapLucideIcon(Lucide.RotateCcwSquare) export const RotateCwInstUIIcon = wrapLucideIcon(Lucide.RotateCw) export const RotateCwSquareInstUIIcon = wrapLucideIcon(Lucide.RotateCwSquare) @@ -1699,12 +1873,16 @@ export const Rows3InstUIIcon = wrapLucideIcon(Lucide.Rows3) export const Rows4InstUIIcon = wrapLucideIcon(Lucide.Rows4) export const RssInstUIIcon = wrapLucideIcon(Lucide.Rss) export const RulerInstUIIcon = wrapLucideIcon(Lucide.Ruler) +export const RulerDimensionLineInstUIIcon = wrapLucideIcon( + Lucide.RulerDimensionLine +) export const RussianRubleInstUIIcon = wrapLucideIcon(Lucide.RussianRuble) export const SailboatInstUIIcon = wrapLucideIcon(Lucide.Sailboat) export const SaladInstUIIcon = wrapLucideIcon(Lucide.Salad) export const SandwichInstUIIcon = wrapLucideIcon(Lucide.Sandwich) export const SatelliteInstUIIcon = wrapLucideIcon(Lucide.Satellite) export const SatelliteDishInstUIIcon = wrapLucideIcon(Lucide.SatelliteDish) +export const SaudiRiyalInstUIIcon = wrapLucideIcon(Lucide.SaudiRiyal) export const SaveInstUIIcon = wrapLucideIcon(Lucide.Save) export const SaveAllInstUIIcon = wrapLucideIcon(Lucide.SaveAll) export const SaveOffInstUIIcon = wrapLucideIcon(Lucide.SaveOff) @@ -1716,6 +1894,7 @@ export const ScanInstUIIcon = wrapLucideIcon(Lucide.Scan) export const ScanBarcodeInstUIIcon = wrapLucideIcon(Lucide.ScanBarcode) export const ScanEyeInstUIIcon = wrapLucideIcon(Lucide.ScanEye) export const ScanFaceInstUIIcon = wrapLucideIcon(Lucide.ScanFace) +export const ScanHeartInstUIIcon = wrapLucideIcon(Lucide.ScanHeart) export const ScanLineInstUIIcon = wrapLucideIcon(Lucide.ScanLine) export const ScanQrCodeInstUIIcon = wrapLucideIcon(Lucide.ScanQrCode) export const ScanSearchInstUIIcon = wrapLucideIcon(Lucide.ScanSearch) @@ -1731,11 +1910,13 @@ export const ScissorsSquareInstUIIcon = wrapLucideIcon(Lucide.ScissorsSquare) export const ScissorsSquareDashedBottomInstUIIcon = wrapLucideIcon( Lucide.ScissorsSquareDashedBottom ) +export const ScooterInstUIIcon = wrapLucideIcon(Lucide.Scooter) export const ScreenShareInstUIIcon = wrapLucideIcon(Lucide.ScreenShare) export const ScreenShareOffInstUIIcon = wrapLucideIcon(Lucide.ScreenShareOff) export const ScrollInstUIIcon = wrapLucideIcon(Lucide.Scroll) export const ScrollTextInstUIIcon = wrapLucideIcon(Lucide.ScrollText) export const SearchInstUIIcon = wrapLucideIcon(Lucide.Search) +export const SearchAlertInstUIIcon = wrapLucideIcon(Lucide.SearchAlert) export const SearchCheckInstUIIcon = wrapLucideIcon(Lucide.SearchCheck) export const SearchCodeInstUIIcon = wrapLucideIcon(Lucide.SearchCode) export const SearchSlashInstUIIcon = wrapLucideIcon(Lucide.SearchSlash) @@ -1773,6 +1954,10 @@ export const ShieldMinusInstUIIcon = wrapLucideIcon(Lucide.ShieldMinus) export const ShieldOffInstUIIcon = wrapLucideIcon(Lucide.ShieldOff) export const ShieldPlusInstUIIcon = wrapLucideIcon(Lucide.ShieldPlus) export const ShieldQuestionInstUIIcon = wrapLucideIcon(Lucide.ShieldQuestion) +export const ShieldQuestionMarkInstUIIcon = wrapLucideIcon( + Lucide.ShieldQuestionMark +) +export const ShieldUserInstUIIcon = wrapLucideIcon(Lucide.ShieldUser) export const ShieldXInstUIIcon = wrapLucideIcon(Lucide.ShieldX) export const ShipInstUIIcon = wrapLucideIcon(Lucide.Ship) export const ShipWheelInstUIIcon = wrapLucideIcon(Lucide.ShipWheel) @@ -1782,6 +1967,8 @@ export const ShoppingBasketInstUIIcon = wrapLucideIcon(Lucide.ShoppingBasket) export const ShoppingCartInstUIIcon = wrapLucideIcon(Lucide.ShoppingCart) export const ShovelInstUIIcon = wrapLucideIcon(Lucide.Shovel) export const ShowerHeadInstUIIcon = wrapLucideIcon(Lucide.ShowerHead) +export const ShredderInstUIIcon = wrapLucideIcon(Lucide.Shredder) +export const ShrimpInstUIIcon = wrapLucideIcon(Lucide.Shrimp) export const ShrinkInstUIIcon = wrapLucideIcon(Lucide.Shrink) export const ShrubInstUIIcon = wrapLucideIcon(Lucide.Shrub) export const ShuffleInstUIIcon = wrapLucideIcon(Lucide.Shuffle) @@ -1820,7 +2007,11 @@ export const SmileInstUIIcon = wrapLucideIcon(Lucide.Smile) export const SmilePlusInstUIIcon = wrapLucideIcon(Lucide.SmilePlus) export const SnailInstUIIcon = wrapLucideIcon(Lucide.Snail) export const SnowflakeInstUIIcon = wrapLucideIcon(Lucide.Snowflake) +export const SoapDispenserDropletInstUIIcon = wrapLucideIcon( + Lucide.SoapDispenserDroplet +) export const SofaInstUIIcon = wrapLucideIcon(Lucide.Sofa) +export const SolarPanelInstUIIcon = wrapLucideIcon(Lucide.SolarPanel) export const SortAscInstUIIcon = wrapLucideIcon(Lucide.SortAsc) export const SortDescInstUIIcon = wrapLucideIcon(Lucide.SortDesc) export const SoupInstUIIcon = wrapLucideIcon(Lucide.Soup) @@ -1833,6 +2024,7 @@ export const SpeechInstUIIcon = wrapLucideIcon(Lucide.Speech) export const SpellCheckInstUIIcon = wrapLucideIcon(Lucide.SpellCheck) export const SpellCheck2InstUIIcon = wrapLucideIcon(Lucide.SpellCheck2) export const SplineInstUIIcon = wrapLucideIcon(Lucide.Spline) +export const SplinePointerInstUIIcon = wrapLucideIcon(Lucide.SplinePointer) export const SplitInstUIIcon = wrapLucideIcon(Lucide.Split) export const SplitSquareHorizontalInstUIIcon = wrapLucideIcon( Lucide.SplitSquareHorizontal @@ -1840,6 +2032,8 @@ export const SplitSquareHorizontalInstUIIcon = wrapLucideIcon( export const SplitSquareVerticalInstUIIcon = wrapLucideIcon( Lucide.SplitSquareVertical ) +export const SpoolInstUIIcon = wrapLucideIcon(Lucide.Spool) +export const SpotlightInstUIIcon = wrapLucideIcon(Lucide.Spotlight) export const SprayCanInstUIIcon = wrapLucideIcon(Lucide.SprayCan) export const SproutInstUIIcon = wrapLucideIcon(Lucide.Sprout) export const SquareInstUIIcon = wrapLucideIcon(Lucide.Square) @@ -1907,6 +2101,9 @@ export const SquareDashedKanbanInstUIIcon = wrapLucideIcon( export const SquareDashedMousePointerInstUIIcon = wrapLucideIcon( Lucide.SquareDashedMousePointer ) +export const SquareDashedTopSolidInstUIIcon = wrapLucideIcon( + Lucide.SquareDashedTopSolid +) export const SquareDivideInstUIIcon = wrapLucideIcon(Lucide.SquareDivide) export const SquareDotInstUIIcon = wrapLucideIcon(Lucide.SquareDot) export const SquareEqualInstUIIcon = wrapLucideIcon(Lucide.SquareEqual) @@ -1926,6 +2123,7 @@ export const SquareParkingInstUIIcon = wrapLucideIcon(Lucide.SquareParking) export const SquareParkingOffInstUIIcon = wrapLucideIcon( Lucide.SquareParkingOff ) +export const SquarePauseInstUIIcon = wrapLucideIcon(Lucide.SquarePause) export const SquarePenInstUIIcon = wrapLucideIcon(Lucide.SquarePen) export const SquarePercentInstUIIcon = wrapLucideIcon(Lucide.SquarePercent) export const SquarePiInstUIIcon = wrapLucideIcon(Lucide.SquarePi) @@ -1934,6 +2132,9 @@ export const SquarePlayInstUIIcon = wrapLucideIcon(Lucide.SquarePlay) export const SquarePlusInstUIIcon = wrapLucideIcon(Lucide.SquarePlus) export const SquarePowerInstUIIcon = wrapLucideIcon(Lucide.SquarePower) export const SquareRadicalInstUIIcon = wrapLucideIcon(Lucide.SquareRadical) +export const SquareRoundCornerInstUIIcon = wrapLucideIcon( + Lucide.SquareRoundCorner +) export const SquareScissorsInstUIIcon = wrapLucideIcon(Lucide.SquareScissors) export const SquareSigmaInstUIIcon = wrapLucideIcon(Lucide.SquareSigma) export const SquareSlashInstUIIcon = wrapLucideIcon(Lucide.SquareSlash) @@ -1945,11 +2146,20 @@ export const SquareSplitVerticalInstUIIcon = wrapLucideIcon( ) export const SquareSquareInstUIIcon = wrapLucideIcon(Lucide.SquareSquare) export const SquareStackInstUIIcon = wrapLucideIcon(Lucide.SquareStack) +export const SquareStarInstUIIcon = wrapLucideIcon(Lucide.SquareStar) +export const SquareStopInstUIIcon = wrapLucideIcon(Lucide.SquareStop) export const SquareTerminalInstUIIcon = wrapLucideIcon(Lucide.SquareTerminal) export const SquareUserInstUIIcon = wrapLucideIcon(Lucide.SquareUser) export const SquareUserRoundInstUIIcon = wrapLucideIcon(Lucide.SquareUserRound) export const SquareXInstUIIcon = wrapLucideIcon(Lucide.SquareX) +export const SquaresExcludeInstUIIcon = wrapLucideIcon(Lucide.SquaresExclude) +export const SquaresIntersectInstUIIcon = wrapLucideIcon( + Lucide.SquaresIntersect +) +export const SquaresSubtractInstUIIcon = wrapLucideIcon(Lucide.SquaresSubtract) +export const SquaresUniteInstUIIcon = wrapLucideIcon(Lucide.SquaresUnite) export const SquircleInstUIIcon = wrapLucideIcon(Lucide.Squircle) +export const SquircleDashedInstUIIcon = wrapLucideIcon(Lucide.SquircleDashed) export const SquirrelInstUIIcon = wrapLucideIcon(Lucide.Squirrel) export const StampInstUIIcon = wrapLucideIcon(Lucide.Stamp) export const StarInstUIIcon = wrapLucideIcon(Lucide.Star) @@ -1991,6 +2201,7 @@ export const TableCellsSplitInstUIIcon = wrapLucideIcon(Lucide.TableCellsSplit) export const TableColumnsSplitInstUIIcon = wrapLucideIcon( Lucide.TableColumnsSplit ) +export const TableConfigInstUIIcon = wrapLucideIcon(Lucide.TableConfig) export const TableOfContentsInstUIIcon = wrapLucideIcon(Lucide.TableOfContents) export const TablePropertiesInstUIIcon = wrapLucideIcon(Lucide.TableProperties) export const TableRowsSplitInstUIIcon = wrapLucideIcon(Lucide.TableRowsSplit) @@ -2020,12 +2231,20 @@ export const TestTubeDiagonalInstUIIcon = wrapLucideIcon( ) export const TestTubesInstUIIcon = wrapLucideIcon(Lucide.TestTubes) export const TextInstUIIcon = wrapLucideIcon(Lucide.Text) +export const TextAlignCenterInstUIIcon = wrapLucideIcon(Lucide.TextAlignCenter) +export const TextAlignEndInstUIIcon = wrapLucideIcon(Lucide.TextAlignEnd) +export const TextAlignJustifyInstUIIcon = wrapLucideIcon( + Lucide.TextAlignJustify +) +export const TextAlignStartInstUIIcon = wrapLucideIcon(Lucide.TextAlignStart) export const TextCursorInstUIIcon = wrapLucideIcon(Lucide.TextCursor) export const TextCursorInputInstUIIcon = wrapLucideIcon(Lucide.TextCursorInput) +export const TextInitialInstUIIcon = wrapLucideIcon(Lucide.TextInitial) export const TextQuoteInstUIIcon = wrapLucideIcon(Lucide.TextQuote) export const TextSearchInstUIIcon = wrapLucideIcon(Lucide.TextSearch) export const TextSelectInstUIIcon = wrapLucideIcon(Lucide.TextSelect) export const TextSelectionInstUIIcon = wrapLucideIcon(Lucide.TextSelection) +export const TextWrapInstUIIcon = wrapLucideIcon(Lucide.TextWrap) export const TheaterInstUIIcon = wrapLucideIcon(Lucide.Theater) export const ThermometerInstUIIcon = wrapLucideIcon(Lucide.Thermometer) export const ThermometerSnowflakeInstUIIcon = wrapLucideIcon( @@ -2049,6 +2268,7 @@ export const TimerResetInstUIIcon = wrapLucideIcon(Lucide.TimerReset) export const ToggleLeftInstUIIcon = wrapLucideIcon(Lucide.ToggleLeft) export const ToggleRightInstUIIcon = wrapLucideIcon(Lucide.ToggleRight) export const ToiletInstUIIcon = wrapLucideIcon(Lucide.Toilet) +export const ToolCaseInstUIIcon = wrapLucideIcon(Lucide.ToolCase) export const TornadoInstUIIcon = wrapLucideIcon(Lucide.Tornado) export const TorusInstUIIcon = wrapLucideIcon(Lucide.Torus) export const TouchpadInstUIIcon = wrapLucideIcon(Lucide.Touchpad) @@ -2064,6 +2284,7 @@ export const TrainFrontTunnelInstUIIcon = wrapLucideIcon( ) export const TrainTrackInstUIIcon = wrapLucideIcon(Lucide.TrainTrack) export const TramFrontInstUIIcon = wrapLucideIcon(Lucide.TramFront) +export const TransgenderInstUIIcon = wrapLucideIcon(Lucide.Transgender) export const TrashInstUIIcon = wrapLucideIcon(Lucide.Trash) export const Trash2InstUIIcon = wrapLucideIcon(Lucide.Trash2) export const TreeDeciduousInstUIIcon = wrapLucideIcon(Lucide.TreeDeciduous) @@ -2076,9 +2297,13 @@ export const TrendingUpInstUIIcon = wrapLucideIcon(Lucide.TrendingUp) export const TrendingUpDownInstUIIcon = wrapLucideIcon(Lucide.TrendingUpDown) export const TriangleInstUIIcon = wrapLucideIcon(Lucide.Triangle) export const TriangleAlertInstUIIcon = wrapLucideIcon(Lucide.TriangleAlert) +export const TriangleDashedInstUIIcon = wrapLucideIcon(Lucide.TriangleDashed) export const TriangleRightInstUIIcon = wrapLucideIcon(Lucide.TriangleRight) export const TrophyInstUIIcon = wrapLucideIcon(Lucide.Trophy) export const TruckInstUIIcon = wrapLucideIcon(Lucide.Truck) +export const TruckElectricInstUIIcon = wrapLucideIcon(Lucide.TruckElectric) +export const TurkishLiraInstUIIcon = wrapLucideIcon(Lucide.TurkishLira) +export const TurntableInstUIIcon = wrapLucideIcon(Lucide.Turntable) export const TurtleInstUIIcon = wrapLucideIcon(Lucide.Turtle) export const TvInstUIIcon = wrapLucideIcon(Lucide.Tv) export const Tv2InstUIIcon = wrapLucideIcon(Lucide.Tv2) @@ -2116,6 +2341,7 @@ export const UserCircleInstUIIcon = wrapLucideIcon(Lucide.UserCircle) export const UserCircle2InstUIIcon = wrapLucideIcon(Lucide.UserCircle2) export const UserCogInstUIIcon = wrapLucideIcon(Lucide.UserCog) export const UserCog2InstUIIcon = wrapLucideIcon(Lucide.UserCog2) +export const UserLockInstUIIcon = wrapLucideIcon(Lucide.UserLock) export const UserMinusInstUIIcon = wrapLucideIcon(Lucide.UserMinus) export const UserMinus2InstUIIcon = wrapLucideIcon(Lucide.UserMinus2) export const UserPenInstUIIcon = wrapLucideIcon(Lucide.UserPen) @@ -2132,6 +2358,7 @@ export const UserRoundXInstUIIcon = wrapLucideIcon(Lucide.UserRoundX) export const UserSearchInstUIIcon = wrapLucideIcon(Lucide.UserSearch) export const UserSquareInstUIIcon = wrapLucideIcon(Lucide.UserSquare) export const UserSquare2InstUIIcon = wrapLucideIcon(Lucide.UserSquare2) +export const UserStarInstUIIcon = wrapLucideIcon(Lucide.UserStar) export const UserXInstUIIcon = wrapLucideIcon(Lucide.UserX) export const UserX2InstUIIcon = wrapLucideIcon(Lucide.UserX2) export const UsersInstUIIcon = wrapLucideIcon(Lucide.Users) @@ -2140,10 +2367,14 @@ export const UsersRoundInstUIIcon = wrapLucideIcon(Lucide.UsersRound) export const UtensilsInstUIIcon = wrapLucideIcon(Lucide.Utensils) export const UtensilsCrossedInstUIIcon = wrapLucideIcon(Lucide.UtensilsCrossed) export const UtilityPoleInstUIIcon = wrapLucideIcon(Lucide.UtilityPole) +export const VanInstUIIcon = wrapLucideIcon(Lucide.Van) export const VariableInstUIIcon = wrapLucideIcon(Lucide.Variable) export const VaultInstUIIcon = wrapLucideIcon(Lucide.Vault) +export const VectorSquareInstUIIcon = wrapLucideIcon(Lucide.VectorSquare) export const VeganInstUIIcon = wrapLucideIcon(Lucide.Vegan) export const VenetianMaskInstUIIcon = wrapLucideIcon(Lucide.VenetianMask) +export const VenusInstUIIcon = wrapLucideIcon(Lucide.Venus) +export const VenusAndMarsInstUIIcon = wrapLucideIcon(Lucide.VenusAndMars) export const VerifiedInstUIIcon = wrapLucideIcon(Lucide.Verified) export const VibrateInstUIIcon = wrapLucideIcon(Lucide.Vibrate) export const VibrateOffInstUIIcon = wrapLucideIcon(Lucide.VibrateOff) @@ -2171,18 +2402,25 @@ export const WarehouseInstUIIcon = wrapLucideIcon(Lucide.Warehouse) export const WashingMachineInstUIIcon = wrapLucideIcon(Lucide.WashingMachine) export const WatchInstUIIcon = wrapLucideIcon(Lucide.Watch) export const WavesInstUIIcon = wrapLucideIcon(Lucide.Waves) +export const WavesArrowDownInstUIIcon = wrapLucideIcon(Lucide.WavesArrowDown) +export const WavesArrowUpInstUIIcon = wrapLucideIcon(Lucide.WavesArrowUp) +export const WavesLadderInstUIIcon = wrapLucideIcon(Lucide.WavesLadder) export const WaypointsInstUIIcon = wrapLucideIcon(Lucide.Waypoints) export const WebcamInstUIIcon = wrapLucideIcon(Lucide.Webcam) export const WebhookInstUIIcon = wrapLucideIcon(Lucide.Webhook) export const WebhookOffInstUIIcon = wrapLucideIcon(Lucide.WebhookOff) export const WeightInstUIIcon = wrapLucideIcon(Lucide.Weight) +export const WeightTildeInstUIIcon = wrapLucideIcon(Lucide.WeightTilde) export const WheatInstUIIcon = wrapLucideIcon(Lucide.Wheat) export const WheatOffInstUIIcon = wrapLucideIcon(Lucide.WheatOff) export const WholeWordInstUIIcon = wrapLucideIcon(Lucide.WholeWord) export const WifiInstUIIcon = wrapLucideIcon(Lucide.Wifi) +export const WifiCogInstUIIcon = wrapLucideIcon(Lucide.WifiCog) export const WifiHighInstUIIcon = wrapLucideIcon(Lucide.WifiHigh) export const WifiLowInstUIIcon = wrapLucideIcon(Lucide.WifiLow) export const WifiOffInstUIIcon = wrapLucideIcon(Lucide.WifiOff) +export const WifiPenInstUIIcon = wrapLucideIcon(Lucide.WifiPen) +export const WifiSyncInstUIIcon = wrapLucideIcon(Lucide.WifiSync) export const WifiZeroInstUIIcon = wrapLucideIcon(Lucide.WifiZero) export const WindInstUIIcon = wrapLucideIcon(Lucide.Wind) export const WindArrowDownInstUIIcon = wrapLucideIcon(Lucide.WindArrowDown) diff --git a/packages/ui-icons-lucide/src/wrapLucideIcon/index.tsx b/packages/ui-icons-lucide/src/wrapLucideIcon/index.tsx index ebf76d860b..0c3e61a662 100644 --- a/packages/ui-icons-lucide/src/wrapLucideIcon/index.tsx +++ b/packages/ui-icons-lucide/src/wrapLucideIcon/index.tsx @@ -22,11 +22,13 @@ * SOFTWARE. */ -import React from 'react' +import React, { useId, useContext } from 'react' import { useStyle } from '@instructure/emotion' -import { passthroughProps, useIconProps } from '@instructure/ui-react-utils' +import { passthroughProps } from '@instructure/ui-react-utils' import type { LucideIcon } from 'lucide-react' +import { IconPropsContext } from '../IconPropsProvider' + import type { LucideIconWrapperProps, InstUIIconOwnProps } from './props' import generateStyle from './styles' @@ -54,7 +56,7 @@ export function wrapLucideIcon( } = props // Get icon props from context (if available) - const contextProps = useIconProps() + const contextProps = useContext(IconPropsContext) // Merge props: direct props take precedence over context props const finalSize = size ?? contextProps?.size @@ -90,6 +92,53 @@ export function wrapLucideIcon( accessibilityProps['role'] = 'presentation' } + const gradientId = useId() + + // AI Gradient Implementation: + // SVG gradients must be defined BEFORE they're referenced. Since Lucide renders + // icon paths before any children we pass, we inject the gradient in a separate + // hidden SVG element that comes BEFORE the icon in the DOM. We use + // gradientUnits="userSpaceOnUse" with coordinates (0,0) to (24,24) matching + // Lucide's viewBox, ensuring one gradient spans the entire icon space rather + // than scaling separately for each shape (which causes small elements to lose + // gradient visibility). The icon then references this gradient via stroke="url(#id)". + if (styles.gradientColors) { + // Use viewBox coordinates for gradient (Lucide icons use 0 0 24 24 viewBox) + const gradientSize = 24 + + return ( + + {/* Hidden SVG to define the gradient - must come before the icon uses it */} + + + + + + + + + + + ) + } + + // Normal rendering (non-gradient) return ( & - InstUIIconOwnProps & { - themeOverride?: ThemeOverrideValue - } & OtherHTMLAttributes + Omit & + InstUIIconOwnProps & { + themeOverride?: ThemeOverrideValue + } & OtherHTMLAttributes, + 'children' | 'style' | 'className' +> type LucideIconStyle = ComponentStyle<'lucideIcon'> & { /** @@ -172,6 +179,15 @@ type LucideIconStyle = ComponentStyle<'lucideIcon'> & { * Custom CSS color value (non-semantic) */ customColor?: string + /** + * Gradient colors for AI gradient (top and bottom) + */ + gradientColors?: { top: string; bottom: string } } -export type { LucideIconWrapperProps, InstUIIconOwnProps, LucideIconStyle } +export type { + LucideIconWrapperProps, + InstUIIconOwnProps, + LucideIconStyle, + SVGIconSizeToken +} diff --git a/packages/ui-icons-lucide/src/wrapLucideIcon/styles.ts b/packages/ui-icons-lucide/src/wrapLucideIcon/styles.ts index c2319a3d6e..2073bba9d7 100644 --- a/packages/ui-icons-lucide/src/wrapLucideIcon/styles.ts +++ b/packages/ui-icons-lucide/src/wrapLucideIcon/styles.ts @@ -24,7 +24,7 @@ import { px } from '@instructure/ui-utils' import type { NewComponentTypes } from '@instructure/ui-themes' -import type { LucideIconWrapperProps, LucideIconStyle } from './props' +import { LucideIconWrapperProps, LucideIconStyle } from './props' type StyleParams = { size?: LucideIconWrapperProps['size'] @@ -36,6 +36,14 @@ type StyleParams = { themeOverride?: LucideIconWrapperProps['themeOverride'] } +const svgIconSizeMapping = { + 'x-small': '1.125rem', + small: '2rem', + medium: '3rem', + large: '5rem', + 'x-large': '10rem' +} + /** * Convert semantic size token to numeric pixels for Lucide */ @@ -44,6 +52,12 @@ const convertSemanticSize = ( componentTheme: NewComponentTypes['Icon'] ) => { if (typeof size === 'string') { + // Check SVGIcon tokens first (for legacy size tokens) + if (size in svgIconSizeMapping) { + return px(svgIconSizeMapping[size as keyof typeof svgIconSizeMapping]) + } + + // Fall back to componentTheme (for other tokens like xs, sm, md, lg, xl, 2xl) const propName = `size${size.charAt(0).toUpperCase()}${size.slice( 1 )}` as keyof typeof componentTheme @@ -86,7 +100,7 @@ const determineColorValues = ( return {} } - if (color === 'inherit') { + if (color === 'inherit' || color === 'ai') { return { colorValue: color } } @@ -125,9 +139,17 @@ const generateStyle = ( ) let colorStyle + let gradientColors: { top: string; bottom: string } | undefined + if (colorValue) { if (colorValue === 'inherit') { colorStyle = { color: 'inherit' } + } else if (colorValue === 'ai') { + // Special handling for AI gradient color + gradientColors = { + top: componentTheme.actionAiSecondaryTopGradientBaseColor, + bottom: componentTheme.actionAiSecondaryBottomGradientBaseColor + } } else if (colorValue in componentTheme) { colorStyle = { color: componentTheme[colorValue as keyof typeof componentTheme] @@ -166,7 +188,8 @@ const generateStyle = ( }, numericSize, numericStrokeWidth, - customColor + customColor, + gradientColors } } diff --git a/packages/ui-react-utils/src/index.ts b/packages/ui-react-utils/src/index.ts index 272680316a..253aa7a5a9 100644 --- a/packages/ui-react-utils/src/index.ts +++ b/packages/ui-react-utils/src/index.ts @@ -28,11 +28,6 @@ export { ensureSingleChild } from './ensureSingleChild' export { getDisplayName } from './getDisplayName' export { getElementType } from './getElementType' export { getInteraction } from './getInteraction' -export { - IconPropsProvider, - IconPropsContext, - useIconProps -} from './IconPropsProvider' export { matchComponentTypes } from './matchComponentTypes' export { omitProps } from './omitProps' export { passthroughProps } from './passthroughProps' @@ -48,7 +43,6 @@ export { export type { GetInteractionOptions } from './getInteraction' export type { InteractionType } from './getInteraction' -export type { IconPropsContextValue } from './IconPropsProvider' export type { DeterministicIdProviderValue, WithDeterministicIdProps diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a00a11b199..5a0215ff01 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1016,9 +1016,9 @@ importers: '@instructure/shared-types': specifier: workspace:* version: link:../shared-types - '@instructure/ui-icons': + '@instructure/ui-icons-lucide': specifier: workspace:* - version: link:../ui-icons + version: link:../ui-icons-lucide '@instructure/ui-react-utils': specifier: workspace:* version: link:../ui-react-utils @@ -1035,9 +1035,6 @@ importers: '@instructure/ui-color-utils': specifier: workspace:* version: link:../ui-color-utils - '@instructure/ui-icons-lucide': - specifier: workspace:* - version: link:../ui-icons-lucide '@instructure/ui-themes': specifier: workspace:* version: link:../ui-themes @@ -2450,8 +2447,8 @@ importers: specifier: workspace:* version: link:../ui-utils lucide-react: - specifier: ^0.460.0 - version: 0.460.0(react@18.3.1) + specifier: ^0.559.0 + version: 0.559.0(react@18.3.1) devDependencies: '@instructure/ui-babel-preset': specifier: workspace:* @@ -10531,8 +10528,8 @@ packages: lru-queue@0.1.0: resolution: {integrity: sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==} - lucide-react@0.460.0: - resolution: {integrity: sha512-BVtq/DykVeIvRTJvRAgCsOwaGL8Un3Bxh8MbDxMhEWlZay3T4IpEKDEpwt5KZ0KJMHzgm6jrltxlT5eXOWXDHg==} + lucide-react@0.559.0: + resolution: {integrity: sha512-3ymrkBPXWk3U2bwUDg6TdA6hP5iGDMgPEAMLhchEgTQmA+g0Zk24tOtKtXMx35w1PizTmsBC3RhP88QYm+7mHQ==} peerDependencies: react: 18.3.1 @@ -19719,7 +19716,7 @@ snapshots: dependencies: es5-ext: 0.10.64 - lucide-react@0.460.0(react@18.3.1): + lucide-react@0.559.0(react@18.3.1): dependencies: react: 18.3.1 From 480c39cdcc922dfe99efdeb6b571c1f1ba543981 Mon Sep 17 00:00:00 2001 From: Peter Pal Hudak Date: Tue, 6 Jan 2026 10:08:19 +0100 Subject: [PATCH 6/7] refactor(ui-icons-lucide,ui-avatar): remove strokeWidth --- .../__docs__/src/Icons/LucideIconsGallery.tsx | 28 ++++----- packages/ui-avatar/src/Avatar/styles.ts | 1 + packages/ui-icons-lucide/README.md | 4 +- .../IconPropsProvider/renderIconWithProps.tsx | 6 +- .../src/__tests__/IconPropsProvider.test.tsx | 16 +++--- packages/ui-icons-lucide/src/index.ts | 4 -- .../src/wrapLucideIcon/index.tsx | 37 ++++++------ .../src/wrapLucideIcon/props.ts | 21 ++----- .../src/wrapLucideIcon/styles.ts | 57 ++++++++----------- 9 files changed, 77 insertions(+), 97 deletions(-) diff --git a/packages/__docs__/src/Icons/LucideIconsGallery.tsx b/packages/__docs__/src/Icons/LucideIconsGallery.tsx index c2a7a71aed..4efdd859b7 100644 --- a/packages/__docs__/src/Icons/LucideIconsGallery.tsx +++ b/packages/__docs__/src/Icons/LucideIconsGallery.tsx @@ -38,7 +38,6 @@ import { Modal } from '@instructure/ui-modal' import { SourceCodeEditor } from '@instructure/ui-source-code-editor' import * as LucideIcons from '@instructure/ui-icons-lucide' import { XInstUIIcon } from '@instructure/ui-icons-lucide' -import { Link } from '@instructure/ui-link' import { Flex } from '@instructure/ui-flex' // Get all exported Lucide icons (excluding utilities and types) @@ -290,29 +289,26 @@ const LucideIconsGallery = () => {
    • - size: Number (pixels) or semantic token - e.g.,{' '} - "sm" + size: Semantic token - e.g.,{' '} + "sm", "md",{' '} + "lg" +
      + + Note: Stroke width is automatically derived from size. Uses{' '} + absoluteStrokeWidth by default to ensure + consistent pixel-perfect rendering across all icon sizes. +
    • - color: CSS color or semantic token- e.g.,{' '} - "successColor" -
    • -
    • - strokeWidth: Number or semantic token - e.g.,{' '} - "sm" + color: Semantic token - e.g.,{' '} + "successColor",{' '} + "baseColor"
    • Plus all standard SVG props (except for className, style, css, children etc.)
    -

    - These icons use the pure Lucide API. See{' '} - - Lucide documentation - {' '} - for more details. -

    diff --git a/packages/ui-avatar/src/Avatar/styles.ts b/packages/ui-avatar/src/Avatar/styles.ts index 61627a1a42..fcfdb33560 100644 --- a/packages/ui-avatar/src/Avatar/styles.ts +++ b/packages/ui-avatar/src/Avatar/styles.ts @@ -167,6 +167,7 @@ const generateStyle = ( display: display === 'inline' ? 'inline-flex' : 'flex', alignItems: 'center', justifyContent: 'center', + verticalAlign: 'middle', color: hasInverseColor ? componentTheme.textOnColor : colorVariants[color!].text, diff --git a/packages/ui-icons-lucide/README.md b/packages/ui-icons-lucide/README.md index a4a698b447..0f0fea1134 100644 --- a/packages/ui-icons-lucide/README.md +++ b/packages/ui-icons-lucide/README.md @@ -32,8 +32,8 @@ const MyComponent = () => { return (
    - - + +
    ) } diff --git a/packages/ui-icons-lucide/src/IconPropsProvider/renderIconWithProps.tsx b/packages/ui-icons-lucide/src/IconPropsProvider/renderIconWithProps.tsx index 3547a9db09..011e579d17 100644 --- a/packages/ui-icons-lucide/src/IconPropsProvider/renderIconWithProps.tsx +++ b/packages/ui-icons-lucide/src/IconPropsProvider/renderIconWithProps.tsx @@ -26,15 +26,15 @@ import React from 'react' import type { Renderable } from '@instructure/shared-types' import { IconPropsProvider } from './IconPropsProvider' -import { InstUIIconOwnProps } from '../wrapLucideIcon' +import { InstUIIconOwnProps } from '../wrapLucideIcon/props' /** * Renders an icon wrapped in IconPropsProvider to apply size and color via React context. * Handles both component references and React elements. * * @param icon - The icon to render (component reference or React element) - * @param size - Semantic size token or numeric pixels. - * @param color - Semantic color token or any valid CSS color string. + * @param size - Semantic size token (e.g., 'xs', 'sm', 'md', 'lg', 'xl', '2xl'). + * @param color - Semantic color token (e.g., 'baseColor', 'errorColor', 'ai'). * @returns Icon element wrapped in IconPropsProvider context */ function renderIconWithProps( diff --git a/packages/ui-icons-lucide/src/__tests__/IconPropsProvider.test.tsx b/packages/ui-icons-lucide/src/__tests__/IconPropsProvider.test.tsx index 5d7ae6bdfa..25727a4740 100644 --- a/packages/ui-icons-lucide/src/__tests__/IconPropsProvider.test.tsx +++ b/packages/ui-icons-lucide/src/__tests__/IconPropsProvider.test.tsx @@ -61,7 +61,7 @@ const MockIconWithProps = ({ color, testId = 'mock-icon-with-props' }: { - size?: string | number + size?: string color?: string testId?: string }) => { @@ -100,14 +100,14 @@ describe('IconPropsProvider', () => { expect(screen.getByTestId('color')).toHaveTextContent('undefined') }) - it('should return numeric size values', () => { + it('should return semantic size tokens', () => { render( - + ) - expect(screen.getByTestId('size')).toHaveTextContent('24') + expect(screen.getByTestId('size')).toHaveTextContent('lg') expect(screen.getByTestId('color')).toHaveTextContent('accentRedColor') }) }) @@ -138,24 +138,24 @@ describe('IconPropsProvider', () => { it('should allow direct props to override context', () => { render( - + ) const icon = screen.getByTestId('mock-icon-with-props') - expect(icon).toHaveAttribute('data-size', '16') + expect(icon).toHaveAttribute('data-size', 'sm') expect(icon).toHaveAttribute('data-color', 'accentRedColor') }) it('should use context when no direct props provided', () => { render( - + ) const icon = screen.getByTestId('mock-icon-with-props') - expect(icon).toHaveAttribute('data-size', '24') + expect(icon).toHaveAttribute('data-size', 'lg') expect(icon).toHaveAttribute('data-color', 'baseColor') }) }) diff --git a/packages/ui-icons-lucide/src/index.ts b/packages/ui-icons-lucide/src/index.ts index 9e632ad374..2725ee5cb6 100644 --- a/packages/ui-icons-lucide/src/index.ts +++ b/packages/ui-icons-lucide/src/index.ts @@ -26,10 +26,6 @@ import * as Lucide from 'lucide-react' import { wrapLucideIcon } from './wrapLucideIcon' export type { LucideProps, LucideIcon } from 'lucide-react' -export type { - LucideIconWrapperProps, - InstUIIconOwnProps -} from './wrapLucideIcon' export { wrapLucideIcon } export { IconPropsProvider, diff --git a/packages/ui-icons-lucide/src/wrapLucideIcon/index.tsx b/packages/ui-icons-lucide/src/wrapLucideIcon/index.tsx index 0c3e61a662..c03ea855b0 100644 --- a/packages/ui-icons-lucide/src/wrapLucideIcon/index.tsx +++ b/packages/ui-icons-lucide/src/wrapLucideIcon/index.tsx @@ -29,21 +29,25 @@ import type { LucideIcon } from 'lucide-react' import { IconPropsContext } from '../IconPropsProvider' -import type { LucideIconWrapperProps, InstUIIconOwnProps } from './props' +import type { LucideIconWrapperProps } from './props' import generateStyle from './styles' /** * Wraps a Lucide icon with InstUI theming, RTL support, and semantic sizing. - * Supports both InstUI semantic props (size="lg", color="baseColor") and - * native Lucide props (size={24}, color="#ff0000"). + * Only accepts InstUI semantic tokens (size="lg", color="baseColor"). + * Stroke width is automatically derived from size for consistent visual weight. + * Uses absoluteStrokeWidth by default to ensure strokes render at actual pixel values + * regardless of icon scaling (solves viewBox scaling issue). + * Numeric and custom CSS values are not supported. */ export function wrapLucideIcon( Icon: LucideIcon ): React.ComponentType { + const iconDisplayName = `wrapLucideIcon(${Icon.displayName})` + const WrappedIcon = (props: LucideIconWrapperProps) => { const { size, - strokeWidth, color, rotate = '0', bidirectional = true, @@ -51,15 +55,18 @@ export function wrapLucideIcon( title, elementRef, themeOverride, - absoluteStrokeWidth, + // Default to true: ensures stroke width renders at actual pixel values + // regardless of icon size (fixes viewBox scaling issue where 24x24 viewBox + // causes strokes to scale proportionally with icon size) + absoluteStrokeWidth = true, ...rest } = props // Get icon props from context (if available) const contextProps = useContext(IconPropsContext) - // Merge props: direct props take precedence over context props - const finalSize = size ?? contextProps?.size + // Merge props: context props take precedence over direct props + const finalSize = contextProps?.size ?? size const finalColor = color ?? contextProps?.color const handleElementRef = (el: SVGSVGElement | null) => { @@ -69,21 +76,20 @@ export function wrapLucideIcon( } const styles = useStyle({ - componentId: 'Icon' as const, + componentId: 'Icon', generateStyle, themeOverride, params: { - size: finalSize as LucideIconWrapperProps['size'], - strokeWidth, + size: finalSize, color: finalColor, rotate, bidirectional, inline }, - displayName: `LucideIcon(${Icon.displayName || Icon.name})` + displayName: iconDisplayName }) - const accessibilityProps: Record = {} + const accessibilityProps: Record = {} if (title) { accessibilityProps['aria-label'] = title accessibilityProps['role'] = 'img' @@ -146,7 +152,7 @@ export function wrapLucideIcon( name={Icon.displayName} ref={handleElementRef} size={styles.numericSize} - color={styles.customColor} + color={styles.resolvedColor} strokeWidth={styles.numericStrokeWidth} absoluteStrokeWidth={absoluteStrokeWidth} {...accessibilityProps} @@ -155,10 +161,7 @@ export function wrapLucideIcon( ) } - WrappedIcon.displayName = `wrapLucideIcon(${Icon.displayName || Icon.name})` + WrappedIcon.displayName = iconDisplayName return WrappedIcon } - -export type { LucideIconWrapperProps, InstUIIconOwnProps } -export { default as generateStyle } from './styles' diff --git a/packages/ui-icons-lucide/src/wrapLucideIcon/props.ts b/packages/ui-icons-lucide/src/wrapLucideIcon/props.ts index 9a31d259e6..7c93ac4fb0 100644 --- a/packages/ui-icons-lucide/src/wrapLucideIcon/props.ts +++ b/packages/ui-icons-lucide/src/wrapLucideIcon/props.ts @@ -36,11 +36,6 @@ type SVGIconSizeToken = 'x-small' | 'small' | 'medium' | 'large' | 'x-large' */ type IconSizeToken = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | SVGIconSizeToken -/** - * Semantic stroke width tokens for icons - */ -type IconStrokeWidthToken = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' - /** * Semantic color tokens from Icon theme */ @@ -118,17 +113,13 @@ type IconColorToken = type InstUIIconOwnProps = { /** - * Semantic size token or numeric pixels - */ - size?: IconSizeToken | number - /** - * Semantic stroke width token or numeric value + * Semantic size token (also determines stroke width automatically) */ - strokeWidth?: IconStrokeWidthToken | number | string + size?: IconSizeToken /** - * Icon theme color token or CSS color value + * Icon theme color token */ - color?: 'inherit' | IconColorToken | string + color?: 'inherit' | IconColorToken /** * Rotation angle in degrees */ @@ -176,9 +167,9 @@ type LucideIconStyle = ComponentStyle<'lucideIcon'> & { */ numericStrokeWidth?: number | string /** - * Custom CSS color value (non-semantic) + * Resolved color from theme for Lucide icon */ - customColor?: string + resolvedColor?: string /** * Gradient colors for AI gradient (top and bottom) */ diff --git a/packages/ui-icons-lucide/src/wrapLucideIcon/styles.ts b/packages/ui-icons-lucide/src/wrapLucideIcon/styles.ts index 2073bba9d7..fe6c38b6ce 100644 --- a/packages/ui-icons-lucide/src/wrapLucideIcon/styles.ts +++ b/packages/ui-icons-lucide/src/wrapLucideIcon/styles.ts @@ -28,7 +28,6 @@ import { LucideIconWrapperProps, LucideIconStyle } from './props' type StyleParams = { size?: LucideIconWrapperProps['size'] - strokeWidth?: LucideIconWrapperProps['strokeWidth'] color?: LucideIconWrapperProps['color'] rotate?: LucideIconWrapperProps['rotate'] bidirectional?: LucideIconWrapperProps['bidirectional'] @@ -64,44 +63,42 @@ const convertSemanticSize = ( if (propName in componentTheme) { return px(componentTheme[propName]) } - } else if (typeof size === 'number') { - return size } return undefined } /** - * Convert semantic stroke width token to numeric value for Lucide + * Convert semantic size token to stroke width value for Lucide + * Derives stroke width from size */ -const convertSemanticStrokeWidth = ( - strokeWidth: LucideIconWrapperProps['strokeWidth'], +const convertSizeToStrokeWidth = ( + size: LucideIconWrapperProps['size'], componentTheme: NewComponentTypes['Icon'] ) => { - if (typeof strokeWidth === 'string') { - const propName = `strokeWidth${strokeWidth - .charAt(0) - .toUpperCase()}${strokeWidth.slice(1)}` as keyof typeof componentTheme + if (typeof size === 'string') { + const propName = `strokeWidth${size.charAt(0).toUpperCase()}${size.slice( + 1 + )}` as keyof typeof componentTheme if (propName in componentTheme) { return px(componentTheme[propName]) } - return strokeWidth } - return strokeWidth + return undefined } /** - * Determine color values: semantic (for CSS) vs custom (for Lucide) + * Determine semantic color value from theme */ -const determineColorValues = ( +const determineColorValue = ( color: LucideIconWrapperProps['color'], componentTheme: NewComponentTypes['Icon'] ) => { if (!color) { - return {} + return undefined } if (color === 'inherit' || color === 'ai') { - return { colorValue: color } + return color } if ( @@ -109,10 +106,10 @@ const determineColorValues = ( !color.startsWith('size') && !color.startsWith('strokeWidth') ) { - return { colorValue: color } + return color } - return { customColor: color } + return undefined } const generateStyle = ( @@ -121,7 +118,6 @@ const generateStyle = ( ): LucideIconStyle => { const { size, - strokeWidth, color, rotate = '0', bidirectional = true, @@ -129,21 +125,17 @@ const generateStyle = ( } = params const numericSize = convertSemanticSize(size, componentTheme) - const numericStrokeWidth = convertSemanticStrokeWidth( - strokeWidth, - componentTheme - ) - const { colorValue, customColor } = determineColorValues( - color, - componentTheme - ) + // Derive stroke width from size + const numericStrokeWidth = convertSizeToStrokeWidth(size, componentTheme) + const colorValue = determineColorValue(color, componentTheme) let colorStyle let gradientColors: { top: string; bottom: string } | undefined + let resolvedColor: string | undefined if (colorValue) { if (colorValue === 'inherit') { - colorStyle = { color: 'inherit' } + resolvedColor = 'inherit' } else if (colorValue === 'ai') { // Special handling for AI gradient color gradientColors = { @@ -151,12 +143,13 @@ const generateStyle = ( bottom: componentTheme.actionAiSecondaryBottomGradientBaseColor } } else if (colorValue in componentTheme) { + const themeColor = + componentTheme[colorValue as keyof typeof componentTheme] colorStyle = { - color: componentTheme[colorValue as keyof typeof componentTheme] + color: themeColor } + resolvedColor = themeColor } - } else if (customColor) { - colorStyle = { color: customColor } } const rotateVariants = { @@ -188,7 +181,7 @@ const generateStyle = ( }, numericSize, numericStrokeWidth, - customColor, + resolvedColor, gradientColors } } From 7f00a09ae6d45d24cfa7a3cf53a35dff26ea5656 Mon Sep 17 00:00:00 2001 From: Peter Pal Hudak Date: Wed, 7 Jan 2026 12:48:00 +0100 Subject: [PATCH 7/7] refactor(ui-icons-lucide): refactor --- .../__docs__/src/Icons/LucideIconsGallery.tsx | 3 +- .../src/wrapLucideIcon/index.tsx | 12 +- .../src/wrapLucideIcon/props.ts | 32 +++-- .../src/wrapLucideIcon/styles.ts | 110 +++++++++++++----- 4 files changed, 110 insertions(+), 47 deletions(-) diff --git a/packages/__docs__/src/Icons/LucideIconsGallery.tsx b/packages/__docs__/src/Icons/LucideIconsGallery.tsx index 4efdd859b7..1c593bad83 100644 --- a/packages/__docs__/src/Icons/LucideIconsGallery.tsx +++ b/packages/__docs__/src/Icons/LucideIconsGallery.tsx @@ -294,8 +294,7 @@ const LucideIconsGallery = () => { "lg"
    - Note: Stroke width is automatically derived from size. Uses{' '} - absoluteStrokeWidth by default to ensure + Note: Stroke width is automatically derived from size for consistent pixel-perfect rendering across all icon sizes. diff --git a/packages/ui-icons-lucide/src/wrapLucideIcon/index.tsx b/packages/ui-icons-lucide/src/wrapLucideIcon/index.tsx index c03ea855b0..e89e293cc1 100644 --- a/packages/ui-icons-lucide/src/wrapLucideIcon/index.tsx +++ b/packages/ui-icons-lucide/src/wrapLucideIcon/index.tsx @@ -36,8 +36,6 @@ import generateStyle from './styles' * Wraps a Lucide icon with InstUI theming, RTL support, and semantic sizing. * Only accepts InstUI semantic tokens (size="lg", color="baseColor"). * Stroke width is automatically derived from size for consistent visual weight. - * Uses absoluteStrokeWidth by default to ensure strokes render at actual pixel values - * regardless of icon scaling (solves viewBox scaling issue). * Numeric and custom CSS values are not supported. */ export function wrapLucideIcon( @@ -55,10 +53,6 @@ export function wrapLucideIcon( title, elementRef, themeOverride, - // Default to true: ensures stroke width renders at actual pixel values - // regardless of icon size (fixes viewBox scaling issue where 24x24 viewBox - // causes strokes to scale proportionally with icon size) - absoluteStrokeWidth = true, ...rest } = props @@ -67,7 +61,7 @@ export function wrapLucideIcon( // Merge props: context props take precedence over direct props const finalSize = contextProps?.size ?? size - const finalColor = color ?? contextProps?.color + const finalColor = contextProps?.color ?? color const handleElementRef = (el: SVGSVGElement | null) => { if (typeof elementRef === 'function') { @@ -137,7 +131,7 @@ export function wrapLucideIcon( size={styles.numericSize} color={`url(#${gradientId})`} strokeWidth={styles.numericStrokeWidth} - absoluteStrokeWidth={absoluteStrokeWidth} + absoluteStrokeWidth={true} {...accessibilityProps} /> @@ -154,7 +148,7 @@ export function wrapLucideIcon( size={styles.numericSize} color={styles.resolvedColor} strokeWidth={styles.numericStrokeWidth} - absoluteStrokeWidth={absoluteStrokeWidth} + absoluteStrokeWidth={true} {...accessibilityProps} /> diff --git a/packages/ui-icons-lucide/src/wrapLucideIcon/props.ts b/packages/ui-icons-lucide/src/wrapLucideIcon/props.ts index 7c93ac4fb0..6d87098cb0 100644 --- a/packages/ui-icons-lucide/src/wrapLucideIcon/props.ts +++ b/packages/ui-icons-lucide/src/wrapLucideIcon/props.ts @@ -22,7 +22,6 @@ * SOFTWARE. */ -import type { LucideProps } from 'lucide-react' import type { ComponentStyle, ThemeOverrideValue } from '@instructure/emotion' import type { OtherHTMLAttributes } from '@instructure/shared-types' @@ -31,6 +30,21 @@ import type { OtherHTMLAttributes } from '@instructure/shared-types' */ type SVGIconSizeToken = 'x-small' | 'small' | 'medium' | 'large' | 'x-large' +/** + * SVGIcon color tokens (legacy) - DEPRECATED + */ +type LegacyColorTokens = + | 'primary' + | 'secondary' + | 'primary-inverse' + | 'secondary-inverse' + | 'success' + | 'error' + | 'alert' + | 'warning' + | 'brand' + | 'auto' + /** * Semantic size tokens for icons - includes SVGIcon legacy tokens, they are DEPRECATED and will be deleted, DON'T USE THEM. */ @@ -119,7 +133,7 @@ type InstUIIconOwnProps = { /** * Icon theme color token */ - color?: 'inherit' | IconColorToken + color?: 'inherit' | IconColorToken | LegacyColorTokens /** * Rotation angle in degrees */ @@ -145,15 +159,14 @@ type InstUIIconOwnProps = { } /** - * Full props: Lucide native + InstUI semantic + theme support. - * InstUI props override Lucide's size, color, strokeWidth, rotate. + * Full props: InstUI semantic + theme support + SVG attributes. + * OtherHTMLAttributes provides SVG props for backward compatibility. * children, style, and className are explicitly omitted. */ type LucideIconWrapperProps = Omit< - Omit & - InstUIIconOwnProps & { - themeOverride?: ThemeOverrideValue - } & OtherHTMLAttributes, + InstUIIconOwnProps & { + themeOverride?: ThemeOverrideValue + } & OtherHTMLAttributes, 'children' | 'style' | 'className' > @@ -180,5 +193,6 @@ export type { LucideIconWrapperProps, InstUIIconOwnProps, LucideIconStyle, - SVGIconSizeToken + SVGIconSizeToken, + LegacyColorTokens } diff --git a/packages/ui-icons-lucide/src/wrapLucideIcon/styles.ts b/packages/ui-icons-lucide/src/wrapLucideIcon/styles.ts index fe6c38b6ce..69e746d2c6 100644 --- a/packages/ui-icons-lucide/src/wrapLucideIcon/styles.ts +++ b/packages/ui-icons-lucide/src/wrapLucideIcon/styles.ts @@ -35,7 +35,34 @@ type StyleParams = { themeOverride?: LucideIconWrapperProps['themeOverride'] } -const svgIconSizeMapping = { +/** + * Maps icon size tokens to theme property names + */ +const SIZE_TOKEN_MAP = { + xs: 'sizeXs', + sm: 'sizeSm', + md: 'sizeMd', + lg: 'sizeLg', + xl: 'sizeXl', + '2xl': 'size2xl' +} + +/** + * Maps size tokens to strokeWidth theme property names + */ +const STROKE_WIDTH_TOKEN_MAP = { + xs: 'strokeWidthXs', + sm: 'strokeWidthSm', + md: 'strokeWidthMd', + lg: 'strokeWidthLg', + xl: 'strokeWidthXl', + '2xl': 'strokeWidth2xl' +} + +/** + * Legacy SVGIcon size tokens (DEPRECATED - will be removed in future version) + */ +const LEGACY_SIZE_MAP = { 'x-small': '1.125rem', small: '2rem', medium: '3rem', @@ -43,6 +70,22 @@ const svgIconSizeMapping = { 'x-large': '10rem' } +/** + * Legacy SVGIcon color tokens (DEPRECATED - will be removed in future version) + */ +const LEGACY_COLOR_MAP = { + primary: 'baseColor', + secondary: 'mutedColor', + 'primary-inverse': 'onColor', + 'secondary-inverse': 'onColor', + success: 'successColor', + error: 'errorColor', + alert: 'infoColor', + warning: 'warningColor', + brand: 'infoColor', + auto: 'inherit' +} + /** * Convert semantic size token to numeric pixels for Lucide */ @@ -50,39 +93,45 @@ const convertSemanticSize = ( size: LucideIconWrapperProps['size'], componentTheme: NewComponentTypes['Icon'] ) => { - if (typeof size === 'string') { - // Check SVGIcon tokens first (for legacy size tokens) - if (size in svgIconSizeMapping) { - return px(svgIconSizeMapping[size as keyof typeof svgIconSizeMapping]) - } + if (!size) return undefined + + // Check legacy SVGIcon tokens first (DEPRECATED) + if (size in LEGACY_SIZE_MAP) { + console.warn( + `Icon size "${size}" is deprecated. Use semantic tokens (xs, sm, md, lg, xl, 2xl) instead.` + ) + return px(LEGACY_SIZE_MAP[size as keyof typeof LEGACY_SIZE_MAP]) + } - // Fall back to componentTheme (for other tokens like xs, sm, md, lg, xl, 2xl) - const propName = `size${size.charAt(0).toUpperCase()}${size.slice( - 1 - )}` as keyof typeof componentTheme - if (propName in componentTheme) { - return px(componentTheme[propName]) - } + const themeKey = SIZE_TOKEN_MAP[size as keyof typeof SIZE_TOKEN_MAP] + if (themeKey && themeKey in componentTheme) { + return px(componentTheme[themeKey as keyof NewComponentTypes['Icon']]) } + + // Warn if unknown token is passed + console.warn( + `Icon size "${size}" is not a valid semantic token. Valid tokens are: xs, sm, md, lg, xl, 2xl.` + ) return undefined } /** * Convert semantic size token to stroke width value for Lucide - * Derives stroke width from size + * Derives stroke width from size using matching token */ const convertSizeToStrokeWidth = ( size: LucideIconWrapperProps['size'], componentTheme: NewComponentTypes['Icon'] ) => { - if (typeof size === 'string') { - const propName = `strokeWidth${size.charAt(0).toUpperCase()}${size.slice( - 1 - )}` as keyof typeof componentTheme - if (propName in componentTheme) { - return px(componentTheme[propName]) - } + if (!size) return undefined + + // Look up stroke width token matching the size + const themeKey = + STROKE_WIDTH_TOKEN_MAP[size as keyof typeof STROKE_WIDTH_TOKEN_MAP] + if (themeKey && themeKey in componentTheme) { + return px(componentTheme[themeKey as keyof NewComponentTypes['Icon']]) } + return undefined } @@ -101,14 +150,21 @@ const determineColorValue = ( return color } - if ( - color in componentTheme && - !color.startsWith('size') && - !color.startsWith('strokeWidth') - ) { + // Check legacy SVGIcon color tokens (DEPRECATED) + if (color in LEGACY_COLOR_MAP) { + const mappedColor = LEGACY_COLOR_MAP[color as keyof typeof LEGACY_COLOR_MAP] + console.warn( + `Icon color "${color}" is deprecated. Use "${mappedColor}" instead.` + ) + return mappedColor + } + + if (color in componentTheme) { return color } + // Warn if unknown token is passed + console.warn(`Icon color "${color}" is not a valid semantic token.`) return undefined } @@ -135,7 +191,7 @@ const generateStyle = ( if (colorValue) { if (colorValue === 'inherit') { - resolvedColor = 'inherit' + resolvedColor = 'currentColor' } else if (colorValue === 'ai') { // Special handling for AI gradient color gradientColors = {