WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit d5c20fd

Browse files
committed
Added Lucide Icons build script and icon set.
1 parent 8c80dcb commit d5c20fd

File tree

12 files changed

+8045
-1
lines changed

12 files changed

+8045
-1
lines changed

docs/src/pages/options/installing-icon-libraries.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ related:
1010
**This page refers to using [webfont icons](/vue-components/icon#webfont-icons) only.** [Svg icons](/vue-components/icon#svg-icons) do not need any installation step.
1111
:::
1212

13-
You'll most likely want icons in your website/app and Quasar offers an easy way out of the box for the following icon libraries: [Material Icons](https://fonts.google.com/icons?icon.set=Material+Icons), [Material Symbols](https://fonts.google.com/icons?icon.set=Material+Symbols), [Font Awesome](https://fontawesome.com/icons), [Ionicons](http://ionicons.com/), [MDI](https://materialdesignicons.com/), [Eva Icons](https://akveo.github.io/eva-icons), [Themify Icons](https://themify.me/themify-icons), [Line Awesome](https://icons8.com/line-awesome) and [Bootstrap Icons](https://icons.getbootstrap.com/). But you can [add support for others](/vue-components/icon#custom-mapping) by yourself.
13+
You'll most likely want icons in your website/app and Quasar offers an easy way out of the box for the following icon libraries: [Material Icons](https://fonts.google.com/icons?icon.set=Material+Icons), [Material Symbols](https://fonts.google.com/icons?icon.set=Material+Symbols), [Font Awesome](https://fontawesome.com/icons), [Ionicons](http://ionicons.com/), [MDI](https://materialdesignicons.com/), [Eva Icons](https://akveo.github.io/eva-icons), [Themify Icons](https://themify.me/themify-icons), [Line Awesome](https://icons8.com/line-awesome), [Bootstrap Icons](https://icons.getbootstrap.com/) and [Lucide Icons](https://lucide.dev/icons/). But you can [add support for others](/vue-components/icon#custom-mapping) by yourself.
1414

1515
::: tip
1616
In regards to webfont icons, you can choose to install one or more of these icon libraries.

extras/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ Please make sure you have latest `@quasar/extras` npm package version installed
7777
| [Themify Icons](https://themify.me/themify-icons) | 1.0.1 | `svg-themify` | `@quasar/extras/themify` | | [License](themify/LICENSE) |
7878
| [Line Awesome](https://icons8.com/line-awesome) | 1.3.0 | `svg-line-awesome` | `@quasar/extras/line-awesome` | Requires: @quasar/extras 1.5+ | [License](line-awesome/LICENSE.md) |
7979
| [Bootstrap Icons](https://icons.getbootstrap.com/) | 1.13.1 | `svg-bootstrap-icons` | `@quasar/extras/bootstrap-icons` | Requires: @quasar/extras 1.10+ | [License](bootstrap-icons/LICENSE.md) |
80+
| [Lucide Icons](https://lucide.dev/icons/) | 0.554.0 | `svg-lucide-icons` | `@quasar/extras/lucide-icons` | Requires: @quasar/extras 1.17.1+ | [License](lucide-icons/LICENSE.md) |
8081

8182
Example:
8283

extras/build/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ async function generate () {
8080
'./eva-icons.js',
8181
'./themify.js',
8282
'./line-awesome.js',
83+
'./lucide-icons.js',
8384
'./bootstrap-icons.js',
8485
// './material-icons.js', // hasn't updated in 2 years
8586
'./material-symbols.js',

extras/build/lucide-icons.js

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
const packageName = 'lucide-static'
2+
const distName = 'lucide-icons'
3+
const iconSetName = 'Lucide'
4+
const prefix = 'luc'
5+
6+
// ------------
7+
8+
const { globSync } = require('tinyglobby')
9+
const { copySync } = require('fs-extra')
10+
const { writeFileSync } = require('fs')
11+
const { resolve, join } = require('path')
12+
13+
const skipped = []
14+
const distFolder = resolve(__dirname, `../${ distName }`)
15+
const { defaultNameMapper, writeExports, extractSvg } = require('./utils')
16+
const { readFileSync } = require('fs')
17+
18+
// Custom extract function for Lucide icons that handles relative paths correctly
19+
function extractLucide(filePath, name) {
20+
const content = readFileSync(filePath, 'utf-8')
21+
22+
// The real issue is that the utils path decoder adds M0 0z prefix for paths starting with 'm'
23+
// We need to override this behavior temporarily
24+
const utils = require('./utils')
25+
26+
// Store the original extractSvg function
27+
const originalExtractSvg = utils.extractSvg
28+
29+
// Create a modified version that doesn't add M0 0z prefix
30+
function customExtractSvg(content, name) {
31+
const { parseSvgContent } = utils
32+
33+
// Monkey-patch the path decoder temporarily
34+
const xmldom = require('@xmldom/xmldom')
35+
const Parser = new xmldom.DOMParser()
36+
37+
// Override just the path processing part
38+
const originalParseSvgContent = parseSvgContent
39+
40+
// Custom parseSvgContent that avoids the M0 0z issue
41+
function lucideParseSvgContent(name, content) {
42+
let viewBox
43+
const pathsDefinitions = []
44+
45+
try {
46+
const dom = Parser.parseFromString(content, 'text/xml')
47+
viewBox = dom.documentElement.getAttribute('viewBox')
48+
49+
if (!viewBox) {
50+
const width = parseFloat(dom.documentElement.getAttribute('width') || '0')
51+
const height = parseFloat(dom.documentElement.getAttribute('height') || '0')
52+
if (width > 0 && height > 0) {
53+
viewBox = `0 0 ${width} ${height}`
54+
}
55+
}
56+
57+
// Parse all elements (paths, circles, rects, etc.) but without the M0 0z issue
58+
function parseElement(el) {
59+
const type = el.nodeName
60+
61+
if (el.getAttribute === undefined || el.getAttribute('opacity') === '0') return
62+
63+
const typeExceptions = ['g', 'svg', 'defs', 'style', 'title']
64+
65+
if (!typeExceptions.includes(type)) {
66+
let pathData = ''
67+
68+
if (type === 'path') {
69+
const points = el.getAttribute('d')?.trim()
70+
if (points) {
71+
// Don't add M0 0z prefix for relative paths!
72+
pathData = points
73+
}
74+
} else if (type === 'circle') {
75+
const cx = parseFloat(el.getAttribute('cx') || 0)
76+
const cy = parseFloat(el.getAttribute('cy') || 0)
77+
const r = parseFloat(el.getAttribute('r') || 0)
78+
pathData = `M${cx} ${cy} m-${r}, 0 a${r},${r} 0 1,0 ${r * 2},0 a${r},${r} 0 1,0 ${-r * 2},0`
79+
} else if (type === 'rect') {
80+
const x = parseFloat(el.getAttribute('x') || 0)
81+
const y = parseFloat(el.getAttribute('y') || 0)
82+
const width = parseFloat(el.getAttribute('width') || 0)
83+
const height = parseFloat(el.getAttribute('height') || 0)
84+
pathData = `M${x} ${y}H${x + width}V${y + height}H${x}Z`
85+
} else if (type === 'line') {
86+
const x1 = parseFloat(el.getAttribute('x1') || 0)
87+
const y1 = parseFloat(el.getAttribute('y1') || 0)
88+
const x2 = parseFloat(el.getAttribute('x2') || 0)
89+
const y2 = parseFloat(el.getAttribute('y2') || 0)
90+
pathData = `M${x1},${y1}L${x2},${y2}`
91+
} else if (type === 'polygon') {
92+
const points = el.getAttribute('points')?.trim()
93+
if (points) {
94+
// Convert polygon points to path data
95+
const coords = points.split(/[\s,]+/).filter(p => p.length > 0)
96+
if (coords.length >= 4 && coords.length % 2 === 0) {
97+
pathData = `M${coords[0]} ${coords[1]}`
98+
for (let i = 2; i < coords.length; i += 2) {
99+
pathData += `L${coords[i]} ${coords[i + 1]}`
100+
}
101+
pathData += 'Z' // Close the polygon
102+
}
103+
}
104+
} else if (type === 'ellipse') {
105+
const cx = parseFloat(el.getAttribute('cx') || 0)
106+
const cy = parseFloat(el.getAttribute('cy') || 0)
107+
const rx = parseFloat(el.getAttribute('rx') || 0)
108+
const ry = parseFloat(el.getAttribute('ry') || 0)
109+
// Convert ellipse to path using arc commands
110+
pathData = `M${cx - rx} ${cy} A${rx} ${ry} 0 1 0 ${cx + rx} ${cy} A${rx} ${ry} 0 1 0 ${cx - rx} ${cy}`
111+
}
112+
113+
if (pathData) {
114+
// Get styles from SVG attributes
115+
const fill = el.getAttribute('fill') || dom.documentElement.getAttribute('fill') || 'none'
116+
const stroke = el.getAttribute('stroke') || dom.documentElement.getAttribute('stroke') || 'currentColor'
117+
const strokeWidth = el.getAttribute('stroke-width') || dom.documentElement.getAttribute('stroke-width') || '2'
118+
const strokeLinecap = el.getAttribute('stroke-linecap') || dom.documentElement.getAttribute('stroke-linecap') || 'round'
119+
const strokeLinejoin = el.getAttribute('stroke-linejoin') || dom.documentElement.getAttribute('stroke-linejoin') || 'round'
120+
121+
const style = `fill:${fill};stroke:${stroke};stroke-width:${strokeWidth};stroke-linecap:${strokeLinecap};stroke-linejoin:${strokeLinejoin};`
122+
123+
pathsDefinitions.push({
124+
path: pathData,
125+
style: style,
126+
transform: el.getAttribute('transform') || ''
127+
})
128+
}
129+
}
130+
131+
// Recursively process child elements
132+
Array.from(el.childNodes).forEach(child => {
133+
if (child.nodeType === 1) { // Element node
134+
parseElement(child)
135+
}
136+
})
137+
}
138+
139+
parseElement(dom.documentElement)
140+
141+
} catch (err) {
142+
console.error(`[Error] "${name}" could not be parsed: ${err.message}`)
143+
throw err
144+
}
145+
146+
if (pathsDefinitions.length === 0) {
147+
throw new Error(`Could not infer any paths for "${name}"`)
148+
}
149+
150+
const tmpView = viewBox !== '0 0 24 24' && viewBox ? `|${viewBox}` : ''
151+
152+
const result = {
153+
viewBox: tmpView
154+
}
155+
156+
if (pathsDefinitions.every((def) => !def.style && !def.transform)) {
157+
result.paths = pathsDefinitions.map((def) => def.path).join('')
158+
} else {
159+
result.paths = pathsDefinitions
160+
.map((def) => {
161+
let stylePart = def.style ? `@@${def.style}` : ''
162+
const transformPart = def.transform ? `@@${def.transform}` : ''
163+
164+
if (!def.style && def.transform) {
165+
stylePart = '@@'
166+
}
167+
168+
return `${def.path}${stylePart}${transformPart}`
169+
})
170+
.join('&&')
171+
}
172+
173+
return result
174+
}
175+
176+
const { paths, viewBox } = lucideParseSvgContent(name, content)
177+
const path = paths.replace(/[\r\n\t]+/gi, ',').replace(/,,/gi, ',')
178+
179+
return {
180+
svgDef: `export const ${name} = '${path}${viewBox}'`,
181+
typeDef: `export declare const ${name}: string;`
182+
}
183+
}
184+
185+
return customExtractSvg(content, name)
186+
}
187+
188+
const svgFolder = resolve(
189+
__dirname,
190+
`../node_modules/${ packageName }/icons/`
191+
)
192+
const svgFiles = globSync(svgFolder + '/*.svg')
193+
let iconNames = new Set()
194+
195+
const svgExports = []
196+
const typeExports = []
197+
198+
svgFiles.forEach((file) => {
199+
const name = defaultNameMapper(file, prefix)
200+
201+
if (iconNames.has(name)) return
202+
203+
try {
204+
const { svgDef, typeDef } = extractLucide(file, name)
205+
svgExports.push(svgDef)
206+
typeExports.push(typeDef)
207+
208+
iconNames.add(name)
209+
}
210+
catch (err) {
211+
console.error(err)
212+
skipped.push(name)
213+
}
214+
})
215+
216+
iconNames = [ ...iconNames ]
217+
svgExports.sort((a, b) => {
218+
return ('' + a).localeCompare(b)
219+
})
220+
typeExports.sort((a, b) => {
221+
return ('' + a).localeCompare(b)
222+
})
223+
iconNames.sort((a, b) => {
224+
return ('' + a).localeCompare(b)
225+
})
226+
227+
writeExports(
228+
iconSetName,
229+
packageName,
230+
distFolder,
231+
svgExports,
232+
typeExports,
233+
skipped
234+
)
235+
236+
copySync(
237+
resolve(__dirname, `../node_modules/${ packageName }/LICENSE`),
238+
resolve(__dirname, `../${ distName }/LICENSE`)
239+
)
240+
241+
// write the JSON file
242+
const file = resolve(__dirname, join('..', distName, 'icons.json'))
243+
writeFileSync(file, JSON.stringify([ ...iconNames ].sort(), null, 2), 'utf-8')
244+
245+
console.log(`${ distName } done with ${ iconNames.length } icons`)

extras/lucide-icons/LICENSE

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
ISC License
2+
3+
Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2023 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2025.
4+
5+
Permission to use, copy, modify, and/or distribute this software for any
6+
purpose with or without fee is hereby granted, provided that the above
7+
copyright notice and this permission notice appear in all copies.
8+
9+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16+
17+
---
18+
19+
The MIT License (MIT) (for portions derived from Feather)
20+
21+
Copyright (c) 2013-2023 Cole Bemis
22+
23+
Permission is hereby granted, free of charge, to any person obtaining a copy
24+
of this software and associated documentation files (the "Software"), to deal
25+
in the Software without restriction, including without limitation the rights
26+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
27+
copies of the Software, and to permit persons to whom the Software is
28+
furnished to do so, subject to the following conditions:
29+
30+
The above copyright notice and this permission notice shall be included in all
31+
copies or substantial portions of the Software.
32+
33+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
34+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
35+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
36+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
37+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
38+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
39+
SOFTWARE.

0 commit comments

Comments
 (0)