feat(@lucide/solid): Support for Solid 2.0 RC (#4751)

* Updates needed to support Solid 2

* Switch to the next tag

* feat(packages): add `@lucide/solid` package for Solid 2

Solid 2's API is a breaking change from Solid 1 and is still an RC, so
`lucide-solid` stays on Solid 1 (`solid-js: ^1.4.7`) untouched and Solid 2
support ships as a new `packages/solid` package named `@lucide/solid` —
the same scheme already used by `@lucide/svelte` and `@lucide/angular`.

The package is a copy of `lucide-solid` on the `@lucide/shared` build
architecture, ported to Solid 2:

- `splitProps` -> `omit`, `solid-js/web` -> `@solidjs/web`,
  `JSXElement` -> `Element`, `Context.Provider` -> `<Context value>`
- `jsxImportSource` is `@solidjs/web` in tsconfig, the esbuild step and
  `ts.createProgram`; rollup externals drop the removed `solid-js/store`
- `vite-plugin-solid` -> `@solidjs/vite-plugin`
- tests keep upstream's coverage with two Solid 2 adaptations: boolean
  attribute literals must be strings (`aria-hidden="false"`), and writes
  are batched so the reactivity tests need a `flush()` before asserting

Rendered output is unchanged — the snapshots are byte-identical to
`lucide-solid`'s.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(solid): fix React leftovers in the Solid guide

The Solid guide was derived from the React guide and three things were
not carried across:

- `advanced/typescript.md` still told readers the types are for "a
  TypeScript React project"
- its `LucideIcon` example kept React's destructured-props signature.
  The `: ButtonProps` annotation had also been dropped in the copy, so
  `ButtonProps` was declared but unused and the parameters were
  implicitly `any`. Destructuring props breaks reactivity in Solid —
  verified: with `({ icon: Icon, label })` a changing `label` never
  reaches the DOM, while `props.label` updates correctly.
- `advanced/aliased-names.md` had an orphaned `###` heading; its parent
  `## Choosing import name style` was dropped during the conversion
  because it documents React-only subpath exports.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: type the shared Sandpack wrapper with `SandpackProps`

The base Sandpack wrapper declared its props as
`FunctionalComponent<LucideProps>` with `LucideProps` imported from
`@lucide/vue`, which has nothing to do with a Sandpack embed. None of
the props the markdown plugin actually passes (`template`, `files`,
`options`, `customSetup`, `theme`) were declared, so they only reached
the inner `<Sandpack />` via Vue's fallthrough attributes.

Declare `SandpackProps` from `sandpack-vue3` instead and forward them
explicitly. This also removes the `@lucide/vue` import that made every
framework guide — including the Solid one, which uses this wrapper
directly — appear to reference the Vue package.

Verified with a full `vitepress build`: each framework guide's chunk
still carries its own template (react/vue/vite-svelte/vanilla/
vite-solid) and all five props reach the component.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(solid): document the Solid 2 flavour in the getting started guide

The Solid guide teaches `lucide-solid` throughout (64 references), so
the one-line note about `@lucide/solid` left Solid 2 readers on a page
whose every install command and live example is Solid 1 — and the
packages list points `@lucide/solid` at this same page.

Replace that note with a "Using Solid 2?" callout holding its own
tabbed install group, the import-specifier swap, and a table of the
Solid 2 changes that affect code written around Lucide (`@solidjs/web`,
`jsxImportSource`, `splitProps` -> `omit`, `<Ctx value>`), linking out
to Solid's own migration guide for the rest.

The claim that both packages expose the same components and props is
checked, not assumed: `dist/types/lucide-solid.d.ts` is identical
between the two packages, and the only differences anywhere in the
public type surface are Solid's own renames (the `JSX` namespace source
and `JSXElement` -> `Element`).

Keeps one guide section for both packages, matching how upstream
handles the two Svelte packages.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(solid): port lucide-solid to Solid 2

- Icon/context/types use Solid 2 APIs: omit() instead of splitProps(),
  Dynamic and JSX types from @solidjs/web.
- solid-js / @solidjs/web 2.0 (rc) as peer and dev dependencies,
  babel-preset-solid 2; rollup and tsconfig updated to match.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Add integration test

* Update rollup file

* Fix tests

* Revert lucide-solid changes

* update lockfile

* Fix docs

