mirror of
https://github.com/lucide-icons/lucide.git
synced 2026-11-10 13:18:25 +00:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
458ea3429b | ||
|
|
4e5b528958 | ||
|
|
2157983c0f | ||
|
|
78ef931b50 | ||
|
|
3041de00a1 | ||
|
|
3df4e2ba2c | ||
|
|
782c0ee791 | ||
|
|
3d4b0e0878 | ||
|
|
8d0fa2e218 | ||
|
|
f5437f0a4c | ||
|
|
06e962e44b | ||
|
|
a23c4b45cd | ||
|
|
2189ae0f55 |
@@ -7,10 +7,13 @@ on:
|
||||
pull_request:
|
||||
paths:
|
||||
- packages/lucide-react/**
|
||||
- integrations/lucide-react/**
|
||||
- packages/shared/**
|
||||
- tools/build-icons/**
|
||||
- tools/rollup-plugins/**
|
||||
- scripts/generateNextJSAliases.mjs
|
||||
- package.json
|
||||
- pnpm-workspace.yaml
|
||||
- pnpm-lock.yaml
|
||||
|
||||
permissions:
|
||||
@@ -48,3 +51,22 @@ jobs:
|
||||
|
||||
- name: Test
|
||||
run: pnpm --filter lucide-react 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-react-vite exec playwright install --with-deps chromium
|
||||
|
||||
- name: Run integration checks
|
||||
run: pnpm test:integrations:react
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
.cache
|
||||
.env
|
||||
node_modules
|
||||
.pnpm-store
|
||||
dist
|
||||
build
|
||||
sandbox
|
||||
@@ -13,6 +14,10 @@ stash
|
||||
coverage
|
||||
stats
|
||||
*.log
|
||||
*.tsbuildinfo
|
||||
integrations/**/.react-router
|
||||
integrations/**/.vitest-attachments
|
||||
integrations/**/__screenshots__
|
||||
outlined
|
||||
lucide-font
|
||||
packages/**/src/icons/*.js
|
||||
@@ -23,6 +28,9 @@ packages/**/src/aliases.ts
|
||||
!packages/**/src/aliases/index.ts
|
||||
packages/**/src/utils/*.ts
|
||||
!packages/shared/src/utils/*.ts
|
||||
!packages/shared/src/build/
|
||||
!packages/shared/src/build/*.ts
|
||||
packages/shared/src/build/types.d.ts
|
||||
packages/**/src/dynamicIconImports.ts
|
||||
packages/**/DynamicIcon.d.ts
|
||||
packages/**/dynamicIconImports.js
|
||||
|
||||
@@ -19,6 +19,13 @@ packages/lucide-static/icon-nodes.json
|
||||
packages/lucide-static/font
|
||||
packages/svelte/.svelte-kit
|
||||
|
||||
# integration test artifacts and generated routes
|
||||
integrations/**/.react-router
|
||||
integrations/**/next-env.d.ts
|
||||
integrations/**/routeTree.gen.ts
|
||||
integrations/**/.vitest-attachments
|
||||
integrations/**/__screenshots__
|
||||
|
||||
# lucide-svelte
|
||||
packages/svelte/src/icons/*.svelte
|
||||
packages/svelte/.svelte-kit
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Lucide React integrations
|
||||
|
||||
These fixtures exercise the built `lucide-react` package in real consumer frameworks.
|
||||
|
||||
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-react-vite exec playwright install chromium
|
||||
pnpm test:integrations:react
|
||||
```
|
||||
|
||||
The fixtures depend on `lucide-react` through `workspace:*`, but import only its public package entry points. The root command builds `lucide-react` before running any consumer checks.
|
||||
@@ -0,0 +1,34 @@
|
||||
'use client';
|
||||
|
||||
import { Camera, Droplet, Edit2, House, LucideProvider, Pen } from 'lucide-react';
|
||||
import { DynamicIcon } from 'lucide-react/dynamic';
|
||||
|
||||
export default function IconShowcase() {
|
||||
return (
|
||||
<main aria-label="Lucide integration">
|
||||
<Camera data-testid="static-icon" />
|
||||
<Droplet
|
||||
data-testid="custom-icon"
|
||||
className="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" />
|
||||
<DynamicIcon
|
||||
aria-label="Dynamic circle"
|
||||
data-testid="dynamic-icon"
|
||||
name="circle"
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export default function RootLayout({ children }: Readonly<{ children: ReactNode }>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import IconShowcase from './IconShowcase';
|
||||
|
||||
export default function Page() {
|
||||
return <IconShowcase />;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/types/routes.d.ts";
|
||||
import "./.next/types/root-params.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "@lucide/integration-react-nextjs",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "next build",
|
||||
"typecheck": "next typegen && tsc --noEmit",
|
||||
"test:browser": "vitest run",
|
||||
"check": "pnpm build && pnpm typecheck && pnpm test:browser"
|
||||
},
|
||||
"dependencies": {
|
||||
"lucide-react": "workspace:*",
|
||||
"next": "16.3.4",
|
||||
"react": "19.2.8",
|
||||
"react-dom": "19.2.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "19.2.18",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"@vitest/browser-playwright": "4.1.10",
|
||||
"typescript": "^6.0.3",
|
||||
"vite": "^7.3.6",
|
||||
"vitest": "4.1.10",
|
||||
"vitest-browser-react": "2.2.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import { render } from 'vitest-browser-react';
|
||||
import Page from '../app/page';
|
||||
|
||||
test('renders Lucide public APIs in a real browser', async () => {
|
||||
const screen = await render(<Page />);
|
||||
|
||||
const staticIcon = screen.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 = screen.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 = screen.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 = document.querySelector('[data-testid="alias-icon"]');
|
||||
const canonicalIcon = document.querySelector('[data-testid="canonical-icon"]');
|
||||
expect(aliasIcon?.innerHTML).toBe(canonicalIcon?.innerHTML);
|
||||
|
||||
const dynamicIcon = screen.getByTestId('dynamic-icon');
|
||||
await expect.element(dynamicIcon).toBeVisible();
|
||||
await expect.element(dynamicIcon).toHaveAttribute('aria-label', 'Dynamic circle');
|
||||
await expect.element(dynamicIcon).not.toHaveAttribute('aria-hidden');
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [{ "name": "next" }],
|
||||
"types": ["vitest/globals"],
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/*.mts",
|
||||
],
|
||||
"exclude": ["node_modules"],
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { playwright } from '@vitest/browser-playwright';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
browser: {
|
||||
enabled: true,
|
||||
headless: true,
|
||||
provider: playwright(),
|
||||
instances: [{ browser: 'chromium' }],
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Camera, Droplet, Edit2, House, LucideProvider, Pen } from 'lucide-react';
|
||||
import { DynamicIcon } from 'lucide-react/dynamic';
|
||||
|
||||
export default function IconShowcase() {
|
||||
return (
|
||||
<main aria-label="Lucide integration">
|
||||
<Camera data-testid="static-icon" />
|
||||
<Droplet
|
||||
data-testid="custom-icon"
|
||||
className="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" />
|
||||
<DynamicIcon
|
||||
aria-label="Dynamic circle"
|
||||
data-testid="dynamic-icon"
|
||||
name="circle"
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router';
|
||||
|
||||
export function Layout({ children }: Readonly<{ children: ReactNode }>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta
|
||||
content="width=device-width, initial-scale=1"
|
||||
name="viewport"
|
||||
/>
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body>
|
||||
{children}
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return <Outlet />;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { index, route, type RouteConfig } from '@react-router/dev/routes';
|
||||
|
||||
export default [
|
||||
index('./routes/home.tsx'),
|
||||
route('about', './routes/about.tsx'),
|
||||
] satisfies RouteConfig;
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
export default function About() {
|
||||
return (
|
||||
<main>
|
||||
<ArrowLeft data-testid="route-icon" />
|
||||
<Link to="/">Home</Link>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Link } from 'react-router';
|
||||
import IconShowcase from '../IconShowcase';
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<IconShowcase />
|
||||
<Link to="/about">About</Link>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "@lucide/integration-react-react-router",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "react-router build",
|
||||
"typecheck": "react-router typegen && tsc --noEmit",
|
||||
"test:browser": "vitest run",
|
||||
"check": "pnpm build && pnpm typecheck && pnpm test:browser"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-router/node": "8.3.1",
|
||||
"@react-router/serve": "8.3.1",
|
||||
"isbot": "5.2.2",
|
||||
"lucide-react": "workspace:*",
|
||||
"react": "19.2.8",
|
||||
"react-dom": "19.2.8",
|
||||
"react-router": "8.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-router/dev": "8.3.1",
|
||||
"@types/node": "^26.1.1",
|
||||
"@types/react": "19.2.18",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"@vitest/browser-playwright": "4.1.10",
|
||||
"typescript": "^6.0.3",
|
||||
"vite": "^7.3.6",
|
||||
"vitest": "4.1.10",
|
||||
"vitest-browser-react": "2.2.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { Config } from '@react-router/dev/config';
|
||||
|
||||
export default {
|
||||
ssr: true,
|
||||
} satisfies Config;
|
||||
@@ -0,0 +1,54 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import { render } from 'vitest-browser-react';
|
||||
import { MemoryRouter, Route, Routes } from 'react-router';
|
||||
import About from '../app/routes/about';
|
||||
import Home from '../app/routes/home';
|
||||
|
||||
test('renders Lucide public APIs and survives router navigation', async () => {
|
||||
const screen = await render(
|
||||
<MemoryRouter initialEntries={['/']}>
|
||||
<Routes>
|
||||
<Route
|
||||
element={<Home />}
|
||||
path="/"
|
||||
/>
|
||||
<Route
|
||||
element={<About />}
|
||||
path="/about"
|
||||
/>
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
const staticIcon = screen.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 = screen.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 = screen.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 = document.querySelector('[data-testid="alias-icon"]');
|
||||
const canonicalIcon = document.querySelector('[data-testid="canonical-icon"]');
|
||||
expect(aliasIcon?.innerHTML).toBe(canonicalIcon?.innerHTML);
|
||||
|
||||
const dynamicIcon = screen.getByTestId('dynamic-icon');
|
||||
await expect.element(dynamicIcon).toBeVisible();
|
||||
await expect.element(dynamicIcon).toHaveAttribute('aria-label', 'Dynamic circle');
|
||||
await expect.element(dynamicIcon).not.toHaveAttribute('aria-hidden');
|
||||
|
||||
await screen.getByRole('link', { name: 'About' }).click();
|
||||
await expect.element(screen.getByTestId('route-icon')).toBeVisible();
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"include": ["**/*", ".react-router/types/**/*"],
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||
"types": ["node", "vite/client", "vitest/globals"],
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "Bundler",
|
||||
"jsx": "react-jsx",
|
||||
"rootDirs": [".", "./.react-router/types"],
|
||||
"verbatimModuleSyntax": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { reactRouter } from '@react-router/dev/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [reactRouter()],
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { playwright } from '@vitest/browser-playwright';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
browser: {
|
||||
enabled: true,
|
||||
headless: true,
|
||||
provider: playwright(),
|
||||
instances: [{ browser: 'chromium' }],
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0"
|
||||
/>
|
||||
<title>Lucide React TanStack Router integration</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script
|
||||
type="module"
|
||||
src="/src/main.tsx"
|
||||
></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "@lucide/integration-react-tanstack-router",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test:browser": "vitest run",
|
||||
"check": "pnpm build && pnpm typecheck && pnpm test:browser"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-router": "1.170.32",
|
||||
"lucide-react": "workspace:*",
|
||||
"react": "19.2.8",
|
||||
"react-dom": "19.2.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tanstack/router-plugin": "1.168.35",
|
||||
"@types/react": "19.2.18",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"@vitest/browser-playwright": "4.1.10",
|
||||
"typescript": "^6.0.3",
|
||||
"vite": "^7.3.6",
|
||||
"vitest": "4.1.10",
|
||||
"vitest-browser-react": "2.2.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Camera, Droplet, Edit2, House, LucideProvider, Pen } from 'lucide-react';
|
||||
import { DynamicIcon } from 'lucide-react/dynamic';
|
||||
|
||||
export default function IconShowcase() {
|
||||
return (
|
||||
<main aria-label="Lucide integration">
|
||||
<Camera data-testid="static-icon" />
|
||||
<Droplet
|
||||
data-testid="custom-icon"
|
||||
className="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" />
|
||||
<DynamicIcon
|
||||
aria-label="Dynamic circle"
|
||||
data-testid="dynamic-icon"
|
||||
name="circle"
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { RouterProvider } from '@tanstack/react-router';
|
||||
import { router } from './router';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<RouterProvider router={router} />
|
||||
</StrictMode>,
|
||||
);
|
||||
@@ -0,0 +1,77 @@
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
|
||||
// This file was automatically generated by TanStack Router.
|
||||
// You should NOT make any changes in this file as it will be overwritten.
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as AboutRouteImport } from './routes/about'
|
||||
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const AboutRoute = AboutRouteImport.update({
|
||||
id: '/about',
|
||||
path: '/about',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/about': typeof AboutRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/about': typeof AboutRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/about': typeof AboutRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/' | '/about'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/' | '/about'
|
||||
id: '__root__' | '/' | '/about'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
AboutRoute: typeof AboutRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/about': {
|
||||
id: '/about'
|
||||
path: '/about'
|
||||
fullPath: '/about'
|
||||
preLoaderRoute: typeof AboutRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
AboutRoute: AboutRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>()
|
||||
@@ -0,0 +1,10 @@
|
||||
import { createRouter } from '@tanstack/react-router';
|
||||
import { routeTree } from './routeTree.gen';
|
||||
|
||||
export const router = createRouter({ routeTree });
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface Register {
|
||||
router: typeof router;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Outlet, createRootRoute } from '@tanstack/react-router';
|
||||
|
||||
export const Route = createRootRoute({
|
||||
component: () => <Outlet />,
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Link, createFileRoute } from '@tanstack/react-router';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
|
||||
export const Route = createFileRoute('/about')({
|
||||
component: About,
|
||||
});
|
||||
|
||||
function About() {
|
||||
return (
|
||||
<main>
|
||||
<ArrowLeft data-testid="route-icon" />
|
||||
<Link to="/">Home</Link>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Link, createFileRoute } from '@tanstack/react-router';
|
||||
import IconShowcase from '../IconShowcase';
|
||||
|
||||
export const Route = createFileRoute('/')({
|
||||
component: Home,
|
||||
});
|
||||
|
||||
function Home() {
|
||||
return (
|
||||
<>
|
||||
<IconShowcase />
|
||||
<Link to="/about">About</Link>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { createMemoryHistory, createRouter, RouterProvider } from '@tanstack/react-router';
|
||||
import { expect, test } from 'vitest';
|
||||
import { render } from 'vitest-browser-react';
|
||||
import { routeTree } from '../src/routeTree.gen';
|
||||
|
||||
test('renders Lucide public APIs and survives router navigation', async () => {
|
||||
const router = createRouter({
|
||||
history: createMemoryHistory({ initialEntries: ['/'] }),
|
||||
routeTree,
|
||||
});
|
||||
const screen = await render(<RouterProvider router={router} />);
|
||||
|
||||
const staticIcon = screen.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 = screen.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 = screen.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 = document.querySelector('[data-testid="alias-icon"]');
|
||||
const canonicalIcon = document.querySelector('[data-testid="canonical-icon"]');
|
||||
expect(aliasIcon?.innerHTML).toBe(canonicalIcon?.innerHTML);
|
||||
|
||||
const dynamicIcon = screen.getByTestId('dynamic-icon');
|
||||
await expect.element(dynamicIcon).toBeVisible();
|
||||
await expect.element(dynamicIcon).toHaveAttribute('aria-label', 'Dynamic circle');
|
||||
await expect.element(dynamicIcon).not.toHaveAttribute('aria-hidden');
|
||||
|
||||
await screen.getByRole('link', { name: 'About' }).click();
|
||||
await expect.element(screen.getByTestId('route-icon')).toBeVisible();
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"types": ["vite/client", "vitest/globals"],
|
||||
},
|
||||
"include": ["src", "tests", "vite.config.ts", "vitest.config.ts"],
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { tanstackRouter } from '@tanstack/router-plugin/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tanstackRouter({
|
||||
target: 'react',
|
||||
autoCodeSplitting: true,
|
||||
}),
|
||||
react(),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { playwright } from '@vitest/browser-playwright';
|
||||
import { defineConfig, mergeConfig } from 'vitest/config';
|
||||
import viteConfig from './vite.config';
|
||||
|
||||
export default mergeConfig(
|
||||
viteConfig,
|
||||
defineConfig({
|
||||
test: {
|
||||
browser: {
|
||||
enabled: true,
|
||||
headless: true,
|
||||
provider: playwright(),
|
||||
instances: [{ browser: 'chromium' }],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "@lucide/integration-react-tanstack-start",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test:browser": "vitest run",
|
||||
"check": "pnpm build && pnpm typecheck && pnpm test:browser"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-router": "1.170.32",
|
||||
"@tanstack/react-start": "1.168.49",
|
||||
"lucide-react": "workspace:*",
|
||||
"react": "19.2.8",
|
||||
"react-dom": "19.2.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^26.1.1",
|
||||
"@types/react": "19.2.18",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"@vitest/browser-playwright": "4.1.10",
|
||||
"typescript": "^6.0.3",
|
||||
"vite": "^7.3.6",
|
||||
"vitest": "4.1.10",
|
||||
"vitest-browser-react": "2.2.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Camera, Droplet, Edit2, House, LucideProvider, Pen } from 'lucide-react';
|
||||
import { DynamicIcon } from 'lucide-react/dynamic';
|
||||
|
||||
export default function IconShowcase() {
|
||||
return (
|
||||
<main aria-label="Lucide integration">
|
||||
<Camera data-testid="static-icon" />
|
||||
<Droplet
|
||||
data-testid="custom-icon"
|
||||
className="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" />
|
||||
<DynamicIcon
|
||||
aria-label="Dynamic circle"
|
||||
data-testid="dynamic-icon"
|
||||
name="circle"
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
|
||||
// This file was automatically generated by TanStack Router.
|
||||
// You should NOT make any changes in this file as it will be overwritten.
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as AboutRouteImport } from './routes/about'
|
||||
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const AboutRoute = AboutRouteImport.update({
|
||||
id: '/about',
|
||||
path: '/about',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/about': typeof AboutRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/about': typeof AboutRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/about': typeof AboutRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/' | '/about'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/' | '/about'
|
||||
id: '__root__' | '/' | '/about'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
AboutRoute: typeof AboutRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/about': {
|
||||
id: '/about'
|
||||
path: '/about'
|
||||
fullPath: '/about'
|
||||
preLoaderRoute: typeof AboutRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
AboutRoute: AboutRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>()
|
||||
|
||||
import type { getRouter } from './router.tsx'
|
||||
import type { createStart } from '@tanstack/react-start'
|
||||
declare module '@tanstack/react-start' {
|
||||
interface Register {
|
||||
ssr: true
|
||||
router: Awaited<ReturnType<typeof getRouter>>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { createRouter } from '@tanstack/react-router';
|
||||
import { routeTree } from './routeTree.gen';
|
||||
|
||||
export function getRouter() {
|
||||
return createRouter({
|
||||
routeTree,
|
||||
scrollRestoration: true,
|
||||
});
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface Register {
|
||||
router: ReturnType<typeof getRouter>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { HeadContent, Outlet, Scripts, createRootRoute } from '@tanstack/react-router';
|
||||
|
||||
export const Route = createRootRoute({
|
||||
head: () => ({
|
||||
meta: [
|
||||
{ charSet: 'utf-8' },
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
||||
{ title: 'Lucide React TanStack Start integration' },
|
||||
],
|
||||
}),
|
||||
component: RootComponent,
|
||||
});
|
||||
|
||||
function RootComponent() {
|
||||
return (
|
||||
<RootDocument>
|
||||
<Outlet />
|
||||
</RootDocument>
|
||||
);
|
||||
}
|
||||
|
||||
function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<HeadContent />
|
||||
</head>
|
||||
<body>
|
||||
{children}
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Link, createFileRoute } from '@tanstack/react-router';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
|
||||
export const Route = createFileRoute('/about')({
|
||||
component: About,
|
||||
});
|
||||
|
||||
function About() {
|
||||
return (
|
||||
<main>
|
||||
<ArrowLeft data-testid="route-icon" />
|
||||
<Link to="/">Home</Link>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Link, createFileRoute } from '@tanstack/react-router';
|
||||
import IconShowcase from '../IconShowcase';
|
||||
|
||||
export const Route = createFileRoute('/')({
|
||||
component: Home,
|
||||
});
|
||||
|
||||
function Home() {
|
||||
return (
|
||||
<>
|
||||
<IconShowcase />
|
||||
<Link to="/about">About</Link>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import { render } from 'vitest-browser-react';
|
||||
import IconShowcase from '../src/IconShowcase';
|
||||
|
||||
test('renders Lucide public APIs in a real browser', async () => {
|
||||
const screen = await render(<IconShowcase />);
|
||||
|
||||
const staticIcon = screen.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 = screen.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 = screen.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 = document.querySelector('[data-testid="alias-icon"]');
|
||||
const canonicalIcon = document.querySelector('[data-testid="canonical-icon"]');
|
||||
expect(aliasIcon?.innerHTML).toBe(canonicalIcon?.innerHTML);
|
||||
|
||||
const dynamicIcon = screen.getByTestId('dynamic-icon');
|
||||
await expect.element(dynamicIcon).toBeVisible();
|
||||
await expect.element(dynamicIcon).toHaveAttribute('aria-label', 'Dynamic circle');
|
||||
await expect.element(dynamicIcon).not.toHaveAttribute('aria-hidden');
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"types": ["node", "vite/client", "vitest/globals"],
|
||||
},
|
||||
"include": ["src", "tests", "vite.config.ts", "vitest.config.ts"],
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { tanstackStart } from '@tanstack/react-start/plugin/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tanstackStart(), react()],
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { playwright } from '@vitest/browser-playwright';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
browser: {
|
||||
enabled: true,
|
||||
headless: true,
|
||||
provider: playwright(),
|
||||
instances: [{ browser: 'chromium' }],
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0"
|
||||
/>
|
||||
<title>Lucide React Vite integration</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script
|
||||
type="module"
|
||||
src="/src/main.tsx"
|
||||
></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "@lucide/integration-react-vite",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test:browser": "vitest run",
|
||||
"check": "pnpm build && pnpm typecheck && pnpm test:browser"
|
||||
},
|
||||
"dependencies": {
|
||||
"lucide-react": "workspace:*",
|
||||
"react": "19.2.8",
|
||||
"react-dom": "19.2.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "19.2.18",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"@vitest/browser-playwright": "4.1.10",
|
||||
"playwright": "1.58.2",
|
||||
"typescript": "^6.0.3",
|
||||
"vite": "^7.3.6",
|
||||
"vitest": "4.1.10",
|
||||
"vitest-browser-react": "2.2.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import IconShowcase from './IconShowcase';
|
||||
|
||||
export default function App() {
|
||||
return <IconShowcase />;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Camera, Droplet, Edit2, House, LucideProvider, Pen } from 'lucide-react';
|
||||
import { DynamicIcon } from 'lucide-react/dynamic';
|
||||
|
||||
export default function IconShowcase() {
|
||||
return (
|
||||
<main aria-label="Lucide integration">
|
||||
<Camera data-testid="static-icon" />
|
||||
<Droplet
|
||||
data-testid="custom-icon"
|
||||
className="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" />
|
||||
<DynamicIcon
|
||||
aria-label="Dynamic circle"
|
||||
data-testid="dynamic-icon"
|
||||
name="circle"
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
@@ -0,0 +1,36 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import { render } from 'vitest-browser-react';
|
||||
import App from '../src/App';
|
||||
|
||||
test('renders Lucide public APIs in a real browser', async () => {
|
||||
const screen = await render(<App />);
|
||||
|
||||
const staticIcon = screen.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 = screen.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 = screen.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 = document.querySelector('[data-testid="alias-icon"]');
|
||||
const canonicalIcon = document.querySelector('[data-testid="canonical-icon"]');
|
||||
expect(aliasIcon?.innerHTML).toBe(canonicalIcon?.innerHTML);
|
||||
|
||||
const dynamicIcon = screen.getByTestId('dynamic-icon');
|
||||
await expect.element(dynamicIcon).toBeVisible();
|
||||
await expect.element(dynamicIcon).toHaveAttribute('aria-label', 'Dynamic circle');
|
||||
await expect.element(dynamicIcon).not.toHaveAttribute('aria-hidden');
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"types": ["vitest/globals"],
|
||||
},
|
||||
"include": ["src", "tests", "vite.config.ts"],
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { playwright } from '@vitest/browser-playwright';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
browser: {
|
||||
enabled: true,
|
||||
headless: true,
|
||||
provider: playwright(),
|
||||
instances: [{ browser: 'chromium' }],
|
||||
},
|
||||
},
|
||||
});
|
||||
+2
-1
@@ -5,6 +5,7 @@
|
||||
"build": "pnpm -r --filter './packages/**' build",
|
||||
"test": "pnpm -r --filter './packages/**' test",
|
||||
"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:font": "pnpm --filter build-font test",
|
||||
"lucide": "pnpm --filter lucide",
|
||||
"lucide-angular": "pnpm --filter lucide-angular",
|
||||
@@ -59,7 +60,7 @@
|
||||
"@vitest/coverage-v8": "4.1.10",
|
||||
"@vitest/ui": "4.1.10",
|
||||
"ajv-cli": "^5.0.0",
|
||||
"cspell": "^10.0.1",
|
||||
"cpy-cli": "^7.0.0",
|
||||
"dotenv": "^17.4.2",
|
||||
"eslint": "^10.8.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
|
||||
@@ -17,15 +17,16 @@
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"prebuild": "pnpm clean && pnpm copy:license && pnpm build:icons",
|
||||
"prebuild": "pnpm clean && pnpm copy:license && pnpm copy:utils && pnpm build:icons",
|
||||
"build": "pnpm prebuild && pnpm build:ng",
|
||||
"copy:license": "cp ../../LICENSE ./LICENSE",
|
||||
"copy:utils": "cpy ../../packages/shared/src/build/{buildLucideIconNode.ts,defaultAttributes.ts,types.ts} ../../packages/shared/src/utils/mergeClasses.ts ./src/utils/",
|
||||
"clean": "rm -rf dist && rm -rf ./src/icons/*.ts",
|
||||
"build:icons": "build-icons --output=./src --templateSrc=./scripts/exportTemplate.mts --renderUniqueKey --iconFileExtension=.ts --exportFileName=lucide-angular.ts --useDefaultExports=0",
|
||||
"build:ng": "ng build --configuration production",
|
||||
"test": "pnpm build:icons && ng test --no-watch",
|
||||
"test:watch": "ng test",
|
||||
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx,html,css,scss}' --quiet --fix",
|
||||
"test": "pnpm copy:utils && pnpm build:icons && ng test --no-watch",
|
||||
"test:watch": "pnpm copy:utils && ng test",
|
||||
"lint": "npx eslint 'src/**/*.{js,jsx,ts,tsx,html,css,scss}' --quiet --fix",
|
||||
"e2e": "ng e2e"
|
||||
},
|
||||
"prettier": {
|
||||
|
||||
@@ -19,6 +19,11 @@ export interface LucideConfig {
|
||||
* @default 2
|
||||
*/
|
||||
strokeWidth: number;
|
||||
/**
|
||||
* Whether to use absolute stroke width.
|
||||
* @deprecated Use `nonScalingStroke` instead.
|
||||
*/
|
||||
absoluteStrokeWidth: boolean;
|
||||
/**
|
||||
* Whether stroke width should be scaled to appear uniform regardless of icon size.
|
||||
* @default false
|
||||
@@ -31,7 +36,7 @@ export interface LucideConfig {
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
absoluteStrokeWidth: boolean;
|
||||
nonScalingStroke: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,6 +47,7 @@ export const lucideDefaultConfig: LucideConfig = {
|
||||
size: 24,
|
||||
strokeWidth: 2,
|
||||
absoluteStrokeWidth: false,
|
||||
nonScalingStroke: false,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,7 +8,7 @@ import { LucideBadgeAlert } from './icons/badge-alert';
|
||||
@Component({
|
||||
// eslint-disable-next-line @angular-eslint/component-selector
|
||||
selector: 'lucide-static-hydration-root',
|
||||
template: `<svg lucideBadgeAlert [absoluteStrokeWidth]="true"></svg>`,
|
||||
template: `<svg lucideBadgeAlert [nonScalingStroke]="true"></svg>`,
|
||||
imports: [LucideBadgeAlert],
|
||||
})
|
||||
class StaticHydrationRootComponent {}
|
||||
|
||||
@@ -47,6 +47,7 @@ describe('LucideIconBase', () => {
|
||||
let size: WritableSignal<string | number | undefined>;
|
||||
let strokeWidth: WritableSignal<string | number | undefined>;
|
||||
let absoluteStrokeWidth: WritableSignal<boolean | undefined>;
|
||||
let nonScalingStroke: WritableSignal<boolean | undefined>;
|
||||
const getSvgAttribute = (attr: string) => fixture.nativeElement.getAttribute(attr);
|
||||
const expectSvgClasses = (classes: string[], svg: SVGElement = fixture.nativeElement) => {
|
||||
for (const cssClass of classes) {
|
||||
@@ -63,6 +64,7 @@ describe('LucideIconBase', () => {
|
||||
inputBinding('size', size),
|
||||
inputBinding('strokeWidth', strokeWidth),
|
||||
inputBinding('absoluteStrokeWidth', absoluteStrokeWidth),
|
||||
inputBinding('nonScalingStroke', nonScalingStroke),
|
||||
],
|
||||
});
|
||||
}
|
||||
@@ -72,6 +74,7 @@ describe('LucideIconBase', () => {
|
||||
size = signal(undefined);
|
||||
strokeWidth = signal(undefined);
|
||||
absoluteStrokeWidth = signal(undefined);
|
||||
nonScalingStroke = signal(undefined);
|
||||
fixture = createComponent();
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
@@ -135,6 +138,11 @@ describe('LucideIconBase', () => {
|
||||
expect(getSvgAttribute('width')).toBe('18');
|
||||
expect(getSvgAttribute('height')).toBe('18');
|
||||
});
|
||||
it('should set viewBox', () => {
|
||||
size.set('18');
|
||||
fixture.detectChanges();
|
||||
expect(getSvgAttribute('viewBox')).toBe('0 0 24 24');
|
||||
});
|
||||
});
|
||||
|
||||
describe('strokeWidth', () => {
|
||||
@@ -155,22 +163,38 @@ describe('LucideIconBase', () => {
|
||||
});
|
||||
|
||||
describe('absoluteStrokeWidth', () => {
|
||||
it('should adjust stroke width', () => {
|
||||
strokeWidth.set(2);
|
||||
size.set(32);
|
||||
absoluteStrokeWidth.set(true);
|
||||
fixture.detectChanges();
|
||||
expect(getSvgAttribute('stroke-width')).toBe('1.5');
|
||||
});
|
||||
it('should not set vector-effect on children', () => {
|
||||
absoluteStrokeWidth.set(true);
|
||||
for (const child of fixture.nativeElement.children) {
|
||||
expect(child.getAttribute('vector-effect')).toBeNull();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('nonScalingStroke', () => {
|
||||
it('should not adjust stroke width', () => {
|
||||
strokeWidth.set(2);
|
||||
size.set(12);
|
||||
absoluteStrokeWidth.set(true);
|
||||
nonScalingStroke.set(false);
|
||||
fixture.detectChanges();
|
||||
expect(getSvgAttribute('stroke-width')).toBe('2');
|
||||
});
|
||||
it('should not set vector-effect on children', () => {
|
||||
absoluteStrokeWidth.set(false);
|
||||
nonScalingStroke.set(false);
|
||||
fixture.detectChanges();
|
||||
for (const child of getRenderedChildren()) {
|
||||
expect(child.getAttribute('vector-effect')).toBeNull();
|
||||
}
|
||||
});
|
||||
it('should set vector-effect on children', () => {
|
||||
absoluteStrokeWidth.set(true);
|
||||
nonScalingStroke.set(true);
|
||||
fixture.detectChanges();
|
||||
for (const child of getRenderedChildren()) {
|
||||
expect(child.getAttribute('vector-effect')).toBe('non-scaling-stroke');
|
||||
@@ -207,6 +231,7 @@ describe('LucideIconBase', () => {
|
||||
color: 'red',
|
||||
strokeWidth: 1,
|
||||
size: 12,
|
||||
nonScalingStroke: true,
|
||||
absoluteStrokeWidth: true,
|
||||
}),
|
||||
],
|
||||
@@ -229,12 +254,12 @@ describe('LucideIconBase', () => {
|
||||
describe('strokeWidth', () => {
|
||||
it('should use stroke width from config', () => {
|
||||
fixture.detectChanges();
|
||||
expect(getSvgAttribute('stroke-width')).toBe('1');
|
||||
expect(getSvgAttribute('stroke-width')).toBe('2');
|
||||
});
|
||||
it('should use override stroke width from config', () => {
|
||||
strokeWidth.set(3);
|
||||
fixture.detectChanges();
|
||||
expect(getSvgAttribute('stroke-width')).toBe('3');
|
||||
expect(getSvgAttribute('stroke-width')).toBe('6');
|
||||
});
|
||||
});
|
||||
describe('size', () => {
|
||||
@@ -253,14 +278,23 @@ describe('LucideIconBase', () => {
|
||||
describe('absoluteStrokeWidth', () => {
|
||||
it('should use absoluteStrokeWidth from config', () => {
|
||||
fixture.detectChanges();
|
||||
for (const child of getRenderedChildren()) {
|
||||
expect(child.getAttribute('vector-effect')).toBe('non-scaling-stroke');
|
||||
}
|
||||
expect(getSvgAttribute('stroke-width')).toBe('2');
|
||||
});
|
||||
it('should override absoluteStrokeWidth', () => {
|
||||
absoluteStrokeWidth.set(false);
|
||||
fixture.detectChanges();
|
||||
for (const child of getRenderedChildren()) {
|
||||
expect(getSvgAttribute('stroke-width')).toBe('1');
|
||||
});
|
||||
});
|
||||
describe('nonScalingStroke', () => {
|
||||
it('should use nonScalingStroke from config', () => {
|
||||
for (const child of fixture.nativeElement.children) {
|
||||
expect(child.getAttribute('vector-effect')).toBe('non-scaling-stroke');
|
||||
}
|
||||
});
|
||||
it('should override nonScalingStroke', () => {
|
||||
nonScalingStroke.set(false);
|
||||
for (const child of fixture.nativeElement.children) {
|
||||
expect(child.getAttribute('vector-effect')).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Component, computed, inject, input, Signal } from '@angular/core';
|
||||
import { LUCIDE_CONFIG } from './lucide-config';
|
||||
import { LucideIconData, Nullable } from './types';
|
||||
import { LucideIconData, LucideIconNode, Nullable } from './types';
|
||||
import defaultAttributes from './default-attributes';
|
||||
import { lucideIconTemplate } from './lucide-icon-template';
|
||||
import buildLucideIconNode from './utils/buildLucideIconNode';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@@ -12,38 +13,17 @@ import { lucideIconTemplate } from './lucide-icon-template';
|
||||
template: lucideIconTemplate,
|
||||
host: {
|
||||
...defaultAttributes,
|
||||
class: 'lucide',
|
||||
'[class]': 'iconClasses()',
|
||||
'[attr.width]': 'size()',
|
||||
'[attr.height]': 'size()',
|
||||
'[attr.stroke]': 'color()',
|
||||
'[attr.stroke-width]': 'strokeWidth()',
|
||||
'[attr.aria-hidden]': '!title()',
|
||||
'[attr.class]': 'computedClass()',
|
||||
'[attr.width]': 'computedWidth()',
|
||||
'[attr.height]': 'computedHeight()',
|
||||
'[attr.viewBox]': 'computedViewbox()',
|
||||
'[attr.stroke]': 'computedStroke()',
|
||||
'[attr.stroke-width]': 'computedStrokeWidth()',
|
||||
'[attr.aria-hidden]': 'computedAriaHidden()',
|
||||
},
|
||||
})
|
||||
export abstract class LucideIconBase {
|
||||
protected abstract readonly icon: Signal<Nullable<LucideIconData>>;
|
||||
protected readonly iconNodes = computed<LucideIconData['node']>(() => {
|
||||
const node = this.icon()?.node ?? [];
|
||||
if (!this.absoluteStrokeWidth()) {
|
||||
return node;
|
||||
}
|
||||
return node.map<LucideIconData['node'][number]>(([name, attrs]) => [
|
||||
name,
|
||||
{
|
||||
'vector-effect': 'non-scaling-stroke',
|
||||
...attrs,
|
||||
},
|
||||
]);
|
||||
});
|
||||
protected readonly iconClasses = computed(() => {
|
||||
const icon = this.icon();
|
||||
if (!icon) {
|
||||
return '';
|
||||
}
|
||||
const { name, aliases = [] } = icon;
|
||||
return [name, ...aliases].map((item) => `lucide-${item}`).join(' ');
|
||||
});
|
||||
protected readonly iconConfig = inject(LUCIDE_CONFIG);
|
||||
/**
|
||||
* An optional accessible label for the icon.
|
||||
@@ -58,6 +38,10 @@ export abstract class LucideIconBase {
|
||||
* - If your icon is functional (e.g. used in place of a label), feel free to use this property.
|
||||
*/
|
||||
readonly title = input<Nullable<string>>();
|
||||
/**
|
||||
* Optional CSS classes for the icon.
|
||||
*/
|
||||
readonly class = input<Nullable<string>>();
|
||||
/**
|
||||
* Width and height.
|
||||
* @default 24
|
||||
@@ -65,6 +49,16 @@ export abstract class LucideIconBase {
|
||||
readonly size = input(this.iconConfig.size, {
|
||||
transform: (value: Nullable<string | number>) => value ?? this.iconConfig.size,
|
||||
});
|
||||
/**
|
||||
* Icon width.
|
||||
* @default 24
|
||||
*/
|
||||
readonly width = input(null);
|
||||
/**
|
||||
* Icon height.
|
||||
* @default 24
|
||||
*/
|
||||
readonly height = input(null);
|
||||
/**
|
||||
* Stroke color.
|
||||
* @default currentColor
|
||||
@@ -80,9 +74,95 @@ export abstract class LucideIconBase {
|
||||
transform: (value: Nullable<string | number>) => value ?? this.iconConfig.strokeWidth,
|
||||
});
|
||||
/**
|
||||
* If set to true, it adds [`vector-effect="non-scaling-stroke"`](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/vector-effect) to child elements.
|
||||
* Whether to use absolute stroke width.
|
||||
*/
|
||||
readonly absoluteStrokeWidth = input(this.iconConfig.absoluteStrokeWidth, {
|
||||
transform: (value: Nullable<boolean>) => value ?? this.iconConfig.absoluteStrokeWidth,
|
||||
});
|
||||
/**
|
||||
* If set to true, it adds [`vector-effect="non-scaling-stroke"`](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/vector-effect) to child elements.
|
||||
*/
|
||||
readonly nonScalingStroke = input(this.iconConfig.nonScalingStroke, {
|
||||
transform: (value: Nullable<boolean>) => value ?? this.iconConfig.nonScalingStroke,
|
||||
});
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected readonly computedIcon = computed(() => {
|
||||
const icon = this.icon();
|
||||
return icon
|
||||
? buildLucideIconNode(icon, {
|
||||
width: this.width() ?? this.size(),
|
||||
height: this.height() ?? this.size(),
|
||||
color: this.color(),
|
||||
strokeWidth: this.strokeWidth(),
|
||||
absoluteStrokeWidth: this.absoluteStrokeWidth(),
|
||||
nonScalingStroke: this.nonScalingStroke(),
|
||||
className: this.class() ?? undefined,
|
||||
hasA11yProp: this.title() != null,
|
||||
})
|
||||
: null;
|
||||
});
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected readonly computedAttributes = computed<LucideIconNode[1] | undefined>(
|
||||
() => this.computedIcon()?.[1],
|
||||
);
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected readonly computedIconNode = computed(() => this.computedIcon()?.[2] ?? []);
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected readonly computedWidth = computed(
|
||||
() => this.computedAttributes()?.['width'] ?? this.iconConfig.size,
|
||||
);
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected readonly computedHeight = computed(
|
||||
() => this.computedAttributes()?.['height'] ?? this.iconConfig.size,
|
||||
);
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected readonly computedStroke = computed(
|
||||
() => this.computedAttributes()?.['stroke'] ?? this.iconConfig.color,
|
||||
);
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected readonly computedStrokeWidth = computed(
|
||||
() => this.computedAttributes()?.['stroke-width'] ?? this.iconConfig.strokeWidth,
|
||||
);
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected readonly computedAriaHidden = computed(
|
||||
() => this.computedAttributes()?.['aria-hidden'] === 'true',
|
||||
);
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected readonly computedViewbox = computed(
|
||||
() => this.computedAttributes()?.['viewBox'] ?? '0 0 24 24',
|
||||
);
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected readonly computedClass = computed(
|
||||
() => this.computedAttributes()?.['class'] ?? 'lucide',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ export const lucideIconTemplate = `@if (title(); as titleValue) {
|
||||
<title>{{ titleValue }}</title>
|
||||
}
|
||||
<ng-content select="title"></ng-content>
|
||||
@for (child of iconNodes(); track child[1]['key'] ?? $index) {
|
||||
@for (child of computedIconNode(); track child[1]['key'] ?? $index) {
|
||||
@let attrs = child[1];
|
||||
@switch (child[0]) {
|
||||
@case ('path') {
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import { Signal, Type } from '@angular/core';
|
||||
import {
|
||||
LucideIconNode as SharedLucideIconNode,
|
||||
LucideIconData as SharedLucideIconData,
|
||||
} from './utils/types';
|
||||
|
||||
type HtmlAttributes = { [key: string]: string | number };
|
||||
export type LucideIconNode = readonly [string, HtmlAttributes];
|
||||
export type LucideIconNode = SharedLucideIconNode;
|
||||
export type LucideIcons = { [key: string]: LucideIconData };
|
||||
|
||||
/**
|
||||
* A Lucide icon object that fully describes an icon to be displayed.
|
||||
*/
|
||||
export type LucideIconData = {
|
||||
name: string;
|
||||
size?: number;
|
||||
node: LucideIconNode[];
|
||||
aliases?: string[];
|
||||
};
|
||||
export type LucideIconData = SharedLucideIconData & { name: string };
|
||||
|
||||
/**
|
||||
* Input signal map of Lucide icon components.
|
||||
@@ -22,7 +20,11 @@ interface LucideIconProps {
|
||||
size: Signal<Nullable<number | string>>;
|
||||
color: Signal<Nullable<string>>;
|
||||
strokeWidth: Signal<Nullable<number | string>>;
|
||||
/**
|
||||
* @deprecated Use `nonScalingStroke` instead.
|
||||
*/
|
||||
absoluteStrokeWidth: Signal<Nullable<boolean>>;
|
||||
nonScalingStroke: Signal<Nullable<boolean>>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"build:icons": "build-icons --output=./src --templateSrc=./scripts/exportTemplate.mts --renderUniqueKey --withAliases --aliasesFileExtension=.ts --iconFileExtension=.ts --exportFileName=index.ts --pretty=false",
|
||||
"clean": "rm -rf dist && rm -rf stats && rm -rf ./src/icons/*.ts",
|
||||
"copy:license": "cp ../../LICENSE ./LICENSE",
|
||||
"copy:utils": "mkdir -p ./src/utils && for f in hasA11yProp toKebabCase mergeClasses; do cp -f ../../packages/shared/src/utils/$f.ts ./src/utils/; done",
|
||||
"copy:utils": "cpy ../../packages/shared/src/build/{buildLucideIconNode.ts,defaultAttributes.ts,types.ts} ../../packages/shared/src/utils/{hasA11yProp.ts,toKebabCase.ts,mergeClasses.ts} ./src/utils/",
|
||||
"test": "pnpm copy:utils && pnpm build:icons && vitest run",
|
||||
"test:watch": "pnpm build:icons && vitest run --watch"
|
||||
},
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
import base64SVG from '@lucide/build-icons/utils/base64SVG';
|
||||
import defineExportTemplate from '@lucide/build-icons/utils/defineExportTemplate';
|
||||
|
||||
export default defineExportTemplate(async ({
|
||||
componentName,
|
||||
iconName,
|
||||
children,
|
||||
getSvg,
|
||||
deprecated,
|
||||
deprecationReason,
|
||||
}) => {
|
||||
const svgContents = await getSvg();
|
||||
const svgBase64 = base64SVG(svgContents);
|
||||
export default defineExportTemplate(
|
||||
async ({ componentName, iconName, iconData, getSvg, deprecated, deprecationReason }) => {
|
||||
const svgContents = await getSvg();
|
||||
const svgBase64 = base64SVG(svgContents);
|
||||
|
||||
// Astro doesn't need keyed children in loops
|
||||
const keylessChildren = children.map((c) => {
|
||||
const [element, { key, ...otherAttrs }] = c;
|
||||
return [element, otherAttrs];
|
||||
});
|
||||
// Astro doesn't need keyed children in loops
|
||||
const keylessIconData = {
|
||||
...iconData,
|
||||
node: iconData.node.map((child) => {
|
||||
const [element, { key, ...otherAttrs }] = child;
|
||||
return [element, otherAttrs];
|
||||
}),
|
||||
};
|
||||
|
||||
// TODO: build-icons' `pretty` is set to false as the prettier
|
||||
// formatter uses babel which I'm not sure it supports typescript
|
||||
return `
|
||||
// TODO: build-icons' `pretty` is set to false as the prettier
|
||||
// formatter uses babel which I'm not sure it supports typescript
|
||||
return `
|
||||
import createLucideIcon from '../createLucideIcon';
|
||||
import type { AstroComponent } from '../types'
|
||||
import type { AstroComponent, LucideIconData } from '../types'
|
||||
|
||||
const iconData: LucideIconData = ${JSON.stringify(keylessIconData)};
|
||||
|
||||
/**
|
||||
* @component @name ${componentName}
|
||||
@@ -35,8 +34,9 @@ import type { AstroComponent } from '../types'
|
||||
* @returns {any} Astro Component
|
||||
* ${deprecated ? `@deprecated ${deprecationReason}` : ''}
|
||||
*/
|
||||
const ${componentName} = createLucideIcon('${iconName}', ${JSON.stringify(keylessChildren)}) as AstroComponent;
|
||||
const ${componentName} = createLucideIcon(iconData) as AstroComponent;
|
||||
|
||||
export default ${componentName};
|
||||
`;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,32 +1,44 @@
|
||||
---
|
||||
import defaultAttributes from './defaultAttributes';
|
||||
import buildLucideIconNode from './utils/buildLucideIconNode';
|
||||
import type { IconProps as Props } from './types';
|
||||
import { hasA11yProp } from './utils/hasA11yProp';
|
||||
|
||||
|
||||
const {
|
||||
color = 'currentColor',
|
||||
size = 24,
|
||||
width,
|
||||
height,
|
||||
'stroke-width': strokeWidth = 2,
|
||||
absoluteStrokeWidth = false,
|
||||
iconNode = [],
|
||||
nonScalingStroke = false,
|
||||
iconNode,
|
||||
icon = {
|
||||
node: iconNode,
|
||||
aliases: [],
|
||||
size: 24,
|
||||
},
|
||||
class: className,
|
||||
...rest
|
||||
} = Astro.props;
|
||||
|
||||
const [, builtIconAttributes, builtIconNode = []] = buildLucideIconNode(icon, {
|
||||
color,
|
||||
width: width ?? size,
|
||||
height: height ?? size,
|
||||
strokeWidth,
|
||||
absoluteStrokeWidth,
|
||||
nonScalingStroke,
|
||||
className,
|
||||
hasA11yProp: hasA11yProp(rest),
|
||||
});
|
||||
|
||||
const iconAttributes = {
|
||||
...builtIconAttributes,
|
||||
...rest,
|
||||
};
|
||||
---
|
||||
|
||||
<svg
|
||||
{...{
|
||||
...defaultAttributes,
|
||||
width: size,
|
||||
height: size,
|
||||
stroke: color,
|
||||
'stroke-width': absoluteStrokeWidth ? (Number(strokeWidth) * 24) / Number(size) : strokeWidth,
|
||||
...(!hasA11yProp(rest) && { 'aria-hidden': 'true' }),
|
||||
...rest,
|
||||
}}
|
||||
class:list={['lucide', className]}
|
||||
>
|
||||
{iconNode.map(([Tag, attrs]) => <Tag {...attrs} />)}
|
||||
<svg {...iconAttributes}>
|
||||
{builtIconNode.map(([Tag, attrs]) => <Tag {...attrs} />)}
|
||||
<slot />
|
||||
</svg>
|
||||
|
||||
@@ -1,11 +1,43 @@
|
||||
import type { AstroComponentFactory } from 'astro/runtime/server/render/astro/factory.js';
|
||||
import type { IconNode } from './types';
|
||||
import type { LucideIconData, LucideIconNode } from './types';
|
||||
import { render, renderSlot, createComponent, renderComponent } from 'astro/compiler-runtime';
|
||||
import Icon from './Icon.astro';
|
||||
import { mergeClasses } from './utils/mergeClasses';
|
||||
import { toKebabCase } from './utils/toKebabCase';
|
||||
|
||||
export default (iconName: string, iconNode: IconNode): AstroComponentFactory => {
|
||||
/**
|
||||
* Creates a Lucide icon component from icon data.
|
||||
*
|
||||
* @param icon Icon data object.
|
||||
* @returns Lucide icon component.
|
||||
*/
|
||||
function createLucideIcon(icon: LucideIconData): AstroComponentFactory;
|
||||
|
||||
/**
|
||||
* Creates a Lucide icon component from the legacy icon arguments.
|
||||
*
|
||||
* @param iconName Icon name.
|
||||
* @param iconNode Icon node.
|
||||
* @param aliases Optional icon aliases.
|
||||
* @returns Lucide icon component.
|
||||
*/
|
||||
function createLucideIcon(
|
||||
iconName: string,
|
||||
iconNode: LucideIconNode[],
|
||||
aliases?: string[],
|
||||
): AstroComponentFactory;
|
||||
|
||||
function createLucideIcon(
|
||||
iconOrIconName: string | LucideIconData,
|
||||
iconNode: LucideIconNode[] = [],
|
||||
aliases: string[] = [],
|
||||
): AstroComponentFactory {
|
||||
const icon =
|
||||
typeof iconOrIconName === 'string'
|
||||
? {
|
||||
name: iconOrIconName,
|
||||
node: iconNode,
|
||||
aliases,
|
||||
}
|
||||
: iconOrIconName;
|
||||
const Component = createComponent(
|
||||
($$result, $$props: Record<string, unknown>, $$slots) => {
|
||||
const { class: className, ...restProps } = $$props;
|
||||
@@ -14,11 +46,8 @@ export default (iconName: string, iconNode: IconNode): AstroComponentFactory =>
|
||||
'Icon',
|
||||
Icon,
|
||||
{
|
||||
class: mergeClasses(
|
||||
Boolean(iconName) && `lucide-${toKebabCase(iconName)}`,
|
||||
Boolean(className) && className,
|
||||
),
|
||||
iconNode,
|
||||
class: className,
|
||||
icon,
|
||||
...restProps,
|
||||
},
|
||||
{ default: () => render`${renderSlot($$result, $$slots['default'])}` },
|
||||
@@ -28,4 +57,6 @@ export default (iconName: string, iconNode: IconNode): AstroComponentFactory =>
|
||||
'none',
|
||||
);
|
||||
return Component;
|
||||
};
|
||||
}
|
||||
|
||||
export default createLucideIcon;
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import type { SVGAttributes } from './types';
|
||||
|
||||
const defaultAttributes: SVGAttributes = {
|
||||
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',
|
||||
};
|
||||
|
||||
export default defaultAttributes;
|
||||
@@ -3,6 +3,6 @@ export * as icons from './icons/index';
|
||||
export * from './aliases';
|
||||
export * from './types';
|
||||
|
||||
export { default as defaultAttributes } from './defaultAttributes';
|
||||
export { default as defaultAttributes } from './utils/defaultAttributes';
|
||||
export { default as createLucideIcon } from './createLucideIcon';
|
||||
export { default as Icon } from './Icon.astro';
|
||||
|
||||
@@ -1,23 +1,47 @@
|
||||
import type { HTMLAttributes } from 'astro/types';
|
||||
import type {
|
||||
LucideIconData as SharedLucideIconData,
|
||||
LucideIconNode as SharedLucideIconNode,
|
||||
} from './utils/types';
|
||||
|
||||
// Type that the Astro language server needs to infer component props in Astro files
|
||||
export type AstroComponent = (_props: IconProps) => unknown;
|
||||
export type AstroComponent = (_props: LucideProps) => any;
|
||||
|
||||
export interface IconProps extends SVGAttributes {
|
||||
export type LucideProps = SVGAttributes & {
|
||||
color?: string;
|
||||
size?: number | string;
|
||||
'stroke-width'?: number | string;
|
||||
/**
|
||||
* @deprecated Use `nonScalingStroke` instead.
|
||||
*/
|
||||
absoluteStrokeWidth?: boolean;
|
||||
nonScalingStroke?: boolean;
|
||||
class?: string;
|
||||
iconNode?: IconNode;
|
||||
title?: string;
|
||||
}
|
||||
};
|
||||
|
||||
export type IconProps = LucideProps &
|
||||
(
|
||||
| {
|
||||
icon: LucideIconData;
|
||||
iconNode?: never;
|
||||
}
|
||||
| {
|
||||
icon?: never;
|
||||
iconNode: LucideIconNode[];
|
||||
}
|
||||
);
|
||||
|
||||
export type SVGAttributes = HTMLAttributes<'svg'>;
|
||||
|
||||
export type IconNode = IconNodeChild[];
|
||||
export type LucideIconNode = SharedLucideIconNode<SVGElements, SVGAttributes>;
|
||||
|
||||
type IconNodeChild = [elementName: SVGElements, attrs: SVGAttributes];
|
||||
export type LucideIconData = SharedLucideIconData<SVGElements, SVGAttributes>;
|
||||
|
||||
/**
|
||||
* @deprecated Use LucideIconNode instead.
|
||||
*/
|
||||
export type IconNode = LucideIconNode[];
|
||||
|
||||
// All possible svg elements according to the Astro definitions
|
||||
type SVGElements =
|
||||
|
||||
@@ -10,8 +10,8 @@ exports[`Using Icon Component > should render icon and match snapshot 1`] = `
|
||||
stroke-width="1"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
class="lucide"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<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">
|
||||
</path>
|
||||
|
||||
@@ -10,8 +10,8 @@ exports[`Using createLucideIcon > should create a component from an iconNode 1`]
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
class="lucide lucide-air-vent"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<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">
|
||||
</path>
|
||||
|
||||
@@ -10,9 +10,9 @@ exports[`Using lucide icon components > should add a non-default attribute to th
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-smile"
|
||||
aria-hidden="true"
|
||||
style="position: absolute"
|
||||
class="lucide lucide-face-slightly-smiling"
|
||||
>
|
||||
<path d="M15 10V9">
|
||||
</path>
|
||||
@@ -38,8 +38,8 @@ exports[`Using lucide icon components > should adjust the size, stroke color and
|
||||
stroke-width="4"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3"
|
||||
aria-hidden="true"
|
||||
class="lucide lucide-grid-3x3"
|
||||
>
|
||||
<rect width="18"
|
||||
height="18"
|
||||
@@ -69,41 +69,50 @@ exports[`Using lucide icon components > should apply all classes to the element
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-droplet lucide-drop my-icon"
|
||||
aria-hidden="true"
|
||||
class="lucide lucide-droplet my-icon"
|
||||
>
|
||||
<path d="M12 22a7 7 0 0 0 7-7c0-2-1-3.9-3-5.5s-3.5-4-4-6.5c-.5 2.5-2 4.9-4 6.5C6 11.1 5 13 5 15a7 7 0 0 0 7 7z">
|
||||
</path>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
exports[`Using lucide icon components > should not scale the strokeWidth when absoluteStrokeWidth is set 1`] = `
|
||||
exports[`Using lucide icon components > should apply vector-effect when nonScalingStroke is set 1`] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
width="48"
|
||||
height="48"
|
||||
width="12"
|
||||
height="12"
|
||||
viewbox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="red"
|
||||
stroke-width="1"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3"
|
||||
aria-hidden="true"
|
||||
class="lucide lucide-grid-3x3"
|
||||
>
|
||||
<rect width="18"
|
||||
<rect vector-effect="non-scaling-stroke"
|
||||
width="18"
|
||||
height="18"
|
||||
x="3"
|
||||
y="3"
|
||||
rx="2"
|
||||
>
|
||||
</rect>
|
||||
<path d="M3 9h18">
|
||||
<path vector-effect="non-scaling-stroke"
|
||||
d="M3 9h18"
|
||||
>
|
||||
</path>
|
||||
<path d="M3 15h18">
|
||||
<path vector-effect="non-scaling-stroke"
|
||||
d="M3 15h18"
|
||||
>
|
||||
</path>
|
||||
<path d="M9 3v18">
|
||||
<path vector-effect="non-scaling-stroke"
|
||||
d="M9 3v18"
|
||||
>
|
||||
</path>
|
||||
<path d="M15 3v18">
|
||||
<path vector-effect="non-scaling-stroke"
|
||||
d="M15 3v18"
|
||||
>
|
||||
</path>
|
||||
</svg>
|
||||
`;
|
||||
@@ -118,8 +127,8 @@ exports[`Using lucide icon components > should pass children to the icon slot 1`
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-smile"
|
||||
aria-hidden="true"
|
||||
class="lucide lucide-face-slightly-smiling"
|
||||
>
|
||||
<path d="M15 10V9">
|
||||
</path>
|
||||
@@ -148,8 +157,8 @@ exports[`Using lucide icon components > should render a component 1`] = `
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3"
|
||||
aria-hidden="true"
|
||||
class="lucide lucide-grid-3x3"
|
||||
>
|
||||
<rect width="18"
|
||||
height="18"
|
||||
@@ -179,8 +188,39 @@ exports[`Using lucide icon components > should render the icon with default attr
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<rect width="18"
|
||||
height="18"
|
||||
x="3"
|
||||
y="3"
|
||||
rx="2"
|
||||
>
|
||||
</rect>
|
||||
<path d="M3 9h18">
|
||||
</path>
|
||||
<path d="M3 15h18">
|
||||
</path>
|
||||
<path d="M9 3v18">
|
||||
</path>
|
||||
<path d="M15 3v18">
|
||||
</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"
|
||||
aria-hidden="true"
|
||||
class="lucide lucide-grid-3x3"
|
||||
>
|
||||
<rect width="18"
|
||||
height="18"
|
||||
|
||||
@@ -5,7 +5,10 @@ import { render } from './utils';
|
||||
|
||||
describe('Using createLucideIcon', () => {
|
||||
it('should create a component from an iconNode', async () => {
|
||||
const AirVent = createLucideIcon('AirVent', airVent);
|
||||
const AirVent = createLucideIcon({
|
||||
name: 'air-vent',
|
||||
node: airVent,
|
||||
});
|
||||
const { container } = await render(AirVent);
|
||||
|
||||
expect(container.innerHTML).toBeDefined();
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Pen, Edit2, Grid, Droplet, Smile, Rocket } from '../src/lucide-astro';
|
||||
import defaultAttributes from '../src/defaultAttributes';
|
||||
import { Pen, Edit2, Grid, Droplet, Smile, Rocket, type LucideIconData } from '../src/lucide-astro';
|
||||
import defaultAttributes from '../src/utils/defaultAttributes';
|
||||
import { createAstroHTMLString, render } from './utils';
|
||||
import createLucideIcon from '../src/createLucideIcon.ts';
|
||||
|
||||
describe('Using lucide icon components', () => {
|
||||
it('should render a component', async () => {
|
||||
@@ -64,11 +65,11 @@ describe('Using lucide icon components', () => {
|
||||
expect(PenIconContainer.innerHTML).toBe(Edit2Container.innerHTML);
|
||||
});
|
||||
|
||||
it('should not scale the strokeWidth when absoluteStrokeWidth is set', async () => {
|
||||
it('should scale the strokeWidth when absoluteStrokeWidth is set', async () => {
|
||||
const { container } = await render(Grid, {
|
||||
props: {
|
||||
size: 48,
|
||||
stroke: 'red',
|
||||
color: 'red',
|
||||
absoluteStrokeWidth: true,
|
||||
},
|
||||
});
|
||||
@@ -83,6 +84,24 @@ describe('Using lucide icon components', () => {
|
||||
expect(container.innerHTML).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should apply vector-effect when nonScalingStroke is set', async () => {
|
||||
const { container } = await render(Grid, {
|
||||
props: {
|
||||
size: 12,
|
||||
nonScalingStroke: true,
|
||||
},
|
||||
});
|
||||
|
||||
const SVGElement = container.firstElementChild;
|
||||
|
||||
expect(SVGElement).toHaveAttribute('width', '12');
|
||||
expect(SVGElement).toHaveAttribute('height', '12');
|
||||
expect(SVGElement).toHaveAttribute('stroke-width', '2');
|
||||
expect(SVGElement?.firstElementChild).toHaveAttribute('vector-effect', 'non-scaling-stroke');
|
||||
|
||||
expect(container.innerHTML).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should add a non-default attribute to the element', async () => {
|
||||
const { container } = await render(Smile, {
|
||||
props: {
|
||||
@@ -112,7 +131,20 @@ describe('Using lucide icon components', () => {
|
||||
});
|
||||
|
||||
it('should apply all classes to the element', async () => {
|
||||
const { container } = await render(Droplet, {
|
||||
const iconData: LucideIconData = {
|
||||
name: 'droplet',
|
||||
size: 24,
|
||||
node: [
|
||||
[
|
||||
'path',
|
||||
{
|
||||
d: 'M12 22a7 7 0 0 0 7-7c0-2-1-3.9-3-5.5s-3.5-4-4-6.5c-.5 2.5-2 4.9-4 6.5C6 11.1 5 13 5 15a7 7 0 0 0 7 7z',
|
||||
},
|
||||
],
|
||||
],
|
||||
aliases: ['drop'],
|
||||
};
|
||||
const { container } = await render(createLucideIcon(iconData), {
|
||||
props: {
|
||||
class: 'my-icon',
|
||||
},
|
||||
@@ -122,6 +154,7 @@ describe('Using lucide icon components', () => {
|
||||
expect(SVGElement).toHaveClass('my-icon');
|
||||
expect(SVGElement).toHaveClass('lucide');
|
||||
expect(SVGElement).toHaveClass('lucide-droplet');
|
||||
expect(SVGElement).toHaveClass('lucide-drop');
|
||||
|
||||
expect(container.innerHTML).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -63,8 +63,9 @@
|
||||
"@lucide/build-icons": "workspace:*",
|
||||
"@lucide/helpers": "^1.0.0",
|
||||
"@lucide/rollup-plugins": "workspace:*",
|
||||
"@rollup/plugin-replace": "^6.0.3",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@lucide/shared": "workspace:*",
|
||||
"@rollup/plugin-replace": "^6.0.2",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"jest-serializer-html": "^7.1.0",
|
||||
"rollup": "^4.62.2",
|
||||
"rollup-plugin-dts": "^6.4.1",
|
||||
|
||||
@@ -2,6 +2,11 @@ import plugins from '@lucide/rollup-plugins';
|
||||
import pkg from './package.json' with { type: 'json' };
|
||||
import dts from 'rollup-plugin-dts';
|
||||
|
||||
const dtsOptions = {
|
||||
includeExternal: ['@lucide/shared/types'],
|
||||
tsconfig: './tsconfig.json',
|
||||
};
|
||||
|
||||
const packageName = '@lucide/icons';
|
||||
const outputFileName = 'lucide-icons';
|
||||
const inputs = [`src/lucide-icons.ts`];
|
||||
@@ -81,7 +86,7 @@ export default [
|
||||
format: 'cjs',
|
||||
},
|
||||
],
|
||||
plugins: [dts()],
|
||||
plugins: [dts(dtsOptions)],
|
||||
})),
|
||||
...configs,
|
||||
];
|
||||
|
||||
@@ -1,30 +1 @@
|
||||
import { LucideBuildParams, LucideIconData } from './types';
|
||||
import buildLucideSvg from './buildLucideSvg';
|
||||
|
||||
/**
|
||||
* Creates a base64 encoded data URI from a Lucide icon object.
|
||||
*
|
||||
* @param icon The icon to build.
|
||||
* @param params Additional build parameters.
|
||||
*/
|
||||
function buildLucideDataUri(icon: LucideIconData, params: LucideBuildParams = {}) {
|
||||
const svg = buildLucideSvg(icon, params);
|
||||
|
||||
// Browser
|
||||
if (typeof btoa === 'function') {
|
||||
// Ensure proper UTF-8 handling before base64
|
||||
const utf8Bytes = new TextEncoder().encode(svg);
|
||||
let binary = '';
|
||||
for (const element of utf8Bytes) binary += String.fromCodePoint(element);
|
||||
return `data:image/svg+xml;base64,${btoa(binary)}`;
|
||||
}
|
||||
|
||||
// Node.js (and other JS runtimes with Buffer)
|
||||
if (typeof Buffer !== 'undefined') {
|
||||
return `data:image/svg+xml;base64,${Buffer.from(svg, 'utf8').toString('base64')}`;
|
||||
}
|
||||
|
||||
throw new Error('No base64 encoder available in this environment.');
|
||||
}
|
||||
|
||||
export default buildLucideDataUri;
|
||||
export { buildLucideDataUri as default } from '@lucide/shared';
|
||||
|
||||
@@ -1,34 +1 @@
|
||||
import { LucideBuildParams, LucideIconData, LucideIconNode } from './types';
|
||||
import buildLucideIconNode from './buildLucideIconNode';
|
||||
|
||||
const buildDomElement = (
|
||||
document: Document,
|
||||
[tagName, attributes, children = []]: LucideIconNode,
|
||||
): Element => {
|
||||
const element = document.createElementNS('http://www.w3.org/2000/svg', tagName);
|
||||
for (const [attrName, value] of Object.entries(attributes)) {
|
||||
element.setAttribute(attrName, value);
|
||||
}
|
||||
for (const node of children) {
|
||||
const childNode = buildDomElement(document, node);
|
||||
element.appendChild(childNode);
|
||||
}
|
||||
return element;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates an SvgElement from a Lucide icon object.
|
||||
*
|
||||
* @param document The document to create the Element in.
|
||||
* @param icon The icon to build.
|
||||
* @param params Additional build parameters.
|
||||
*/
|
||||
function buildLucideIconElement(
|
||||
document: Document,
|
||||
icon: LucideIconData,
|
||||
params: LucideBuildParams = {},
|
||||
) {
|
||||
return buildDomElement(document, buildLucideIconNode(icon, params));
|
||||
}
|
||||
|
||||
export default buildLucideIconElement;
|
||||
export { buildLucideIconElement as default } from '@lucide/shared';
|
||||
|
||||
@@ -1,55 +1 @@
|
||||
import { LucideBuildParams, LucideIconData, LucideIconNode } from './types';
|
||||
|
||||
const defaultAttributes = {
|
||||
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',
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a Lucide icon node (an svgson-like format) from a Lucide icon object.
|
||||
*
|
||||
* @param icon The icon to build.
|
||||
* @param params Additional build parameters.
|
||||
*/
|
||||
function buildLucideIconNode(icon: LucideIconData, params: LucideBuildParams = {}): LucideIconNode {
|
||||
const viewBoxWidth = ('size' in icon ? icon.size : icon.width) ?? defaultAttributes.width;
|
||||
const viewBoxHeight = ('size' in icon ? icon.size : icon.height) ?? defaultAttributes.height;
|
||||
const aliasClassNames = icon.aliases?.map((alias) => `lucide-${alias}`) ?? [];
|
||||
const classList = ['lucide', `lucide-${icon.name}`, ...aliasClassNames];
|
||||
if (params['className']) {
|
||||
classList.push(params['className']);
|
||||
}
|
||||
const attributes = {
|
||||
...defaultAttributes,
|
||||
...('color' in params && { stroke: params['color'] }),
|
||||
...('size' in params &&
|
||||
params['size'] && {
|
||||
width: params['size'].toString(10),
|
||||
height: params['size'].toString(10),
|
||||
}),
|
||||
...('width' in params && params['width'] && { width: params['width'].toString(10) }),
|
||||
...('height' in params && params['height'] && { height: params['height'].toString(10) }),
|
||||
...('strokeWidth' in params &&
|
||||
params['strokeWidth'] && { 'stroke-width': params['strokeWidth'].toString(10) }),
|
||||
class: classList.join(' '),
|
||||
viewBox: `0 0 ${viewBoxWidth} ${viewBoxHeight}`,
|
||||
...('attributes' in params && params.attributes),
|
||||
};
|
||||
return [
|
||||
'svg',
|
||||
attributes,
|
||||
icon.node.map(([name, attrs]) => [
|
||||
name,
|
||||
params['absoluteStrokeWidth'] ? { 'vector-effect': 'non-scaling-stroke', ...attrs } : attrs,
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
export default buildLucideIconNode;
|
||||
export { buildLucideIconNode as default } from '@lucide/shared';
|
||||
|
||||
@@ -1,19 +1 @@
|
||||
import { LucideBuildParams, LucideIconData, LucideIconNode } from './types';
|
||||
import buildLucideIconNode from './buildLucideIconNode';
|
||||
|
||||
const buildDomNode = ([tagName, attributes, children = []]: LucideIconNode): string =>
|
||||
`<${tagName} ${Object.entries(attributes)
|
||||
.map(([attrName, value]) => `${attrName}="${value}"`)
|
||||
.join(' ')}>${children?.map((child) => buildDomNode(child)).join('')}</${tagName}>`;
|
||||
|
||||
/**
|
||||
* Creates an SVG string from a Lucide icon object.
|
||||
*
|
||||
* @param icon The icon to build.
|
||||
* @param params Additional build parameters.
|
||||
*/
|
||||
function buildLucideSvg(icon: LucideIconData, params: LucideBuildParams = {}) {
|
||||
return buildDomNode(buildLucideIconNode(icon, params));
|
||||
}
|
||||
|
||||
export default buildLucideSvg;
|
||||
export { buildLucideSvg as default } from '@lucide/shared';
|
||||
|
||||
+10
-43
@@ -1,43 +1,10 @@
|
||||
export type SVGProps = Record<string, string>;
|
||||
|
||||
/**
|
||||
* A Lucide icon node (an svgson-like internal format)
|
||||
*/
|
||||
export type LucideIconNode =
|
||||
| [attrName: string, attributes: SVGProps]
|
||||
| [attrName: string, attributes: SVGProps, children: LucideIconNode[]];
|
||||
|
||||
/**
|
||||
* A Lucide icon object that fully describes an icon to be displayed.
|
||||
*/
|
||||
export type LucideIconData = {
|
||||
name: string;
|
||||
node: LucideIconNode[];
|
||||
aliases?: string[];
|
||||
} & ({ size: number } | { width: number; height: number });
|
||||
|
||||
/**
|
||||
* Build parameters for creating a Lucide icon instance for display.
|
||||
*/
|
||||
export type LucideBuildParams = {
|
||||
/**
|
||||
* The color of the icon.
|
||||
*/
|
||||
color?: string;
|
||||
/**
|
||||
* The stroke width.
|
||||
*/
|
||||
strokeWidth?: number;
|
||||
/**
|
||||
* Adds [`vector-effect="non-scaling-stroke"`](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/vector-effect) to child elements.
|
||||
*/
|
||||
absoluteStrokeWidth?: boolean;
|
||||
/**
|
||||
* Extra CSS class names to pass to the SVG element.
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* Any extra attributes to pass to the SVG element.
|
||||
*/
|
||||
attributes?: SVGProps;
|
||||
} & ({ size?: number } | { width?: number; height?: number });
|
||||
import type {
|
||||
LucideBuildParams as SharedLucideBuildParams,
|
||||
LucideIconData as SharedLucideIconData,
|
||||
LucideIconNode as SharedLucideIconNode,
|
||||
SVGProps as SharedSvgProps,
|
||||
} from '@lucide/shared/types';
|
||||
export type LucideBuildParams = SharedLucideBuildParams;
|
||||
export type LucideIconData = SharedLucideIconData;
|
||||
export type LucideIconNode = SharedLucideIconNode;
|
||||
export type SVGProps = SharedSvgProps;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`buildLucideIconNode > should match the snapshot 1`] = `"{"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-house lucide-home"}"`;
|
||||
exports[`buildLucideIconNode > should match the snapshot 1`] = `"{"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-house lucide-home"}"`;
|
||||
|
||||
@@ -18,24 +18,24 @@ describe('buildLucideIconNode', () => {
|
||||
it('should override dimensions, but not viewBox', () => {
|
||||
const HouseSVG = buildLucideIconNode(House, { size: 12 });
|
||||
|
||||
expect(HouseSVG[1]['width']).toBe('12');
|
||||
expect(HouseSVG[1]['height']).toBe('12');
|
||||
expect(HouseSVG[1]['width']).toBe(12);
|
||||
expect(HouseSVG[1]['height']).toBe(12);
|
||||
expect(HouseSVG[1]['viewBox']).toBe('0 0 24 24');
|
||||
});
|
||||
|
||||
it('should override width, but not height', () => {
|
||||
const HouseSVG = buildLucideIconNode(House, { width: 12 });
|
||||
|
||||
expect(HouseSVG[1]['width']).toBe('12');
|
||||
expect(HouseSVG[1]['height']).toBe('24');
|
||||
expect(HouseSVG[1]['width']).toBe(12);
|
||||
expect(HouseSVG[1]['height']).toBe(24);
|
||||
expect(HouseSVG[1]['viewBox']).toBe('0 0 24 24');
|
||||
});
|
||||
|
||||
it('should override height, but not width', () => {
|
||||
const HouseSVG = buildLucideIconNode(House, { height: 12 });
|
||||
|
||||
expect(HouseSVG[1]['width']).toBe('24');
|
||||
expect(HouseSVG[1]['height']).toBe('12');
|
||||
expect(HouseSVG[1]['width']).toBe(24);
|
||||
expect(HouseSVG[1]['height']).toBe(12);
|
||||
expect(HouseSVG[1]['viewBox']).toBe('0 0 24 24');
|
||||
});
|
||||
|
||||
@@ -49,11 +49,31 @@ describe('buildLucideIconNode', () => {
|
||||
it('should override stroke width', () => {
|
||||
const HouseSVG = buildLucideIconNode(House, { strokeWidth: 12 });
|
||||
|
||||
expect(HouseSVG[1]['stroke-width']).toBe('12');
|
||||
expect(HouseSVG[1]['stroke-width']).toBe(12);
|
||||
});
|
||||
|
||||
it('should scale the stroke width', () => {
|
||||
const HouseSVG = buildLucideIconNode(House, {
|
||||
size: 12,
|
||||
strokeWidth: 2,
|
||||
absoluteStrokeWidth: true,
|
||||
});
|
||||
|
||||
expect(HouseSVG[1]['stroke-width']).toBe(4);
|
||||
});
|
||||
|
||||
it('should not scale the stroke width', () => {
|
||||
const HouseSVG = buildLucideIconNode(House, {
|
||||
size: 12,
|
||||
strokeWidth: 2,
|
||||
absoluteStrokeWidth: false,
|
||||
});
|
||||
|
||||
expect(HouseSVG[1]['stroke-width']).toBe(2);
|
||||
});
|
||||
|
||||
it('should set non-scaling-stroke to child nodes', () => {
|
||||
const HouseSVG = buildLucideIconNode(House, { absoluteStrokeWidth: true });
|
||||
const HouseSVG = buildLucideIconNode(House, { nonScalingStroke: true });
|
||||
|
||||
for (const node of HouseSVG[2]!) {
|
||||
expect(node[1]['vector-effect']).toBe('non-scaling-stroke');
|
||||
@@ -61,7 +81,7 @@ describe('buildLucideIconNode', () => {
|
||||
});
|
||||
|
||||
it('should not set non-scaling-stroke', () => {
|
||||
const HouseSVG = buildLucideIconNode(House, { absoluteStrokeWidth: false });
|
||||
const HouseSVG = buildLucideIconNode(House, { nonScalingStroke: false });
|
||||
|
||||
expect(HouseSVG[1]['vector-effect']).toBeUndefined();
|
||||
});
|
||||
@@ -89,4 +109,30 @@ describe('buildLucideIconNode', () => {
|
||||
|
||||
expect(HouseSVG[1]['class']).toBe('lucide lucide-house lucide-home icon');
|
||||
});
|
||||
|
||||
it('should support className with multiple classes', () => {
|
||||
const HouseSVG = buildLucideIconNode(House, {
|
||||
className: ' icon custom lucide-house ',
|
||||
});
|
||||
|
||||
expect(HouseSVG[1]['class']).toBe('lucide lucide-house lucide-home icon custom');
|
||||
});
|
||||
|
||||
it('should not include default classes when includeDefaultClasses is false', () => {
|
||||
const HouseSVG = buildLucideIconNode(House, { includeDefaultClasses: false });
|
||||
|
||||
expect(HouseSVG[1]['class']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should add aria-hidden when hasA11yProp is false', () => {
|
||||
const HouseSVG = buildLucideIconNode(House, { hasA11yProp: false });
|
||||
|
||||
expect(HouseSVG[1]['aria-hidden']).toBe('true');
|
||||
});
|
||||
|
||||
it('should not add aria-hidden when hasA11yProp is true', () => {
|
||||
const HouseSVG = buildLucideIconNode(House, { hasA11yProp: true });
|
||||
|
||||
expect(HouseSVG[1]['aria-hidden']).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
"moduleResolution": "node",
|
||||
"module": "ESNext",
|
||||
"target": "ESNext",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@lucide/shared": ["../shared/src/index.ts"],
|
||||
"@lucide/shared/types": ["../shared/src/build/types.ts"],
|
||||
},
|
||||
"esModuleInterop": true,
|
||||
"isolatedModules": true,
|
||||
"lib": ["esnext", "dom"],
|
||||
|
||||
@@ -2,6 +2,11 @@ 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 = 'LucidePreact';
|
||||
const outputFileName = 'lucide-preact';
|
||||
const outputDir = 'dist';
|
||||
@@ -58,7 +63,7 @@ export default [
|
||||
format: 'es',
|
||||
},
|
||||
],
|
||||
plugins: [dts()],
|
||||
plugins: [dts(dtsOptions)],
|
||||
},
|
||||
{
|
||||
input: `src/${outputFileName}.suffixed.ts`,
|
||||
@@ -68,7 +73,7 @@ export default [
|
||||
format: 'es',
|
||||
},
|
||||
],
|
||||
plugins: [dts()],
|
||||
plugins: [dts(dtsOptions)],
|
||||
},
|
||||
{
|
||||
input: `src/${outputFileName}.prefixed.ts`,
|
||||
@@ -78,7 +83,7 @@ export default [
|
||||
format: 'es',
|
||||
},
|
||||
],
|
||||
plugins: [dts()],
|
||||
plugins: [dts(dtsOptions)],
|
||||
},
|
||||
...configs,
|
||||
];
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import base64SVG from '@lucide/build-icons/utils/base64SVG';
|
||||
import defineExportTemplate from '@lucide/build-icons/utils/defineExportTemplate';
|
||||
|
||||
export default defineExportTemplate(async ({
|
||||
componentName,
|
||||
iconName,
|
||||
children,
|
||||
getSvg,
|
||||
deprecated,
|
||||
deprecationReason,
|
||||
}) => {
|
||||
const svgContents = await getSvg();
|
||||
const svgBase64 = base64SVG(svgContents);
|
||||
export default defineExportTemplate(
|
||||
async ({ componentName, iconName, iconData, getSvg, deprecated, deprecationReason }) => {
|
||||
const svgContents = await getSvg();
|
||||
const svgBase64 = base64SVG(svgContents);
|
||||
|
||||
return `
|
||||
return `
|
||||
import createLucideIcon from '../createLucideIcon';
|
||||
import type { LucideIconData } from '../types';
|
||||
|
||||
const iconData: LucideIconData = ${JSON.stringify(iconData)};
|
||||
|
||||
/**
|
||||
* @component @name ${componentName}
|
||||
@@ -26,8 +23,9 @@ import createLucideIcon from '../createLucideIcon';
|
||||
* @returns {JSX.Element} JSX Element
|
||||
* ${deprecated ? `@deprecated ${deprecationReason}` : ''}
|
||||
*/
|
||||
const ${componentName} = createLucideIcon('${iconName}', ${JSON.stringify(children)});
|
||||
const ${componentName} = createLucideIcon(iconData);
|
||||
|
||||
export default ${componentName};
|
||||
`;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { h, toChildArray } from 'preact';
|
||||
import defaultAttributes from './defaultAttributes';
|
||||
import type { IconNode, LucideProps } from './types';
|
||||
import { buildLucideIconNode, hasA11yProp, mergeClasses } from '@lucide/shared';
|
||||
import type { LucideIconData, LucideIconNode, LucideProps } from './types';
|
||||
import { useLucideContext } from './context';
|
||||
import { mergeClasses } from '@lucide/shared';
|
||||
import { hasA11yProp } from '@lucide/shared';
|
||||
|
||||
interface IconComponentProps extends LucideProps {
|
||||
iconNode: IconNode;
|
||||
}
|
||||
type IconComponentProps = LucideProps &
|
||||
(
|
||||
| {
|
||||
icon: LucideIconData;
|
||||
iconNode?: never;
|
||||
}
|
||||
| {
|
||||
iconNode: LucideIconNode[];
|
||||
icon?: never;
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Lucide icon component
|
||||
@@ -18,19 +24,28 @@ interface IconComponentProps extends LucideProps {
|
||||
* @param {number} props.size - The size of the icon
|
||||
* @param {number} props.strokeWidth - The stroke width of the icon
|
||||
* @param {boolean} props.absoluteStrokeWidth - Whether to use absolute stroke width
|
||||
* @param {boolean} props.nonScalingStroke - Whether to use non scaling strokes
|
||||
* @param {string} props.class - The class name of the icon
|
||||
* @param {IconNode} props.children - The children of the icon
|
||||
* @param {IconNode} props.iconNode - The icon node of the icon
|
||||
* @param {LucideIconNode[]} props.children - The children of the icon
|
||||
* @param {LucideIconNode[]} props.iconNode - The icon node of the icon
|
||||
*
|
||||
* @returns {ForwardRefExoticComponent} LucideIcon
|
||||
*/
|
||||
const Icon = ({
|
||||
color,
|
||||
size,
|
||||
width,
|
||||
height,
|
||||
strokeWidth,
|
||||
absoluteStrokeWidth,
|
||||
nonScalingStroke,
|
||||
children,
|
||||
iconNode,
|
||||
iconNode = [],
|
||||
icon = {
|
||||
node: iconNode,
|
||||
aliases: [],
|
||||
size: 24,
|
||||
},
|
||||
class: classes = '',
|
||||
...rest
|
||||
}: IconComponentProps) => {
|
||||
@@ -38,29 +53,27 @@ const Icon = ({
|
||||
size: contextSize = 24,
|
||||
strokeWidth: contextStrokeWidth = 2,
|
||||
absoluteStrokeWidth: contextAbsoluteStrokeWidth = false,
|
||||
nonScalingStroke: contextNonScalingStroke = false,
|
||||
color: contextColor = 'currentColor',
|
||||
class: contextClass = '',
|
||||
} = useLucideContext() ?? {};
|
||||
|
||||
const calculatedStrokeWidth =
|
||||
absoluteStrokeWidth ?? contextAbsoluteStrokeWidth
|
||||
? (Number(strokeWidth ?? contextStrokeWidth) * 24) / Number(size ?? contextSize)
|
||||
: strokeWidth ?? contextStrokeWidth;
|
||||
const [name, svgAttributes, builtIconNode = []] = buildLucideIconNode(icon, {
|
||||
color: color ?? contextColor,
|
||||
width: width ?? size ?? contextSize,
|
||||
height: height ?? size ?? contextSize,
|
||||
strokeWidth: strokeWidth ?? contextStrokeWidth,
|
||||
absoluteStrokeWidth: absoluteStrokeWidth ?? contextAbsoluteStrokeWidth,
|
||||
nonScalingStroke: nonScalingStroke ?? contextNonScalingStroke,
|
||||
className: mergeClasses(contextClass, classes as string),
|
||||
hasA11yProp: Boolean(children) || hasA11yProp(rest),
|
||||
attributes: rest,
|
||||
});
|
||||
|
||||
return h(
|
||||
'svg',
|
||||
{
|
||||
...defaultAttributes,
|
||||
width: size ?? contextSize ?? 24,
|
||||
height: size ?? contextSize ?? 24,
|
||||
stroke: color ?? contextColor,
|
||||
['stroke-width' as 'strokeWidth']: calculatedStrokeWidth,
|
||||
class: mergeClasses('lucide', contextClass, classes),
|
||||
...(!children && !hasA11yProp(rest) && { 'aria-hidden': 'true' }),
|
||||
...rest,
|
||||
},
|
||||
[...iconNode.map(([tag, attrs]) => h(tag, attrs)), ...toChildArray(children)],
|
||||
);
|
||||
return h(name, { ...svgAttributes }, [
|
||||
...builtIconNode.map(([tag, attrs]) => h(tag, attrs)),
|
||||
...toChildArray(children),
|
||||
]);
|
||||
};
|
||||
|
||||
export default Icon;
|
||||
|
||||
@@ -5,13 +5,18 @@ 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: '',
|
||||
});
|
||||
|
||||
@@ -20,7 +25,11 @@ interface LucideProviderProps {
|
||||
size?: number;
|
||||
color?: string;
|
||||
strokeWidth?: number;
|
||||
/**
|
||||
* @deprecated Use `nonScalingStroke` instead.
|
||||
*/
|
||||
absoluteStrokeWidth?: boolean;
|
||||
nonScalingStroke?: boolean;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
@@ -30,6 +39,7 @@ export function LucideProvider({
|
||||
color,
|
||||
strokeWidth,
|
||||
absoluteStrokeWidth,
|
||||
nonScalingStroke,
|
||||
class: className,
|
||||
}: LucideProviderProps) {
|
||||
const value = useMemo(
|
||||
@@ -38,9 +48,10 @@ export function LucideProvider({
|
||||
color,
|
||||
strokeWidth,
|
||||
absoluteStrokeWidth,
|
||||
nonScalingStroke,
|
||||
class: className,
|
||||
}),
|
||||
[size, color, strokeWidth, absoluteStrokeWidth, className],
|
||||
[size, color, strokeWidth, absoluteStrokeWidth, nonScalingStroke, className],
|
||||
);
|
||||
|
||||
return h(LucideContext.Provider, { value }, children);
|
||||
|
||||
@@ -1,34 +1,56 @@
|
||||
import { h, type JSX } from 'preact';
|
||||
import { mergeClasses, toKebabCase, toPascalCase } from '@lucide/shared';
|
||||
import { h, JSX } from 'preact';
|
||||
import { mergeClasses, toLucideIconData, toPascalCase } from '@lucide/shared';
|
||||
import Icon from './Icon';
|
||||
import type { IconNode, LucideIcon, LucideProps } from './types';
|
||||
import type { LucideIcon, LucideIconData, LucideIconNode, LucideProps } from './types';
|
||||
|
||||
/**
|
||||
* Create a Lucide icon component
|
||||
* @param {string} iconName
|
||||
* @param {array} iconNode
|
||||
* @returns {FunctionComponent} LucideIcon
|
||||
* Creates a Lucide icon component from icon data.
|
||||
*
|
||||
* @param icon Icon data object.
|
||||
* @returns Lucide icon component.
|
||||
*/
|
||||
const createLucideIcon = (iconName: string, iconNode: IconNode): LucideIcon => {
|
||||
function createLucideIcon(icon: LucideIconData): LucideIcon;
|
||||
|
||||
/**
|
||||
* Creates a Lucide icon component from the legacy icon arguments.
|
||||
*
|
||||
* @param iconName Icon name.
|
||||
* @param iconNode Icon node.
|
||||
* @param aliases Optional icon aliases.
|
||||
* @returns Lucide icon component.
|
||||
*/
|
||||
function createLucideIcon(
|
||||
iconName: string,
|
||||
iconNode: LucideIconNode[],
|
||||
aliases?: string[],
|
||||
): LucideIcon;
|
||||
|
||||
function createLucideIcon(
|
||||
iconDataOrName: LucideIconData | string,
|
||||
iconNode?: LucideIconNode[],
|
||||
aliases: string[] = [],
|
||||
): LucideIcon {
|
||||
const iconData =
|
||||
typeof iconDataOrName === 'string'
|
||||
? toLucideIconData(iconDataOrName, iconNode as LucideIconNode[], aliases)
|
||||
: iconDataOrName;
|
||||
|
||||
const Component = ({ class: classes = '', className = '', children, ...props }: LucideProps) =>
|
||||
h(
|
||||
Icon,
|
||||
{
|
||||
...props,
|
||||
iconNode,
|
||||
class: mergeClasses<string | JSX.SignalLike<string | undefined>>(
|
||||
`lucide-${toKebabCase(toPascalCase(iconName))}`,
|
||||
`lucide-${toKebabCase(iconName)}`,
|
||||
classes,
|
||||
className,
|
||||
),
|
||||
icon: iconData,
|
||||
class: mergeClasses<string | JSX.SignalLike<string | undefined>>(classes, className),
|
||||
},
|
||||
children,
|
||||
);
|
||||
|
||||
Component.displayName = toPascalCase(iconName);
|
||||
if (iconData.name) {
|
||||
Component.displayName = toPascalCase(iconData.name);
|
||||
}
|
||||
|
||||
return Component;
|
||||
};
|
||||
}
|
||||
|
||||
export default createLucideIcon;
|
||||
|
||||
@@ -1,13 +1 @@
|
||||
import type { JSX } from 'preact';
|
||||
|
||||
export default {
|
||||
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',
|
||||
} satisfies JSX.SVGAttributes;
|
||||
export { defaultAttributes as default } from '@lucide/shared';
|
||||
|
||||
@@ -1,12 +1,29 @@
|
||||
import { type FunctionComponent, type JSX } from 'preact';
|
||||
import type {
|
||||
LucideIconData as SharedLucideIconData,
|
||||
LucideIconNode as SharedLucideIconNode,
|
||||
} from '@lucide/shared/types';
|
||||
|
||||
export type IconNode = [elementName: keyof JSX.IntrinsicElements, attrs: Record<string, string>][];
|
||||
export type LucideIconNode = SharedLucideIconNode;
|
||||
|
||||
export type LucideIconData = SharedLucideIconData;
|
||||
|
||||
/**
|
||||
* @deprecated Use LucideIconNode instead.
|
||||
*/
|
||||
export type IconNode = LucideIconNode[];
|
||||
|
||||
export interface LucideProps extends Partial<Omit<JSX.SVGAttributes, 'ref' | 'size'>> {
|
||||
color?: string;
|
||||
size?: string | number;
|
||||
width?: string | number;
|
||||
height?: string | number;
|
||||
strokeWidth?: string | number;
|
||||
/**
|
||||
* @deprecated Use `nonScalingStroke` instead.
|
||||
*/
|
||||
absoluteStrokeWidth?: boolean;
|
||||
nonScalingStroke?: boolean;
|
||||
}
|
||||
|
||||
export type LucideIcon = FunctionComponent<LucideProps>;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
exports[`Using LucideProvider > should render the icon with LucideProvider 1`] = `
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="lucide lucide-house"
|
||||
class="lucide lucide-house lucide-home"
|
||||
fill="none"
|
||||
height="48"
|
||||
stroke="red"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`Using createLucideIcon > should create a component from an iconNode 1`] = `
|
||||
exports[`Using createLucideIcon > should create a component from icon data 1`] = `
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="lucide lucide-air-vent"
|
||||
@@ -29,10 +29,10 @@ exports[`Using createLucideIcon > should create a component from an iconNode 1`]
|
||||
</svg>
|
||||
`;
|
||||
|
||||
exports[`Using createLucideIcon > should create a component from an iconNode with iconName 1`] = `
|
||||
exports[`Using createLucideIcon > should include aliases from icon data 1`] = `
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="lucide lucide-air-vent"
|
||||
class="lucide lucide-circle-check lucide-check-circle"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
@@ -58,10 +58,10 @@ exports[`Using createLucideIcon > should create a component from an iconNode wit
|
||||
</svg>
|
||||
`;
|
||||
|
||||
exports[`Using createLucideIcon > should include backwards compatible className 1`] = `
|
||||
exports[`Using createLucideIcon > should merge className prop with icon classes 1`] = `
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="lucide lucide-layout2 lucide-layout-2"
|
||||
class="lucide lucide-circle-check lucide-check-circle custom-class"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
|
||||
@@ -10,7 +10,7 @@ exports[`Using lucide icon components > should adjust the size, stroke color and
|
||||
stroke-width="4"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-grid3x3 lucide-grid-3x3"
|
||||
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<rect width="18"
|
||||
@@ -31,33 +31,42 @@ exports[`Using lucide icon components > should adjust the size, stroke color and
|
||||
</svg>
|
||||
`;
|
||||
|
||||
exports[`Using lucide icon components > should not scale the strokeWidth when absoluteStrokeWidth is set 1`] = `
|
||||
exports[`Using lucide icon components > should apply vector-effect when nonScalingStroke 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-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-grid3x3 lucide-grid-3x3"
|
||||
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<rect width="18"
|
||||
<rect vector-effect="non-scaling-stroke"
|
||||
width="18"
|
||||
height="18"
|
||||
x="3"
|
||||
y="3"
|
||||
rx="2"
|
||||
>
|
||||
</rect>
|
||||
<path d="M3 9h18">
|
||||
<path vector-effect="non-scaling-stroke"
|
||||
d="M3 9h18"
|
||||
>
|
||||
</path>
|
||||
<path d="M3 15h18">
|
||||
<path vector-effect="non-scaling-stroke"
|
||||
d="M3 15h18"
|
||||
>
|
||||
</path>
|
||||
<path d="M9 3v18">
|
||||
<path vector-effect="non-scaling-stroke"
|
||||
d="M9 3v18"
|
||||
>
|
||||
</path>
|
||||
<path d="M15 3v18">
|
||||
<path vector-effect="non-scaling-stroke"
|
||||
d="M15 3v18"
|
||||
>
|
||||
</path>
|
||||
</svg>
|
||||
`;
|
||||
@@ -72,7 +81,38 @@ exports[`Using lucide icon components > should render an component 1`] = `
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-grid3x3 lucide-grid-3x3"
|
||||
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<rect width="18"
|
||||
height="18"
|
||||
x="3"
|
||||
y="3"
|
||||
rx="2"
|
||||
>
|
||||
</rect>
|
||||
<path d="M3 9h18">
|
||||
</path>
|
||||
<path d="M3 15h18">
|
||||
</path>
|
||||
<path d="M9 3v18">
|
||||
</path>
|
||||
<path d="M15 3v18">
|
||||
</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"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<rect width="18"
|
||||
|
||||
@@ -51,6 +51,22 @@ describe('Using LucideProvider', () => {
|
||||
expect(IconComponent).toHaveAttribute('stroke-width', '1');
|
||||
});
|
||||
|
||||
it('should render the icon with LucideProvider and custom nonScalingStroke', () => {
|
||||
const { container } = render(
|
||||
<LucideProvider
|
||||
size={48}
|
||||
color="red"
|
||||
nonScalingStroke
|
||||
>
|
||||
<House />
|
||||
</LucideProvider>,
|
||||
);
|
||||
|
||||
const IconComponent = container.firstElementChild;
|
||||
|
||||
expect(IconComponent?.firstElementChild).toHaveAttribute('vector-effect', 'non-scaling-stroke');
|
||||
});
|
||||
|
||||
it("should override the provider's global props when passing props to the icon", () => {
|
||||
const { container } = render(
|
||||
<LucideProvider
|
||||
@@ -83,6 +99,9 @@ describe('Using LucideProvider', () => {
|
||||
|
||||
const IconComponent = container.firstElementChild;
|
||||
|
||||
expect(IconComponent).toHaveAttribute('class', 'lucide provider-class lucide-house icon-class');
|
||||
expect(IconComponent).toHaveAttribute(
|
||||
'class',
|
||||
'lucide lucide-house lucide-home provider-class icon-class',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,30 +4,58 @@ import { airVent } from './testIconNodes';
|
||||
import { render } from '@testing-library/preact';
|
||||
|
||||
describe('Using createLucideIcon', () => {
|
||||
it('should create a component from an iconNode', () => {
|
||||
it('should create a component from icon data', () => {
|
||||
const AirVent = createLucideIcon({
|
||||
name: 'air-vent',
|
||||
node: airVent,
|
||||
});
|
||||
|
||||
const { container } = render(<AirVent />);
|
||||
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
expect(container.firstChild).toBeDefined();
|
||||
});
|
||||
|
||||
it('should include aliases from icon data', () => {
|
||||
const CircleCheck = createLucideIcon({
|
||||
name: 'circle-check',
|
||||
aliases: ['check-circle'],
|
||||
node: airVent,
|
||||
});
|
||||
|
||||
const { container } = render(<CircleCheck />);
|
||||
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
expect(container.firstChild).toBeDefined();
|
||||
});
|
||||
|
||||
it('should merge className prop with icon classes', () => {
|
||||
const CircleCheck = createLucideIcon({
|
||||
name: 'circle-check',
|
||||
aliases: ['check-circle'],
|
||||
node: airVent,
|
||||
});
|
||||
|
||||
const { container } = render(<CircleCheck class="custom-class" />);
|
||||
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
expect(container.firstChild).toBeDefined();
|
||||
});
|
||||
|
||||
it('should create a component from legacy icon arguments', () => {
|
||||
const AirVent = createLucideIcon('AirVent', airVent);
|
||||
|
||||
const { container } = render(<AirVent />);
|
||||
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
expect(container.firstChild).toBeDefined();
|
||||
expect(container.firstChild).toHaveClass('lucide-air-vent');
|
||||
});
|
||||
|
||||
it('should create a component from an iconNode with iconName', () => {
|
||||
const AirVent = createLucideIcon('air-vent', airVent);
|
||||
it('should include aliases from legacy icon arguments', () => {
|
||||
const CircleCheck = createLucideIcon('CircleCheck', airVent, ['check-circle']);
|
||||
|
||||
const { container } = render(<AirVent />);
|
||||
const { container } = render(<CircleCheck />);
|
||||
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
expect(container.firstChild).toBeDefined();
|
||||
});
|
||||
|
||||
it('should include backwards compatible className', () => {
|
||||
const Layout2 = createLucideIcon('layout-2', airVent);
|
||||
|
||||
const { container } = render(<Layout2 />);
|
||||
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
expect(container.firstChild).toBeDefined();
|
||||
expect(container.firstChild).toHaveClass('lucide-circle-check');
|
||||
expect(container.firstChild).toHaveClass('lucide-check-circle');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { render, cleanup } from '@testing-library/preact';
|
||||
import { Pen, Edit2, Grid, Droplet } from '../src/lucide-preact';
|
||||
import { Pen, Edit2, Grid, Droplet, LucideIconData, createLucideIcon } from '../src/lucide-preact';
|
||||
import defaultAttributes from '../src/defaultAttributes';
|
||||
|
||||
type AttributesAssertion = { attributes: Record<string, { value: string }> };
|
||||
@@ -70,7 +70,7 @@ describe('Using lucide icon components', () => {
|
||||
expect(PenIconRenderedHTML).toBe(Edit2Container.innerHTML);
|
||||
});
|
||||
|
||||
it('should not scale the strokeWidth when absoluteStrokeWidth is set', () => {
|
||||
it('should scale the strokeWidth when absoluteStrokeWidth is set', () => {
|
||||
const { container } = render(
|
||||
<Grid
|
||||
size={48}
|
||||
@@ -89,12 +89,48 @@ describe('Using lucide icon components', () => {
|
||||
expect(container.innerHTML).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should apply vector-effect when nonScalingStroke is set', () => {
|
||||
const { container } = render(
|
||||
<Grid
|
||||
size={48}
|
||||
stroke="red"
|
||||
nonScalingStroke
|
||||
/>,
|
||||
);
|
||||
|
||||
const SVGElement = container.firstElementChild;
|
||||
|
||||
expect(SVGElement).toHaveAttribute('stroke', 'red');
|
||||
expect(SVGElement).toHaveAttribute('width', '48');
|
||||
expect(SVGElement).toHaveAttribute('height', '48');
|
||||
expect(SVGElement).toHaveAttribute('stroke-width', '2');
|
||||
expect(SVGElement?.firstElementChild).toHaveAttribute('vector-effect', 'non-scaling-stroke');
|
||||
|
||||
expect(container.innerHTML).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should apply all classes to the element', () => {
|
||||
const iconData: LucideIconData = {
|
||||
name: 'droplet',
|
||||
size: 24,
|
||||
node: [
|
||||
[
|
||||
'path',
|
||||
{
|
||||
d: 'M12 22a7 7 0 0 0 7-7c0-2-1-3.9-3-5.5s-3.5-4-4-6.5c-.5 2.5-2 4.9-4 6.5C6 11.1 5 13 5 15a7 7 0 0 0 7 7z',
|
||||
key: 'abc123',
|
||||
},
|
||||
],
|
||||
],
|
||||
aliases: ['drop'],
|
||||
};
|
||||
const testClass = 'my-class';
|
||||
const { container } = render(<Droplet class={testClass} />);
|
||||
const Icon = createLucideIcon(iconData);
|
||||
const { container } = render(<Icon class={testClass} />);
|
||||
|
||||
expect(container.firstChild).toHaveClass(testClass);
|
||||
expect(container.firstChild).toHaveClass('lucide');
|
||||
expect(container.firstChild).toHaveClass('lucide-droplet');
|
||||
expect(container.firstChild).toHaveClass('lucide-drop');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
"moduleResolution": "node",
|
||||
"module": "ESNext",
|
||||
"target": "ESNext",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@lucide/shared": ["../shared/src/index.ts"],
|
||||
"@lucide/shared/types": ["../shared/src/build/types.ts"],
|
||||
},
|
||||
"esModuleInterop": true,
|
||||
"isolatedModules": true,
|
||||
"lib": ["esnext", "dom"],
|
||||
|
||||
@@ -3,6 +3,11 @@ import dts from 'rollup-plugin-dts';
|
||||
import pkg from './package.json' with { type: 'json' };
|
||||
import getIconEntryNamesAndAliases from './scripts/getIconEntryNamesAndAliases.mts';
|
||||
|
||||
const dtsOptions = {
|
||||
includeExternal: ['@lucide/shared/types'],
|
||||
tsconfig: './tsconfig.json',
|
||||
};
|
||||
|
||||
const packageName = 'LucideReact';
|
||||
const outputFileName = 'lucide-react-native';
|
||||
const outputDir = 'dist';
|
||||
@@ -60,7 +65,7 @@ export default [
|
||||
format: 'es',
|
||||
},
|
||||
],
|
||||
plugins: [dts()],
|
||||
plugins: [dts(dtsOptions)],
|
||||
},
|
||||
{
|
||||
input: 'src/icons/index.ts',
|
||||
@@ -70,7 +75,7 @@ export default [
|
||||
format: 'es',
|
||||
},
|
||||
],
|
||||
plugins: [dts()],
|
||||
plugins: [dts(dtsOptions)],
|
||||
},
|
||||
{
|
||||
input: iconAndAliasEntries,
|
||||
@@ -95,7 +100,7 @@ export default [
|
||||
format: 'es',
|
||||
},
|
||||
],
|
||||
plugins: [dts()],
|
||||
plugins: [dts(dtsOptions)],
|
||||
},
|
||||
{
|
||||
input: `src/${outputFileName}.prefixed.ts`,
|
||||
@@ -105,7 +110,7 @@ export default [
|
||||
format: 'es',
|
||||
},
|
||||
],
|
||||
plugins: [dts()],
|
||||
plugins: [dts(dtsOptions)],
|
||||
},
|
||||
...configs,
|
||||
];
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user