diff --git a/packages/lucide-preact/package.json b/packages/lucide-preact/package.json index 216b921f7..57d45a8ac 100644 --- a/packages/lucide-preact/package.json +++ b/packages/lucide-preact/package.json @@ -37,6 +37,7 @@ "build:icons": "build-icons --output=./src --templateSrc=./scripts/exportTemplate.mts --renderUniqueKey --withAliases --aliasesFileExtension=.ts --iconFileExtension=.ts --exportFileName=index.ts", "build:bundles": "rollup -c ./rollup.config.mjs", "test": "pnpm build:icons && vitest run", + "test:watch": "vitest watch", "version": "pnpm version --git-tag-version=false" }, "devDependencies": { @@ -47,7 +48,7 @@ "@testing-library/jest-dom": "^6.6.3", "@testing-library/preact": "^3.2.3", "jest-serializer-html": "^7.1.0", - "preact": "^10.19.2", + "preact": "^10.26.9", "rollup": "^4.53.3", "rollup-plugin-dts": "^6.2.3", "typescript": "^5.8.3", diff --git a/packages/lucide-preact/src/Icon.ts b/packages/lucide-preact/src/Icon.ts index bfbda2297..41b4b8bfc 100644 --- a/packages/lucide-preact/src/Icon.ts +++ b/packages/lucide-preact/src/Icon.ts @@ -1,6 +1,8 @@ import { h, toChildArray } from 'preact'; import defaultAttributes from './defaultAttributes'; import type { IconNode, LucideProps } from './types'; +import { useLucideContext } from './context'; +import { mergeClasses } from '@lucide/shared'; interface IconComponentProps extends LucideProps { iconNode: IconNode; @@ -22,29 +24,41 @@ interface IconComponentProps extends LucideProps { * @returns {ForwardRefExoticComponent} LucideIcon */ const Icon = ({ - color = 'currentColor', - size = 24, - strokeWidth = 2, + color, + size, + strokeWidth, absoluteStrokeWidth, children, iconNode, class: classes = '', ...rest -}: IconComponentProps) => - h( +}: IconComponentProps) => { + const { + size: contextSize = 24, + strokeWidth: contextStrokeWidth = 2, + absoluteStrokeWidth: contextAbsoluteStrokeWidth = false, + color: contextColor = 'currentColor', + class: contextClass = '', + } = useLucideContext() ?? {}; + + const calculatedStrokeWidth = + absoluteStrokeWidth ?? contextAbsoluteStrokeWidth + ? (Number(strokeWidth ?? contextStrokeWidth) * 24) / Number(size ?? contextSize) + : strokeWidth ?? contextStrokeWidth; + + return h( 'svg', { ...defaultAttributes, - width: String(size), - height: size, - stroke: color, - ['stroke-width' as 'strokeWidth']: absoluteStrokeWidth - ? (Number(strokeWidth) * 24) / Number(size) - : strokeWidth, - class: ['lucide', classes].join(' '), + width: size ?? contextSize ?? 24, + height: size ?? contextSize ?? 24, + stroke: color ?? contextColor, + ['stroke-width' as 'strokeWidth']: calculatedStrokeWidth, + class: mergeClasses('lucide', contextClass, classes), ...rest, }, [...iconNode.map(([tag, attrs]) => h(tag, attrs)), ...toChildArray(children)], ); +}; export default Icon; diff --git a/packages/lucide-preact/src/context.ts b/packages/lucide-preact/src/context.ts new file mode 100644 index 000000000..1e4c9edb1 --- /dev/null +++ b/packages/lucide-preact/src/context.ts @@ -0,0 +1,49 @@ +import { createContext, h, type ComponentChildren } from 'preact'; +import { useContext, useMemo } from 'preact/hooks'; + +const LucideContext = createContext<{ + size?: number; + color?: string; + strokeWidth?: number; + absoluteStrokeWidth?: boolean; + class?: string; +}>({ + size: 24, + color: 'currentColor', + strokeWidth: 2, + absoluteStrokeWidth: false, + class: '', +}); + +interface LucideProviderProps { + children: ComponentChildren; + size?: number; + color?: string; + strokeWidth?: number; + absoluteStrokeWidth?: boolean; + class?: string; +} + +export function LucideProvider({ + children, + size, + color, + strokeWidth, + absoluteStrokeWidth, + class: className, +}: LucideProviderProps) { + const value = useMemo( + () => ({ + size, + color, + strokeWidth, + absoluteStrokeWidth, + class: className, + }), + [size, color, strokeWidth, absoluteStrokeWidth, className], + ); + + return h(LucideContext.Provider, { value }, children); +} + +export const useLucideContext = () => useContext(LucideContext); diff --git a/packages/lucide-preact/src/lucide-preact.ts b/packages/lucide-preact/src/lucide-preact.ts index 6636c2259..816491ad4 100644 --- a/packages/lucide-preact/src/lucide-preact.ts +++ b/packages/lucide-preact/src/lucide-preact.ts @@ -2,6 +2,7 @@ export * from './icons'; export * as icons from './icons'; export * from './aliases'; export * from './types'; +export * from './context'; export { default as createLucideIcon } from './createLucideIcon'; export { default as Icon } from './Icon'; diff --git a/packages/lucide-preact/tests/__snapshots__/Icon.spec.tsx.snap b/packages/lucide-preact/tests/__snapshots__/Icon.spec.tsx.snap index 2e30bf9a6..635ca428a 100644 --- a/packages/lucide-preact/tests/__snapshots__/Icon.spec.tsx.snap +++ b/packages/lucide-preact/tests/__snapshots__/Icon.spec.tsx.snap @@ -2,7 +2,7 @@ exports[`Using Icon Component > should render icon and match snapshot 1`] = ` should render the icon with LucideProvider 1`] = ` + + + + +`; diff --git a/packages/lucide-preact/tests/context.spec.tsx b/packages/lucide-preact/tests/context.spec.tsx new file mode 100644 index 000000000..539dda1d8 --- /dev/null +++ b/packages/lucide-preact/tests/context.spec.tsx @@ -0,0 +1,88 @@ +import { render } from '@testing-library/preact'; +import { describe, expect, it } from 'vitest'; +import { House, LucideProvider } from '../src/lucide-preact'; + +describe('Using LucideProvider', () => { + it('should render the icon with LucideProvider', () => { + const { container } = render( + + + , + ); + + expect(container.firstChild).toMatchSnapshot(); + }); + + it('should render the icon with LucideProvider and custom strokeWidth', () => { + const { container } = render( + + + , + ); + + const IconComponent = container.firstElementChild; + + expect(IconComponent).toHaveAttribute('width', '48'); + expect(IconComponent).toHaveAttribute('height', '48'); + expect(IconComponent).toHaveAttribute('stroke', 'red'); + expect(IconComponent).toHaveAttribute('stroke-width', '4'); + }); + + it('should render the icon with LucideProvider and custom absoluteStrokeWidth', () => { + const { container } = render( + + + , + ); + + const IconComponent = container.firstElementChild; + + expect(IconComponent).toHaveAttribute('stroke-width', '1'); + }); + + it("should override the provider's global props when passing props to the icon", () => { + const { container } = render( + + + , + ); + + const IconComponent = container.firstElementChild; + + expect(IconComponent).toHaveAttribute('width', '24'); + expect(IconComponent).toHaveAttribute('height', '24'); + expect(IconComponent).toHaveAttribute('stroke', 'blue'); + expect(IconComponent).toHaveAttribute('stroke-width', '2'); + }); + + it('should merge class names from LucideProvider and icon props', () => { + const { container } = render( + + + , + ); + + const IconComponent = container.firstElementChild; + + expect(IconComponent).toHaveAttribute('class', 'lucide provider-class lucide-house icon-class'); + }); +}); diff --git a/packages/lucide-react-native/package.json b/packages/lucide-react-native/package.json index 8e0fbb1d4..69951f10f 100644 --- a/packages/lucide-react-native/package.json +++ b/packages/lucide-react-native/package.json @@ -52,6 +52,7 @@ "build:icons": "build-icons --output=./src --templateSrc=./scripts/exportTemplate.mts --renderUniqueKey --iconFileExtension=.ts --exportFileName=index.ts --withAliases --aliasesFileExtension=.ts", "build:bundles": "rollup -c ./rollup.config.mjs", "test": "pnpm build:icons && vitest run", + "test:watch": "vitest watch", "version": "pnpm version --git-tag-version=false" }, "devDependencies": { diff --git a/packages/lucide-react-native/src/Icon.ts b/packages/lucide-react-native/src/Icon.ts index 7fc4cb4aa..e5c3e19e9 100644 --- a/packages/lucide-react-native/src/Icon.ts +++ b/packages/lucide-react-native/src/Icon.ts @@ -2,9 +2,12 @@ import { createElement, forwardRef, type FunctionComponent } from 'react'; import * as NativeSvg from 'react-native-svg'; import defaultAttributes, { childDefaultAttributes } from './defaultAttributes'; import { IconNode, LucideProps } from './types'; +import { useLucideContext } from './context'; interface IconComponentProps extends LucideProps { iconNode: IconNode; + testID?: string; + className?: string; } /** @@ -25,20 +28,33 @@ interface IconComponentProps extends LucideProps { const Icon = forwardRef( ( { - color = 'currentColor', - size = 24, - strokeWidth = 2, + color, + size, + strokeWidth, absoluteStrokeWidth, children, iconNode, className, + testID, ...rest }, ref, ) => { + const { + size: contextSize = 24, + strokeWidth: contextStrokeWidth = 2, + absoluteStrokeWidth: contextAbsoluteStrokeWidth = false, + color: contextColor = 'currentColor', + } = useLucideContext() ?? {}; + + const calculatedStrokeWidth = + absoluteStrokeWidth ?? contextAbsoluteStrokeWidth + ? (Number(strokeWidth ?? contextStrokeWidth) * 24) / Number(size ?? contextSize) + : strokeWidth ?? contextStrokeWidth; + const customAttrs = { - stroke: color, - strokeWidth: absoluteStrokeWidth ? (Number(strokeWidth) * 24) / Number(size) : strokeWidth, + stroke: color ?? contextColor ?? defaultAttributes.stroke, + strokeWidth: calculatedStrokeWidth, ...rest, }; @@ -47,9 +63,10 @@ const Icon = forwardRef( { ref, ...defaultAttributes, + width: size ?? contextSize ?? defaultAttributes.width, + height: size ?? contextSize ?? defaultAttributes.height, + 'data-testid': testID, className, - width: size, - height: size, ...customAttrs, }, [ diff --git a/packages/lucide-react-native/src/context.ts b/packages/lucide-react-native/src/context.ts new file mode 100644 index 000000000..c63d8a412 --- /dev/null +++ b/packages/lucide-react-native/src/context.ts @@ -0,0 +1,43 @@ +import { createContext, createElement, type ReactNode, useContext, useMemo } from 'react'; + +const LucideContext = createContext<{ + size?: number; + color?: string; + strokeWidth?: number; + absoluteStrokeWidth?: boolean; +}>({ + size: 24, + color: 'currentColor', + strokeWidth: 2, + absoluteStrokeWidth: false, +}); + +interface LucideProviderProps { + children: ReactNode; + size?: number; + color?: string; + strokeWidth?: number; + absoluteStrokeWidth?: boolean; +} + +export function LucideProvider({ + children, + size, + color, + strokeWidth, + absoluteStrokeWidth, +}: LucideProviderProps) { + const value = useMemo( + () => ({ + size, + color, + strokeWidth, + absoluteStrokeWidth, + }), + [size, color, strokeWidth, absoluteStrokeWidth], + ); + + return createElement(LucideContext.Provider, { value }, children); +} + +export const useLucideContext = () => useContext(LucideContext); diff --git a/packages/lucide-react-native/src/createLucideIcon.ts b/packages/lucide-react-native/src/createLucideIcon.ts index 51c044789..4374b0b32 100644 --- a/packages/lucide-react-native/src/createLucideIcon.ts +++ b/packages/lucide-react-native/src/createLucideIcon.ts @@ -1,55 +1,24 @@ -import { forwardRef, createElement, FunctionComponent } from 'react'; -import * as NativeSvg from 'react-native-svg'; -import defaultAttributes, { childDefaultAttributes } from './defaultAttributes'; -import { IconNode, LucideIcon, LucideProps } from './types'; +import { forwardRef, createElement } from 'react'; +import { IconNode, LucideProps } from './types'; +import { toPascalCase } from '@lucide/shared'; +import Icon from './Icon'; -const createLucideIcon = (iconName: string, iconNode: IconNode): LucideIcon => { - const Component = forwardRef( - ( - { - color = 'currentColor', - size = 24, - strokeWidth = 2, - absoluteStrokeWidth, - children, - 'data-testid': dataTestId, - ...rest - }: LucideProps, +/** + * Create a Lucide icon component + * @param {string} iconName + * @param {array} iconNode + * @returns {ForwardRefExoticComponent} LucideIcon + */ +const createLucideIcon = (iconName: string, iconNode: IconNode) => { + const Component = forwardRef((props, ref) => + createElement(Icon, { ref, - ) => { - const customAttrs = { - stroke: color, - strokeWidth: absoluteStrokeWidth ? (Number(strokeWidth) * 24) / Number(size) : strokeWidth, - ...rest, - }; - - return createElement( - NativeSvg.Svg as unknown as string, - { - ref, - ...defaultAttributes, - width: size, - height: size, - 'data-testid': dataTestId, - ...customAttrs, - }, - [ - ...iconNode.map(([tag, attrs]) => { - const upperCasedTag = (tag.charAt(0).toUpperCase() + - tag.slice(1)) as keyof typeof NativeSvg; - // duplicating the attributes here because generating the OTA update bundles don't inherit the SVG properties from parent (codepush, expo-updates) - return createElement( - NativeSvg[upperCasedTag] as FunctionComponent, - { ...childDefaultAttributes, ...customAttrs, ...attrs } as LucideProps, - ); - }), - ...((Array.isArray(children) ? children : [children]) || []), - ], - ); - }, + iconNode, + ...props, + }), ); - Component.displayName = `${iconName}`; + Component.displayName = toPascalCase(iconName); return Component; }; diff --git a/packages/lucide-react-native/src/lucide-react-native.ts b/packages/lucide-react-native/src/lucide-react-native.ts index b761dcce9..7d94dd2ca 100644 --- a/packages/lucide-react-native/src/lucide-react-native.ts +++ b/packages/lucide-react-native/src/lucide-react-native.ts @@ -1,6 +1,7 @@ export * from './icons'; export * from './aliases'; export * from './types'; +export * from './context'; export { default as createLucideIcon } from './createLucideIcon'; export { default as Icon } from './Icon'; diff --git a/packages/lucide-react-native/tests/__snapshots__/context.spec.tsx.snap b/packages/lucide-react-native/tests/__snapshots__/context.spec.tsx.snap new file mode 100644 index 000000000..870ee8816 --- /dev/null +++ b/packages/lucide-react-native/tests/__snapshots__/context.spec.tsx.snap @@ -0,0 +1,32 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Using LucideProvider > should render the icon with LucideProvider 1`] = ` + + + + +`; diff --git a/packages/lucide-react-native/tests/__snapshots__/lucide-react-native.spec.tsx.snap b/packages/lucide-react-native/tests/__snapshots__/lucide-react-native.spec.tsx.snap index 14af0d45e..eb68bbd49 100644 --- a/packages/lucide-react-native/tests/__snapshots__/lucide-react-native.spec.tsx.snap +++ b/packages/lucide-react-native/tests/__snapshots__/lucide-react-native.spec.tsx.snap @@ -1,60 +1,62 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`Using lucide icon components > should adjust the size, stroke color and stroke width 1`] = ` - - - - - - - - - - - + + + + + `; diff --git a/packages/lucide-react-native/tests/context.spec.tsx b/packages/lucide-react-native/tests/context.spec.tsx new file mode 100644 index 000000000..d4a166b9a --- /dev/null +++ b/packages/lucide-react-native/tests/context.spec.tsx @@ -0,0 +1,80 @@ +import { cleanup, render } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { House, LucideProvider } from '../src/lucide-react-native'; + +vi.mock('react-native-svg'); + +describe('Using LucideProvider', () => { + it('should render the icon with LucideProvider', () => { + cleanup(); + const { container } = render( + + + , + ); + + expect(container.firstChild).toMatchSnapshot(); + }); + + it('should render the icon with LucideProvider and custom strokeWidth', () => { + cleanup(); + const testId = 'house-icon'; + const { getByTestId } = render( + + + , + ); + + const IconComponent = getByTestId(testId); + + expect(IconComponent).toHaveAttribute('width', '48'); + expect(IconComponent).toHaveAttribute('height', '48'); + expect(IconComponent).toHaveAttribute('stroke', 'red'); + expect(IconComponent).toHaveAttribute('stroke-width', '4'); + }); + + it('should render the icon with LucideProvider and custom absoluteStrokeWidth', () => { + cleanup(); + const { container } = render( + + + , + ); + + const IconComponent = container.firstElementChild; + + expect(IconComponent).toHaveAttribute('stroke-width', '1'); + }); + + it("should override the provider's global props when passing props to the icon", () => { + cleanup(); + const { container } = render( + + + , + ); + + const IconComponent = container.firstElementChild; + + expect(IconComponent).toHaveAttribute('width', '32'); + expect(IconComponent).toHaveAttribute('height', '32'); + expect(IconComponent).toHaveAttribute('stroke', 'blue'); + expect(IconComponent).toHaveAttribute('stroke-width', '3'); + }); +}); diff --git a/packages/lucide-react-native/tests/lucide-react-native.spec.tsx b/packages/lucide-react-native/tests/lucide-react-native.spec.tsx index de5970981..940de6976 100644 --- a/packages/lucide-react-native/tests/lucide-react-native.spec.tsx +++ b/packages/lucide-react-native/tests/lucide-react-native.spec.tsx @@ -14,49 +14,53 @@ describe('Using lucide icon components', () => { }); it('should adjust the size, stroke color and stroke width', () => { - const { container } = render( + const testId = 'grid-icon'; + const { getByTestId } = render( , ); - const SVGElement = container.firstElementChild; + const GridIcon = getByTestId(testId); - expect(SVGElement).toHaveAttribute('stroke', 'red'); - expect(SVGElement).toHaveAttribute('width', '48'); - expect(SVGElement).toHaveAttribute('height', '48'); - expect(SVGElement).toHaveAttribute('stroke-width', '4'); + expect(GridIcon).toHaveAttribute('stroke', 'red'); + expect(GridIcon).toHaveAttribute('width', '48'); + expect(GridIcon).toHaveAttribute('height', '48'); + expect(GridIcon).toHaveAttribute('stroke-width', '4'); - expect(container.innerHTML).toMatchSnapshot(); + expect(GridIcon).toMatchSnapshot(); }); it('should render the alias icon', () => { - const testId = 'pen-icon'; - const { container } = render( + const penTestId = 'pen-icon'; + const { getByTestId: getByTestId1 } = render( , ); - const PenIconRenderedHTML = container.innerHTML; + const penIcon = getByTestId1(penTestId); cleanup(); - const { container: Edit2Container } = render( + const { getByTestId: getByTestId2 } = render( , ); - expect(PenIconRenderedHTML).toBe(Edit2Container.innerHTML); + const edit2Icon = getByTestId2(penTestId); + + expect(penIcon).toStrictEqual(edit2Icon); }); it('should not scale the strokeWidth when absoluteStrokeWidth is set', () => { @@ -83,8 +87,8 @@ describe('Using lucide icon components', () => { const childId = 'child'; const { container, getByTestId } = render( - - + + , ); const { children } = container.firstElementChild ?? {}; @@ -100,9 +104,9 @@ describe('Using lucide icon components', () => { const childId2 = 'child2'; const { container, getByTestId } = render( - - - + + + , ); const { children } = getByTestId(testId) as unknown as { children: HTMLCollection }; @@ -123,7 +127,7 @@ describe('Using lucide icon components', () => { const { container, getByTestId } = render( ( ( - { - color = 'currentColor', - size = 24, - strokeWidth = 2, - absoluteStrokeWidth, - className = '', - children, - iconNode, - ...rest - }, + { color, size, strokeWidth, absoluteStrokeWidth, className = '', children, iconNode, ...rest }, ref, - ) => - createElement( + ) => { + const { + size: contextSize = 24, + strokeWidth: contextStrokeWidth = 2, + absoluteStrokeWidth: contextAbsoluteStrokeWidth = false, + color: contextColor = 'currentColor', + className: contextClass = '', + } = useLucideContext() ?? {}; + + const calculatedStrokeWidth = + absoluteStrokeWidth ?? contextAbsoluteStrokeWidth + ? (Number(strokeWidth ?? contextStrokeWidth) * 24) / Number(size ?? contextSize) + : strokeWidth ?? contextStrokeWidth; + + return createElement( 'svg', { ref, ...defaultAttributes, - width: size, - height: size, - stroke: color, - strokeWidth: absoluteStrokeWidth ? (Number(strokeWidth) * 24) / Number(size) : strokeWidth, - className: mergeClasses('lucide', className), + width: size ?? contextSize ?? defaultAttributes.width, + height: size ?? contextSize ?? defaultAttributes.height, + stroke: color ?? contextColor, + strokeWidth: calculatedStrokeWidth, + className: mergeClasses('lucide', contextClass, className), ...(!children && !hasA11yProp(rest) && { 'aria-hidden': 'true' }), ...rest, }, @@ -53,7 +58,8 @@ const Icon = forwardRef( ...iconNode.map(([tag, attrs]) => createElement(tag, attrs)), ...(Array.isArray(children) ? children : [children]), ], - ), + ); + }, ); export default Icon; diff --git a/packages/lucide-react/src/context.ts b/packages/lucide-react/src/context.ts new file mode 100644 index 000000000..68b33f454 --- /dev/null +++ b/packages/lucide-react/src/context.ts @@ -0,0 +1,40 @@ +import { createContext, createElement, type ReactNode, useContext, useMemo } from 'react'; +import { LucideProps } from './types'; + +type LucideConfig = { + size: number; + color: string; + strokeWidth: number; + absoluteStrokeWidth: boolean; + className: string; +}; + +const LucideContext = createContext({}); + +type LucideProviderProps = { + children: ReactNode; +} & Partial; + +export function LucideProvider({ + children, + size, + color, + strokeWidth, + absoluteStrokeWidth, + className, +}: LucideProviderProps) { + const value = useMemo( + () => ({ + size, + color, + strokeWidth, + absoluteStrokeWidth, + className, + }), + [size, color, strokeWidth, absoluteStrokeWidth, className], + ); + + return createElement(LucideContext.Provider, { value }, children); +} + +export const useLucideContext = () => useContext(LucideContext); diff --git a/packages/lucide-react/src/lucide-react.ts b/packages/lucide-react/src/lucide-react.ts index 6636c2259..816491ad4 100644 --- a/packages/lucide-react/src/lucide-react.ts +++ b/packages/lucide-react/src/lucide-react.ts @@ -2,6 +2,7 @@ export * from './icons'; export * as icons from './icons'; export * from './aliases'; export * from './types'; +export * from './context'; export { default as createLucideIcon } from './createLucideIcon'; export { default as Icon } from './Icon'; diff --git a/packages/lucide-react/tests/__snapshots__/context.spec.tsx.snap b/packages/lucide-react/tests/__snapshots__/context.spec.tsx.snap new file mode 100644 index 000000000..b572910a9 --- /dev/null +++ b/packages/lucide-react/tests/__snapshots__/context.spec.tsx.snap @@ -0,0 +1,24 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Using LucideProvider > should render the icon with LucideProvider 1`] = ` + +`; diff --git a/packages/lucide-react/tests/context.spec.tsx b/packages/lucide-react/tests/context.spec.tsx new file mode 100644 index 000000000..8d8974c9f --- /dev/null +++ b/packages/lucide-react/tests/context.spec.tsx @@ -0,0 +1,99 @@ +import { render } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import { House, LucideProvider } from '../src/lucide-react'; + +describe('Using LucideProvider', () => { + it('should render the icon with LucideProvider', () => { + const { container } = render( + + + , + ); + + expect(container.firstChild).toMatchSnapshot(); + }); + + it('should render the icon with default props when no provider is used', () => { + const { container } = render(); + + const IconComponent = container.firstElementChild; + + expect(IconComponent).toHaveAttribute('width', '24'); + expect(IconComponent).toHaveAttribute('height', '24'); + expect(IconComponent).toHaveAttribute('stroke', 'currentColor'); + expect(IconComponent).toHaveAttribute('stroke-width', '2'); + }); + + it('should render the icon with LucideProvider and custom strokeWidth', () => { + const { container } = render( + + + , + ); + + const IconComponent = container.firstElementChild; + + expect(IconComponent).toHaveAttribute('width', '48'); + expect(IconComponent).toHaveAttribute('height', '48'); + expect(IconComponent).toHaveAttribute('stroke', 'red'); + expect(IconComponent).toHaveAttribute('stroke-width', '4'); + }); + + it('should render the icon with LucideProvider and custom absoluteStrokeWidth', () => { + const { container } = render( + + + , + ); + + const IconComponent = container.firstElementChild; + + expect(IconComponent).toHaveAttribute('stroke-width', '1'); + }); + + it("should override the provider's global props when passing props to the icon", () => { + const { container } = render( + + + , + ); + + const IconComponent = container.firstElementChild; + + expect(IconComponent).toHaveAttribute('width', '24'); + expect(IconComponent).toHaveAttribute('height', '24'); + expect(IconComponent).toHaveAttribute('stroke', 'blue'); + expect(IconComponent).toHaveAttribute('stroke-width', '2'); + }); + + it('should merge className from provider and icon', () => { + const { container } = render( + + + , + ); + + const IconComponent = container.firstElementChild; + + expect(IconComponent).toHaveAttribute('class', 'lucide provider-class lucide-house icon-class'); + }); +}); diff --git a/packages/lucide-solid/package.json b/packages/lucide-solid/package.json index c6bb61334..3a59b2880 100644 --- a/packages/lucide-solid/package.json +++ b/packages/lucide-solid/package.json @@ -62,7 +62,10 @@ "build:version": "node ./scripts/replaceVersion.mjs", "build:bundle": "rollup -c rollup.config.mjs", "build:icons": "build-icons --output=./src --templateSrc=./scripts/exportTemplate.mts --renderUniqueKey --withAliases --separateAliasesFile --separateAliasesFileIgnore=fingerprint --aliasesFileExtension=.ts --iconFileExtension=.tsx --exportFileName=index.ts", + "typecheck": "tsc", + "typecheck:watch": "tsc -w", "test": "pnpm build:icons && vitest run", + "test:watch": "vitest watch", "version": "pnpm version --git-tag-version=false" }, "devDependencies": { diff --git a/packages/lucide-solid/src/Icon.tsx b/packages/lucide-solid/src/Icon.tsx index fb3f07713..1bd8a3852 100644 --- a/packages/lucide-solid/src/Icon.tsx +++ b/packages/lucide-solid/src/Icon.tsx @@ -1,8 +1,9 @@ -import { For, splitProps } from 'solid-js'; +import { For, splitProps, useContext } from 'solid-js'; import { Dynamic } from 'solid-js/web'; import defaultAttributes from './defaultAttributes'; import { IconNode, LucideProps } from './types'; import { mergeClasses, toKebabCase, toPascalCase } from '@lucide/shared'; +import { LucideContext } from './context'; interface IconProps { name?: string; @@ -21,28 +22,40 @@ const Icon = (props: LucideProps & IconProps) => { 'absoluteStrokeWidth', ]); + const globalProps = useContext(LucideContext); + return ( diff --git a/packages/lucide-solid/src/context.tsx b/packages/lucide-solid/src/context.tsx new file mode 100644 index 000000000..709de0cc9 --- /dev/null +++ b/packages/lucide-solid/src/context.tsx @@ -0,0 +1,36 @@ +import { createContext, splitProps, type JSXElement } from 'solid-js'; + +export const LucideContext = createContext<{ + size?: number; + color?: string; + strokeWidth?: number; + absoluteStrokeWidth?: boolean; + class?: string; +}>({ + size: 24, + color: 'currentColor', + strokeWidth: 2, + absoluteStrokeWidth: false, + class: '', +}); + +interface LucideProviderProps { + children: JSXElement; + size?: number; + color?: string; + strokeWidth?: number; + absoluteStrokeWidth?: boolean; + class?: string; +} + +export function LucideProvider(props: LucideProviderProps) { + const [value, rest] = splitProps(props, [ + 'size', + 'color', + 'strokeWidth', + 'absoluteStrokeWidth', + 'class', + ]); + + return {rest.children}; +} diff --git a/packages/lucide-solid/src/lucide-solid.ts b/packages/lucide-solid/src/lucide-solid.ts index 3bfeb70ea..6242889b9 100644 --- a/packages/lucide-solid/src/lucide-solid.ts +++ b/packages/lucide-solid/src/lucide-solid.ts @@ -2,5 +2,6 @@ export * from './icons'; export * as icons from './icons'; export * from './aliases'; export * from './types'; +export * from './context'; export { default as Icon } from './Icon'; diff --git a/packages/lucide-solid/tests/__snapshots__/context.spec.tsx.snap b/packages/lucide-solid/tests/__snapshots__/context.spec.tsx.snap new file mode 100644 index 000000000..0684a027d --- /dev/null +++ b/packages/lucide-solid/tests/__snapshots__/context.spec.tsx.snap @@ -0,0 +1,25 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Using LucideProvider > should render the icon with LucideProvider 1`] = ` + + + + +`; diff --git a/packages/lucide-solid/tests/context.spec.tsx b/packages/lucide-solid/tests/context.spec.tsx new file mode 100644 index 000000000..6ae047a6d --- /dev/null +++ b/packages/lucide-solid/tests/context.spec.tsx @@ -0,0 +1,102 @@ +import { render } from '@solidjs/testing-library'; +import { describe, expect, it } from 'vitest'; +import { House, LucideProvider } from '../src/lucide-solid'; + +describe('Using LucideProvider', () => { + it('should render the icon with LucideProvider', () => { + const { container } = render(() => ( + + + + )); + + expect(container.firstChild).toMatchSnapshot(); + }); + + it('should render the icon with default props when no provider is used', () => { + const { container } = render(() => ); + + const IconComponent = container.firstElementChild; + + expect(IconComponent).toHaveAttribute('width', '24'); + expect(IconComponent).toHaveAttribute('height', '24'); + expect(IconComponent).toHaveAttribute('stroke', 'currentColor'); + expect(IconComponent).toHaveAttribute('stroke-width', '2'); + }); + + it('should render the icon with LucideProvider and custom strokeWidth', () => { + const { container } = render(() => ( + + + + )); + + const IconComponent = container.firstElementChild; + + expect(IconComponent).toHaveAttribute('width', '48'); + expect(IconComponent).toHaveAttribute('height', '48'); + expect(IconComponent).toHaveAttribute('stroke', 'red'); + expect(IconComponent).toHaveAttribute('stroke-width', '4'); + }); + + it('should render the icon with LucideProvider and custom absoluteStrokeWidth', () => { + const { container } = render(() => ( + + + + )); + + const IconComponent = container.firstElementChild; + + expect(IconComponent).toHaveAttribute('stroke-width', '1'); + }); + + it("should override the provider's global props when passing props to the icon", () => { + const { container } = render(() => ( + + + + )); + + const IconComponent = container.firstElementChild; + + expect(IconComponent).toHaveAttribute('width', '24'); + expect(IconComponent).toHaveAttribute('height', '24'); + expect(IconComponent).toHaveAttribute('stroke', 'blue'); + expect(IconComponent).toHaveAttribute('stroke-width', '2'); + }); + + it('should merge className from provider and icon', () => { + const { container } = render(() => ( + + + + )); + + const IconComponent = container.firstElementChild; + + expect(IconComponent).toHaveAttribute( + 'class', + 'lucide lucide-icon provider-class lucide-house icon-class', + ); + }); +}); diff --git a/packages/lucide-vue-next/scripts/exportTemplate.mts b/packages/lucide-vue-next/scripts/exportTemplate.mts index 19f401e67..d42df2a61 100644 --- a/packages/lucide-vue-next/scripts/exportTemplate.mts +++ b/packages/lucide-vue-next/scripts/exportTemplate.mts @@ -1,6 +1,7 @@ import base64SVG from '@lucide/build-icons/utils/base64SVG'; +import defineExportTemplate from '@lucide/build-icons/utils/defineExportTemplate'; -export default async ({ +export default defineExportTemplate(async({ componentName, iconName, children, @@ -13,6 +14,9 @@ export default async ({ return ` import createLucideIcon from '../createLucideIcon'; +import { IconNode } from '../types'; + +export const __iconNode: IconNode = ${JSON.stringify(children)} /** * @component @name ${componentName} @@ -25,8 +29,8 @@ import createLucideIcon from '../createLucideIcon'; * @returns {FunctionalComponent} Vue component * ${deprecated ? `@deprecated ${deprecationReason}` : ''} */ -const ${componentName} = createLucideIcon('${iconName}', ${JSON.stringify(children)}); +const ${componentName} = createLucideIcon('${iconName}', __iconNode); export default ${componentName}; `; -}; +}); diff --git a/packages/svelte/src/Icon.svelte b/packages/svelte/src/Icon.svelte index afe9154ad..5e43335e4 100644 --- a/packages/svelte/src/Icon.svelte +++ b/packages/svelte/src/Icon.svelte @@ -1,17 +1,24 @@ {#each iconNode as [tag, attrs]} + setContext(LucideContext, globalProps); + +export const getLucideContext = () => getContext(LucideContext); diff --git a/packages/svelte/src/lucide-svelte.ts b/packages/svelte/src/lucide-svelte.ts index 71e607847..77a738f95 100644 --- a/packages/svelte/src/lucide-svelte.ts +++ b/packages/svelte/src/lucide-svelte.ts @@ -4,3 +4,4 @@ export * from './aliases/index.js'; export { default as defaultAttributes } from './defaultAttributes.js'; export * from './types.js'; export { default as Icon } from './Icon.svelte'; +export * from './context.js'; diff --git a/packages/svelte/tests/ContextWrapper.svelte b/packages/svelte/tests/ContextWrapper.svelte new file mode 100644 index 000000000..61a2f6973 --- /dev/null +++ b/packages/svelte/tests/ContextWrapper.svelte @@ -0,0 +1,15 @@ + + + + Test + diff --git a/packages/svelte/tests/__snapshots__/lucide-svelte.spec.ts.snap b/packages/svelte/tests/__snapshots__/lucide-svelte.spec.ts.snap index 56adb5875..2c207dffa 100644 --- a/packages/svelte/tests/__snapshots__/lucide-svelte.spec.ts.snap +++ b/packages/svelte/tests/__snapshots__/lucide-svelte.spec.ts.snap @@ -126,7 +126,6 @@ exports[`Using lucide icon components > should not scale the strokeWidth when ab stroke-width="1" stroke-linecap="round" stroke-linejoin="round" - data-testid="smile-icon" class="lucide-icon lucide lucide-smile" > { afterEach(() => cleanup()); @@ -12,11 +13,9 @@ describe('Using lucide icon components', () => { it('should adjust the size, stroke color and stroke width', () => { const { container } = render(Smile, { - props: { - size: 48, - color: 'red', - strokeWidth: 4, - }, + size: 48, + color: 'red', + strokeWidth: 4, }); expect(container).toMatchSnapshot(); @@ -24,30 +23,27 @@ describe('Using lucide icon components', () => { it('should add a class to the element', () => { const testClass = 'my-icon'; - render(Smile, { - props: { - class: testClass, - }, + const { container } = render(Smile, { + class: testClass, }); - const [icon] = document.getElementsByClassName(testClass); + const IconComponent = container.firstElementChild; - expect(icon).toBeInTheDocument(); - expect(icon).toMatchSnapshot(); - expect(icon).toHaveClass(testClass); - expect(icon).toHaveClass('lucide'); - expect(icon).toHaveClass('lucide-smile'); + expect(IconComponent).toBeInTheDocument(); + expect(IconComponent).toMatchSnapshot(); + expect(IconComponent).toHaveClass(testClass); + expect(IconComponent).toHaveClass('lucide'); + expect(IconComponent).toHaveClass('lucide-smile'); }); it('should add a style attribute to the element', () => { - render(Smile, { - props: { - style: 'position: absolute;', - }, + const { container } = render(Smile, { + style: 'position: absolute;', }); - const [icon] = document.getElementsByClassName('lucide'); - expect(icon.getAttribute('style')).toContain('position: absolute'); + const IconComponent = container.firstElementChild; + + expect(IconComponent).toHaveAttribute('style', 'position: absolute;'); }); it('should render an icon slot', () => { @@ -71,22 +67,30 @@ describe('Using lucide icon components', () => { }); it('should not scale the strokeWidth when absoluteStrokeWidth is set', () => { - const testId = 'smile-icon'; - const { container, getByTestId } = render(Smile, { - 'data-testid': testId, + const { container } = render(Smile, { color: 'red', size: 48, absoluteStrokeWidth: true, }); - const { attributes } = getByTestId(testId) as unknown as { - attributes: Record; - }; - expect(attributes.stroke.value).toBe('red'); - expect(attributes.width.value).toBe('48'); - expect(attributes.height.value).toBe('48'); - expect(attributes['stroke-width'].value).toBe('1'); + const IconComponent = container.firstElementChild; + + expect(IconComponent).toHaveAttribute('width', '48'); + expect(IconComponent).toHaveAttribute('height', '48'); + expect(IconComponent).toHaveAttribute('stroke', 'red'); + expect(IconComponent).toHaveAttribute('stroke-width', '1'); expect(container.innerHTML).toMatchSnapshot(); }); + + it('should use context values from the global set properties', () => { + const { container } = render(ContextWrapper); + + const IconComponent = container.firstElementChild; + + expect(IconComponent).toHaveAttribute('width', '32'); + expect(IconComponent).toHaveAttribute('height', '32'); + expect(IconComponent).toHaveAttribute('stroke', 'red'); + expect(IconComponent).toHaveAttribute('stroke-width', '1'); + }); }); diff --git a/packages/vue/scripts/exportTemplate.mts b/packages/vue/scripts/exportTemplate.mts index 02cfae27f..d42df2a61 100644 --- a/packages/vue/scripts/exportTemplate.mts +++ b/packages/vue/scripts/exportTemplate.mts @@ -23,10 +23,10 @@ export const __iconNode: IconNode = ${JSON.stringify(children)} * @description Lucide SVG icon component, renders SVG Element with children. * * @preview ![img](data:image/svg+xml;base64,${svgBase64}) - https://lucide.dev/icons/${iconName} - * @see https://lucide.dev/guide/packages/lucide-react - Documentation + * @see https://lucide.dev/guide/packages/lucide-vue-next - Documentation * * @param {Object} props - Lucide icons props and any valid SVG attribute - * @returns {JSX.Element} JSX Element + * @returns {FunctionalComponent} Vue component * ${deprecated ? `@deprecated ${deprecationReason}` : ''} */ const ${componentName} = createLucideIcon('${iconName}', __iconNode); diff --git a/packages/vue/src/Icon.ts b/packages/vue/src/Icon.ts index 73c1037f1..75d85aee4 100644 --- a/packages/vue/src/Icon.ts +++ b/packages/vue/src/Icon.ts @@ -1,7 +1,8 @@ -import { type FunctionalComponent, h } from 'vue'; -import { mergeClasses, toKebabCase, toPascalCase, isEmptyString } from '@lucide/shared'; +import { computed, type FunctionalComponent, h } from 'vue'; +import { isEmptyString, mergeClasses, toKebabCase, toPascalCase } from '@lucide/shared'; import defaultAttributes from './defaultAttributes'; import { IconNode, LucideProps } from './types'; +import { useLucideProps } from './context'; interface IconProps { iconNode: IconNode; @@ -16,32 +17,55 @@ const Icon: FunctionalComponent = ( 'absolute-stroke-width': absoluteStrokeWidthKebabCase, strokeWidth, 'stroke-width': strokeWidthKebabCase, - size = defaultAttributes.width, - color = defaultAttributes.stroke, + size, + color, ...props }, { slots }, ) => { + const { + size: contextSize, + color: contextColor, + strokeWidth: contextStrokeWidth = 2, + absoluteStrokeWidth: contextAbsoluteStrokeWidth = false, + class: contextClass = '', + } = useLucideProps(); + + const calculatedStrokeWidth = computed(() => { + const isAbsoluteStrokeWidth = + isEmptyString(absoluteStrokeWidth) || + isEmptyString(absoluteStrokeWidthKebabCase) || + absoluteStrokeWidth === true || + absoluteStrokeWidthKebabCase === true || + contextAbsoluteStrokeWidth === true; + + const strokeWidthValue = + strokeWidth || + strokeWidthKebabCase || + contextStrokeWidth || + defaultAttributes['stroke-width']; + + if (isAbsoluteStrokeWidth) { + return ( + (Number(strokeWidthValue) * 24) / Number(size ?? contextSize ?? defaultAttributes.width) + ); + } + + return strokeWidthValue; + }); + return h( 'svg', { ...defaultAttributes, ...props, - width: size, - height: size, - stroke: color, - 'stroke-width': - isEmptyString(absoluteStrokeWidth) || - isEmptyString(absoluteStrokeWidthKebabCase) || - absoluteStrokeWidth === true || - absoluteStrokeWidthKebabCase === true - ? (Number(strokeWidth || strokeWidthKebabCase || defaultAttributes['stroke-width']) * - 24) / - Number(size) - : strokeWidth || strokeWidthKebabCase || defaultAttributes['stroke-width'], + width: size ?? contextSize ?? defaultAttributes.width, + height: size ?? contextSize ?? defaultAttributes.height, + stroke: color ?? contextColor ?? defaultAttributes.stroke, + 'stroke-width': calculatedStrokeWidth.value, class: mergeClasses( 'lucide', - props.class, + contextClass, ...(name ? [`lucide-${toKebabCase(toPascalCase(name))}-icon`, `lucide-${toKebabCase(name)}`] : ['lucide-icon']), diff --git a/packages/vue/src/context.ts b/packages/vue/src/context.ts new file mode 100644 index 000000000..7af325ec7 --- /dev/null +++ b/packages/vue/src/context.ts @@ -0,0 +1,19 @@ +import { provide, inject } from 'vue'; + +export const LUCIDE_CONTEXT = Symbol('lucide-icons'); + +interface LucideIconsContext { + size?: number; + color?: string; + strokeWidth?: number; + absoluteStrokeWidth?: boolean; + class?: string; +} + +export function setLucideProps(props: LucideIconsContext) { + return provide(LUCIDE_CONTEXT, props); +} + +export function useLucideProps() { + return inject(LUCIDE_CONTEXT, {}); +} diff --git a/packages/vue/src/createLucideIcon.ts b/packages/vue/src/createLucideIcon.ts index 10f0ad80e..ec5386c35 100644 --- a/packages/vue/src/createLucideIcon.ts +++ b/packages/vue/src/createLucideIcon.ts @@ -1,10 +1,7 @@ import { h } from 'vue'; -import type { FunctionalComponent } from 'vue'; -import { IconNode, LucideProps } from './types'; +import { IconNode, LucideIcon } from './types'; import Icon from './Icon'; -// Create interface extending SVGAttributes - /** * Create a Lucide icon component * @param {string} iconName @@ -12,7 +9,7 @@ import Icon from './Icon'; * @returns {FunctionalComponent} LucideIcon */ const createLucideIcon = - (iconName: string, iconNode: IconNode): FunctionalComponent => + (iconName: string, iconNode: IconNode): LucideIcon => (props, { slots, attrs }) => h( Icon, diff --git a/packages/vue/src/types.ts b/packages/vue/src/types.ts index 638af5b7f..79ccce924 100644 --- a/packages/vue/src/types.ts +++ b/packages/vue/src/types.ts @@ -4,10 +4,8 @@ export interface LucideProps extends Partial { size?: 24 | number; strokeWidth?: number | string; absoluteStrokeWidth?: boolean; + 'absolute-stroke-width'?: boolean; } export type IconNode = [elementName: string, attrs: Record][]; export type LucideIcon = FunctionalComponent; - -// Legacy exports -export type SVGProps = LucideProps; diff --git a/packages/vue/tests/ContextWrapper.vue b/packages/vue/tests/ContextWrapper.vue new file mode 100644 index 000000000..9cbed8792 --- /dev/null +++ b/packages/vue/tests/ContextWrapper.vue @@ -0,0 +1,21 @@ + + + diff --git a/packages/vue/tests/__snapshots__/context.spec.ts.snap b/packages/vue/tests/__snapshots__/context.spec.ts.snap new file mode 100644 index 000000000..97fe3629e --- /dev/null +++ b/packages/vue/tests/__snapshots__/context.spec.ts.snap @@ -0,0 +1,23 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Using lucide icon context > should render the icon with LucideProvider 1`] = ` + + + + +`; diff --git a/packages/vue/tests/__snapshots__/lucide-vue.spec.ts.snap b/packages/vue/tests/__snapshots__/lucide-vue.spec.ts.snap index 809afc8b3..88156ce1b 100644 --- a/packages/vue/tests/__snapshots__/lucide-vue.spec.ts.snap +++ b/packages/vue/tests/__snapshots__/lucide-vue.spec.ts.snap @@ -3,7 +3,7 @@ exports[`Using lucide icon components > should add a class to the element 1`] = `
{ + it('should render the icon with LucideProvider', () => { + const { container } = render(ContextWrapper); + + expect(container.firstChild).toMatchSnapshot(); + }); + + it('should render the icon with LucideProvider and custom strokeWidth', () => { + const { container } = render(ContextWrapper); + + const IconComponent = container.firstElementChild; + + expect(IconComponent).toHaveAttribute('width', '48'); + expect(IconComponent).toHaveAttribute('height', '48'); + expect(IconComponent).toHaveAttribute('stroke', 'red'); + expect(IconComponent).toHaveAttribute('stroke-width', '4'); + }); + + it('should render the icon with LucideProvider and custom absoluteStrokeWidth', () => { + const { container } = render(ContextWrapper); + + const IconComponent = container.firstElementChild; + expect(IconComponent).toHaveAttribute('width', '48'); + expect(IconComponent).toHaveAttribute('height', '48'); + expect(IconComponent).toHaveAttribute('stroke', 'red'); + expect(IconComponent).toHaveAttribute('stroke-width', '4'); + }); + + it("should override the provider's global props when passing props to the icon", () => { + const { container } = render(ContextWrapper, { + props: { + strokeWidth: 1, + color: 'blue', + size: 24, + }, + }); + + const IconComponent = container.firstElementChild; + + expect(IconComponent).toHaveAttribute('width', '24'); + expect(IconComponent).toHaveAttribute('height', '24'); + expect(IconComponent).toHaveAttribute('stroke', 'blue'); + expect(IconComponent).toHaveAttribute('stroke-width', '1'); + }); + + it('should merge className from provider and icon', () => { + const { container } = render(ContextWrapper, { + props: { + class: 'icon-class', + }, + }); + + const IconComponent = container.firstElementChild; + + expect(IconComponent).toHaveAttribute( + 'class', + 'lucide provider-class lucide-house-icon lucide-house icon-class', + ); + }); +}); diff --git a/packages/vue/tests/lucide-vue.spec.ts b/packages/vue/tests/lucide-vue.spec.ts index 09aacc1c7..61f3b330b 100644 --- a/packages/vue/tests/lucide-vue.spec.ts +++ b/packages/vue/tests/lucide-vue.spec.ts @@ -85,7 +85,7 @@ describe('Using lucide icon components', () => { const icon = container.firstElementChild; - await fireEvent.click(icon); + await fireEvent.click(icon as Element); expect(onClick).toHaveBeenCalled(); }); @@ -153,7 +153,7 @@ describe('Using lucide icon components', () => { props: { size: 48, color: 'red', - absoluteStrokeWidth: '', + absoluteStrokeWidth: '' as unknown as boolean, // Vue treats empty string as true for boolean props }, }); @@ -170,7 +170,7 @@ describe('Using lucide icon components', () => { size: 48, color: 'red', 'stroke-width': '2', - 'absolute-stroke-width': '', + 'absolute-stroke-width': '' as unknown as boolean, // Vue treats empty string as true for boolean props }, }); diff --git a/packages/vue/tsconfig.json b/packages/vue/tsconfig.json index 6eec02325..c38e018fa 100644 --- a/packages/vue/tsconfig.json +++ b/packages/vue/tsconfig.json @@ -13,6 +13,6 @@ "lib": ["ESNext", "DOM"], "skipLibCheck": true, "noEmit": true, - "types": ["@testing-library/jest-dom"], + "types": ["@testing-library/jest-dom", "vitest/globals"], }, } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aa74e2ce1..04aca6ae0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -409,7 +409,7 @@ importers: specifier: ^7.1.0 version: 7.1.0 preact: - specifier: ^10.19.2 + specifier: ^10.26.9 version: 10.27.2 rollup: specifier: ^4.53.3 @@ -448,7 +448,7 @@ importers: specifier: ^18.2.37 version: 18.3.27 '@vitejs/plugin-react': - specifier: ^4.4.1 + specifier: ^4.6.0 version: 4.7.0(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(less@4.2.0)(sass@1.77.8)(stylus@0.56.0)(terser@5.44.1)(yaml@2.8.0)) jest-serializer-html: specifier: ^7.1.0 @@ -8827,6 +8827,9 @@ packages: resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} engines: {node: '>= 0.4'} + is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + is-bigint@1.1.0: resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} engines: {node: '>= 0.4'} @@ -8835,6 +8838,10 @@ packages: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} + is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + is-boolean-object@1.2.2: resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} engines: {node: '>= 0.4'} @@ -8861,6 +8868,10 @@ packages: resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} engines: {node: '>= 0.4'} + is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + is-date-object@1.1.0: resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} engines: {node: '>= 0.4'} @@ -8918,6 +8929,9 @@ packages: is-lambda@1.0.1: resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} + is-map@2.0.2: + resolution: {integrity: sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==} + is-map@2.0.3: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} @@ -8929,6 +8943,10 @@ packages: resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} engines: {node: '>= 0.4'} + is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + is-number-object@1.1.1: resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} engines: {node: '>= 0.4'} @@ -9014,6 +9032,9 @@ packages: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} + is-weakmap@2.0.1: + resolution: {integrity: sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==} + is-weakmap@2.0.2: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} engines: {node: '>= 0.4'} @@ -9022,6 +9043,9 @@ packages: resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} engines: {node: '>= 0.4'} + is-weakset@2.0.2: + resolution: {integrity: sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==} + is-weakset@2.0.4: resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} engines: {node: '>= 0.4'} @@ -11710,6 +11734,10 @@ packages: resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} engines: {node: '>= 0.4'} + side-channel@1.0.6: + resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} + engines: {node: '>= 0.4'} + side-channel@1.1.0: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} @@ -12883,6 +12911,46 @@ packages: terser: optional: true + vite@6.3.6: + resolution: {integrity: sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + vite@6.4.1: resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -13190,6 +13258,9 @@ packages: whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -13198,6 +13269,9 @@ packages: resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} engines: {node: '>= 0.4'} + which-collection@1.0.1: + resolution: {integrity: sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==} + which-collection@1.0.2: resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} engines: {node: '>= 0.4'} @@ -15704,7 +15778,7 @@ snapshots: '@babel/parser': 7.28.5 '@babel/template': 7.27.2 '@babel/types': 7.28.5 - debug: 4.4.3 + debug: 4.3.5 transitivePeerDependencies: - supports-color @@ -18036,7 +18110,7 @@ snapshots: '@sveltejs/vite-plugin-svelte-inspector@1.0.4(@sveltejs/vite-plugin-svelte@2.5.3(svelte@4.2.20)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(less@4.2.0)(sass@1.77.8)(terser@5.44.1)(yaml@2.8.0)))(svelte@4.2.20)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(less@4.2.0)(sass@1.77.8)(terser@5.44.1)(yaml@2.8.0))': dependencies: '@sveltejs/vite-plugin-svelte': 2.5.3(svelte@4.2.20)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(less@4.2.0)(sass@1.77.8)(terser@5.44.1)(yaml@2.8.0)) - debug: 4.4.3 + debug: 4.4.0 svelte: 4.2.20 vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(less@4.2.0)(sass@1.77.8)(stylus@0.56.0)(terser@5.44.1)(yaml@2.8.0) transitivePeerDependencies: @@ -18045,7 +18119,7 @@ snapshots: '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.43.14)(vite@6.4.1(@types/node@24.10.1)(jiti@2.6.1)(less@4.2.0)(sass@1.77.8)(stylus@0.56.0)(terser@5.44.1)(yaml@2.8.0)))(svelte@5.43.14)(vite@6.4.1(@types/node@24.10.1)(jiti@2.6.1)(less@4.2.0)(sass@1.77.8)(stylus@0.56.0)(terser@5.44.1)(yaml@2.8.0))': dependencies: '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.43.14)(vite@6.4.1(@types/node@24.10.1)(jiti@2.6.1)(less@4.2.0)(sass@1.77.8)(stylus@0.56.0)(terser@5.44.1)(yaml@2.8.0)) - debug: 4.4.3 + debug: 4.4.1 svelte: 5.43.14 vite: 6.4.1(@types/node@24.10.1)(jiti@2.6.1)(less@4.2.0)(sass@1.77.8)(stylus@0.56.0)(terser@5.44.1)(yaml@2.8.0) transitivePeerDependencies: @@ -18187,7 +18261,7 @@ snapshots: '@testing-library/dom@8.20.1': dependencies: '@babel/code-frame': 7.27.1 - '@babel/runtime': 7.27.1 + '@babel/runtime': 7.26.0 '@types/aria-query': 5.0.4 aria-query: 5.1.3 chalk: 4.1.2 @@ -18198,7 +18272,7 @@ snapshots: '@testing-library/dom@9.3.4': dependencies: '@babel/code-frame': 7.27.1 - '@babel/runtime': 7.27.1 + '@babel/runtime': 7.26.0 '@types/aria-query': 5.0.4 aria-query: 5.1.3 chalk: 4.1.2 @@ -18654,7 +18728,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.9.3) '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.9.3) - debug: 4.4.3 + debug: 4.4.0 eslint: 8.57.1 ts-api-utils: 1.4.3(typescript@5.9.3) optionalDependencies: @@ -18700,7 +18774,7 @@ snapshots: dependencies: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.3 + debug: 4.4.0 globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 @@ -18863,13 +18937,13 @@ snapshots: chai: 6.2.1 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.12(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(less@4.2.0)(sass@1.77.8)(terser@5.44.1)(yaml@2.8.0))': + '@vitest/mocker@4.0.12(vite@6.3.6(@types/node@24.10.1)(jiti@2.6.1)(less@4.2.0)(sass@1.77.8)(terser@5.44.1)(yaml@2.8.0))': dependencies: '@vitest/spy': 4.0.12 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(less@4.2.0)(sass@1.77.8)(stylus@0.56.0)(terser@5.44.1)(yaml@2.8.0) + vite: 6.3.6(@types/node@24.10.1)(jiti@2.6.1)(less@4.2.0)(sass@1.77.8)(stylus@0.56.0)(terser@5.44.1)(yaml@2.8.0) '@vitest/pretty-format@4.0.12': dependencies: @@ -19948,7 +20022,7 @@ snapshots: '@npmcli/move-file': 1.1.2 chownr: 2.0.0 fs-minipass: 2.1.0 - glob: 7.2.0 + glob: 7.2.3 infer-owner: 1.0.4 lru-cache: 6.0.0 minipass: 3.3.6 @@ -20620,7 +20694,7 @@ snapshots: deep-equal@1.1.2: dependencies: is-arguments: 1.1.1 - is-date-object: 1.1.0 + is-date-object: 1.0.5 is-regex: 1.2.1 object-is: 1.1.6 object-keys: 1.1.1 @@ -20634,7 +20708,7 @@ snapshots: get-intrinsic: 1.3.0 is-arguments: 1.1.1 is-array-buffer: 3.0.5 - is-date-object: 1.1.0 + is-date-object: 1.0.5 is-regex: 1.2.1 is-shared-array-buffer: 1.0.4 isarray: 2.0.5 @@ -20642,9 +20716,9 @@ snapshots: object-keys: 1.1.1 object.assign: 4.1.7 regexp.prototype.flags: 1.5.4 - side-channel: 1.1.0 - which-boxed-primitive: 1.1.1 - which-collection: 1.0.2 + side-channel: 1.0.6 + which-boxed-primitive: 1.0.2 + which-collection: 1.0.1 which-typed-array: 1.1.19 deep-is@0.1.4: {} @@ -21089,7 +21163,7 @@ snapshots: get-intrinsic: 1.3.0 has-symbols: 1.1.0 is-arguments: 1.1.1 - is-map: 2.0.3 + is-map: 2.0.2 is-set: 2.0.3 is-string: 1.1.1 isarray: 2.0.5 @@ -22467,7 +22541,7 @@ snapshots: dependencies: '@tootallnate/once': 1.1.2 agent-base: 6.0.2 - debug: 4.4.3 + debug: 4.3.3 transitivePeerDependencies: - supports-color @@ -22699,6 +22773,10 @@ snapshots: has-tostringtag: 1.0.2 safe-regex-test: 1.1.0 + is-bigint@1.0.4: + dependencies: + has-bigints: 1.1.0 + is-bigint@1.1.0: dependencies: has-bigints: 1.1.0 @@ -22707,6 +22785,11 @@ snapshots: dependencies: binary-extensions: 2.3.0 + is-boolean-object@1.1.2: + dependencies: + call-bind: 1.0.8 + has-tostringtag: 1.0.2 + is-boolean-object@1.2.2: dependencies: call-bound: 1.0.4 @@ -22734,6 +22817,10 @@ snapshots: get-intrinsic: 1.3.0 is-typed-array: 1.1.15 + is-date-object@1.0.5: + dependencies: + has-tostringtag: 1.0.2 + is-date-object@1.1.0: dependencies: call-bound: 1.0.4 @@ -22776,12 +22863,18 @@ snapshots: is-lambda@1.0.1: {} + is-map@2.0.2: {} + is-map@2.0.3: {} is-module@1.0.0: {} is-negative-zero@2.0.3: {} + is-number-object@1.0.7: + dependencies: + has-tostringtag: 1.0.2 + is-number-object@1.1.1: dependencies: call-bound: 1.0.4 @@ -22851,12 +22944,19 @@ snapshots: is-unicode-supported@0.1.0: {} + is-weakmap@2.0.1: {} + is-weakmap@2.0.2: {} is-weakref@1.1.1: dependencies: call-bound: 1.0.4 + is-weakset@2.0.2: + dependencies: + call-bind: 1.0.8 + get-intrinsic: 1.3.0 + is-weakset@2.0.4: dependencies: call-bound: 1.0.4 @@ -23858,7 +23958,7 @@ snapshots: metro-runtime@0.81.0: dependencies: - '@babel/runtime': 7.27.1 + '@babel/runtime': 7.26.0 flow-enums-runtime: 0.0.6 metro-runtime@0.81.5: @@ -25785,7 +25885,7 @@ snapshots: qs@6.13.0: dependencies: - side-channel: 1.1.0 + side-channel: 1.0.6 quansync@0.2.11: {} @@ -26696,6 +26796,13 @@ snapshots: object-inspect: 1.13.4 side-channel-map: 1.0.1 + side-channel@1.0.6: + dependencies: + call-bind: 1.0.8 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel@1.1.0: dependencies: es-errors: 1.3.0 @@ -26787,7 +26894,7 @@ snapshots: socks-proxy-agent@6.2.1: dependencies: agent-base: 6.0.2 - debug: 4.4.3 + debug: 4.3.3 socks: 2.8.3 transitivePeerDependencies: - supports-color @@ -26795,7 +26902,7 @@ snapshots: socks-proxy-agent@7.0.0: dependencies: agent-base: 6.0.2 - debug: 4.4.3 + debug: 4.3.3 socks: 2.8.3 transitivePeerDependencies: - supports-color @@ -27095,7 +27202,7 @@ snapshots: dependencies: css: 3.0.0 debug: 4.4.3 - glob: 7.2.0 + glob: 7.2.3 safer-buffer: 2.1.2 sax: 1.2.4 source-map: 0.7.4 @@ -28010,6 +28117,24 @@ snapshots: stylus: 0.56.0 terser: 5.44.1 + vite@6.3.6(@types/node@24.10.1)(jiti@2.6.1)(less@4.2.0)(sass@1.77.8)(stylus@0.56.0)(terser@5.44.1)(yaml@2.8.0): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.53.3 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.10.1 + fsevents: 2.3.3 + jiti: 2.6.1 + less: 4.2.0 + sass: 1.77.8 + stylus: 0.56.0 + terser: 5.44.1 + yaml: 2.8.0 + vite@6.4.1(@types/node@24.10.1)(jiti@2.6.1)(less@4.2.0)(sass@1.77.8)(stylus@0.56.0)(terser@5.44.1)(yaml@2.8.0): dependencies: esbuild: 0.25.10 @@ -28110,7 +28235,7 @@ snapshots: vitest@4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(jiti@2.6.1)(jsdom@20.0.3)(less@4.2.0)(sass@1.77.8)(stylus@0.56.0)(terser@5.44.1)(yaml@2.8.0): dependencies: '@vitest/expect': 4.0.12 - '@vitest/mocker': 4.0.12(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(less@4.2.0)(sass@1.77.8)(terser@5.44.1)(yaml@2.8.0)) + '@vitest/mocker': 4.0.12(vite@6.3.6(@types/node@24.10.1)(jiti@2.6.1)(less@4.2.0)(sass@1.77.8)(terser@5.44.1)(yaml@2.8.0)) '@vitest/pretty-format': 4.0.12 '@vitest/runner': 4.0.12 '@vitest/snapshot': 4.0.12 @@ -28127,7 +28252,7 @@ snapshots: tinyexec: 0.3.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(less@4.2.0)(sass@1.77.8)(stylus@0.56.0)(terser@5.44.1)(yaml@2.8.0) + vite: 6.3.6(@types/node@24.10.1)(jiti@2.6.1)(less@4.2.0)(sass@1.77.8)(stylus@0.56.0)(terser@5.44.1)(yaml@2.8.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 @@ -28329,6 +28454,14 @@ snapshots: tr46: 0.0.3 webidl-conversions: 3.0.1 + which-boxed-primitive@1.0.2: + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.1.1 + is-symbol: 1.1.1 + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -28353,6 +28486,13 @@ snapshots: which-collection: 1.0.2 which-typed-array: 1.1.19 + which-collection@1.0.1: + dependencies: + is-map: 2.0.2 + is-set: 2.0.3 + is-weakmap: 2.0.1 + is-weakset: 2.0.2 + which-collection@1.0.2: dependencies: is-map: 2.0.3