* Format code

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Eric <eric.fennis@gmail.com>
This commit is contained in:
David Di Biase
2026-10-02 19:02:20 +02:00
committed by GitHub
co-authored by Claude Opus 5.5 Eric
parent 0c0e3ab3ae
commit 4b3f93d9d0
53 changed files with 2230 additions and 43 deletions
+1
View File
@@ -82,6 +82,7 @@
- changed-files:
- any-glob-to-any-file:
- 'packages/lucide-solid/*'
- 'packages/solid/*'
# For changes in the Lucide astro package
🚀 astro package:
+1
View File
@@ -53,6 +53,7 @@ jobs:
'@lucide/angular',
'@lucide/astro',
'@lucide/icons',
'@lucide/solid',
'@lucide/svelte',
'@lucide/vue',
]
+71
View File
@@ -0,0 +1,71 @@
name: Lucide Solid 2 Checks
on:
push:
branches:
- next
pull_request:
paths:
- packages/solid/**
- packages/shared/**
- tools/build-icons/**
- tools/rollup-plugins/**
- integrations/lucide-solid-2/**
- package.json
- pnpm-workspace.yaml
- pnpm-lock.yaml
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
cache: 'pnpm'
node-version-file: 'package.json'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm --filter @lucide/solid build
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
cache: 'pnpm'
node-version-file: 'package.json'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Test
run: pnpm --filter @lucide/solid test
integration:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
cache: 'pnpm'
node-version-file: 'package.json'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Install Chromium
run: pnpm --filter @lucide/integration-solid-2-vite exec playwright install --with-deps chromium
- name: Run integration checks
run: pnpm test:integrations:solid-2
+1
View File
@@ -23,6 +23,7 @@ packages/svelte/.svelte-kit
integrations/**/.react-router
integrations/**/next-env.d.ts
integrations/**/routeTree.gen.ts
integrations/**/file-routes.d.ts
integrations/**/.vitest-attachments
integrations/**/__screenshots__
integrations/**/.output
@@ -41,7 +41,6 @@ export default eventHandler((event) => {
<Diff
oldSrc={oldSrc}
newSrc={newSrc}
showGrid
height={height}
width={width}
>
@@ -48,7 +48,6 @@ export default eventHandler((event) => {
<Diff
oldSrc={oldSrc}
newSrc={newSrc}
showGrid
height={height}
width={width}
>
+6 -5
View File
@@ -18,11 +18,12 @@ export default eventHandler((event) => {
return sendError(event, error);
}
const width = getQuery(event).width || undefined;
const height = getQuery(event).height || undefined;
const color = getQuery(event).color || undefined;
const strokeWidth = getQuery(event).strokeWidth || undefined;
const background = getQuery(event).background || undefined;
const query = getQuery<Record<string, string | undefined>>(event);
const width = query.width || undefined;
const height = query.height || undefined;
const color = query.color || undefined;
const strokeWidth = query.strokeWidth || undefined;
const background = query.background || undefined;
const LucideIcon = createLucideIcon(params.iconName, iconNode);
+20 -2
View File
@@ -85,8 +85,26 @@
}
]
},
"lucide-react-native": {
"@lucide/solid": {
"order": 5,
"icon": "solid",
"docsAlias": "solid",
"packageDirname": "solid",
"shields": [
{
"alt": "npm",
"src": "https://img.shields.io/npm/v/@lucide/solid",
"href": "https://www.npmjs.com/package/@lucide/solid"
},
{
"alt": "npm",
"src": "https://img.shields.io/npm/dw/@lucide/solid",
"href": "https://www.npmjs.com/package/@lucide/solid"
}
]
},
"lucide-react-native": {
"order": 6,
"icon": "react-native",
"docsAlias": "react-native",
"shields": [
@@ -103,7 +121,7 @@
]
},
"@lucide/angular": {
"order": 6,
"order": 7,
"icon": "angular",
"docsAlias": "angular",
"packageDirname": "angular",
+8 -5
View File
@@ -397,10 +397,14 @@ interface SvgPreviewProps extends React.SVGProps<SVGSVGElement> {
children?: React.ReactNode;
}
const SvgPreview = (
{ src, children, height = 24, width = 24, showGrid = false, ...props }: SvgPreviewProps,
ref,
) => {
const SvgPreview = ({
src,
children,
height = 24,
width = 24,
showGrid = false,
...props
}: SvgPreviewProps) => {
const subGridSize =
Math.max(height, width) % 3 === 0
? Math.max(height, width) > 24
@@ -413,7 +417,6 @@ const SvgPreview = (
return (
<svg
ref={ref}
xmlns="http://www.w3.org/2000/svg"
width={width}
height={height}
@@ -1,13 +1,11 @@
<script lang="ts" setup>
import { type FunctionalComponent } from 'vue';
import { type LucideProps } from '@lucide/vue/src/types';
import { Sandpack } from 'sandpack-vue3';
import { Sandpack, type SandpackProps } from 'sandpack-vue3';
defineProps<FunctionalComponent<LucideProps>>();
const props = defineProps<SandpackProps>();
</script>
<template>
<Sandpack />
<Sandpack v-bind="props" />
</template>
<style>
+1
View File
@@ -153,6 +153,7 @@ deno add lucide-solid
```
:::
> `@lucide/solid` is for Solid 2, for Solid 1 use the `lucide-solid` package. Solid 2 is still a release candidate.
For more details, see the [documentation](./solid/index.md).
+1 -1
View File
@@ -18,7 +18,7 @@ import {
} from "lucide-solid";
```
### Turn off autocomplete in your IDE
## Turn off autocomplete in your IDE
```json [.vscode/settings.json]
{
+4 -4
View File
@@ -6,7 +6,7 @@ description: Learn about the different types exported by the `lucide-solid` pack
# TypeScript Support
List of exported types from the `lucide-solid` package.
These can be used to type your components when using Lucide icons in a TypeScript React project
These can be used to type your components when using Lucide icons in a TypeScript Solid project.
## `LucideProps`
@@ -61,10 +61,10 @@ interface ButtonProps {
label: string;
}
const IconButton = ({ icon: Icon, label }) => {
const IconButton = (props: ButtonProps) => {
return (
<button aria-label={label}>
<Icon size={16} />
<button aria-label={props.label}>
<props.icon size={16} />
</button>
);
};
+46
View File
@@ -36,6 +36,52 @@ bun add lucide-solid
:::
:::: info Using Solid 2?
Lucide ships one package per major Solid version. This guide covers **Solid 1** and the `lucide-solid` package. If your app is on Solid 2, install `@lucide/solid` instead:
::: code-group
```sh [pnpm]
pnpm add @lucide/solid
```
```sh [yarn]
yarn add @lucide/solid
```
```sh [npm]
npm install @lucide/solid
```
```sh [bun]
bun add @lucide/solid
```
:::
Both packages export the same icon components with the same props, so the rest of this guide applies unchanged — only the import specifier differs:
```jsx
import { Camera } from '@lucide/solid';
```
What does differ is Solid itself. The changes most likely to affect the code you write around Lucide:
| | `lucide-solid` | `@lucide/solid` |
| -------------------- | ----------------------------- | ----------------------- |
| Solid version | `^1.4.7` | `^2.0.0-rc.0` |
| DOM runtime package | `solid-js/web` | `@solidjs/web` |
| `jsxImportSource` | `solid-js` | `@solidjs/web` |
| Splitting icon props | `splitProps(props, ['size'])` | `omit(props, 'size')` |
| Providing context | `<Ctx.Provider value={…}>` | `<Ctx value={…}>` |
See the [Solid 2 migration guide](https://github.com/solidjs/solid/blob/next/documentation/solid-2.0/MIGRATION.md) for the full list.
Solid 2 is still a release candidate, so treat `@lucide/solid` as pre-release too. The live examples throughout this guide run on Solid 1.
::::
## Importing your first icon
Lucide is built with ES Modules, so it's completely tree-shakable.
+2
View File
@@ -34,6 +34,8 @@
"@lucide/helpers": "workspace:*",
"@lucide/shared": "workspace:*",
"@rollup/plugin-replace": "^6.0.3",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"@types/semver": "^7.8.0",
"h3": "^1.15.11",
"markdown-it": "^15.0.1",
+1
View File
@@ -34,6 +34,7 @@ export default defineConfig([
'docs/**/examples/',
'docs/.vitepress/theme/components/editors/preact/index.js',
'packages/svelte/.svelte-kit',
'integrations/**/file-routes.d.ts',
// Tracked in git despite matching a .gitignore pattern, so lint it.
'!packages/lucide-react/dynamicIconImports.mjs',
],
+36
View File
@@ -0,0 +1,36 @@
# Lucide Solid 2 integrations
These fixtures exercise the built `@lucide/solid` package in a real Solid 2 (release candidate) consumer app.
The `vite` fixture was scaffolded with the Solid 2.0 `basic` template:
```sh
npm create solid@latest -- --solid -t basic --ts
```
It keeps the template's turnkey client mode (`@solidjs/vite-plugin` with `start: true`, `filesystem-routing` and `@solidjs/router`), with the demo pages replaced by the icon showcase. The template's tooling versions are aligned with the workspace: TypeScript 6 and the workspace's pinned Vitest 4.
There is no SolidStart fixture yet, because SolidStart still targets Solid 1. The Solid 1 fixtures for `lucide-solid` live in [`../lucide-solid`](../lucide-solid).
Every fixture runs three checks:
- a framework production build;
- TypeScript with `noEmit`;
- Vitest Browser Mode in Chromium.
From the repository root, install Chromium once and run the complete suite:
```sh
pnpm --filter @lucide/integration-solid-2-vite exec playwright install chromium
pnpm test:integrations:solid-2
```
The fixtures depend on `@lucide/solid` through `workspace:*`, but import only its public package entry points. The root command builds `@lucide/solid` before running any consumer checks.
## Two TypeScript projects per fixture
`tsconfig.json` typechecks the fixture's own sources with `skipLibCheck: true`.
`tsconfig.declarations.json` compiles only `src/IconShowcase.tsx` (the file that imports the public entry points) with `skipLibCheck: false` and no ambient `types`. This makes `tsc` read the declarations shipped in `dist/types`, so a `.d.ts` that points at a file the build never emitted fails the check. Both run under `pnpm typecheck`.
`file-routes.d.ts` is generated by `filesystem-routing` on every build and is committed so `tsc` can resolve `virtual:file-routes` before a build has run.
+71
View File
@@ -0,0 +1,71 @@
// Generated by filesystem-routing. Do not edit.
// Regenerated whenever a route file is added, changed or removed.
declare module "virtual:file-routes" {
/** A code-split ref: the delivery adapter emits it as a dynamic import. */
export interface FileRouteLazyRef<M = Record<string, unknown>> {
src: string;
import(): Promise<M>;
}
/** An eager ref: its picked exports are imported statically. */
export interface FileRouteEagerRef<M = Record<string, unknown>> {
/** The source module path — present when the convention emitted the ref lazy and `codeSplitting: false` delivered it eagerly. */
src?: string;
require(): M;
}
/** A flat entry of the route manifest. */
export interface FileRouteEntry {
path: string;
page?: boolean;
/** The page component is a server function; its `$component` is delivered eagerly. */
server?: boolean;
$component?: FileRouteLazyRef<any> | FileRouteEagerRef<any>;
$$route?: FileRouteEagerRef<any>;
[key: string]: unknown;
}
/** An entry of the nested `pageRoutes` view. */
export interface FileRouteTreeEntry extends FileRouteEntry {
id: string;
children?: readonly FileRouteTreeEntry[];
}
/** The flat route manifest, in scan order. */
const routes: readonly [
{
path: "/about";
page: true;
$component: FileRouteLazyRef<typeof import("./src/routes/about")>;
$$route?: undefined;
},
{
path: "/";
page: true;
$component: FileRouteLazyRef<typeof import("./src/routes/index")>;
$$route?: undefined;
}
];
export default routes;
/** The page entries, nested by path with grouping segments stripped. */
export const pageRoutes: readonly [
{
path: "/";
id: "/";
page: true;
$component: FileRouteLazyRef<typeof import("./src/routes/index")>;
$$route?: undefined;
children?: undefined;
},
{
path: "/about";
id: "/about";
page: true;
$component: FileRouteLazyRef<typeof import("./src/routes/about")>;
$$route?: undefined;
children?: undefined;
}
];
}
@@ -0,0 +1,29 @@
{
"name": "@lucide/integration-solid-2-vite",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"build": "vite build",
"typecheck": "tsc --noEmit && tsc --noEmit --project tsconfig.declarations.json",
"test:browser": "vitest run",
"check": "pnpm build && pnpm typecheck && pnpm test:browser"
},
"dependencies": {
"@lucide/solid": "workspace:*",
"@solidjs/meta": "1.0.0-next.2",
"@solidjs/router": "2.0.0-next.34",
"@solidjs/web": "2.0.0-rc.13",
"solid-js": "2.0.0-rc.13"
},
"devDependencies": {
"@solidjs/testing-library": "1.0.0-beta.3",
"@solidjs/vite-plugin": "3.0.0-next.47",
"@vitest/browser-playwright": "5.0.3",
"filesystem-routing": "0.4.0",
"playwright": "1.58.2",
"typescript": "^6.0.3",
"vite": "^8.3.2",
"vitest": "5.0.3"
}
}
@@ -0,0 +1,6 @@
import { Loading } from 'solid-js';
import { Router } from './router';
export default function App() {
return <Router>{(props) => <Loading>{props.children}</Loading>}</Router>;
}
@@ -0,0 +1,19 @@
import { HydrationScript } from '@solidjs/web';
import type { ParentProps } from 'solid-js';
export default function Document(props: ParentProps) {
return (
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1"
/>
<title>Lucide Solid 2 Vite integration</title>
<HydrationScript />
</head>
<body>{props.children}</body>
</html>
);
}
@@ -0,0 +1,31 @@
import { Camera, Droplet, Edit2, House, LucideProvider, Pen } from '@lucide/solid';
import Circle from '@lucide/solid/icons/circle';
export default function IconShowcase() {
return (
<main aria-label="Lucide integration">
<Camera data-testid="static-icon" />
<Droplet
data-testid="custom-icon"
class="consumer-icon"
color="red"
size={48}
strokeWidth={4}
absoluteStrokeWidth
/>
<LucideProvider
color="purple"
size={32}
strokeWidth={3}
>
<House data-testid="provider-icon" />
</LucideProvider>
<Pen data-testid="alias-icon" />
<Edit2 data-testid="canonical-icon" />
<Circle
aria-label="Deep import circle"
data-testid="deep-import-icon"
/>
</main>
);
}
@@ -0,0 +1,7 @@
import { createRouter } from '@solidjs/router';
import { fileRoutes } from '@solidjs/router/fs';
import { pageRoutes } from 'virtual:file-routes';
export const routes = fileRoutes(pageRoutes);
export const Router = createRouter({ routes });
@@ -0,0 +1,10 @@
import { ArrowLeft } from '@lucide/solid';
export default function About() {
return (
<main>
<ArrowLeft data-testid="route-icon" />
<a href="/">Home</a>
</main>
);
}
@@ -0,0 +1,10 @@
import IconShowcase from '../IconShowcase';
export default function Home() {
return (
<>
<IconShowcase />
<a href="/about">About</a>
</>
);
}
+2
View File
@@ -0,0 +1,2 @@
/// <reference types="vite/client" />
/// <reference types="../file-routes.d.ts" />
@@ -0,0 +1,46 @@
import { createRouter, memoryHistory } from '@solidjs/router';
import { render } from '@solidjs/testing-library';
import { Loading } from 'solid-js';
import { expect, test } from 'vitest';
import { page } from 'vitest/browser';
import { routes } from '../src/router';
test('renders Lucide public APIs and survives router navigation', async () => {
const Router = createRouter({ routes, history: memoryHistory('/') });
render(() => <Router>{(props) => <Loading>{props.children}</Loading>}</Router>);
const staticIcon = page.getByTestId('static-icon');
await expect.element(staticIcon).toBeVisible();
await expect.element(staticIcon).toHaveAttribute('width', '24');
await expect.element(staticIcon).toHaveAttribute('height', '24');
await expect.element(staticIcon).toHaveAttribute('viewBox', '0 0 24 24');
await expect.element(staticIcon).toHaveAttribute('aria-hidden', 'true');
const customIcon = page.getByTestId('custom-icon');
await expect.element(customIcon).toBeVisible();
await expect.element(customIcon).toHaveAttribute('width', '48');
await expect.element(customIcon).toHaveAttribute('height', '48');
await expect.element(customIcon).toHaveAttribute('stroke', 'red');
await expect.element(customIcon).toHaveAttribute('stroke-width', '2');
await expect.element(customIcon).toHaveClass('consumer-icon');
const providerIcon = page.getByTestId('provider-icon');
await expect.element(providerIcon).toHaveAttribute('width', '32');
await expect.element(providerIcon).toHaveAttribute('stroke', 'purple');
await expect.element(providerIcon).toHaveAttribute('stroke-width', '3');
const aliasIcon = page.getByTestId('alias-icon');
const canonicalIcon = page.getByTestId('canonical-icon');
await expect.element(aliasIcon).toBeVisible();
await expect.element(canonicalIcon).toBeVisible();
expect(aliasIcon.element().innerHTML).toBe(canonicalIcon.element().innerHTML);
const deepImportIcon = page.getByTestId('deep-import-icon');
await expect.element(deepImportIcon).toBeVisible();
await expect.element(deepImportIcon).toHaveAttribute('aria-label', 'Deep import circle');
await expect.element(deepImportIcon).not.toHaveAttribute('aria-hidden');
await page.getByRole('link', { name: 'About' }).click();
await expect.element(page.getByTestId('route-icon')).toBeVisible();
});
@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"skipLibCheck": false,
"types": []
},
"include": ["src/IconShowcase.tsx"]
}
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"jsx": "preserve",
"jsxImportSource": "@solidjs/web",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"isolatedModules": true,
"module": "ESNext",
"moduleResolution": "bundler",
"noEmit": true,
"strict": true,
"skipLibCheck": true,
"types": ["vite/client"]
},
"include": ["src", "tests", "file-routes.d.ts", "vite.config.ts"]
}
@@ -0,0 +1,22 @@
import solid from '@solidjs/vite-plugin';
import { playwright } from '@vitest/browser-playwright';
import { fileRoutes } from 'filesystem-routing/vite';
import { defineConfig } from 'vitest/config';
export default defineConfig({
// Scaffolded with `npm create solid@latest -- --solid -t basic`: turnkey client
// mode generates the entries around src/App.tsx (wrapped in src/Document.tsx)
// and `vite build` prerenders the shell into dist/client.
plugins: [solid({ start: true, extensions: ['.jsx', '.tsx'] }), fileRoutes({ types: true })],
test: {
browser: {
enabled: true,
headless: true,
provider: playwright(),
instances: [{ browser: 'chromium' }],
},
},
build: {
target: 'esnext',
},
});
+1
View File
@@ -7,6 +7,7 @@
"test:update": "pnpm -r --filter './packages/**' --filter !'./packages/lucide-angular' test -- -u",
"test:integrations:react": "pnpm --filter lucide-react build && pnpm -r --workspace-concurrency=1 --filter './integrations/lucide-react/**' check",
"test:integrations:solid": "pnpm --filter lucide-solid build && pnpm -r --workspace-concurrency=1 --filter './integrations/lucide-solid/**' check",
"test:integrations:solid-2": "pnpm --filter @lucide/solid build && pnpm -r --workspace-concurrency=1 --filter './integrations/lucide-solid-2/**' check",
"test:integrations:vue": "pnpm --filter @lucide/vue build && pnpm -r --workspace-concurrency=1 --filter './integrations/lucide-vue/**' check",
"test:font": "pnpm --filter build-font test",
"lucide": "pnpm --filter lucide",
+89
View File
@@ -0,0 +1,89 @@
<p align="center">
<a href="https://github.com/lucide-icons/lucide">
<img src="https://lucide.dev/package-logos/lucide-solid.svg" alt="Lucide icon library for Solid 2 applications." width="540">
</a>
</p>
<p align="center">
Lucide icon library for Solid 2 applications.
</p>
<div align="center">
[![npm](https://img.shields.io/npm/v/%40lucide%2Fsolid?color=blue)](https://www.npmjs.com/package/@lucide/solid)
![NPM Downloads](https://img.shields.io/npm/dw/%40lucide%2Fsolid)
[![License](https://img.shields.io/badge/license-ISC-green)](https://lucide.dev/license)
</div>
<p align="center">
<a href="https://lucide.dev/guide/">About</a>
·
<a href="https://lucide.dev/icons/">Icons</a>
·
<a href="https://lucide.dev/guide/solid">Documentation</a>
·
<a href="https://lucide.dev/license">License</a>
</p>
# Lucide Solid
Implementation of the Lucide icon library for Solid 2 applications.
## Installation
```sh
pnpm add @lucide/solid
```
```sh
npm install @lucide/solid
```
```sh
yarn add @lucide/solid
```
```sh
bun add @lucide/solid
```
> `@lucide/solid` is only for Solid 2, for Solid 1 use the [`lucide-solid`](https://www.npmjs.com/package/lucide-solid) package. Solid 2 is still a release candidate.
## Documentation
For full documentation, visit [lucide.dev](https://lucide.dev/guide/solid)
## Community
Join the [Discord server](https://discord.gg/EH6nSts) to chat with the maintainers and other users.
## License
Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/license).
[//]: <> (Sponsors)
## Sponsors
<a href="https://vercel.com?utm_source=lucide&utm_campaign=oss">
<img src="https://lucide.dev/vercel.svg" alt="Powered by Vercel" width="200" />
</a>
<a href="https://www.digitalocean.com/?refcode=b0877a2caebd&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge"><img src="https://lucide.dev/digitalocean.svg" width="200" alt="DigitalOcean Referral Badge" /></a>
### Hero backers 🦸
<a href="https://zephyr-cloud.io/"><img src="https://lucide.dev/sponsors/zephyr-cloud.svg" width="180" alt="Zephyr Cloud – From idea to prod: fast micro-frontend delivery!" /></a>
### Awesome backers 🍺
<a href="https://github.com/pdfme/pdfme"><img src="https://lucide.dev/sponsors/pdfme.svg" width="180" alt="pdfme – Open-source PDF generation library built with TypeScript and React." /></a>
<a href="https://www.paxhistoria.co/"><img src="https://lucide.dev/sponsors/paxhistoria.svg?" width="180" alt="Pax Historia – An alternate history sandbox game" /></a>
### Backers ☕
<a href="https://www.fina.money/"><img src="https://lucide.dev/sponsors/fina-money.png" width="180" alt="Fina Money – Modular Finance Tracker" /></a>
### Other contributors 💸
You can find all our past and non-recurring financial contributors at [our Open Collective page](https://opencollective.com/lucide-icons).
+94
View File
@@ -0,0 +1,94 @@
{
"name": "@lucide/solid",
"description": "A Lucide icon library package for Solid 2 applications.",
"version": "0.0.1",
"license": "ISC",
"homepage": "https://lucide.dev",
"bugs": "https://github.com/lucide-icons/lucide/issues",
"repository": {
"type": "git",
"url": "https://github.com/lucide-icons/lucide.git",
"directory": "packages/solid"
},
"keywords": [
"Lucide",
"Solid",
"SolidJS",
"Feather",
"Icons",
"Icon",
"SVG",
"Feather Icons",
"Fontawesome",
"Font Awesome"
],
"author": "Eric Fennis",
"source": "src/lucide-solid.ts",
"main": "dist/cjs/lucide-solid.js",
"module": "dist/esm/lucide-solid.mjs",
"types": "dist/types/lucide-solid.d.ts",
"files": [
"dist"
],
"exports": {
".": {
"types": "./dist/types/lucide-solid.d.ts",
"solid": "./dist/source/lucide-solid.jsx",
"import": "./dist/esm/lucide-solid.mjs",
"browser": "./dist/esm/lucide-solid.mjs",
"require": "./dist/cjs/lucide-solid.js"
},
"./icons": {
"types": "./dist/types/lucide-solid.d.ts",
"solid": "./dist/source/lucide-solid.jsx",
"import": "./dist/esm/lucide-solid.mjs",
"browser": "./dist/esm/lucide-solid.mjs",
"require": "./dist/cjs/lucide-solid.js"
},
"./icons/*": {
"types": "./dist/types/icons/*.d.ts",
"solid": "./dist/source/icons/*.jsx",
"import": "./dist/esm/icons/*.mjs",
"browser": "./dist/esm/icons/*.mjs",
"require": "./dist/cjs/icons/*.js"
}
},
"sideEffects": false,
"scripts": {
"build": "pnpm clean && pnpm copy:license && pnpm build:icons && pnpm build:bundle",
"copy:license": "cp ../../LICENSE ./LICENSE",
"clean": "rm -rf dist && rm -rf stats && rm -rf ./src/icons/*.ts",
"build:transpile": "tsc --jsx preserve -t es2020 --rootDir src --outDir dist --noEmit false",
"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"
},
"devDependencies": {
"@babel/core": "^7.29.7",
"@babel/preset-env": "^7.29.7",
"@babel/preset-typescript": "^7.29.7",
"@lucide/build-icons": "workspace:*",
"@lucide/rollup-plugins": "workspace:*",
"@lucide/shared": "workspace:*",
"@rollup/plugin-babel": "^6.1.0",
"@solidjs/testing-library": "next",
"@solidjs/vite-plugin": "next",
"@solidjs/web": "next",
"@testing-library/jest-dom": "^7.0.1",
"babel-preset-solid": "next",
"esbuild": "^0.28.2",
"jest-serializer-html": "^7.1.0",
"rollup-plugin-dts": "^6.5.1",
"rollup": "^4.63.1",
"solid-js": "next",
"typescript": "^6.0.3",
"vite": "^7.3.6"
},
"peerDependencies": {
"solid-js": "^2.0.0-rc.0"
}
}
+137
View File
@@ -0,0 +1,137 @@
import path from 'path';
import { babel } from '@rollup/plugin-babel';
import esbuild from 'esbuild';
import plugins from '@lucide/rollup-plugins';
import dts from 'rollup-plugin-dts';
import pkg from './package.json' with { type: 'json' };
const dtsOptions = {
includeExternal: ['@lucide/shared/types'],
tsconfig: './tsconfig.json',
};
const packageName = '@lucide/solid';
const outputFileName = 'lucide-solid';
const outputDir = 'dist';
const inputs = ['src/lucide-solid.ts'];
const bundles = [
{
format: 'cjs',
inputs,
outputDir,
preserveModules: true,
},
{
format: 'esm',
inputs,
outputDir,
preserveModules: true,
extension: 'mjs',
},
];
const configs = bundles
.map(({ inputs, outputDir, format, preserveModules, extension = 'js' }) =>
inputs.map((input) => ({
input,
plugins: [
babel({
extensions: ['.ts', '.tsx', '.js', '.jsx'],
babelHelpers: 'bundled',
presets: [
'babel-preset-solid',
'@babel/preset-typescript',
['@babel/preset-env', { bugfixes: true, targets: 'last 2 years' }],
],
}),
...plugins({
pkg,
withEsbuild: false,
}),
format === 'esm'
? {
name: 'ts',
buildEnd() {
// Transpile typescript to './dist/source'
esbuild.build({
entryPoints: ['./src/**/*.tsx', './src/**/*.ts'],
outdir: './dist/source',
outExtension: {
'.js': '.jsx',
},
loader: {
'.js': 'jsx',
},
jsx: 'preserve',
jsxImportSource: '@solidjs/web',
bundle: true,
format: 'esm',
sourcemap: true,
target: ['esnext'],
banner: {
js: `/**
* @license ${pkg.name} v${pkg.version} - ${pkg.license}
*
* This source code is licensed under the ${pkg.license} license.
* See the LICENSE file in the root directory of this source tree.
*/`,
},
plugins: [
{
name: 'externalize-everything-except-own-dependencies',
setup(build) {
build.onResolve({ filter: /(.*)/ }, (args) => {
const modulePath = path.join(args.resolveDir, args.path);
if (
args.kind === 'import-statement' &&
args.path !== '@lucide/shared' &&
!modulePath.includes('packages/shared')
) {
return { path: args.path, external: true };
}
});
},
},
],
external: ['solid-js', '@solidjs/web'],
});
},
}
: null,
],
external: ['solid-js', '@solidjs/web'],
output: {
name: packageName,
...(preserveModules
? {
dir: `${outputDir}/${format}`,
exports: 'auto',
entryFileNames: `[name].${extension}`,
}
: {
file: `${outputDir}/${format}/${outputFileName}.${extension}`,
}),
format: format === 'source' ? 'esm' : format,
preserveModules,
preserveModulesRoot: 'src',
sourcemap: true,
},
})),
)
.flat();
const typesConfig = {
input: inputs[0],
output: {
dir: `${outputDir}/types`,
format: 'es',
preserveModules: true,
preserveModulesRoot: 'src',
entryFileNames: '[name].d.ts',
},
plugins: [dts(dtsOptions)],
};
export default [...configs, typesConfig];
+33
View File
@@ -0,0 +1,33 @@
import base64SVG from '@lucide/build-icons/utils/base64SVG';
import defineExportTemplate from '@lucide/build-icons/utils/defineExportTemplate';
export default defineExportTemplate(
async ({ componentName, iconName, iconData, getSvg, deprecated, deprecationReason }) => {
const svgContents = await getSvg();
const svgBase64 = base64SVG(svgContents);
return `
import Icon from '../Icon';
import type { LucideIconData, LucideProps } from '../types';
const iconData: LucideIconData = ${JSON.stringify(iconData)};
/**
* @component @name ${componentName}
* @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/solid - Documentation
*
* @param {Object} props - Lucide icons props and any valid SVG attribute
* @returns {JSX.Element} JSX Element
* ${deprecated ? `@deprecated ${deprecationReason}` : ''}
*/
const ${componentName} = (props: LucideProps) => (
<Icon {...props} icon={iconData} />
)
export default ${componentName};
`;
},
);
+75
View File
@@ -0,0 +1,75 @@
import { createMemo, For, omit, useContext } from 'solid-js';
import { Dynamic } from '@solidjs/web';
import { buildLucideIconNode, hasA11yProp, mergeClasses } from '@lucide/shared';
import { LucideIconData, LucideIconNode, LucideProps } from './types';
import { LucideContext } from './context';
type IconProps =
| {
icon: LucideIconData;
iconNode?: never;
}
| {
icon?: never;
iconNode?: LucideIconNode[];
};
const LOCAL_PROP_KEYS = [
'color',
'size',
'width',
'height',
'strokeWidth',
'children',
'class',
'icon',
'iconNode',
'absoluteStrokeWidth',
'nonScalingStroke',
] as const;
const Icon = (props: LucideProps & IconProps) => {
const globalProps = useContext(LucideContext);
const rest = () => omit(props, ...LOCAL_PROP_KEYS);
const icon = createMemo<LucideIconData>(
() =>
props.icon ?? {
node: props.iconNode ?? ([] as LucideIconNode[]),
size: 24,
aliases: [],
},
);
const builtIcon = createMemo(() =>
buildLucideIconNode(icon(), {
color: props.color ?? globalProps.color,
width: props.width ?? props.size ?? globalProps.size,
height: props.height ?? props.size ?? globalProps.size,
strokeWidth: props.strokeWidth ?? globalProps.strokeWidth,
absoluteStrokeWidth: props.absoluteStrokeWidth ?? globalProps.absoluteStrokeWidth,
nonScalingStroke: props.nonScalingStroke ?? globalProps.nonScalingStroke,
className: mergeClasses('lucide-icon', globalProps.class, props.class),
hasA11yProp: Boolean(props.children) || hasA11yProp(rest()),
attributes: rest(),
}),
);
return (
<svg {...builtIcon()[1]}>
<For each={builtIcon()[2] ?? []}>
{([elementName, attrs]) => {
return (
<Dynamic
component={elementName}
{...attrs}
/>
);
}}
</For>
</svg>
);
};
export default Icon;
+3
View File
@@ -0,0 +1,3 @@
export * from './aliases';
export * from './prefixed';
export * from './suffixed';
+37
View File
@@ -0,0 +1,37 @@
import { createContext, omit, type Element } from 'solid-js';
export const LucideContext = createContext<{
size?: number;
color?: string;
strokeWidth?: number;
/**
* @deprecated Use `nonScalingStroke` instead.
*/
absoluteStrokeWidth?: boolean;
nonScalingStroke?: boolean;
class?: string;
}>({
size: 24,
color: 'currentColor',
strokeWidth: 2,
absoluteStrokeWidth: false,
nonScalingStroke: false,
class: '',
});
interface LucideProviderProps {
children: Element;
size?: number;
color?: string;
strokeWidth?: number;
/**
* @deprecated Use `nonScalingStroke` instead.
*/
absoluteStrokeWidth?: boolean;
nonScalingStroke?: boolean;
class?: string;
}
export function LucideProvider(props: LucideProviderProps) {
return <LucideContext value={omit(props, 'children')}>{props.children}</LucideContext>;
}
+1
View File
@@ -0,0 +1 @@
export { defaultAttributes as default } from '@lucide/shared';
+1
View File
@@ -0,0 +1 @@
Folder for generated icons
+7
View File
@@ -0,0 +1,7 @@
export * from './icons';
export * as icons from './icons';
export * from './aliases';
export * from './types';
export * from './context';
export { default as Icon } from './Icon';
+33
View File
@@ -0,0 +1,33 @@
import type { JSX } from '@solidjs/web';
import type {
LucideIconData as SharedLucideIconData,
LucideIconNode as SharedLucideIconNode,
} from '@lucide/shared/types';
export type SVGAttributes = Partial<JSX.SvgSVGAttributes<SVGSVGElement>>;
export type LucideIconNode = SharedLucideIconNode<keyof JSX.IntrinsicElements, SVGAttributes>;
export type LucideIconData = SharedLucideIconData<keyof JSX.IntrinsicElements, SVGAttributes>;
/**
* @deprecated Use LucideIconNode instead.
*/
export type IconNode = LucideIconNode[];
export interface LucideProps extends SVGAttributes {
key?: string | number;
class?: string;
size?: string | number;
width?: string | number;
height?: string | number;
color?: string;
strokeWidth?: string | number;
/**
* @deprecated Use `nonScalingStroke` instead.
*/
absoluteStrokeWidth?: boolean;
nonScalingStroke?: boolean;
}
export type LucideIcon = (props: LucideProps) => JSX.Element;
+154
View File
@@ -0,0 +1,154 @@
import { describe, it, expect } from 'vitest';
import { render } from '@solidjs/testing-library';
import { createSignal, flush } from 'solid-js';
import { airVent } from './testIconNodes';
import { Icon } from '../src/lucide-solid';
describe('Using Icon Component', () => {
const airVentIcon = { name: 'air-vent', node: airVent };
it('should render icon based on a iconNode', async () => {
const { container } = render(() => (
<Icon
iconNode={airVent}
size={48}
stroke="red"
absoluteStrokeWidth
/>
));
expect(container.firstChild).toBeDefined();
});
it('should render icon and match snapshot', async () => {
const { container } = render(() => (
<Icon
iconNode={airVent}
size={48}
stroke="red"
absoluteStrokeWidth
/>
));
expect(container.firstChild).toMatchSnapshot();
});
it('should render icon based on icon data', async () => {
const { container } = render(() => (
<Icon
icon={airVentIcon}
size={48}
stroke="red"
/>
));
expect(container.firstChild).toBeDefined();
});
it('should support nonScalingStroke', async () => {
const { container } = render(() => (
<Icon
icon={airVentIcon}
size={48}
stroke="red"
strokeWidth={2}
nonScalingStroke
/>
));
expect(container.firstChild?.firstChild).toHaveAttribute('vector-effect', 'non-scaling-stroke');
});
it('should update when reactive props change', async () => {
const [size, setSize] = createSignal(24);
const { container } = render(() => (
<Icon
icon={airVentIcon}
size={size()}
/>
));
expect(container.firstChild).toHaveAttribute('width', '24');
expect(container.firstChild).toHaveAttribute('height', '24');
setSize(48);
flush();
expect(container.firstChild).toHaveAttribute('width', '48');
expect(container.firstChild).toHaveAttribute('height', '48');
});
});
describe('Icon Component Accessibility', () => {
it('should have aria-hidden prop when no aria prop is present', async () => {
const { container } = render(() => (
<Icon
iconNode={airVent}
size={48}
stroke="red"
absoluteStrokeWidth
/>
));
expect(container.firstChild).toHaveAttribute('aria-hidden', 'true');
});
it('should not have aria-hidden prop when aria prop is present', async () => {
const { container } = render(() => (
<Icon
iconNode={airVent}
size={48}
stroke="red"
absoluteStrokeWidth
aria-label="Air conditioning"
/>
));
expect(container.firstChild).not.toHaveAttribute('aria-hidden');
});
it('should not have aria-hidden prop when title prop is present', async () => {
const { container } = render(() => (
<Icon
iconNode={airVent}
size={48}
stroke="red"
absoluteStrokeWidth
// @ts-expect-error
title="Air conditioning"
/>
));
expect(container.firstChild).not.toHaveAttribute('aria-hidden');
});
it('should not have aria-hidden prop when there are children that could be a <title> element', async () => {
const { container } = render(() => (
<Icon
iconNode={airVent}
size={48}
stroke="red"
absoluteStrokeWidth
>
<title>Some title</title>
</Icon>
));
expect(container.firstChild).not.toHaveAttribute('aria-hidden');
});
it('should never override aria-hidden prop', async () => {
const { container } = render(() => (
<Icon
iconNode={airVent}
size={48}
stroke="red"
absoluteStrokeWidth
aria-hidden="false"
/>
));
expect(container.firstChild).toHaveAttribute('aria-hidden', 'false');
});
});
@@ -0,0 +1,34 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Using Icon Component > should render icon and match snapshot 1`] = `
<svg
aria-hidden="true"
class="lucide lucide-icon"
fill="none"
height="48"
stroke="red"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1"
viewBox="0 0 24 24"
width="48"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 12H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"
key="larmp2"
/>
<path
d="M6 8h12"
key="6g4wlu"
/>
<path
d="M18.3 17.7a2.5 2.5 0 0 1-3.16 3.83 2.53 2.53 0 0 1-1.14-2V12"
key="1bo8pg"
/>
<path
d="M6.6 15.6A2 2 0 1 0 10 17v-5"
key="t9h90c"
/>
</svg>
`;
@@ -0,0 +1,26 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Using LucideProvider > should render the icon with LucideProvider 1`] = `
<svg
aria-hidden="true"
class="lucide lucide-house lucide-home lucide-icon"
fill="none"
height="48"
stroke="red"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="48"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"
key="5wwlr5"
/>
<path
d="M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"
key="r6nss1"
/>
</svg>
`;
@@ -0,0 +1,123 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Using lucide icon components > should adjust the size, stroke color and stroke width 1`] = `
<svg xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
viewbox="0 0 24 24"
fill="none"
stroke="red"
stroke-width="4"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3 lucide-icon"
aria-hidden="true"
data-testid="grid-icon"
>
<rect width="18"
height="18"
x="3"
y="3"
rx="2"
key="afitv7"
>
</rect>
<path d="M3 9h18"
key="1pudct"
>
</path>
<path d="M3 15h18"
key="5xshup"
>
</path>
<path d="M9 3v18"
key="fh3hqa"
>
</path>
<path d="M15 3v18"
key="14nvp0"
>
</path>
</svg>
`;
exports[`Using lucide icon components > should render a component 1`] = `
<svg xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewbox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3 lucide-icon"
aria-hidden="true"
>
<rect width="18"
height="18"
x="3"
y="3"
rx="2"
key="afitv7"
>
</rect>
<path d="M3 9h18"
key="1pudct"
>
</path>
<path d="M3 15h18"
key="5xshup"
>
</path>
<path d="M9 3v18"
key="fh3hqa"
>
</path>
<path d="M15 3v18"
key="14nvp0"
>
</path>
</svg>
`;
exports[`Using lucide icon components > should scale the strokeWidth when absoluteStrokeWidth is set 1`] = `
<svg xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
viewbox="0 0 24 24"
fill="none"
stroke="red"
stroke-width="1"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3 lucide-icon"
aria-hidden="true"
data-testid="grid-icon"
>
<rect width="18"
height="18"
x="3"
y="3"
rx="2"
key="afitv7"
>
</rect>
<path d="M3 9h18"
key="1pudct"
>
</path>
<path d="M3 15h18"
key="5xshup"
>
</path>
<path d="M9 3v18"
key="fh3hqa"
>
</path>
<path d="M15 3v18"
key="14nvp0"
>
</path>
</svg>
`;
+121
View File
@@ -0,0 +1,121 @@
import { render } from '@solidjs/testing-library';
import { createSignal, flush } from 'solid-js';
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(() => (
<LucideProvider
size={48}
color="red"
>
<House />
</LucideProvider>
));
expect(container.firstChild).toMatchSnapshot();
});
it('should render the icon with default props when no provider is used', () => {
const { container } = render(() => <House />);
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(() => (
<LucideProvider
size={48}
color="red"
strokeWidth={4}
>
<House />
</LucideProvider>
));
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 update when reactive LucideProvider props change', () => {
const [size, setSize] = createSignal(24);
const { container } = render(() => (
<LucideProvider size={size()}>
<House />
</LucideProvider>
));
expect(container.firstElementChild).toHaveAttribute('width', '24');
expect(container.firstElementChild).toHaveAttribute('height', '24');
setSize(48);
flush();
expect(container.firstElementChild).toHaveAttribute('width', '48');
expect(container.firstElementChild).toHaveAttribute('height', '48');
});
it('should render the icon with LucideProvider and custom absoluteStrokeWidth', () => {
const { container } = render(() => (
<LucideProvider
size={48}
color="red"
absoluteStrokeWidth
>
<House />
</LucideProvider>
));
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(() => (
<LucideProvider
size={48}
color="red"
strokeWidth={4}
>
<House
size={24}
color="blue"
strokeWidth={2}
/>
</LucideProvider>
));
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(() => (
<LucideProvider class="provider-class">
<House class="icon-class" />
</LucideProvider>
));
const IconComponent = container.firstElementChild;
expect(IconComponent).toHaveAttribute(
'class',
'lucide lucide-house lucide-home lucide-icon provider-class icon-class',
);
});
});
@@ -0,0 +1,90 @@
import { describe, it, expect } from 'vitest';
import { render, cleanup } from '@solidjs/testing-library';
import { Edit2, Grid, Pen, Droplet } from '../src/lucide-solid';
describe('Using lucide icon components', () => {
it('should render a component', () => {
const { container } = render(() => <Grid />);
expect(container.innerHTML).toMatchSnapshot();
});
it('should adjust the size, stroke color and stroke width', async () => {
const testId = 'grid-icon';
const { container, getByTestId } = render(() => (
<Grid
data-testid={testId}
size={48}
color="red"
strokeWidth={4}
/>
));
const { attributes } = (await getByTestId(testId)) as unknown as {
attributes: Record<string, { value: string }>;
};
expect(attributes.stroke.value).toBe('red');
expect(attributes.width.value).toBe('48');
expect(attributes.height.value).toBe('48');
expect(attributes['stroke-width'].value).toBe('4');
expect(container.innerHTML).toMatchSnapshot();
});
it('should render the alias icon', () => {
const testId = 'pen-icon';
const { container } = render(() => (
<Pen
data-testid={testId}
size={48}
stroke="red"
strokeWidth={4}
/>
));
const PenIconRenderedHTML = container.innerHTML;
cleanup();
const { container: Edit2Container } = render(() => (
<Edit2
data-testid={testId}
size={48}
stroke="red"
strokeWidth={4}
/>
));
expect(PenIconRenderedHTML).toBe(Edit2Container.innerHTML);
});
it('should scale the strokeWidth when absoluteStrokeWidth is set', () => {
const testId = 'grid-icon';
const { container, getByTestId } = render(() => (
<Grid
data-testid={testId}
size={48}
color="red"
absoluteStrokeWidth
/>
));
const { attributes } = getByTestId(testId) as unknown as {
attributes: Record<string, { value: string }>;
};
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');
expect(container.innerHTML).toMatchSnapshot();
});
it('should add all classes to the element', () => {
const testClass = 'my-class';
const { container } = render(() => <Droplet class={testClass} />);
expect(container.firstChild).toHaveClass(testClass);
expect(container.firstChild).toHaveClass('lucide');
expect(container.firstChild).toHaveClass('lucide-droplet');
});
});
+10
View File
@@ -0,0 +1,10 @@
import { expect, afterEach } from 'vitest';
import { cleanup } from '@solidjs/testing-library';
import '@testing-library/jest-dom/vitest';
import htmlSerializer from 'jest-serializer-html';
expect.addSnapshotSerializer(htmlSerializer);
afterEach(() => {
cleanup();
});
+22
View File
@@ -0,0 +1,22 @@
import { IconNode } from '../src/types';
export const airVent: IconNode = [
[
'path',
{
d: 'M6 12H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2',
key: 'larmp2',
},
],
['path', { d: 'M6 8h12', key: '6g4wlu' }],
['path', { d: 'M18.3 17.7a2.5 2.5 0 0 1-3.16 3.83 2.53 2.53 0 0 1-1.14-2V12', key: '1bo8pg' }],
['path', { d: 'M6.6 15.6A2 2 0 1 0 10 17v-5', key: 't9h90c' }],
];
export const coffee: IconNode = [
['path', { d: 'M17 8h1a4 4 0 1 1 0 8h-1', key: 'jx4kbh' }],
['path', { d: 'M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z', key: '1bxrl0' }],
['line', { x1: '6', x2: '6', y1: '2', y2: '4', key: '1cr9l3' }],
['line', { x1: '10', x2: '10', y1: '2', y2: '4', key: '170wym' }],
['line', { x1: '14', x2: '14', y1: '2', y2: '4', key: '1c5f70' }],
];
+22
View File
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"strict": true,
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"paths": {
"@lucide/shared": ["../shared/src/index.ts"],
"@lucide/shared/types": ["../shared/src/build/types.ts"]
},
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"jsx": "preserve",
"jsxImportSource": "@solidjs/web",
"types": ["vite/client", "@testing-library/jest-dom/vitest"],
"noEmit": true,
"isolatedModules": true,
"outDir": "./dist",
"declarationDir": "./dist",
"declaration": true
}
}
+11
View File
@@ -0,0 +1,11 @@
import { defineConfig } from 'vitest/config'
import solidPlugin from '@solidjs/vite-plugin';
export default defineConfig({
plugins: [solidPlugin()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './tests/setupVitest.js',
},
});
+625 -19
View File
File diff suppressed because it is too large Load Diff