Compare commits

...
133 Commits
Author SHA1 Message Date
Daniel BayleyandEric Fennis ee77147aff Add circle-dot-* (GitHub issue) icons (#1066)
* Improve `circle-dot` (GitHub issue) metadata

* Add `refresh-ccw-dot` icon

* Add `circle-dot-dashed` icon

* Add `circle-dashed` icon

* Improve `circle-dot` metadata

---------

Co-authored-by: Eric Fennis <eric.fennis@gmail.com>
2023-06-11 19:38:12 +02:00
Daniel Bayley 3b7b74fe86 Improve toggle/binary metadata (#1238) 2023-06-08 13:12:42 +02:00
Daniel BayleyandKarsa 3a2f052ce9 Add presentation/projector icons (#1327)
* Add `projector` icon

* Add `presentation-screen` icon

* Add `presentation` (whiteboard) icon

* Consolidate `presentation` icons

* Refine `presentation` icon

* Update icons/projector.svg

Co-authored-by: Karsa <contact@karsa.org>

---------

Co-authored-by: Karsa <contact@karsa.org>
2023-06-07 10:14:32 +02:00
Eric Fennis cf34d61971 Revert title change 2023-06-07 08:38:21 +02:00
Eric Fennis 2814a63b8f Add sitemap and OG image to site (#1347)
* Add sitemap generation

* Add og image

* Fix links

* remove comments
2023-06-07 08:25:46 +02:00
Daniel Bayley 4bcab462dc Optimise/add search[-*] icons (#1261)
* Optimise `search` icon

* Add `search-code` icon

* Add `search-x` (stop/clear) icon

* Add `search-check` (complete) icon

* Add `circle-slash` (stop/clear) icon

* Refine `search-x` icon

* Refine `search-slash` icon
2023-06-06 16:03:11 +02:00
Daniel Bayley 6c93bb97c7 Add candlestick-chart icon/bar-chart-big variants (#1320)
* Add `candlestick-chart` icon

* Add `bar-chart-big` alternate icon

* Add `bar-chart-horizontal-big` alternate icon

* Refine `bar-chart-big` icons
2023-06-06 15:59:43 +02:00
Eric Fennis 3c1993c463 Fix orbit json 2023-06-06 15:57:12 +02:00
Daniel Bayley 7a57c306c3 Optimise book icons/add book-up (git force push) alternate icon (#1205)
* Optimise `book` icons

* Add `book-up` (force `push`) alternate icon
2023-06-06 15:18:40 +02:00
Jakob Guddas 32637199f5 Added round corners to network (#1190)
* Added round corners to `network`

* Update network.svg

* Update network.svg

* Update network.svg

* Update network.svg
2023-06-06 15:11:25 +02:00
e490bc35b8 Add Goal icon (#1251)
* Add more music icons and another mic icon (#746)

* Revert "Add more music icons and another mic icon (#746)" (#750)

This reverts commit 57cba6ae0e.

* Add `goal` icon

* Update icons/goal.svg

Co-authored-by: Jakob Guddas <github@jguddas.de>

* Update icons/goal.svg

Co-authored-by: Karsa <contact@karsa.org>

---------

Co-authored-by: it-is-not <72697755+it-is-not@users.noreply.github.com>
Co-authored-by: Karsa <contact@karsa.org>
Co-authored-by: Guillermo Angeles <guillermo.angeles@adoptaunabuelo.com>
Co-authored-by: Guillermo Angeles <67046262+g-angeles-aua@users.noreply.github.com>
Co-authored-by: Jakob Guddas <github@jguddas.de>
2023-06-06 15:09:18 +02:00
Jakob Guddas 496058cc15 Update orbit.json (#1342) 2023-06-06 15:03:01 +02:00
Daniel BayleyandKarsa 4ee46673af Add key variants (#1257)
* Add `key` alternate icon

* Update icons/key-2.svg

Co-authored-by: Karsa <contact@karsa.org>

* Refine `key` alternate icon

* Add `key` variant

* Refine `key` alternate icons

* Rename `key-2` to `key-round`

* Rename `key-3` to `key-square`

* Improve metadata

---------

Co-authored-by: Karsa <contact@karsa.org>
2023-06-04 23:16:11 +02:00
Jakob GuddasandKarsa 5a46f4b87c Optimized circle-off (#1262)
* Optimized `circle-off`

* Update icons/circle-off.svg

Co-authored-by: Karsa <contact@karsa.org>

---------

Co-authored-by: Karsa <contact@karsa.org>
2023-06-04 22:20:07 +02:00
Daniel Bayley 875e8a2d06 Add versions icons (#1141)
* Add `versions` icon

* Add `versions-files` icon

* Rename `versions-files` icon to `file-stack`

* Rename `versions` icon to `square-stack`
2023-06-04 22:12:28 +02:00
Daniel BayleyandEric Fennis e006a171c1 Improve operator icons metadata (#1263)
* Improve operator icons metadata

* Update icons/square-slash.json

---------

Co-authored-by: Eric Fennis <eric.fennis@gmail.com>
2023-06-04 22:10:29 +02:00
Eric Fennis 606706e8e0 Fix gh-api for strokewidths 2023-06-04 17:52:03 +02:00
ffc03ea1f6 Add pocket-knife icon (#1140)
* Add `pocket-knife` icon

* Update icons/pocket-knife.svg

Co-authored-by: Jakob Guddas <github@jguddas.de>

* Update icons/pocket-knife.svg

Co-authored-by: Jakob Guddas <github@jguddas.de>

* Refine `pocket-knife` icon

* Preview alternate `pocket knife` icon

* Refine `pocket-knife` icon

* Rename `pocket-knife-2` to `pocket-knife`

* Update icons/pocket-knife.svg

Co-authored-by: Karsa <contact@karsa.org>

* Improve `pocket-knife` twizzle/swoosh

---------

Co-authored-by: Jakob Guddas <github@jguddas.de>
Co-authored-by: Karsa <contact@karsa.org>
2023-06-04 17:23:01 +02:00
Daniel Bayley b2e685262b Fix list-end icon (#1325) 2023-06-04 17:19:15 +02:00
Eric FennisandKarsa 5bfc736b61 New site! 🚀 (#1275)
* add new docs

* Add styling

* Move files

* Add active selection

* improve grid overview

* improve grid

* Add icon detail page

* Minor changes

* Fix icon preview

* optimize home card

* Add code examples

* Add gitignore

* correct temp directory

* Add first cusotmizer

* Add customizer

* fix images paths

* Add reset function

* Adjust category rendering

* Add packages section

* Some fixes

* Fix vercel build icon

* Small code adjustment

* move file

* Try this

* Add code groups with syntax highlighting

* Add search icon

* Cleanup

* update lockfile

* turnoff 404

* remove docs/iconMetaData.ts

* fix build

* Fix build 2

* cleanup

* Add icon customizer

* Fix build

* Add steps

* Add Button menu

* A lot off fixes!

* cleanup

* Fix build

* Css fixes

* Override menu icon

* try this to improve preformance

* minor improvements

* add comment

* add readme

* Add title

* minor fixes

* Fixes sliders + removes random backticks from index

* Added package list base, still WIP

* Added Guide+Source buttons to package list items

* Responsive support for XS screens

* Map categories count

* Adjust tooltip hover position

* Add see in action icon

* Add download options

* Aligns category list items to baseline and decreases category count weight

* Fixes event target error for categorylistitems

* Added icon release metadata builder

* Adds version badges to hero + icon detail overlay

* Added contributors.
Added Copy Angular.
Added release info to icon page.

* Centres contributor tooltip

* Fixed stroke step + added reset button

* Extracted reset button as a separate component for reusability

* Makes HomeIconCustomizerIcons less dense

* Fixes Button menu

* adjust versions and contributors styling on detail page

* Fix build?

* Fix build 2?

* Fix build 3

* Fix build 4

* Fix build 5?

* Add latest gh-icon changes

* Add comment

* Try fetch tags to retrieve release data

* try fetch all tags

* Add related icons

* Add stikcy search bar

* Add no results components

* Try to fix animation

* Try optimizing for categories

* Hide buggy animated icon

* minor fixes

* Add footer

* Add contributute link in footer

* Add copy name

* Add 100% preview icons

* remove site directory

* clean up

* Add redirects

* Fix build?

* fix redirect?

* minor improvements

* Fix icons preview on mobile

* Small preformance improvement

* Dark mode fixes for package icons

* Sort related icons by similarity + somewhat better name similarity matching

* Replace icon design guide images with uniform SVGs

* update lockfile

* Adds git clone to manually fetch the main repository for creating release metadata

* Remove initial v0.0.0 from release metadata

* Add extra CTA to no results behaviour

* Remove tags, as they are too overused

* Revert "Remove tags, as they are too overused"

This reverts commit 909b7563c0.

* Checkout icons from main

* Add absoluteStrokeWidth switch

* Add absolute strokewidth to home customizer

* Add absolute strokewidth to copy code button

* remove unused import

* compare build time

* improve build speed

* Try new release meta data script

* add fetch tags

* try with branch and remote

* try with url

* try without ssh

* Fix fetch tags in build file

* Cleanup

* Fix fallback

* improve release data

* delete relatedIcons.json, because it should be gitignored

* Add icon details

* Fix import

* minor fixes

* Try running script parallel

* Revert icon details

* include aliases in  release meta data

* Final fixes

* Final fixes 2

* minor code adjustment

* Fix build

* test

* Revert concurrent build flow

* switch back to concurrent build strategy

* revert icon changes

* update package.json

* update package.json

* dedube packages

---------

Co-authored-by: Karsa <karsa@karsa.org>
2023-06-04 16:59:38 +02:00
Karsa 2ebf99f591 Restores removed aliases (#1324)
* Restores removed aliases.

* Makes sure not to remove aliases next time.
2023-06-01 17:58:36 +02:00
Jakob GuddasandKarsa 7a17a2f343 feat: update contributors on release (#1218)
* feat: add contributors on release

* fix: added contributors to icon schema

* fix: added missing dependencies

* feat: changed lost contributor handling

* fix: improved git log arguments

* Add all previous contributors

* Reordered icon JSON attributes

* Merged main and updated contributors

* fix: resolved contributor resolution issue

* chore: added previous contributors

* Added commits with odd e-mails manually

* Added latest icons + removed github workflow

---------

Co-authored-by: Karsa <karsa@karsa.org>
2023-06-01 12:17:23 +02:00
Daniel BayleyandKarsa 4b5d343791 Add squirrel icon (#1229)
* Add `squirrel` icon

* Refine `squirrel` icon

* Refine `rat` icon

---------

Co-authored-by: Karsa <contact@karsa.org>
2023-06-01 09:25:52 +02:00
Daniel BayleyandKarsa b19b01d323 Add text-quote icon (#1230)
* Add `text-quote` icon

* Update icons/text-quote.json

Co-authored-by: Karsa <contact@karsa.org>

---------

Co-authored-by: Karsa <contact@karsa.org>
2023-06-01 09:23:58 +02:00
Daniel BayleyandKarsa d2dc5bf75f Add ampersand icon (#1264)
* Add `ampersand` icon

* Add `ampersands` icon

* Refine `ampersand` icon

* Refine `ampersand` icons

* Update icons/ampersand.svg

---------

Co-authored-by: Karsa <contact@karsa.org>
2023-06-01 09:14:58 +02:00
Karsa 9b93200567 Increases mouse in size as per #1192 (#1291) 2023-05-31 19:30:45 +02:00
locness3andEric Fennis a878596572 Add a distinct React Native logo (#1139)
Co-authored-by: Eric Fennis <eric.fennis@gmail.com>
2023-05-31 19:29:41 +02:00
Jasper Zonneveld 9d50c05937 docs: add nuxt-lucide-icons package (#1313) 2023-05-31 19:27:45 +02:00
Daniel BayleyandJakob Guddas 6196c261d3 Add spell-check icons (#1216)
* Add `spell-check` icon

Co-authored-by: Jakob Guddas <github@jguddas.de>

* Add `spell-check` alternate icon

Co-authored-by: Jakob Guddas <github@jguddas.de>

* Improve `spell-check` icons metadata

---------

Co-authored-by: Jakob Guddas <github@jguddas.de>
2023-05-30 22:15:26 +02:00
85cec0dea1 Refine [rotate/refresh]-*/history icons (#1176)
* Refine `rotate-cw` icon

* Refine `rotate-cw` icon

* Refine `rotate-ccw` icon

* Refine `history` icon

* Refine `refresh-cw` icon

* Refine `refresh-ccw` icon

* Add `refresh-cw-off` icon

* Update icons/history.svg

Co-authored-by: Jakob Guddas <github@jguddas.de>

* Update icons/refresh-ccw.svg

Co-authored-by: Jakob Guddas <github@jguddas.de>

* Refine `history` icon

* Refine `refresh-cww` icon

* Update history.svg

* Optimize refresh-ccw.svg

* Optimize refresh-cw-off.svg

* Optimize refresh-cw.svg

* Optimize rotate-ccw.svg

* Optimize rotate-cw.svg

---------

Co-authored-by: Jakob Guddas <github@jguddas.de>
Co-authored-by: Karsa <contact@karsa.org>
2023-05-30 22:13:03 +02:00
Daniel BayleyandKarsa 07039b7619 Add radar icon (#1152)
* Add `radar` icon

* Fix `radar` optimisation

* Update icons/radar.svg

Co-authored-by: Karsa <contact@karsa.org>

---------

Co-authored-by: Karsa <contact@karsa.org>
2023-05-30 21:57:35 +02:00
Daniel BayleyandKarsa cf05bd766f Add missing playing cards suit icons (#1018)
* Add `spade` (missing playing card suit) icon

* Add `club` (missing playing card suit) icon

* Improve `heart` and `diamond` metadata

* Update spade.svg

* Optimize club.svg

---------

Co-authored-by: Karsa <contact@karsa.org>
2023-05-30 21:56:17 +02:00
Daniel Bayley f05855d1d1 Add spray-can icon (#1277)
* Add `spray-can` icon

* Refine `spray-can` icon
2023-05-30 21:49:20 +02:00
Daniel Bayley 6f39d3743a Improve disc icons metadata/add alternate CD icon (#1307)
* Optimise `disc` alternate icon

* Improve `disc` icons metadata

* Add `disc` alternate icon
2023-05-30 21:48:29 +02:00
Jonas Höbenreich 7ed206af4a Add alternative arrow icons (#1227)
* add move-left icon

* add move-right icon

* add move-down-left icon

* add move-down-right icon

* add move-down icon

* add move-up-right icon

* add move-up-left icon

* add move-up icon

* fix formatting
2023-05-30 21:47:04 +02:00
Daniel Bayley 95daa7c313 Add pilcrow-square icon (#1311)
* Add `pilcrow-square` icon

* Improve `pilcrow` metadata
2023-05-30 21:00:15 +02:00
17ecb92946 [Packages][Lucide] Switch to data-lucide (#1169)
* [packages][lucide] Switch to the HTML attribute data-lucide instead of icon-name

* fix tests

* Update lucide.ts

Add BC for `icon-name`.

* Update packages/lucide/src/lucide.ts

Co-authored-by: Karsa <contact@karsa.org>

---------

Co-authored-by: Karsa <karsa@karsa.org>
Co-authored-by: Eric Fennis <eric.fennis@gmail.com>
2023-05-30 20:42:41 +02:00
Daniel BayleyandEric Fennis 9ef9921f04 Add scatter-chart icon (#1165)
* Add `scatter-chart` icon

---------

Co-authored-by: Eric Fennis <eric.fennis@gmail.com>
2023-05-29 13:24:42 +02:00
Jesús Ferretti ac08bb92c1 fix(docs): fix typos and improve docs in Comparison (#1309)
* fix(docs): fix typos and improve docs in Comparison

* Update docs/comparison.md
2023-05-28 19:38:01 +02:00
Karsa 53109037ec Increases ticket in size as per #1192 (#1292) 2023-05-28 12:17:43 +02:00
66de90d63e Optimise monitor icons/add [monitor-]play/dot/check icons (#1282)
* Optimise `monitor` icons

* Add `monitor-play` icon

* Add `monitor-check` icon

* Add `monitor-dot` icon

* Added monitor-x, monitor-stop & monitor-pause

* Update monitor-pause.json

* Update icons/monitor-play.svg

Co-authored-by: Jakob Guddas <github@jguddas.de>

* Decrease monitor-x x size.

---------

Co-authored-by: Karsa <contact@karsa.org>
Co-authored-by: Jakob Guddas <github@jguddas.de>
2023-05-28 12:12:43 +02:00
Daniel Bayley f3c7e44a3d Add [square-]pi/sigma icons (#1278)
* Add `pi-square` icon

* Add `sigma-square` icon
2023-05-28 11:57:00 +02:00
Jakob Guddas 3823993c39 Optimized umbrella (#1295) 2023-05-28 11:49:28 +02:00
Daniel BayleyandKarsa 36c53f956a Add play-square icon (#1283)
* Improve `play` icon metadata

* Add `play-square` icon

* Update play-square.svg

---------

Co-authored-by: Karsa <contact@karsa.org>
2023-05-28 11:48:32 +02:00
Eric Fennis 58c652908a Add strokeWidth type for lucide-vue-next (#1246) 2023-05-28 11:46:23 +02:00
KarsaandKarsa f4d887339e Adds ferris-wheel and roller-coaster (#1214)
* Adds ferris-wheel and roller-coaster

* Shorten roller-coaster

* Update roller-coaster.svg

---------

Co-authored-by: Karsa <karsa@karsa.org>
2023-05-24 12:14:11 +02:00
Jakob GuddasandKarsa bde11234ea Fix diamond (#1294)
* Fix `diamond`

* Update icons/diamond.svg

Co-authored-by: Karsa <contact@karsa.org>

---------

Co-authored-by: Karsa <contact@karsa.org>
2023-05-19 09:52:38 +02:00
NeutronandKarsa 3449097f77 Add sparkle icon (#1220)
Co-authored-by: Karsa <contact@karsa.org>
2023-05-17 21:32:39 +02:00
Daniel BayleyandKarsa aec41eae39 Fix/optimise life-buoy icon (#1279)
* Fix/optimise `life-buoy` icon

* Update icons/life-buoy.json

Co-authored-by: Karsa <contact@karsa.org>

* Update icons/life-buoy.json

Co-authored-by: Karsa <contact@karsa.org>

* Update icons/life-buoy.svg

Co-authored-by: Karsa <contact@karsa.org>

---------

Co-authored-by: Karsa <contact@karsa.org>
2023-05-17 17:45:18 +02:00
Daniel Bayley 3da3cbc63f Improve qr-code icon metadata (#1281) 2023-05-17 10:48:01 +02:00
3fc3122054 Adds various badge icons, rebases verified as badge-check (#1118)
* Adds various badge icons, rebases verified as badge-check

* Add some more tags

* Revert "fix: fixed github icon api route issue (#1117)"

This reverts commit 49bd49b843.

* manually revert merge

* manually optimize badge shape

* Update icons/circle-dollar-sign.svg

Co-authored-by: Jakob Guddas <github@jguddas.de>

---------

Co-authored-by: Karsa <karsa@karsa.org>
Co-authored-by: Jakob Guddas <github@jguddas.de>
2023-05-17 09:52:58 +02:00
Jakob Guddas 871de752e7 Optimized cloud-moon (#1260) 2023-05-11 09:18:24 +02:00
Daniel BayleyandJakob Guddas 25d7b55459 Add symbols (constant, variable etc…) icons (#1134)
* Add `variable` icon

* Add `constant` icon

* Add `pi` (constant) icon

* Update icons/pi.svg

Co-authored-by: Jakob Guddas <github@jguddas.de>

* Refine `pi` icon

---------

Co-authored-by: Jakob Guddas <github@jguddas.de>
2023-05-10 10:13:23 +02:00
Daniel Bayley 4d8a8091b6 Improve circle-dot (radio button) metadata (#1243) 2023-05-10 10:09:39 +02:00
Daniel Bayley a17c1aafbd Improve sigma metadata (#1245) 2023-05-10 09:58:50 +02:00
Daniel Bayley d1d6eec36e Improve triangle metadata (#1240) 2023-05-09 23:45:24 +02:00
Daniel Bayley abec311bc9 Optimise/refine case-*/word/space icons (#1217)
* Optimise `case-*` icons

* Optimise `whole-word`/`space` icons

* Refine `whole-word`/`space` icons
2023-05-06 15:14:51 +02:00
KarsaandKarsa 3df9be04a8 Renames stars to sparkles (#1221)
Co-authored-by: Karsa <karsa@karsa.org>
2023-05-06 15:12:21 +02:00
Jakob Guddas 016c9d1fac feat: added 100% preview with different stroke widths and in context with other icons (#1180)
* feat: added 100% preview with different stroke widths

* feat: added 100% preview with different stroke widths

* fix: fixed detail section issue

* fix: added lost detail section

* feat: added cohesion preview
2023-05-06 15:02:52 +02:00
Daniel Bayley 17f9509f71 Add/optimise layout-* alternate icons (#1184)
* Optimise `layout-template`

* Add `layout-grid` alternate icon

* Add `layout-template` alternate icon`

* Refine `layout-*` icons

* Refine `layout-*` icons

* Rename `layout-grid-2` to `layout-panel-left`

* Rename `layout-template-2` to `layout-panel-top`
2023-05-06 15:01:35 +02:00
KarsaandKarsa b6c7434e92 [lucide-angular] Adds support for ng@16.x (#1219)
Co-authored-by: Karsa <karsa@karsa.org>
2023-05-06 07:27:43 +02:00
Daniel Bayley 47aa3c2664 Optimiseat-sign icon (#1213)
* Optimise `at-sign` icon

* Refine `at-sign` icon

* Restore `at-sign` ascender
2023-05-05 14:14:23 +02:00
KarsaandKarsa e50b03f316 Adds leafy-green icon (#1168)
Co-authored-by: Karsa <karsa@karsa.org>
2023-05-05 13:27:35 +02:00
KarsaandKarsa 0065b5952b Adds alternate user icons (#1116)
Co-authored-by: Karsa <karsa@karsa.org>
2023-05-05 09:32:23 +02:00
Daniel Bayley b35b586eda Add [un]group icons (#1108)
* Add `group` icon

* Add `ungroup` icon
2023-05-05 09:31:56 +02:00
Jakob Guddas 8b57fab71b Increased size of orbit (#1195) 2023-05-05 09:31:03 +02:00
Daniel Bayley badd34374d Add list-filter icon (#1185) 2023-05-05 08:51:35 +02:00
Daniel Bayley 902431199c Fix unplug metadata (#1212)
This broke the `pre-commit` hook again, because it was `merge`d after the `coding` category was removed.
2023-05-03 22:33:22 +02:00
bdbb4834b0 Replace shuffle icon (#1162)
* Add `shuffle` alternate icon

* Improve `shuffle` metadata

* Replace `shuffle` icon

* Update icons/shuffle.svg

Co-authored-by: Karsa <contact@karsa.org>

* Refine `shuffle` icon

---------

Co-authored-by: Karsa <contact@karsa.org>
Co-authored-by: Eric Fennis <eric.fennis@gmail.com>
2023-05-03 17:24:45 +02:00
Daniel Bayley 07fc4da6fa Optimise/add scroll[-text] (script) icon (#1211)
* Optimise `scroll` icon

* Add `scroll-text` (script) icon
2023-05-03 17:21:14 +02:00
e1815242cf Fix folder-git icons (#1128)
* Fix `folder-git*` icons

* Refine `folder-git` icon

* Update icons/folder-git-2.svg

Co-authored-by: Karsa <contact@karsa.org>

* Update icons/folder-git.svg

Co-authored-by: Karsa <contact@karsa.org>

---------

Co-authored-by: Karsa <contact@karsa.org>
Co-authored-by: Eric Fennis <eric.fennis@gmail.com>
2023-05-03 16:41:28 +02:00
d104ad5c8a Add file-code-2 icon (#1058)
* Add `file-code-2` icon

* Update icons/file-code-2.svg

Co-authored-by: Eric Fennis <eric.fennis@gmail.com>

* Swap `file-code`/`-2` icons

* Update icons/file-code-2.json

Co-authored-by: Karsa <contact@karsa.org>

* Improve `file-code` metadata

---------

Co-authored-by: Eric Fennis <eric.fennis@gmail.com>
Co-authored-by: Karsa <contact@karsa.org>
2023-05-03 16:40:51 +02:00
Daniel BayleyandKarsa 69989c5ae5 Optimize replace icons (#1107)
* Optimize `replace` icons

* Update replace.svg

* Update replace-all.svg

---------

Co-authored-by: Karsa <contact@karsa.org>
2023-05-03 16:16:17 +02:00
Daniel BayleyandKarsa 9e996ef63c Add/refine [un]plug icons (#1035)
* Refine `plug-zap` icon

* Fix `plug` icon

* Add `unplug` icon

* Refine `plug-zap` alternate icon

* Update plug-zap-2.svg

* Update unplug.svg

* Update icons/plug.svg

---------

Co-authored-by: Karsa <contact@karsa.org>
2023-05-03 16:11:04 +02:00
Daniel BayleyandEric Fennis 6ec9cc3dcf Add arrow-*-[to/from]-line icons (#1158)
* Add `arrow-right-to-line` (tab) icon

<kbd>tab</kbd>

* Add `arrow-left-to-line` icon

* Add `arrow-up-to-line` icon

* Add `arrow-down-to-line` (download) icon

* Add `arrow-up-from-line` icon

* Add `arrow-down-from-line` icon

* Add `arrow-left-from-line` icon

* Add `arrow-right-from-line` icon

* Extend `arrow-*-line`s

* Add `expand`/`collapse` metadata

Closes #980.

---------

Co-authored-by: Eric Fennis <eric.fennis@gmail.com>
2023-05-03 15:50:55 +02:00
Daniel Bayley 01fa96ced3 Add menu-square icon (#1181)
* Add `menu` alternate icon

* Rename `menu-2` to `menu-square`
2023-05-03 15:43:36 +02:00
Jakob Guddas 481b27cc49 feat: added radii helper to svg preview (#1183) 2023-05-03 15:42:46 +02:00
Daniel BayleyandKarsa c5df7e73c6 Add list-restart icon (#1053)
* Add `list-restart` icon

* Update icons/list-restart.svg

Co-authored-by: Karsa <contact@karsa.org>

---------

Co-authored-by: Karsa <contact@karsa.org>
2023-05-03 15:40:49 +02:00
Daniel BayleyandKarsa 428088436d Refine database-backup icon (#1051)
* Refine `database-backup` icon

* Refine `database-backup` icon

* Update icons/database-backup.svg

Co-authored-by: Karsa <contact@karsa.org>

---------

Co-authored-by: Karsa <contact@karsa.org>
2023-05-03 15:40:00 +02:00
Daniel BayleyandKarsa eec2c97595 Add *-dot icons (#1042)
* Add `undo-dot` icon

* Add `redo-dot` icon

* Add `arrow-up-from-dot` icon

* Add `arrow-down-to-dot` icon

* Add `dot` icon
Update icons/dot.json

Co-authored-by: Karsa <contact@karsa.org>

---------

Co-authored-by: Karsa <contact@karsa.org>
2023-05-03 15:23:17 +02:00
Daniel BayleyandEric Fennis f0529b9ef7 Add combine icon (#1069)
Co-authored-by: Eric Fennis <eric.fennis@gmail.com>
2023-05-03 15:19:54 +02:00
KarsaandKarsa 0c216b41c5 optimizes sun and moon icons (#1210)
Co-authored-by: Karsa <karsa@karsa.org>
2023-05-03 14:11:30 +02:00
Daniel BayleyandKarsa ac892e5476 Optimise info icon (#1209)
* Optimise `info` icon

Fixes #1206.

* Update icons/info.svg

---------

Co-authored-by: Karsa <contact@karsa.org>
2023-05-03 13:37:50 +02:00
Daniel Bayley 38f62a571c Improve screen/monitor/smartphone etc. icons metadata (#1202)
* Improve `screen-share` icons metadata

* Improve screen/`monitor`/`smartphone` icons metadata

* Improve `laptop` icons metadata
2023-05-03 08:01:29 +02:00
Daniel Bayley 507750d0a7 Optimise help-circle icon (#1207)
Fixes #1206.
2023-05-03 08:00:12 +02:00
Daniel Bayley 33a0ed9539 Improve corner-down-right metadata (#1178) 2023-05-02 20:53:25 +02:00
Daniel Bayley 37cb860ebe Add square-asterisk icon (#1179) 2023-05-02 20:47:11 +02:00
Daniel Bayley bd74ac880e Refine lightbulb icons (#1197) 2023-05-02 20:28:02 +02:00
e81b76f445 Adds computer hardware icons from #527 (#1170)
* Adds computer hardware icons from #527

* extend top line

* Added satellite

---------

Co-authored-by: Karsa <karsa@karsa.org>
Co-authored-by: Eric Fennis <eric.fennis@gmail.com>
2023-05-01 19:18:15 +02:00
Daniel Bayley c50c0e435b Improve align-* metadata (#1193) 2023-05-01 19:00:31 +02:00
Daniel Bayley afd2db296c Improve milestone metadata (#1194) 2023-05-01 18:59:57 +02:00
Daniel BayleyandKarsa 175b2cd483 Add/rename braces/brackets/parenthesis icons and improve braces/json metadata (#1063)
* Add `square-brackets` icon

* Add `parentheses` icon

* Improve `curly-braces`/`json` metadata

* Refine `parentheses` icon

Match `path`s in #1134.

* Rename `square-brackets` to `brackets`

* Rename `curly-braces` to `braces`

* Update icons/parentheses.json

---------

Co-authored-by: Karsa <contact@karsa.org>
2023-05-01 18:58:43 +02:00
Daniel BayleyandEric Fennis 716c5baea0 Add radiation/biohazard icons (#1153)
* Add `radiation` icon

* Refine `radiation` icon

* Refine `radiation` icon

* Fix `radiation` corner radius

* Add `biohazard` icon

---------

Co-authored-by: Eric Fennis <eric.fennis@gmail.com>
2023-04-30 23:57:34 +02:00
Jakob Guddas 890474889a Simplified microscope (#1189) 2023-04-30 22:11:54 +02:00
Jakob Guddas e596cd2bad Optimized parking-square-off.svg (#1182) 2023-04-30 22:07:29 +02:00
Quentin Sommer d4641a4641 Fix children handling in lucide-react-native (#1177) 2023-04-30 22:06:49 +02:00
3e3409cee2 Adds generic merge and split icons (#1167)
Co-authored-by: Karsa <karsa@karsa.org>
Co-authored-by: Eric Fennis <eric.fennis@gmail.com>
2023-04-30 21:44:15 +02:00
Daniel Bayley 477f2b2aff Improve heading[-1–6] metadata (#1173) 2023-04-29 11:24:35 +02:00
Jakob Guddas 000ff56278 Optimized and fixed plug-zap (#1120) 2023-04-29 11:08:03 +02:00
Daniel BayleyandKarsa 7117220943 Add [un]fold icons (#1060)
* Add `fold-vertical` icon

* Add `unfold-vertical` icon

* Add `fold-horizontal` icon

* Add `unfold-horizontal` icon

* Update icons/fold-horizontal.svg

Co-authored-by: Karsa <contact@karsa.org>

* Update icons/fold-vertical.svg

Co-authored-by: Karsa <contact@karsa.org>

* Update icons/unfold-horizontal.svg

Co-authored-by: Karsa <contact@karsa.org>

* Update icons/unfold-vertical.svg

Co-authored-by: Karsa <contact@karsa.org>

---------

Co-authored-by: Karsa <contact@karsa.org>
2023-04-29 11:06:08 +02:00
Eric Fennis f820da257d Create FUNDING.yml 2023-04-28 22:59:05 +02:00
Daniel Bayley de629f0fcc Fix removal of sidebar.json (#1174)
Not sure what happened, but `sidebar.json` should have been deleted when replaced with
`panel-left.json` in #1003… This is currently breaking the `pre-commit` hook!
2023-04-28 21:04:02 +02:00
Daniel Bayley 8f7e9b3cde Rename text-selection to text-select (#1131)
This should have been called `text-select`, consistent with `box-select`…
2023-04-28 10:35:35 +02:00
Daniel Bayley 4e79f147cf Add ligature icon (#1160)
* Add `ligature` icon

* Refine `ligature` icon

* Fix missing serifs
2023-04-28 10:33:41 +02:00
Daniel Bayley 3482cd0949 Add rows icon (#1059)
* Add `rows` icon

* Improve `columns` metadata
2023-04-28 10:17:35 +02:00
Daniel BayleyandEric Fennis 701f2a1a41 Add panel-* icons (#1003)
* Add `panel-bottom` icon

* Add `panel-bottom-open` icon

* Add `panel-bottom-close` icon

* Add `panel-bottom-inactive` icon

* Rename `sidebar` to `panel-left`

* Rename `sidebar-open` to `panel-left-open`

* Rename `sidebar-close` to `panel-left-close`

* Add `panel-left-inactive` icon

* Add `panel-right` icon

* Add `panel-right-open` icon

* Add `panel-right-close` icon

* Add `panel-right-inactive` icon

* Add `panel-top` icon

* Add `panel-top-open` icon

* Add `panel-top-close` icon

* Add `panel-top-inactive` icon

* Refine `panel-*-inactive` (dashed) icons

---------

Co-authored-by: Eric Fennis <eric.fennis@gmail.com>
2023-04-28 09:19:14 +02:00
Daniel BayleyandKarsa 79f5c6e584 Refine gauge icon (#1159)
* Refine `gauge` icon

* Update icons/gauge.svg

Co-authored-by: Karsa <contact@karsa.org>

---------

Co-authored-by: Karsa <contact@karsa.org>
2023-04-28 09:12:04 +02:00
KarsaandKarsa 02fddd3aac [lucide-angular] Export aliases + fix global configuration issues (#1166)
* [lucide-angular] Exports injection token and provider

* [lucide-angular] Export aliases + fix global config issues

* Add aliases of aliases :)

* Update public-api.ts

---------

Co-authored-by: Karsa <karsa@karsa.org>
2023-04-28 06:26:30 +02:00
Daniel Bayley 7816ed88f6 Add bell-dot icon and refine bell-* icons (#1064)
* Add `bell-dot` icon

* Refine `bell` icon

* Refine `bell-ring` icon

* Refine `bell-plus` icon

* Refine `bell-minus` icon

* Refine `bell-off` icon
2023-04-27 23:03:48 +02:00
Jakob Guddas 9c2d57b0dc feat: backdrop improvements (#1138)
* fix: reduced z-fighting

* fix: fixed outline intensity issue

* feat: removed useless outline
2023-04-27 18:08:32 +02:00
Jakob Guddas 6d9a0c3d63 Optimized book-template (#1145) 2023-04-27 18:06:13 +02:00
6f647c58bf Add slash/dot/equal/arrow-*-square icons and tags for git diff (#994)
* Add `tags` to `git diff`-like icons

* Add `square-dot` icon

* Add `square-slash` icon

* Add `square-equal` icon

* Add `arrow-left-square` icon

* Add `arrow-right-square` icon

* Add `arrow-up-square` icon

* Add `arrow-down-square` icon

* Update icons/arrow-right-square.json

Co-authored-by: Karsa <contact@karsa.org>

* Update icons/plus-square.json

Co-authored-by: Karsa <contact@karsa.org>

* Update icons/minus-square.json

Co-authored-by: Karsa <contact@karsa.org>

* Update icons/square-equal.json

Co-authored-by: Karsa <contact@karsa.org>

---------

Co-authored-by: Karsa <contact@karsa.org>
Co-authored-by: Eric Fennis <eric.fennis@gmail.com>
2023-04-26 18:11:35 +02:00
Eric Fennis 41375d5b05 Fix site build 2023-04-26 16:46:15 +02:00
Daniel BayleyandKarsa 11ce2b6ff3 Add generic window icons (#1045)
* Add generic `app-window` icon

* Update icons/app-window.svg

Co-authored-by: Karsa <contact@karsa.org>

* Update icons/app-window.json

---------

Co-authored-by: Karsa <contact@karsa.org>
2023-04-25 19:49:03 +02:00
Jakob Guddas cc7881e759 Changed spline and scale-3d start and end to be circular (#1137)
* Changed `spline` start and end to be circular

* Update scale-3d.svg
2023-04-25 10:46:47 +02:00
Daniel BayleyandJakob Guddas bf530d39d3 Add workflow icon (#1132)
* Add `workflow` icon

* Update icons/workflow.svg

Co-authored-by: Jakob Guddas <github@jguddas.de>

---------

Co-authored-by: Jakob Guddas <github@jguddas.de>
2023-04-25 10:36:52 +02:00
Daniel BayleyandKarsa 71e8df6354 Add iteration icons (#997)
* Add `iteration-ccw` icon

* Add `iteration-cw` icon

* Refine `iteration-*` icons

---

Co-authored-by: Karsa <karsa@karsa.org>
2023-04-25 06:41:26 +02:00
KarsaandKarsa cafd2a838b [lucide-angular] Exports injection token and provider (#1155)
Co-authored-by: Karsa <karsa@karsa.org>
2023-04-25 06:39:01 +02:00
Daniel Bayley e16f368502 Fix chevron-up metadata! (#1150) 2023-04-24 17:37:56 +02:00
Daniel BayleyandKarsa d38509a03d Add monitor-[up/down] icons (#1136)
* Add `monitor-up` icon

* Add `monitor-down` icon

* Update icons/monitor-up.json

Co-authored-by: Karsa <contact@karsa.org>

* Fix `monitor-down` metadata

---------

Co-authored-by: Karsa <contact@karsa.org>
2023-04-23 13:12:32 +02:00
Daniel BayleyandKarsa 6550e22874 Improve metadata for keys, and modify arrow-big-* icons to include shift key (#1002)
* Improve metadata for keys

* Modify `arrow-big-up` (`shift` key)

* Modify `arrow-big-right` to match `arrow-big-up` (`shift` key) icon

* Modify `arrow-big-down` to match `arrow-big-up` (`shift` key) icon

* Modify `arrow-big-left` to match `arrow-big-up` (`shift` key) icon

* Add `arrow-big-up-dash` (`caps lock` key) icon

* Fix `arrow-big-up` metadata

* Refine `arrow-big-up-dash` icon

* Add `arrow-big-down-dash` icon

* Fix `arrow-big-left` metadata

* Add `arrow-big-left-dash` icon

* Add `arrow-big-right-dash` icon

* Optimize `arrow-big-*` icons

---------

Co-authored-by: Karsa <karsa@karsa.org>
2023-04-23 11:47:31 +02:00
KarsaandKarsa 22193420c7 Sorting out our sort icons (#1028)
* Refactors sort icons to cover all use cases

* increases arrow head size

* Apply suggestions from code review

---------

Co-authored-by: Karsa <karsa@karsa.org>
2023-04-23 11:44:33 +02:00
KarsaandKarsa 70827d4571 Added and (#1115)
Co-authored-by: Karsa <karsa@karsa.org>
2023-04-23 11:39:11 +02:00
Daniel BayleyandKarsa 7d980f6cc1 Add repeat-2 (repost/retweet) icon (#1135)
* Add `repeat-2` icon

* Update icons/repeat-2.svg

---------

Co-authored-by: Karsa <contact@karsa.org>
2023-04-23 11:39:04 +02:00
locness3 67131489c8 lucide-static.md : rephrase warning and improve small details (#1123)
* lucide-static.md : rephrase warning and improve small details

* more capitalization of "svg"
2023-04-23 10:31:02 +02:00
ebf03a5434 Add save-all icon (#1125)
* Add `save-all` icon

* Improve `save` metadata

* Update icons/save-all.json

Co-authored-by: Karsa <contact@karsa.org>

* Update icons/save-all.svg

Co-authored-by: Jakob Guddas <github@jguddas.de>

---------

Co-authored-by: Karsa <contact@karsa.org>
Co-authored-by: Jakob Guddas <github@jguddas.de>
2023-04-22 17:19:45 +02:00
Daniel Bayley 8fda42c719 Improve ban metadata (#1130) 2023-04-21 19:58:45 +02:00
Daniel BayleyandJakob Guddas b17627b82d Add shield-question icon (#1112)
Update icons/shield-question.svg

Co-authored-by: Jakob Guddas <github@jguddas.de>
2023-04-20 18:28:33 +02:00
KarsaandKarsa 84ec1620a8 [lucide-angular] Fixes change detection issue with legacy img input setter (#1129)
* [lucide-angular] Fixes img setter alias

* fixes change detection issue with img setter

---------

Co-authored-by: Karsa <karsa@karsa.org>
2023-04-20 18:25:18 +02:00
a87ae2a92b Introducing absoluteStrokeWidth option on Lucide Components (#939)
* Add more music icons and another mic icon (#746)

* Revert "Add more music icons and another mic icon (#746)" (#750)

This reverts commit 57cba6ae0e.

* add scale Stroke width

* Added scaleStrokeWidth prop to all packages

* Add scaleStrokeWidth to types

* Rename scaleStrokeWidth to absoluteStrokeWidth

* Adds common API elements to the Angular package (#949)

* Almost complete rewrite of the Angular package

* Update tsconfig.spec.json

* fixes icon build export file name

* Updates Angular documentation with the new properties + provider injection

* Update lucide-angular.md

* refactored scaleStrokeWidth to be absoluteStrokeWidth to match other packages

* removed codelyzer from devDeps + added flexible angular core dependencies

* Deprecates createElement helper in favour of Renderer2 to support SSR

---------

Co-authored-by: Karsa <karsa@karsa.org>

* Add absoluteStrokeWidth in docs

* update snapshots

* Manual merge of main

* Fixed incorrectly merged pnpm-lock.yaml

* Fixes lucide-angular build

* [lucide-angular] Global configuration for properties + bugfix for legacy icon provider   (#1012)

* Almost complete rewrite of the Angular package

* Update tsconfig.spec.json

* fixes icon build export file name

* Updates Angular documentation with the new properties + provider injection

* Update lucide-angular.md

* refactored scaleStrokeWidth to be absoluteStrokeWidth to match other packages

* removed codelyzer from devDeps + added flexible angular core dependencies

* Deprecates createElement helper in favour of Renderer2 to support SSR

* Added global configuration and fixed undefined bug in legacy icon provider. Also updated README.md

* Replaces removed line in README.md

* Fixes merge error

* Updates export template to use the non-deprecated type

* downgrade building to ng-cli@13

* downgrade to es2020

---------

Co-authored-by: Karsa <karsa@karsa.org>

* rename scaleStrokeWidth to absoluteStrokeWidth in readme

---------

Co-authored-by: it-is-not <72697755+it-is-not@users.noreply.github.com>
Co-authored-by: Karsa <contact@karsa.org>
Co-authored-by: Eric Fennis <eric@dreamteam.nl>
Co-authored-by: Karsa <karsa@karsa.org>
Co-authored-by: Eric Fennis <eric.fennis@nac41112.nedap.local>
2023-04-20 16:08:34 +02:00
Daniel Bayley f35f45c7b3 Add messages-square icon (#1030)
* Add `messages-square` icon

* Add `message-square-plus` icon

* Add `message-square-dashed` icon
2023-04-20 16:02:27 +02:00
Daniel Bayley c9a418dfc4 Improve play metadata (#1062)
* Improve `play` metadata

* Improve `un`/`re`/`do`/`fresh`/`reload` metadata
2023-04-20 15:53:28 +02:00
Daniel Bayley 70be608a58 Add folder-git icon (#1009)
* Add `folder-git` icon

* Add `folder-git-2` icon
2023-04-20 15:52:03 +02:00
1745 changed files with 27094 additions and 16692 deletions
+2
View File
@@ -0,0 +1,2 @@
# These are supported funding model platforms
open_collective: lucide-icons
+86 -3
View File
@@ -22,8 +22,78 @@ jobs:
uses: tj-actions/changed-files@v35
with:
files: icons/*.svg
- name: Generate comment
id: generate-comment
- name: Generate cohesion check random
id: generate-cohesion-check-random
run: |
delimiter="$(openssl rand -hex 8)"
echo "body<<$delimiter" >> $GITHUB_OUTPUT
for file in $(printf "%s\\n" icons/*.svg | shuf | head -n$(awk -F' ' '{print NF}' <<< '${{ steps.changed-files.outputs.all_changed_files }}')); do
cat "$file" | # get file content
tr '\n' ' ' | # remove line breaks
sed -e 's/<svg[^>]*>/<svg>/g' | # remove attributes from svg element
base64 -w 0 | # encode svg
sed "s|.*|<img title=\"$file\" alt=\"$file\" src=\"https://lucide.dev/api/gh-icon/stroke-width/2/&.svg\"/> |"
done | tr '\n' ' ' >> $GITHUB_OUTPUT
echo >> $GITHUB_OUTPUT
echo "$delimiter" >> $GITHUB_OUTPUT
- name: Generate cohesion check squares
id: generate-cohesion-check-squares
run: |
delimiter="$(openssl rand -hex 8)"
echo "body<<$delimiter" >> $GITHUB_OUTPUT
for file in $(printf "%s\\n" icons/*square*.svg | shuf | head -n$(awk -F' ' '{print NF}' <<< '${{ steps.changed-files.outputs.all_changed_files }}')); do
cat "$file" | # get file content
tr '\n' ' ' | # remove line breaks
sed -e 's/<svg[^>]*>/<svg>/g' | # remove attributes from svg element
base64 -w 0 | # encode svg
sed "s|.*|<img title=\"$file\" alt=\"$file\" src=\"https://lucide.dev/api/gh-icon/stroke-width/2/&.svg\"/> |"
done | tr '\n' ' ' >> $GITHUB_OUTPUT
echo >> $GITHUB_OUTPUT
echo "$delimiter" >> $GITHUB_OUTPUT
- name: Generate 1px stroke-width
id: generate-1px-stroke-width
run: |
delimiter="$(openssl rand -hex 8)"
echo "body<<$delimiter" >> $GITHUB_OUTPUT
for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
cat "$file" | # get file content
tr '\n' ' ' | # remove line breaks
sed -e 's/<svg[^>]*>/<svg>/g' | # remove attributes from svg element
base64 -w 0 | # encode svg
sed "s|.*|<img title=\"$file\" alt=\"$file\" src=\"https://lucide.dev/api/gh-icon/stroke-width/1/&.svg\"/> |"
done | tr '\n' ' ' >> $GITHUB_OUTPUT
echo >> $GITHUB_OUTPUT
echo "$delimiter" >> $GITHUB_OUTPUT
- name: Generate 2px stroke-width
id: generate-2px-stroke-width
run: |
delimiter="$(openssl rand -hex 8)"
echo "body<<$delimiter" >> $GITHUB_OUTPUT
for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
cat "$file" | # get file content
tr '\n' ' ' | # remove line breaks
sed -e 's/<svg[^>]*>/<svg>/g' | # remove attributes from svg element
base64 -w 0 | # encode svg
sed "s|.*|<img title=\"$file\" alt=\"$file\" src=\"https://lucide.dev/api/gh-icon/stroke-width/2/&.svg\"/> |"
done | tr '\n' ' ' >> $GITHUB_OUTPUT
echo >> $GITHUB_OUTPUT
echo "$delimiter" >> $GITHUB_OUTPUT
- name: Generate 3px stroke-width
id: generate-3px-stroke-width
run: |
delimiter="$(openssl rand -hex 8)"
echo "body<<$delimiter" >> $GITHUB_OUTPUT
for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
cat "$file" | # get file content
tr '\n' ' ' | # remove line breaks
sed -e 's/<svg[^>]*>/<svg>/g' | # remove attributes from svg element
base64 -w 0 | # encode svg
sed "s|.*|<img title=\"$file\" alt=\"$file\" src=\"https://lucide.dev/api/gh-icon/stroke-width/3/&.svg\"/> |"
done | tr '\n' ' ' >> $GITHUB_OUTPUT
echo >> $GITHUB_OUTPUT
echo "$delimiter" >> $GITHUB_OUTPUT
- name: Generate X-rays
id: generate-x-rays
run: |
delimiter="$(openssl rand -hex 8)"
echo "body<<$delimiter" >> $GITHUB_OUTPUT
@@ -50,8 +120,21 @@ jobs:
issue-number: ${{ github.event.pull_request.number }}
body: |
### Added or changed icons
${{ steps.generate-2px-stroke-width.outputs.body }}<br/>
<details>
<summary>Preview cohesion</summary>
${{ steps.generate-cohesion-check-squares.outputs.body }}<br/>
${{ steps.generate-2px-stroke-width.outputs.body }}<br/>
${{ steps.generate-cohesion-check-random.outputs.body }}<br/>
</details>
<details>
<summary>Preview stroke widths</summary>
${{ steps.generate-1px-stroke-width.outputs.body }}<br/>
${{ steps.generate-2px-stroke-width.outputs.body }}<br/>
${{ steps.generate-3px-stroke-width.outputs.body }}<br/>
</details>
<details>
<summary>Icon X-rays</summary>
${{ steps.generate-comment.outputs.body }}
${{ steps.generate-x-rays.outputs.body }}
</details>
edit-mode: replace
+13
View File
@@ -20,3 +20,16 @@ packages/**/src/aliases.ts
packages/**/LICENSE
categories.json
tags.json
.vercel
# docs
docs/.vitepress/cache
docs/.vitepress/dist
docs/.vitepress/.temp
docs/.vitepress/data/iconNodes
docs/.vitepress/data/iconMetaData.ts
docs/.vitepress/data/releaseMetaData.json
docs/.vitepress/data/releaseMetaData
docs/.vitepress/data/relatedIcons.json
docs/.vercel
docs/.nitro
+8 -8
View File
@@ -25,19 +25,19 @@ Guidelines for pull requests:
Please make sure you follow the icon guidelines, that should be followed to keep quality and consistency when making icons for Lucide.
Read it here: [ICON_GUIDELINES](/docs/icon-design-guide.md).
Read it here: [ICON_GUIDELINES](https://lucide.dev/docs/icon-design-guide).
### Editor guides
Here you can find instructions on how to implement the guidelines with different vector graphics editors:
#### [Adobe Illustrator Guide](/docs/illustrator-guide.md)
#### [Adobe Illustrator Guide](https://lucide.dev/docs/illustrator-guide)
You can also [download an Adobe Illustrator template](/docs/templates/illustrator-template.ai).
You can also [download an Adobe Illustrator template](https://lucide.dev/templates/illustrator-template.ai).
#### [Inkscape Guide](/docs/inkscape-guide.md)
#### [Inkscape Guide](https://lucide.dev/docs/inkscape-guide)
#### [Figma Guide](/docs/figma-guide.md)
#### [Figma Guide](https://lucide.dev/docs/figma-guide)
### Submitting Multiple Icons
@@ -70,7 +70,7 @@ pnpm install # Install dependencies, including the workspace packages
### Packages -> PNPM Workspaces
To distribute different packages we use PNPM workspaces. Before you start make sure you are familiar with this concept. The concept of working in workspaces is created by Yarn, they have a well written introduction: [yarn workspaces](https://classic.yarnpkg.com/lang/en/docs/workspaces).
To distribute different packages we use PNPM workspaces. Before you start make sure you are familiar with this concept. The concept of working in workspaces is created by Yarn, they have a well written introduction: [yarn workspaces](https://classic.yarnpkg.com/lang/enhttps://lucide.dev/docs/workspaces).
The configured directory for workspaces is the [packages](./packages) directory, located in the root directory. There you will find all the current packages from lucide.
There are more workspaces defined, see [`pnpm-workspace.yaml`](./pnpm-workspace.yaml).
@@ -172,11 +172,11 @@ Includes usefully scripts to automate certain jobs. Big part of the scripts is t
### site
The lucide.dev website using [Nextjs](https://nextjs.org).
The lucide.dev website is using [vitepress](https://vitepress.dev/) to generate the static website. The markdown files are located in the docs directory.
## Documentation
The documentation files are located in the [docs](./docs) directory. All these markdown files will be loaded in the build of the lucide.dev website.
The documentation files are located in the [docs](https://github.com/lucide-icons/lucide/tree/main/docs) directory. All these markdown files will be loaded in the build of the lucide.dev website.
Feel free to write, adjust or add new markdown files to improve our documentation.
@@ -0,0 +1,41 @@
import { eventHandler, setResponseHeader, defaultContentType } from 'h3'
import { renderToString, renderToStaticMarkup } from 'react-dom/server'
import { createElement } from 'react'
import SvgPreview from '../../lib/SvgPreview/index.tsx';
import iconNodes from '../../data/iconNodes'
import createLucideIcon from 'lucide-react/src/createLucideIcon'
import Backdrop from '../../lib/SvgPreview/Backdrop.tsx';
export default eventHandler((event) => {
const { params } = event.context
const [name, svgData] = params.data.split('/');
const data = svgData.slice(0, -4);
const src = Buffer.from(data, 'base64').toString('utf8');
const children = []
if (name in iconNodes) {
const iconNode = iconNodes[name]
const LucideIcon = createLucideIcon(name, iconNode)
const svg = renderToStaticMarkup(createElement(LucideIcon))
const backdropString = svg.replace(/<svg[^>]*>|<\/svg>/g, '');
children.push(createElement(Backdrop, { backdropString, src }))
}
const svg = Buffer.from(
// We can't use jsx here, is not supported here by nitro.
renderToString(createElement(SvgPreview, {src, showGrid: true}, children)).replace(
/>/,
'><style>@media screen and (prefers-color-scheme: dark) { svg { stroke: #fff } }</style>'
)
).toString('utf8');
defaultContentType(event, 'image/svg+xml')
setResponseHeader(event, 'Cache-Control', 'public,max-age=31536000')
return svg
})
@@ -0,0 +1,35 @@
import { eventHandler, setResponseHeader, defaultContentType } from 'h3'
import { renderToString } from 'react-dom/server'
import { createElement } from 'react'
import SvgPreview from '../../../lib/SvgPreview/index.tsx';
import createLucideIcon, { IconNode } from 'lucide-react/src/createLucideIcon'
import { parseSync } from 'svgson';
export default eventHandler((event) => {
const { params } = event.context
const [strokeWidth, svgData] = params.data.split('/');
const data = svgData.slice(0, -4);
const src = Buffer.from(data, 'base64').toString('utf8');
const Icon = createLucideIcon(
'icon',
parseSync(src.includes('<svg') ? src : `<svg>${src}</svg>`).children.map(
({ name, attributes }) => [name, attributes]
) as IconNode
);
const svg = Buffer.from(
// We can't use jsx here, is not supported here by nitro.
renderToString(createElement(Icon, { strokeWidth })).replace(
/>/,
'><style>@media screen and (prefers-color-scheme: dark) { svg { stroke: #fff } }</style>'
)
).toString('utf8');
defaultContentType(event, 'image/svg+xml')
setResponseHeader(event, 'Cache-Control', 'public,max-age=31536000')
return svg
})
@@ -0,0 +1,29 @@
import { eventHandler, getQuery, setResponseHeader } from 'h3'
import iconNodes from '../../data/iconNodes'
import { IconNodeWithKeys } from '../../theme/types'
export default eventHandler((event) => {
const query = getQuery(event)
const withUniqueKeys = query.withUniqueKeys === 'true'
setResponseHeader(event, 'Cache-Control', 'public, max-age=86400')
if (withUniqueKeys) {
return iconNodes
}
return Object.entries(iconNodes).reduce((acc, [name, iconNode]) => {
if (withUniqueKeys) {
return [name, iconNode]
}
const newIconNode = (iconNode as IconNodeWithKeys).map(([name, { key, ...attrs}]) => {
return [name, attrs]
})
acc[name] = newIconNode
return acc
}, {})
})
@@ -0,0 +1,44 @@
import { eventHandler, getQuery, setResponseHeader, createError } from 'h3'
import iconNodes from '../../data/iconNodes'
import createLucideIcon from 'lucide-react/src/createLucideIcon'
import { renderToString } from 'react-dom/server'
import { createElement } from 'react'
export default eventHandler((event) => {
const { params } = event.context
const iconNode = iconNodes[params.iconName]
if (iconNode == null) {
const error = createError({
statusCode: 404,
message: `Icon "${params.iconName}" not found`,
})
return sendError(event, error)
}
const width = getQuery(event).width || undefined
const height = getQuery(event).height || undefined
const color = getQuery(event).color || undefined
const strokeWidth = getQuery(event).strokeWidth || undefined
const LucideIcon = createLucideIcon(params.iconName, iconNode)
const svg = Buffer.from(
renderToString(
createElement(LucideIcon, {
width,
height,
color: color ? `#${color}` : undefined,
strokeWidth,
}
))
).toString('utf8');
defaultContentType(event, 'image/svg+xml')
setResponseHeader(event, 'Cache-Control', 'public,max-age=31536000')
return svg
})
+10
View File
@@ -0,0 +1,10 @@
import { eventHandler, setResponseHeader } from 'h3'
import iconMetaData from '../../data/iconMetaData'
export default eventHandler((event) => {
setResponseHeader(event, 'Cache-Control', 'public, max-age=86400')
return Object.fromEntries(
Object.entries(iconMetaData).map(([name, { tags }]) => [ name, tags ])
)
})
+3
View File
@@ -0,0 +1,3 @@
export default eventHandler(() => {
return { nitro: 'Is Awesome! asda' }
})
+160
View File
@@ -0,0 +1,160 @@
import { fileURLToPath, URL } from 'node:url'
import path from 'path';
import { defineConfig } from 'vitepress'
import { createWriteStream } from 'node:fs'
import { resolve } from 'node:path'
import { SitemapStream } from 'sitemap'
import sidebar from './sidebar';
const links = []
const title = "Lucide";
const socialTitle = "Lucide Icons";
const description = "Beautiful & consistent icon toolkit made by the community."
// https://vitepress.dev/reference/site-config
export default defineConfig({
title,
description,
cleanUrls: true,
outDir: '.vercel/output/static',
vite: {
resolve: {
alias: [
{
find: /^.*\/VPIconAlignLeft\.vue$/,
replacement: fileURLToPath(
new URL('./theme/components/overrides/VPIconAlignLeft.vue', import.meta.url)
)
},
{
find: /^.*\/VPFooter\.vue$/,
replacement: fileURLToPath(
new URL('./theme/components/overrides/VPFooter.vue', import.meta.url)
)
}
]
}
},
head: [
[ 'script', {
src: 'https://plausible.io/js/script.js',
'data-domain': 'lucide.dev',
defer: ''
}],
[ 'meta', {
property:"og:locale",
content:"en_US"
}],
[ 'meta', {
property:"og:type",
content:"website"
}],
[ 'meta', {
property:"og:site_name",
content: title,
}],
[ 'meta', {
property:"og:title",
content: socialTitle,
}],
[ 'meta', {
property:"og:description",
content: description
}],
[ 'meta', {
property:"og:url",
content:"https://lucide.dev"
}],
[ 'meta', {
property:"og:image",
content: "https://lucide.dev/og.png"
}],
[ 'meta', {
property:"og:image:width",
content:"1200"
}],
[ 'meta', {
property:"og:image:height",
content:"630"
}],
[ 'meta', {
property:"og:image:type",
content:"image/png"
}],
[ 'meta', {
property:"twitter:card",
content:"summary_large_image"
}],
[ 'meta', {
property:"twitter:title",
content: socialTitle,
}],
[ 'meta', {
property:"twitter:description",
content: description
}],
[ 'meta', {
property:"twitter:image",
content:"https://lucide.dev/og.png"
}],
],
themeConfig: {
// https://vitepress.dev/reference/default-theme-config
logo: {
light: '/logo.light.svg',
dark: '/logo.dark.svg'
},
nav: [
{ text: 'Icons', link: '/icons/' },
{ text: 'Guide', link: '/guide/' },
{ text: 'Packages', link: '/packages' },
{ text: 'License', link: '/license' },
],
sidebar,
socialLinks: [
{ icon: 'github', link: 'https://github.com/lucide-icons/lucide' },
{ icon: 'discord', link: 'https://discord.gg/EH6nSts' }
],
footer: {
message: 'Released under the ISC License.',
copyright: `Copyright © ${new Date().getFullYear()} Lucide Contributors`
},
editLink: {
pattern: 'https://github.com/lucide-icons/lucide/edit/main/docs/:path'
},
},
transformHtml: (_, id, { pageData }) => {
if (/[\\/]404\.html$/.test(id)) {
return
}
if (pageData.relativePath === 'index.md') {
console.log('Home!');
}
if (pageData.relativePath.startsWith('icons/')) {
links.push({
url: pageData.relativePath.replace(/((^|\/)index)?\.md$/, '$2'),
lastmod: pageData?.params?.changedRelease?.date
})
return
}
links.push({
url: pageData.relativePath.replace(/((^|\/)index)?\.md$/, '$2'),
lastmod: pageData.lastUpdated
})
},
buildEnd: async ({ outDir }) => {
const sitemap = new SitemapStream({
hostname: 'https://lucide.dev/'
})
const writeStream = createWriteStream(resolve(outDir, 'sitemap.xml'))
sitemap.pipe(writeStream)
links.forEach((link) => sitemap.write(link))
sitemap.end()
await new Promise((r) => writeStream.on('finish', r))
},
})
@@ -22,6 +22,7 @@
"name": "hyva-lucide-icons",
"description": "Implementation of Lucide icon's using Hyvä's svg php viewmodal to render icons for Magento 2 Hyva theme based projects.",
"icon": "/framework-logos/hyva.svg",
"iconDark": "/framework-logos/hyva-dark.svg",
"shields": [
{
"alt": "Latest Stable Version",
@@ -41,6 +42,7 @@
"name": "eleventy-lucide-icons",
"description": "Using this plugin, Eleventy projects can incorporate Lucide icons. it makes it simple to use Lucide icons into your themes via shortcodes, improving your website's overall usability and visual appeal.",
"icon": "/framework-logos/11ty.svg",
"iconClass": "package-icon-invert",
"shields": [
{
"alt": "Latest Stable Version",
@@ -55,5 +57,24 @@
],
"source": "https://github.com/GrimLink/eleventy-plugin-lucide-icons",
"documentation": "https://github.com/GrimLink/eleventy-plugin-lucide-icons/blob/main/README.md"
},
{
"name": "nuxt-lucide-icons",
"description": "Using this module, Nuxt projects can incorporate Lucide icons. It is fully configurable and supports auto imports and tree-shaking.",
"icon": "/framework-logos/nuxt.svg",
"shields": [
{
"alt": "Latest Stable Version",
"src": "https://img.shields.io/npm/v/nuxt-lucide-icons",
"href": "https://www.npmjs.com/package/nuxt-lucide-icons"
},
{
"alt": "Total Downloads",
"src": "https://img.shields.io/npm/dw/nuxt-lucide-icons",
"href": "https://www.npmjs.com/package/nuxt-lucide-icons"
}
],
"source": "https://github.com/swisnl/nuxt-lucide-icons",
"documentation": "https://github.com/swisnl/nuxt-lucide-icons/blob/main/README.md"
}
]
@@ -0,0 +1,71 @@
import React from 'react';
interface BackdropProps {
src: string
backdropString: string
}
const Backdrop = ({ src, backdropString }: BackdropProps): JSX.Element => {
return (
<>
<defs xmlns="http://www.w3.org/2000/svg">
<pattern
id="pattern"
width=".1"
height=".1"
patternUnits="userSpaceOnUse"
patternTransform="rotate(45 50 50)"
>
<line stroke="red" strokeWidth={0.1} y2={1} />
<line stroke="red" strokeWidth={0.1} y2={1} />
</pattern>
</defs>
<mask id="svg-preview-backdrop-mask-outline" maskUnits="userSpaceOnUse">
<g stroke="#fff" dangerouslySetInnerHTML={{ __html: backdropString }} />
<g dangerouslySetInnerHTML={{ __html: src }} strokeWidth={2.05} />
</mask>
<mask id="svg-preview-backdrop-mask-fill" maskUnits="userSpaceOnUse">
<g stroke="#fff" dangerouslySetInnerHTML={{ __html: backdropString }} />
<g dangerouslySetInnerHTML={{ __html: src }} strokeWidth={2.05} />
<g strokeWidth={1.75} dangerouslySetInnerHTML={{ __html: backdropString }} />
</mask>
<g
strokeWidth={2.25}
stroke="url(#pattern)"
mask={'url(#svg-preview-backdrop-mask-outline)'}
>
<rect
x="0"
y="0"
width="24"
height="24"
fill="url(#pattern)"
opacity={0.5}
stroke="none"
/>
</g>
<rect
x="0"
y="0"
width="24"
height="24"
fill="url(#pattern)"
stroke="none"
mask={'url(#svg-preview-backdrop-mask-fill)'}
/>
<rect
x="0"
y="0"
width="24"
height="24"
fill="red"
opacity={0.5}
stroke="none"
mask={'url(#svg-preview-backdrop-mask-fill)'}
/>
</>
)
}
export default Backdrop;
@@ -173,6 +173,31 @@ const ControlPath = ({
);
};
const Radii = ({
paths,
...props
}: { paths: Path[] } & PathProps<
'strokeWidth' | 'stroke' | 'strokeDasharray' | 'strokeOpacity',
any
>) => {
return (
<g className="svg-preview-radii-group" {...props}>
{paths
.filter(({ circle }) => circle)
.map(({ c, prev, next, circle: { x, y, r } }) =>
c.name === 'circle' ? (
<path d={`M${x} ${y}h.01`} />
) : (
<>
<path d={`M${prev.x} ${prev.y} ${x} ${y} ${next.x} ${next.y}`} />
<circle cy={y} cx={x} r={r} />
</>
)
)}
</g>
);
};
const SvgPreview = React.forwardRef<
SVGSVGElement,
{
@@ -184,6 +209,7 @@ const SvgPreview = React.forwardRef<
const darkModeCss = `@media screen and (prefers-color-scheme: dark) {
.svg-preview-grid-group,
.svg-preview-radii-group,
.svg-preview-shadow-mask-group,
.svg-preview-shadow-group {
stroke: #fff;
@@ -223,6 +249,13 @@ const SvgPreview = React.forwardRef<
'#52A675',
]}
/>
<Radii
paths={paths}
strokeWidth={0.12}
strokeDasharray="0 0.25 0.25"
stroke="#777"
strokeOpacity={0.3}
/>
<ControlPath radius={1} paths={paths} pointSize={1} stroke="#fff" strokeWidth={0.125} />
{children}
</svg>
@@ -8,6 +8,7 @@ export type Path = {
prev: Point;
next: Point;
isStart: boolean;
circle: { x: number; y: number; r: number };
c: ReturnType<typeof getCommands>[number];
};
@@ -12,33 +12,66 @@ export function assert(value: unknown): asserts value {
}
}
const extractPaths = (node: INode): { d: string; name: typeof node.name }[] => {
if (/(rect|circle|ellipse|polygon|polyline|line|path)/.test(node.name)) {
return [{ d: toPath(node), name: node.name }];
const convertToPathNode = (node: INode): { d: string; name: typeof node.name } => {
if (node.name === 'path') {
return { d: node.attributes.d, name: node.name };
}
if (node.name === 'circle') {
const cx = parseFloat(node.attributes.cx);
const cy = parseFloat(node.attributes.cy);
const r = parseFloat(node.attributes.r);
return {
d: [
`M ${cx} ${cy - r}`,
`a ${r} ${r} 0 0 1 ${r} ${r}`,
`a ${r} ${r} 0 0 1 ${0 - r} ${r}`,
`a ${r} ${r} 0 0 1 ${0 - r} ${0 - r}`,
`a ${r} ${r} 0 0 1 ${r} ${0 - r}`,
].join(''),
name: node.name,
};
}
return { d: toPath(node).replace(/z$/i, ''), name: node.name };
};
const extractNodes = (node: INode): INode[] => {
if (['rect', 'circle', 'ellipse', 'polygon', 'polyline', 'line', 'path'].includes(node.name)) {
return [node];
} else if (node.children && Array.isArray(node.children)) {
return node.children.flatMap(extractPaths);
return node.children.flatMap(extractNodes);
}
return [];
};
export const getNodes = (src: string) =>
extractNodes(parseSync(src.includes('<svg') ? src : `<svg>${src}</svg>`));
export const getCommands = (src: string) =>
extractPaths(parseSync(src)).flatMap(({ d, name }, idx) =>
new SVGPathData(d).toAbs().commands.map((c) => ({ ...c, id: idx, name }))
);
getNodes(src)
.map(convertToPathNode)
.flatMap(({ d, name }, idx) =>
new SVGPathData(d).toAbs().commands.map((c, cIdx) => ({ ...c, id: idx, idx: cIdx, name }))
);
export const getPaths = (src: string) => {
const commands = getCommands(src.includes('<svg') ? src : `<svg>${src}</svg>`);
const paths: Path[] = [];
let prev: Point | undefined = undefined;
let start: Point | undefined = undefined;
const addPath = (c: (typeof commands)[number], next: Point, d?: string) => {
const addPath = (
c: typeof commands[number],
next: Point,
d?: string,
circle?: Path['circle']
) => {
assert(prev);
paths.push({
c,
d: d || `M${prev.x} ${prev.y}L${next.x} ${next.y}`,
d: d || `M ${prev.x} ${prev.y} L ${next.x} ${next.y}`,
prev,
next,
circle,
isStart: start === prev,
});
prev = next;
@@ -77,7 +110,7 @@ export const getPaths = (src: string) => {
}
case SVGPathData.CURVE_TO: {
assert(prev);
addPath(c, c, `M ${prev.x},${prev.y} ${encodeSVGPath(c)}`);
addPath(c, c, `M ${prev.x} ${prev.y} ${encodeSVGPath(c)}`);
break;
}
case SVGPathData.SMOOTH_CURVE_TO: {
@@ -86,16 +119,16 @@ export const getPaths = (src: string) => {
const reflectedCp1 = {
x:
previousCommand &&
(previousCommand.type === SVGPathData.SMOOTH_CURVE_TO ||
previousCommand.type === SVGPathData.CURVE_TO)
(previousCommand.type === SVGPathData.SMOOTH_CURVE_TO ||
previousCommand.type === SVGPathData.CURVE_TO)
? previousCommand.relative
? previousCommand.x2 - previousCommand.x
: previousCommand.x2 - prev.x
: 0,
y:
previousCommand &&
(previousCommand.type === SVGPathData.SMOOTH_CURVE_TO ||
previousCommand.type === SVGPathData.CURVE_TO)
(previousCommand.type === SVGPathData.SMOOTH_CURVE_TO ||
previousCommand.type === SVGPathData.CURVE_TO)
? previousCommand.relative
? previousCommand.y2 - previousCommand.y
: previousCommand.y2 - prev.y
@@ -104,7 +137,7 @@ export const getPaths = (src: string) => {
addPath(
c,
c,
`M ${prev.x},${prev.y} ${encodeSVGPath({
`M ${prev.x} ${prev.y} ${encodeSVGPath({
type: SVGPathData.CURVE_TO,
relative: false,
x: c.x,
@@ -119,7 +152,7 @@ export const getPaths = (src: string) => {
}
case SVGPathData.QUAD_TO: {
assert(prev);
addPath(c, c, `M ${prev.x},${prev.y} ${encodeSVGPath(c)}`);
addPath(c, c, `M ${prev.x} ${prev.y} ${encodeSVGPath(c)}`);
break;
}
case SVGPathData.SMOOTH_QUAD_TO: {
@@ -157,7 +190,7 @@ export const getPaths = (src: string) => {
addPath(
c,
c,
`M ${prev.x},${prev.y} ${encodeSVGPath({
`M ${prev.x} ${prev.y} ${encodeSVGPath({
type: SVGPathData.QUAD_TO,
relative: false,
x: c.x,
@@ -170,10 +203,22 @@ export const getPaths = (src: string) => {
}
case SVGPathData.ARC: {
assert(prev);
const center = arcEllipseCenter(
prev.x,
prev.y,
c.rX,
c.rY,
c.xRot,
c.lArcFlag,
c.sweepFlag,
c.x,
c.y
);
addPath(
c,
c,
`M ${prev.x},${prev.y} A ${c.rX} ${c.rY} ${c.xRot} ${c.lArcFlag} ${c.sweepFlag} ${c.x} ${c.y}`
`M ${prev.x} ${prev.y} A${c.rX} ${c.rY} ${c.xRot} ${c.lArcFlag} ${c.sweepFlag} ${c.x} ${c.y}`,
c.rX === c.rY ? { ...center, r: c.rX } : undefined
);
break;
}
@@ -184,3 +229,58 @@ export const getPaths = (src: string) => {
}
return paths;
};
export const arcEllipseCenter = (
x1: number,
y1: number,
rx: number,
ry: number,
a: number,
fa: number,
fs: number,
x2: number,
y2: number
) => {
const phi = (a * Math.PI) / 180;
const M = [
[Math.cos(phi), Math.sin(phi)],
[-Math.sin(phi), Math.cos(phi)],
];
const V = [(x1 - x2) / 2, (y1 - y2) / 2];
const [x1p, y1p] = [M[0][0] * V[0] + M[0][1] * V[1], M[1][0] * V[0] + M[1][1] * V[1]];
rx = Math.abs(rx);
ry = Math.abs(ry);
const lambda = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry);
if (lambda > 1) {
rx = Math.sqrt(lambda) * rx;
ry = Math.sqrt(lambda) * ry;
}
const sign = fa === fs ? -1 : 1;
const co =
sign *
Math.sqrt(
Math.max(rx * rx * ry * ry - rx * rx * y1p * y1p - ry * ry * x1p * x1p, 0) /
(rx * rx * y1p * y1p + ry * ry * x1p * x1p)
);
const V2 = [(rx * y1p) / ry, (-ry * x1p) / rx];
const Cp = [V2[0] * co, V2[1] * co];
const M2 = [
[Math.cos(phi), -Math.sin(phi)],
[Math.sin(phi), Math.cos(phi)],
];
const V3 = [(x1 + x2) / 2, (y1 + y2) / 2];
const C = [
M2[0][0] * Cp[0] + M2[0][1] * Cp[1] + V3[0],
M2[1][0] * Cp[0] + M2[1][1] * Cp[1] + V3[1],
];
return { x: C[0], y: C[1] };
};
+28
View File
@@ -0,0 +1,28 @@
import fs from "fs";
import path from "path";
import {Category, IconEntity} from "../theme/types";
const directory = path.join(process.cwd(), "../categories");
export function getAllCategoryFiles(): Category[] {
const fileNames = fs.readdirSync(directory).filter((file) => path.extname(file) === '.json');
return fileNames.map((fileName) => {
const name = path.basename(fileName, '.json')
const fileContent = fs.readFileSync(path.join(directory, fileName), 'utf8')
const parsedFileContent = JSON.parse(fileContent)
return {
name,
title: parsedFileContent.title,
}
});
}
export function mapCategoryIconCount(categories: Category[], icons: { categories: IconEntity['categories'] }[]) {
return categories.map((category) => ({
...category,
iconCount: icons.reduce((acc, curr) => (curr.categories.includes(category.name) ? ++acc : acc), 0)
}))
}
+218
View File
@@ -0,0 +1,218 @@
import {
BUNDLED_LANGUAGES,
type IThemeRegistration
} from 'shiki'
import {
getHighlighter,
} from 'shiki-processor'
type CodeExampleType = {
title: string,
lang: string,
codes: {
language?: string,
code: string,
metastring?: string,
}[],
}[]
const getIconCodes = (): CodeExampleType => {
return [
{
lang: 'html',
title: 'HTML',
codes: [
{
language: 'html',
code: `<i data-lucide-name="Name"></i>
`,
},
],
},
{
lang: 'tsx',
title: 'React',
codes: [
{
language: 'tsx',
code: `import { PascalCase } from 'lucide-react';
const App = () => {
return (
<PascalCase />
);
};
export default App;
`,
},
],
},
{
lang: 'vue',
title: 'Vue 3',
codes: [
{
language: 'vue',
code: `<script setup>
import { PascalCase } from 'lucide-vue-next';
</script>
<template>
<PascalCase />
</template>
`,
},
],
},
{
lang: 'svelte',
title: 'Svelte',
codes: [
{
language: 'svelte',
code: `<script>
import { PascalCase } from 'lucide-svelte';
</script>
<PascalCase />
`,
},
],
},
{
lang: 'preact',
title: 'Preact',
codes: [
{
language: 'tsx',
code: `import { PascalCase } from 'lucide-preact';
const App = () => {
return (
<PascalCase />
);
};
export default App;
`,
},
],
},
{
lang: 'solid',
title: 'Solid',
codes: [
{
language: 'tsx',
code: `import { PascalCase } from 'lucide-solid';
const App = () => {
return (
<PascalCase />
);
};
export default App;
`,
},
],
},
{
lang: 'angular',
title: 'Angular',
codes: [
{
language: 'tsx',
code: `// app.module.ts
import { LucideAngularModule, PascalCase } from 'lucide-angular';
@NgModule({
imports: [
LucideAngularModule.pick({ PascalCase })
],
})
// app.component.html
<lucide-icon name="Name"></lucide-icon>
`,
},
],
},
{
lang: 'html',
title: 'Icon Font',
codes: [
{
language: 'html',
code: `<style>
@import ('~lucide-static/font/Lucide.css');
</style>
<div class="icon-Name"></div>
`,
},
],
},
{
lang: 'dart',
title: 'Flutter',
codes: [
{
language: 'dart',
code: `Icon(LucideIcons.Name);
`,
},
],
},
]
}
export type ThemeOptions =
| IThemeRegistration
| { light: IThemeRegistration; dark: IThemeRegistration }
const highLightCode = async (code: string, lang: string, active?: boolean) => {
const highlighter = await getHighlighter({
themes: ['material-theme-palenight'],
langs: [...BUNDLED_LANGUAGES],
processors: []
})
const highlightedCode = highlighter.codeToHtml(code, {
lang,
// lineOptions,
theme: 'material-theme-palenight'
}).replace('background-color: #292D3E', '')
return `<div class="language-${lang} ${active ? 'active' : ''}">
<button title="Copy Code" class="copy"></button>
<span class="lang">${lang}</span>
${highlightedCode}
</div>`
}
export default async function createCodeExamples() {
const codes = getIconCodes();
const codeExamplePromises = codes.map(async (codeTemplate, index) => {
const { title, lang, codes } = codeTemplate;
const isFirst = index === 0;
const code = await highLightCode(codes[0].code, codes[0].language || lang, isFirst);
return {
title,
language: codes[0].language || lang,
code,
};
})
return Promise.all(codeExamplePromises);
}
@@ -1,7 +1,7 @@
import { promises as fs, constants } from 'fs';
import path from 'path';
import yaml from 'js-yaml'
import { PackageItem } from '../components/Package';
import { PackageItem } from '../theme/types';
const fileExist = (filePath) => fs.access(filePath, constants.F_OK).then(() => true).catch(() => false)
@@ -1,12 +1,13 @@
import { createLucideIcon, LucideProps } from "lucide-react"
import { IconEntity } from "src/types"
import { createLucideIcon } from "lucide-react/src/lucide-react"
import { type LucideProps, type IconNode } from "lucide-react/src/createLucideIcon"
import { IconEntity } from "../theme/types"
import { renderToStaticMarkup } from 'react-dom/server';
import { IconContent } from "./generateZip";
const getFallbackZip = (icons: IconEntity[], params: LucideProps) => {
return icons
.map<IconContent>((icon) => {
const Icon = createLucideIcon(icon.name, icon.iconNode)
const Icon = createLucideIcon(icon.name, icon.iconNode as IconNode)
const src = renderToStaticMarkup(<Icon {...params} />)
return [icon.name, src]
})
+55
View File
@@ -0,0 +1,55 @@
import fs from "fs";
import path from "path";
import { IconNodeWithKeys } from "../theme/types";
import iconNodes from '../data/iconNodes'
import releaseMeta from "../data/releaseMetaData.json";
const DATE_OF_FORK = '2020-06-08T16:39:52+0100';
const directory = path.join(process.cwd(), "../icons");
export function getAllNames() {
const fileNames = fs.readdirSync(directory).filter((file) => path.extname(file) === '.json');
return fileNames
.filter((fileName) => fs.existsSync(directory + '/' + path.basename(fileName, '.json') + '.svg'))
.map((fileName) => path.basename(fileName, '.json'));
}
export interface GetDataOptions {
withChildKeys?: boolean
}
export async function getData(name: string) {
const jsonPath = path.join(directory, `${name}.json`);
const jsonContent = fs.readFileSync(jsonPath, "utf8");
const { tags, categories, contributors } = JSON.parse(jsonContent);
const iconNode = iconNodes[name]
const releaseData = releaseMeta?.[name] ?? {
"createdRelease": {
"version": "0.0.0",
"date": DATE_OF_FORK
},
"changedRelease": {
"version": "0.0.0",
"date": DATE_OF_FORK
}
}
return {
name,
tags,
categories,
iconNode,
contributors,
...releaseData
};
}
export async function getAllData(): Promise<{ name: string, iconNode: IconNodeWithKeys}[]> {
const names = getAllNames();
return Promise.all(names.map((name) => getData(name)));
}
+118
View File
@@ -0,0 +1,118 @@
import { DefaultTheme, UserConfig } from "vitepress"
const sidebar: UserConfig<DefaultTheme.Config>['themeConfig']['sidebar'] = {
'guide':[
{
text: 'Introduction',
items: [
{ text: 'What is lucide?', link: '/guide/' },
{ text: 'Installation', link: '/guide/installation' },
{ text: 'Comparison', link: '/guide/comparison' }
]
},
// {
// text: 'Using Icons',
// items: [
// {
// text: 'How to use icons',
// link: 'how-to-use-icons'
// },
// {
// text: 'Styling icons',
// link: 'styling-icons'
// },
// {
// text: 'Accessibility',
// link: 'accessibility'
// },
// {
// text: 'What should I use',
// link: 'what-should-i-use'
// },
// ]
// },
{
text: 'Packages',
items: [
{
text: 'Lucide',
link: '/guide/packages/lucide'
},
{
text: 'Lucide React',
link: '/guide/packages/lucide-react'
},
{
text: 'Lucide React Native',
link: '/guide/packages/lucide-react-native'
},
{
text: 'Lucide Vue',
link: '/guide/packages/lucide-vue'
},
{
text: 'Lucide Vue Next (Vue 3)',
link: '/guide/packages/lucide-vue-next'
},
{
text: 'Lucide Svelte',
link: '/guide/packages/lucide-svelte'
},
{
text: 'Lucide Solid',
link: '/guide/packages/lucide-solid'
},
{
text: 'Lucide Preact',
link: '/guide/packages/lucide-preact'
},
{
text: 'Lucide Angular',
link: '/guide/packages/lucide-angular'
},
{
text: 'Lucide Static',
link: '/guide/packages/lucide-static'
},
{
text: 'Lucide Flutter',
link: '/guide/packages/lucide-flutter'
},
]
},
{
text: 'Contributing',
items: [
{
text: 'Icon Design Principles',
link: '/guide/design/icon-design-guide'
},
{
text: 'Designing in Illustrator',
link: '/guide/design/illustrator-guide'
},
{
text: 'Designing in InkScape',
link: '/guide/design/inkscape-guide'
},
{
text: 'Designing in Figma',
link: '/guide/design/figma-guide'
},
]
},
],
'icons': [
{ text: '', link: '/' },
// { text: 'Categorized', link: '/icons/categorized' },
// {
// text: 'Categories',
// items: [
// ...(getAllCategoryFiles().map((category) => ({ text: category, link: `/icons/category/${category}` })))
// ]
// }
],
}
export default sidebar
@@ -0,0 +1,11 @@
<template>
<div class="container">
<slot />
</div>
</template>
<style scoped>
.container {
padding: 32px;
}
</style>
@@ -0,0 +1,62 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useRouter } from 'vitepress';
const { go } = useRouter()
const props = defineProps<{
href?: string
}>()
const component = computed(() => props.href ? 'a' : 'div')
</script>
<template>
<component
:is="component"
:href="href"
class="badge"
@click="props?.href ? go(href) : undefined"
>
<slot/>
</component>
</template>
<style>
.badge, a.badge {
display: block;
border: 1px solid transparent;
text-align: center;
font-weight: 600;
padding: 2px 12px;
white-space: nowrap;
transition: color 0.25s, border-color 0.25s, background-color 0.25s;
border-radius: 6px;
background-color: var(--vp-c-bg-alt);
color: var(--vp-c-text-1);
/* width: 56px;
height: 56px; */
font-size: 16px;
}
.badge[href]:hover, a.badge:hover {
border-color: var(--vp-button-alt-hover-border);
color: var(--vp-button-alt-hover-text);
background-color: var(--vp-button-alt-hover-bg);
text-decoration: none;
}
.badge[href]:active {
border-color: var(--vp-button-alt-active-border);
color: var(--vp-button-alt-active-text);
background-color: var(--vp-button-alt-active-bg);
}
.badge.active {
border-color: var(--vp-c-brand);
/* color: var(--vp-button-alt-active-text);
background-color: var(--vp-button-alt-active-bg); */
}
</style>
@@ -0,0 +1,187 @@
<script setup lang="ts">
import VPButton from 'vitepress/dist/client/theme-default/components/VPButton.vue'
import { computed, ref } from 'vue'
import {
Listbox,
ListboxButton,
ListboxOptions,
ListboxOption,
} from '@headlessui/vue'
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon'
import { chevronUp } from '../../../data/iconNodes'
import { useStorage } from '@vueuse/core'
interface Props {
options: {
text: string
onClick?: () => void
}[],
callOptionOnClick?: boolean
buttonClass?: string
id: string
popoverPosition?: 'top' | 'bottom'
}
const props = withDefaults(defineProps<Props>(), {
callOptionOnClick: false,
popoverPosition: 'bottom'
})
const emit = defineEmits(['click', 'optionClick'])
const buttonRef = ref(null)
const selectedOption = useStorage(props.id, props.options[0].text)
const selectionOptionAction = computed(() => props.options.find(option => option.text === selectedOption.value).onClick)
function onClick(event) {
selectionOptionAction.value()
emit('click', event)
}
function onOptionClick(event, option) {
if(!props.callOptionOnClick) {
return
}
option.onClick()
emit('optionClick', event)
}
const ChevronUp = createLucideIcon('ChevronUp', chevronUp)
</script>
<template>
<Listbox v-model="selectedOption">
<div class="menu" >
<div class="button-wrapper">
<VPButton
v-bind="$attrs"
:text="selectedOption"
@click="onClick"
theme="alt"
class="main-button"
:class="[props.buttonClass]"
ref="buttonRef"
/>
<ListboxButton
:as="VPButton"
:text="''"
theme="alt"
class="arrow-up-button"
:class="popoverPosition"
/>
</div>
<ListboxOptions class="menu-items" :class="popoverPosition">
<ListboxOption
as="button"
class="menu-item"
v-for="option in options"
:value="option.text"
@click="onOptionClick($event, option)"
>
{{ option.text }}
</ListboxOption>
</ListboxOptions>
</div>
</Listbox>
</template>
<style>
.menu {
position: relative;
}
.menu-items {
--menu-offset: 44px;
position: absolute;
display: flex;
flex-direction: column;
border-radius: 12px;
padding: 12px;
min-width: 128px;
border: 1px solid var(--vp-c-divider);
background-color: var(--vp-c-bg);
box-shadow: var(--vp-shadow-3);
transition: background-color 0.5s;
max-height: calc(100vh - var(--vp-nav-height));
overflow-y: auto;
z-index: 90;
right: 0;
}
.menu-item {
padding: 2px 8px;
text-align: left;
display: block;
border-radius: 6px;
padding: 0 12px;
line-height: 32px;
font-size: 14px;
font-weight: 500;
color: var(--vp-c-text-1);
white-space: nowrap;
transition: background-color .25s,color .25s;
}
.menu-item:hover {
color: var(--vp-c-brand);
background-color: var(--vp-c-bg-elv-mute);
}
.menu-item:active {
color: var(--vp-c-brand);
background-color: var(--vp-c-bg-elv);
}
.main-button {
border-top-right-radius: 0 !important;
border-bottom-right-radius: 0 !important;
padding-right: 12px !important;
}
.button-wrapper {
display: flex;
}
.arrow-up-button {
display: inline-flex;
height: 40px;
border-top-left-radius: 0 !important;
border-bottom-left-radius: 0 !important;
padding-left: 4px !important;
padding-right: 8px !important;
position: relative;
left: -1px;
}
.arrow-up-button::before {
content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%0A%3E%3Cpolyline points='18 15 12 9 6 15' /%3E%3C/svg%3E%0A");
width: 20px;
height: 28px;
margin: auto;
display: block;
}
.dark .arrow-up-button::before {
content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%0A%3E%3Cpolyline points='18 15 12 9 6 15' /%3E%3C/svg%3E%0A");
}
.menu-items.bottom {
top: var(--menu-offset);
}
.menu-items.top {
bottom: var(--menu-offset);
}
.arrow-up-button.top::before {
transform: rotate(0deg);
}
.arrow-up-button.bottom::before {
transform: rotate(180deg);
}
</style>
@@ -0,0 +1,51 @@
<script setup lang="ts">
import { computed, defineProps, onMounted } from 'vue'
const props = defineProps<{
groups: string[] | undefined,
groupName: string,
}>()
const getSaveIdname = (name: string) => {
return name.toLowerCase().replace(/\s/g, '-')
}
const tabs = computed(() => props.groups?.map((group) => {
return {
id: getSaveIdname(group),
name: group,
}
}))
const saveTabId = (id: string) => {
localStorage.setItem(props.groupName, id)
}
onMounted(() => {
const id = localStorage.getItem(props.groupName)
if (id) {
const tab = document.getElementById(`label-tab-${id}`)
if (tab) {
tab.click()
}
}
})
</script>
<template>
<div class="vp-code-group">
<div class="tabs">
<template v-for="(tab, index) in tabs">
<input
type="radio"
:id="`tab-${tab.id}`"
:name="`group-${groupName}`"
:checked="index === 0"
@change="saveTabId(tab.id)"
>
<label :for="`tab-${tab.id}`" :id="`label-tab-${tab.id}`">{{ tab.name }}</label>
</template>
</div>
<slot />
</div>
</template>
@@ -0,0 +1,93 @@
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps<{
modelValue: string
id: string
}>()
const emit = defineEmits(['update:modelValue'])
const value = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
</script>
<template>
<div class="color-picker">
<div class="color-input-wrapper">
<!-- TODO: Add currentColor div if value is currentColor -->
<input
type="color"
:id="id"
:name="id"
class="color-input"
v-model="value"
/>
</div>
<input
type="text"
:id="`${id}-input`"
:name="`${id}-input`"
class="color-input-text"
aria-label="Color picker input"
v-model="value"
/>
</div>
</template>
<style scoped>
.color-input {
width: 34px;
height: 34px;
position: absolute;
top: -5px;
left: -5px;
}
.color-input-wrapper {
height: 24px;
width: 24px;
overflow: hidden;
position: relative;
border-radius: 12px;
flex-shrink: 0;
}
.color-picker {
background: var(--color-picker-bg, var(--vp-c-bg-soft));
border-radius: 8px;
color: var(--vp-c-text-2);
padding: 4px 8px;
height: auto;
font-size: 14px;
text-align: left;
border: 1px solid transparent;
cursor: text;
display: flex;
align-items: center;
gap: 2px;
}
.color-input-text {
width: 100%;
height: 100%;
padding: 0 8px;
border: none;
background: transparent;
color: var(--vp-c-text-2);
font-size: 14px;
text-align: left;
border-radius: 8px;
cursor: text;
transition: border-color 0.25s, background 0.4s ease;
}
.color-picker:hover, .color-picker:focus {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-alt);
}
.color-input[value="currentColor"] {
}
</style>
@@ -0,0 +1,15 @@
<script setup lang="ts">
import { vIntersectionObserver } from '@vueuse/components'
const emit = defineEmits(['end-of-page'])
const onIntersectionObserver: IntersectionObserverCallback = ([{ isIntersecting }]) => {
if (isIntersecting) {
emit('end-of-page')
}
}
</script>
<template>
<div v-intersection-observer="onIntersectionObserver" />
</template>
@@ -0,0 +1,36 @@
<script setup>
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon'
import { search } from '../../../data/iconNodes'
const SearchIcon = createLucideIcon('search', search)
</script>
<template>
<button class="fake-input">
<component :is="SearchIcon" class="search-icon"/>
<slot/>
</button>
</template>
<style scoped>
.fake-input {
background: var(--vp-c-bg-soft);
border-radius: 8px;
color: var(--vp-c-text-2);
padding: 12px 16px;
height: auto;
font-size: 14px;
/* box-shadow: var(--vp-shadow-4), var(--vp-shadow-2); */
text-align: left;
border: 1px solid transparent;
cursor: text;
display: flex;
gap: 12px;
transition: color 0.25s, border-color 0.25s, background-color 0.25s;
}
.fake-input:hover, .fake-input:focus {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-alt);
}
</style>
@@ -0,0 +1,43 @@
<template>
<button v-bind="$attrs" class="icon-button">
<slot />
</button>
</template>
<style scoped>
.icon-button {
display: inline-flex;
border: 1px solid transparent;
text-align: center;
font-weight: 600;
padding: 6px;
border-radius: 8px;
white-space: nowrap;
transition: color 0.25s, border-color 0.25s, background-color 0.25s;
border-radius: 6px;
background-color: var(--vp-c-bg-alt);
/* width: 56px;
height: 56px; */
font-size: 24px;
transition: color 0.1s, border-color 0.1s, background-color 0.1s;
}
.icon-button:hover {
border-color: var(--vp-button-alt-hover-border);
color: var(--vp-button-alt-hover-text);
background-color: var(--vp-button-alt-hover-bg);
}
.icon-button:active {
border-color: var(--vp-button-alt-active-border);
color: var(--vp-button-alt-active-text);
background-color: var(--vp-button-alt-active-bg);
}
.icon-button.active {
border-color: var(--vp-c-brand);
/* color: var(--vp-button-alt-active-text);
background-color: var(--vp-button-alt-active-bg); */
}
</style>
@@ -0,0 +1,81 @@
<script lang="ts">
export default {
inheritAttrs: false,
}
export interface InputProps {
type: string
modelValue: string
}
</script>
<script setup lang="ts">
import { ref } from 'vue'
const props = withDefaults(defineProps<InputProps>(), {
type: 'text'
})
const input = ref()
defineEmits(['change', 'input', 'update:modelValue'])
defineExpose({
focus: () => {
input.value.focus()
}
})
</script>
<template>
<div class="input-wrapper">
<slot name="icon" class="icon" />
<input
:type="type"
class="input"
:class="{'has-icon': $slots.icon}"
ref="input"
:value="modelValue"
v-bind="$attrs"
@input="$emit('update:modelValue', $event.target.value)"
/>
</div>
</template>
<style scoped>
.input-wrapper {
position: relative;
}
.input {
justify-content: flex-start;
border: 1px solid transparent;
border-radius: 8px;
padding: 0 10px 0 12px;
width: 100%;
height: 40px;
background-color: var(--vp-c-bg-alt);
font-size: 14px;
}
.input:hover, .input:focus {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-alt);
}
.input.has-icon {
padding-left: 52px;
}
</style>
<style>
.input-wrapper svg {
position: absolute;
left: 16px;
top: 12px;
z-index: 1;
color: var(--vp-c-text-2);
pointer-events: none;
}
</style>
@@ -0,0 +1,48 @@
<script setup lang="ts">
defineProps<{
label: string
id: string
}>()
</script>
<template>
<div class="input-field">
<div class="input-label" v-if="label">
<label :for="id" class="customize-label">
{{ label }}
</label>
<div class="display-value" >
<slot name="display"/>
</div>
</div>
<slot />
</div>
</template>
<style scoped>
.customize-label {
line-height: 20px;
font-size: 13px;
font-weight: 600;
color: var(--vt-c-text-1);
transition: color .5s;
display: block;
}
.input-field:not(:last-child) {
margin-bottom: 16px;
}
.display-value {
font-size: 13px;
color: var(--vp-c-text-2);
}
.input-label {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
gap: 12px;
}
</style>
@@ -0,0 +1,74 @@
<script lang="ts">
export default {
inheritAttrs: false,
}
</script>
<script setup lang="ts">
import { computed, ref } from 'vue'
import Input from './Input.vue'
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon'
import { search } from '../../../data/iconNodes'
const SearchIcon = createLucideIcon('search', search)
interface Props {
modelValue: string
}
const props = defineProps<Props>()
const input = ref()
const emit = defineEmits(['update:modelValue'])
defineExpose({
focus: () => {
input.value.focus()
}
})
const value = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
</script>
<template>
<Input
ref="input"
type="search"
v-bind="$attrs"
v-model="value"
class="input-wrapper"
>
<template #icon>
<component :is="SearchIcon" class="search-icon" />
</template>
</Input>
</template>
<style scoped>
.input {
justify-content: flex-start;
border: 1px solid transparent;
border-radius: 8px;
padding: 0 10px 0 12px;
width: 100%;
height: 40px;
background-color: var(--vp-c-bg-alt);
}
.input:hover, .input:focus {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-alt);
}
.input-wrapper:deep(.input) {
/* padding: 12px 24px; */
padding-block: 12px;
font-size: 14px;
height: 48px;
}
</style>
@@ -0,0 +1,16 @@
<template>
<h2 class="label"><slot/></h2>
</template>
<style scoped>
.label {
letter-spacing: 0.4px;
line-height: 28px;
font-size: 14px;
font-weight: 600;
border: 0;
padding: 0;
margin: 0;
}
</style>
@@ -0,0 +1,19 @@
<script setup lang="ts">
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon';
export type IconNode = [elementName: string, attrs: Record<string, string>][]
const props = defineProps<{
name: string;
tags: string[];
categories: string[];
// contributors: Contributor[];
iconNode: IconNode;
}>()
const icon = createLucideIcon(props.name, props.iconNode)
</script>
<template>
<component :is="icon" />
</template>
@@ -0,0 +1,127 @@
<script lang="ts">
export default {
inheritAttrs: false
}
</script>
<script lang="ts" setup>
import { computed } from "vue";
interface Props {
modelValue: number | string;
min?: number;
max?: number;
step?: number;
id: string;
}
const props = withDefaults(defineProps<Props>(), {
min: 0,
max: 48,
})
defineEmits(['update:modelValue'])
const percentage = computed<string>(() => `${((Number(props.modelValue) - props.min) / (props.max - props.min)) * 100}%`);
// TODO: Steps must be implemented
</script>
<template>
<div class="slider">
<input
:id="id"
type="range"
v-bind="$attrs"
v-bind:value="modelValue"
v-on:input="$emit('update:modelValue', Number($event.target.value))"
:min="min"
:max="max"
:step="step"
/>
<div class="bar"></div>
</div>
</template>
<style>
.slider {
position: relative;
width: 100%;
line-height: 10px;
height: 20px;
--bar-color: var(--slider-bar-color, var(--vp-c-bg-soft));
}
.slider:hover input{
opacity: 1;
}
.slider .bar {
position: absolute;
top: 8px;
left: 0;
width: 100%;
height: 100%;
}
.slider .bar:before, .slider .bar:after {
content: "";
position: absolute;
height: 4px;
pointer-events: none;
}
.slider .bar:before {
width: v-bind(percentage);
z-index: 1;
left: 0;
background: var(--vp-c-brand);
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
.slider .bar:after {
background: var(--bar-color);
width: calc(100% - v-bind(percentage));
right: 0;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
.slider input {
-webkit-appearance: none;
width: calc(100% + 20px);
height: 20px;
left: -10px;
position: relative;
z-index: 2;
background: transparent;
outline: none;
/* opacity: 0.7; */
-webkit-transition: 0.2s;
transition: opacity 0.2s;
margin: 0;
cursor: pointer;
/* @apply
md:opacity-0 */
}
.slider input::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
background: #FFFFFF;
border-radius: 10px;
box-shadow: var(--vp-shadow-2);
cursor: pointer;
}
.slider input::-moz-range-thumb {
width: 20px;
height: 20px;
appearance: none;
background: #FFFFFF;
border-radius: 10px;
outline: none;
border: none;
box-shadow: var(--vp-shadow-2);
cursor: pointer;
}
</style>
@@ -0,0 +1,43 @@
<script setup lang="ts">
import { rotateCw } from '../../../data/iconNodes'
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon'
import IconButton from "./IconButton.vue";
const RotateIcon = createLucideIcon('RotateIcon', rotateCw)
</script>
<template>
<IconButton class="reset-button">
<RotateIcon :size="20"/>
</IconButton>
</template>
<style scoped>
.reset-button {
background: none;
padding: 0;
}
.reset-button .lucide {
transition: ease-in-out 0.1s transform;
}
.reset-button:hover {
background: none;
border-color: transparent;
}
/* a rotate css animation keyframes */
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(359deg);
}
}
.reset-button:active .lucide {
transform: rotate(45deg);
}
</style>
@@ -0,0 +1,77 @@
<script setup>
import { ref } from 'vue'
import { Switch } from '@headlessui/vue'
const enabled = ref(false)
</script>
<template>
<Switch
v-model="enabled"
class="switch"
:class="{ enabled }"
>
<span class="thumb" />
</Switch>
</template>
<style>
.switch {
display: inline-flex;
position: relative;
width: 40px;
height: 22px;
flex-shrink: 0;
border: 1px solid var(--vp-input-border-color);
background-color: var(--vp-input-switch-bg-color);
transition: border-color 0.25s, background-color 0.4s ease;
border-radius: 11px;
}
.switch.enabled {
background-color: var(--vp-c-brand);
}
</style>
<style scoped>
.switch:hover {
border-color: var(--vp-input-hover-border-color);
}
.thumb {
display: inline-block;
background-color: #fff;
transition: transform 0.25s;
width: 20px;
height: 20px;
border-radius: 50%;
/* background-color: var(--vp-c-neutral); */
box-shadow: var(--vp-shadow-1);
}
.switch.enabled .thumb {
transform: translateX(18px);
}
/* ------------------- */
.VPSwitch {
position: relative;
border-radius: 11px;
display: block;
width: 40px;
height: 22px;
flex-shrink: 0;
border: 1px solid var(--vp-input-border-color);
background-color: var(--vp-input-switch-bg-color);
transition: border-color 0.25s;
}
.VPSwitch:hover {
border-color: var(--vp-input-hover-border-color);
}
.dark .icon :deep(svg) {
fill: var(--vp-c-text-1);
transition: opacity 0.25s;
}
</style>
@@ -0,0 +1,35 @@
<template>
<div class="home-container">
<slot />
</div>
</template>
<style scoped>
.home-container {
position: relative;
padding-inline: 24px;
margin-block: 24px;
}
@media (min-width: 640px) {
.home-container {
padding-inline: 48px;
margin-block: 48px;
}
}
@media (min-width: 960px) {
.home-container {
padding-inline: 64px;
margin-block: 64px;
}
}
@media (min-width: 1280px) {
.home-container {
margin: 64px auto;
max-width: 1152px;
padding: 0;
}
}
</style>
@@ -0,0 +1,16 @@
export default {
async load() {
const version = await fetch('https://api.github.com/repos/lucide-icons/lucide/releases/latest').then(res => {
if (res.ok) {
const releaseData = res.json() as Promise<{ tag_name: string }>
return releaseData
}
return null
}).then(res => res.tag_name)
return {
version
}
}
}
@@ -0,0 +1,44 @@
<script setup lang="ts">
import Badge from '../base/Badge.vue';
import HomeContainer from './HomeContainer.vue';
import { data } from './HomeHeroBefore.data'
</script>
<template>
<HomeContainer class="container">
<Badge
:href="`https://github.com/lucide-icons/lucide/releases/tag/${data.version}`"
target="_blank"
rel="noreferrer noopener"
>{{ data.version }}</Badge>
</HomeContainer>
</template>
<style scoped>
.container {
margin-block: 0;;
margin-top: 37px;
margin-bottom: -96px;
display: flex;
justify-content: center;
}
.badge {
display: inline-block;
}
@media (min-width: 640px) {
.container {
margin-bottom: -131px;
}
}
@media (min-width: 960px) {
.container {
justify-content: flex-start;
}
.badge {
display: inline-block;
}
}
</style>
@@ -0,0 +1,16 @@
import iconNodes from '../../../data/iconNodes'
const getRandomItem = <Item>(items: Item[]): Item => items[Math.floor(Math.random()*items.length)];
export default {
async load() {
const icons = Object.entries(iconNodes).map(([name, iconNode]) => ({ name, iconNode }))
const randomIcons = Array.from({ length: 200 }, () => getRandomItem(icons))
return {
icons: randomIcons,
iconsCount: icons.length,
}
}
}
@@ -0,0 +1,160 @@
<script setup lang="ts">
import { ref, onMounted, shallowRef, onBeforeUnmount} from 'vue';
import { data } from './HomeHeroIconsCard.data'
import LucideIcon from '../base/LucideIcon.vue'
import { useRouter } from 'vitepress';
import { random } from 'lodash-es'
import FakeInput from '../base/FakeInput.vue'
const { go } = useRouter()
const intervalTime = shallowRef()
const getInitialItems = () => data.icons.slice(0, 48)
const items = ref(getInitialItems())
let id = items.value.length + 1
function getRandomNewIcon() {
const randomIndex = random(0, 200)
const newRandomIcon = data.icons[randomIndex]
if (items.value.some((item) => item.name === newRandomIcon.name)) {
return getRandomNewIcon()
}
return newRandomIcon
}
function insert() {
const replaceIndex = random(0, 48)
const newIcon = getRandomNewIcon()
// items.value.splice(replaceIndex, 0, newIcon);
items.value[replaceIndex] = newIcon
}
function startInterval() {
intervalTime.value = setInterval(() => {
insert()
}, 2000)
}
// TODO: Try maybe something else for better pref performance
onMounted(() => {
window.addEventListener('mousemove', startInterval, { once: true })
})
onBeforeUnmount(() => {
clearInterval(intervalTime.value)
})
</script>
<template>
<div class="card-wrapper">
<div class="icons-card">
<div class="card-grid">
<TransitionGroup name="list" mode="out-in">
<div
v-for="icon in items"
:key="icon.name"
class="random-icon"
>
<LucideIcon
v-bind="icon"
/>
</div>
</TransitionGroup>
</div>
<FakeInput @click="go('/icons/?focus')" class="search-box">
Search {{ data.iconsCount }} icons...
</FakeInput>
</div>
</div>
</template>
<style scoped>
.card-wrapper {
/* padding: 0 24px; */
margin-left: auto;
margin-bottom: auto;
margin-top: 48px;
}
.icons-card {
background: var(--vp-c-bg-alt);
padding: 24px;
border-radius: 8px;
width: 100%;
height:100%;
/* box-shadow: var(--vp-shadow-2); */
max-height: 220px;
max-width: 560px;
margin: 0 auto;
position: relative;
/* max-height: 240px; */
/* margin-top: 96px; */
}
.card-grid {
display: grid;
gap: 8px;
grid-template-columns: repeat(auto-fill, minmax(36px, 1fr));
grid-template-rows: repeat(auto-fill, minmax(36px, 1fr));
width: 100%;
height:100%;
max-height: 168px;
max-width: 512px;
overflow: hidden;
position: relative;
/* white-space: nowrap; */
}
.list-enter-active {
transition: all 0.5s cubic-bezier(.85,.85,.25,1.1);
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: scale(0.01);
}
.list-leave-active {
position: absolute;
opacity: 0;
display: none;
}
.search-box {
position: absolute;
width: 100%;
left: 0;
top: -64px;
}
.random-icon {
display: inline-flex;
justify-content: center;
align-items: center;
}
@media (min-width: 960px) {
.search-box {
top: unset;
bottom: -24px;
left: -24px;
box-shadow: var(--vp-shadow-3);
background: var(--vp-c-bg);
}
.dark .search-box {
background: var(--vp-c-bg-soft);
}
.card-wrapper {
margin-top: 8px;
}
}
</style>
@@ -0,0 +1,195 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
import { syncRef, useCssVar } from '@vueuse/core'
import HomeContainer from './HomeContainer.vue'
import RangeSlider from '../base/RangeSlider.vue'
import InputField from '../base/InputField.vue'
import ColorPicker from '../base/ColorPicker.vue'
import ResetButton from '../base/ResetButton.vue'
import HomeIconCustomizerIcons from './HomeIconCustomizerIcons.vue'
import Switch from '../base/Switch.vue'
const iconContainer = ref<HTMLElement | null>()
const color = ref('currentColor')
const strokeWidth = ref(2)
const size = ref(24)
const absoluteStrokeWidth = ref(false)
const colorCssVar = useCssVar(
'--customize-color',
iconContainer,
{
initialValue: 'default'
}
)
const strokeWidthCssVar = useCssVar(
'--customize-strokeWidth',
iconContainer,
{
initialValue: '2'
}
)
const sizeCssVar = useCssVar(
'--customize-size',
iconContainer,
{
initialValue: '24'
}
)
syncRef(color, colorCssVar)
syncRef(strokeWidth, strokeWidthCssVar)
syncRef(size, sizeCssVar)
function resetStyle () {
color.value = 'currentColor'
strokeWidth.value = 2
size.value = 24
}
watch(absoluteStrokeWidth, (enabled) => {
iconContainer.value?.classList.toggle('absolute-stroke-width', enabled)
})
</script>
<template>
<HomeContainer>
<div class="card">
<div class="card-column">
<h2 class="title">
Style as you please
<ResetButton @click="resetStyle"></ResetButton>
</h2>
<p class="copy">
Lucide has a lot of customization options to match the icons with you UI.
</p>
<div class="customizer">
<InputField
id="icon-color"
label="Color"
class="color-picker-field"
>
<template #display>
<ColorPicker v-model="color" id="icon-color" />
</template>
</InputField>
<InputField
id="stroke-width"
label="Stroke width"
>
<template #display>
<span class="customize-label">{{ strokeWidth }}px</span>
</template>
<RangeSlider
id="stroke-width"
name="stroke-width"
v-model="strokeWidth"
:min="1"
:max="3"
:step="0.25"
/>
</InputField>
<InputField
id="size"
label="Size"
>
<template #display>
<span class="customize-label">{{ size }}px</span>
</template>
<RangeSlider
id="size"
name="size"
v-model="size"
:min="16"
:max="48"
:step="4"
/>
</InputField>
<InputField
id="absolute-stroke-width"
label="Absolute Stroke width"
>
<template #display>
<Switch
id="absolute-stroke-width"
name="absolute-stroke-width"
v-model="absoluteStrokeWidth"
/>
</template>
</InputField>
</div>
</div>
<div class="icons-container card-column" ref="iconContainer">
<HomeIconCustomizerIcons />
</div>
</div>
</HomeContainer>
</template>
<style scoped>
.card {
display: block;
border-radius: 12px;
height: 100%;
background-color: var(--vp-c-bg-soft);
padding: 24px;
--slider-bar-color: var(--vp-c-bg-soft-down);
--color-picker-bg: var(--vp-c-bg-soft-down);
}
.title {
line-height: 32px;
font-size: 24px;
font-weight: 600;
display: flex;
flex-direction: row;
gap: 8px;
justify-content: space-between;
align-items: center;
}
.copy {
padding-top: 8px;
line-height: 24px;
font-size: 14px;
font-weight: 500;
color: var(--vp-c-text-2);
}
.customizer {
margin-top: 32px;
padding: 0;
background: none;
max-width: 280px;
}
@media (min-width: 640px) {
.card {
display: grid;
grid-template-columns: 8fr 10fr;
}
/*
.card-column {
flex: 1;
} */
}
@media (min-width: 960px) {
.card {
grid-template-columns: 1fr 2fr;
}
}
.color-picker-field:deep(.display-value) {
width: 138px;
}
</style>
@@ -0,0 +1,77 @@
<script setup lang="ts">
import { ref } from 'vue';
import { data } from './HomeHeroIconsCard.data'
import LucideIcon from '../base/LucideIcon.vue'
import { vIntersectionObserver } from '@vueuse/components'
const getInitialItems = () => data.icons.slice(0, 64)
const items = ref(getInitialItems())
const showIcons = ref(false)
// Added intersection observer to improve performance
const onIntersectionObserver: IntersectionObserverCallback = ([{ isIntersecting }]) => {
if (isIntersecting) {
showIcons.value = true
}
}
</script>
<template>
<div class="icon-grid" v-intersection-observer="onIntersectionObserver">
<template v-if="showIcons">
<div
v-for="icon in items"
class="icon-grid-item"
>
<LucideIcon v-bind="icon" class="lucide-icon"/>
</div>
</template>
</div>
</template>
<style scoped>
.icon-grid {
display: grid;
gap: 1px;
grid-template-columns: repeat(auto-fill, minmax(68px, 1fr));
grid-template-rows: repeat(auto-fill, minmax(68px, 1fr));
width: 100%;
height:100%;
max-height: 360px;
gap: 1px;
overflow: hidden;
position: relative;
border-radius: 12px;
border: 8px solid var(--vp-c-bg);
position: relative;
top: 48px;
right: 0;
box-shadow: var(--vp-shadow-4);
}
.icon-grid-item {
display: flex;
align-items: center;
justify-content: center;
background: var(--vp-c-bg);
}
@media (min-width: 640px) {
.icon-grid {
top: 0;
right: -48px;
}
}
.lucide-icon {
will-change: width, height, stroke-width, stroke;
color: var(--customize-color, currentColor);
stroke-width: var(--customize-strokeWidth, 2);
width: calc(var(--customize-size, 24) * 1px);
height: calc(var(--customize-size, 24) * 1px);
}
.icons-container.absolute-stroke-width .lucide-icon {
stroke-width: calc(var(--customize-strokeWidth, 2) * 24 / var(--customize-size, 24));
}
</style>
@@ -0,0 +1,53 @@
export default {
async load() {
return {
packages: [
{
name: 'lucide',
logo: '/framework-logos/js.svg',
label: 'Lucide documentation for JavaScript',
},
{
name: 'lucide-react',
logo: '/framework-logos/react.svg',
label: 'Lucide documentation for React',
},
{
name: 'lucide-vue-next',
logo: '/framework-logos/vue.svg',
label: 'Lucide documentation for Vue 3',
},
{
name: 'lucide-svelte',
logo: '/framework-logos/svelte.svg',
label: 'Lucide documentation for Svelte',
},
{
name: 'lucide-preact',
logo: '/framework-logos/preact.svg',
label: 'Lucide documentation for Preact',
},
{
name: 'lucide-solid',
logo: '/framework-logos/solid.svg',
label: 'Lucide documentation for Solid',
},
{
name: 'lucide-angular',
logo: '/framework-logos/angular.svg',
label: 'Lucide documentation for Angular',
},
{
name: 'lucide-react-native',
logo: '/framework-logos/react-native.svg',
label: 'Lucide documentation for React Native',
},
{
name: 'lucide-flutter',
logo: '/framework-logos/flutter.svg',
label: 'Lucide documentation for Flutter',
},
]
}
}
}
@@ -0,0 +1,67 @@
<script setup lang="ts">
import HomeContainer from './HomeContainer.vue'
import { useRouter } from 'vitepress';
import { data } from './HomePackagesSection.data'
import VPButton from 'vitepress/dist/client/theme-default/components/VPButton.vue';
const { go } = useRouter()
</script>
<template>
<HomeContainer>
<h2 class="section-title">Available For:</h2>
<div class="packages-list">
<a
v-for="{ name, logo } in data.packages"
:href="`/guide/packages/${name}`"
class="package-logo"
:aria-label="`Read more about: ${name} package`"
@click.prevent="go(`/guide/packages/${name}`)"
>
<img
:src="logo"
height="36"
width="36"
loading="lazy"
:alt="`${name} logo`"
/>
</a>
</div>
<div class="more-button-wrapper">
<VPButton text="And more" href="/packages" theme="alt" class="more-button"/>
</div>
</HomeContainer>
</template>
<style scoped>
.section-title {
color: var(--vp-c-text-2);
font-weight: 500;
line-height: 32px;
font-size: 16px;
text-align: center;
margin-bottom: 16px;
}
.packages-list {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
margin: 0 -0.5rem;
gap: 16px;
}
.more-button-wrapper {
margin-top: 24px;
display: flex;
justify-content: center;
}
.package-logo {
transition: opacity ease-in .15s;
}
.package-logo:hover {
opacity: .6;
}
</style>
@@ -0,0 +1,16 @@
import { getAllData } from '../../../lib/icons';
import { getAllCategoryFiles, mapCategoryIconCount } from '../../../lib/categories';
import iconsMetaData from '../../../data/iconMetaData'
export default {
async load() {
let categories = getAllCategoryFiles()
categories = mapCategoryIconCount(categories, Object.values(iconsMetaData))
return {
categories,
}
}
}
@@ -0,0 +1,99 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useData } from 'vitepress'
import VPLink from 'vitepress/dist/client/theme-default/components/VPLink.vue'
import { isActive } from 'vitepress/dist/client/shared'
import { useActiveAnchor } from '../../composables/useActiveAnchor'
import { data } from './CategoryList.data'
import CategoryListItem from './CategoryListItem.vue'
const { page } = useData()
const categoriesIsActive = computed(() => {
return isActive(page.value.relativePath, '/icons/categories');
});
const overviewIsActive = computed(() => {
return isActive(page.value.relativePath, '/icons/');
});
const headers = computed(() => {
const linkPrefix = page.value.relativePath.startsWith('icons/categories')
? '' : '/icons/categories'
return data.categories.map(({ name, title, iconCount }) => ({
level: 2,
link: `${linkPrefix}#${name}`,
title,
iconCount
}))
})
const container = ref()
const marker = ref()
useActiveAnchor(container, marker)
</script>
<template>
<div class="category-list" ref="container">
<VPLink class="sidebar-title" href="/icons/" :class="{ 'active': overviewIsActive } ">
All
</VPLink>
<VPLink class="sidebar-title" href="/icons/categories" :class="{ 'active': categoriesIsActive } ">
Categories
</VPLink>
<div class="content">
<div class="outline-marker" ref="marker" />
<nav aria-labelledby="doc-outline-aria-label">
<CategoryListItem :headers="headers" :root="true" />
</nav>
</div>
</div>
</template>
<style scoped>
.sidebar-title {
font-weight: 500;
color: var(--vp-c-text-2);
margin-bottom: 6px;
line-height: 24px;
font-size: 14px;
display: block;
transition: color 0.25s;
}
.sidebar-title:hover, .sidebar-title.active {
color: var(--vp-c-brand);
}
.content {
margin-top: 12px;
position: relative;
border-left: 1px solid var(--vp-c-divider);
padding-left: 16px;
font-size: 13px;
font-weight: 500;
}
.outline-marker {
position: absolute;
top: 32px;
left: -1px;
z-index: 0;
opacity: 0;
width: 1px;
height: 18px;
background-color: var(--vp-c-brand);
transition: top 0.25s cubic-bezier(0, 1, 0.5, 1), background-color 0.5s, opacity 0.25s;
}
.outline-title {
letter-spacing: 0.4px;
line-height: 28px;
font-size: 13px;
font-weight: 600;
}
.root {
z-index: 0;
}
</style>
@@ -0,0 +1,95 @@
<script setup lang="ts">
import { onMounted } from 'vue'
import { useCategoryView } from '../../composables/useCategoryView'
interface Header {
level: number
title: string
slug: string
iconCount: number
link: string
children: Header[]
}
type MenuItem = Omit<Header, 'slug' | 'children'> & {
children?: MenuItem[]
}
const props = defineProps<{
headers: MenuItem[]
root?: boolean
}>()
const { selectedCategory } = useCategoryView()
function onClick(event: Event) {
const target = (event.target as HTMLElement).nodeName === 'span' ? (event.target as HTMLElement).parentNode : event.target as HTMLElement
const id = '#' + (target as HTMLAnchorElement).href!.split('#')[1]
const decodedId = decodeURIComponent(id)
selectedCategory.value = decodedId.replace('#', '')
const heading = document.querySelector<HTMLAnchorElement>(decodedId)
heading?.focus()
}
</script>
<template>
<ul :class="root ? 'root' : 'nested'">
<li v-for="{ children, link, title, iconCount } in headers">
<a
class="outline-link"
:href="link"
@click="onClick"
:title="title"
>
<span>
{{ title }}
</span>
<span class="icon-count" :aria-label="`Count of icons in ${title}`">
{{ iconCount }}
</span>
</a>
</li>
</ul>
</template>
<style scoped>
.root {
position: relative;
z-index: 1;
}
.nested {
padding-left: 13px;
}
.outline-link {
display: flex;
align-items: baseline;
line-height: 28px;
color: var(--vp-c-text-2);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: color 0.5s;
font-weight: 500;
}
.outline-link:hover,
.outline-link.active {
color: var(--vp-c-text-1);
transition: color 0.25s;
}
.outline-link.nested {
padding-left: 13px;
}
.icon-count {
opacity: 0.5;
margin-left: auto;
font-size: 11px;
font-weight: 400;
}
</style>
@@ -0,0 +1,111 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import { startCase, camelCase } from 'lodash-es'
import ButtonMenu from '../base/ButtonMenu.vue'
import { useIconStyleContext } from '../../composables/useIconStyle';
import useConfetti from '../../composables/useConfetti';
const props = defineProps<{
name: string
popoverPosition?: 'top' | 'bottom'
}>()
const { size, color, strokeWidth, absoluteStrokeWidth } = useIconStyleContext()
const { animate, confetti } = useConfetti()
const componentName = computed(() => {
return startCase(camelCase(props.name)).replace(/\s/g, '')
})
function copyJSX() {
let attrs = ['']
if (size.value && size.value !== 24) {
attrs.push(`size={${size.value}}`)
}
if (color.value && color.value !== 'currentColor') {
attrs.push(`color="${color.value}"`)
}
if (strokeWidth.value && strokeWidth.value !== 2) {
attrs.push(`strokeWidth={${strokeWidth.value}}`)
}
if (absoluteStrokeWidth.value) {
attrs.push(`absoluteStrokeWidth`)
}
const code = `<${componentName.value}${attrs.join(' ')} />`
navigator.clipboard.writeText(code)
}
function copyVue() {
let attrs = ['']
if (size.value && size.value !== 24) {
attrs.push(`:size="${size.value}"`)
}
if (color.value && color.value !== 'currentColor') {
attrs.push(`color="${color.value}"`)
}
if (strokeWidth.value && strokeWidth.value !== 2) {
attrs.push(`:stroke-width="${strokeWidth.value}"`)
}
if (absoluteStrokeWidth.value) {
attrs.push(`absoluteStrokeWidth`)
}
const code = `<${componentName.value}${attrs.join(' ')} />`
navigator.clipboard.writeText(code)
}
function copyAngular() {
let attrs = ['']
attrs.push(`name="${props.name}"`)
if (size.value && size.value !== 24) {
attrs.push(`[size]="${size.value}"`)
}
if (color.value && color.value !== 'currentColor') {
attrs.push(`color="${color.value}"`)
}
if (strokeWidth.value && strokeWidth.value !== 2) {
attrs.push(`[strokeWidth]="${strokeWidth.value}"`)
}
if (absoluteStrokeWidth.value) {
attrs.push(`[absoluteStrokeWidth]="true"`)
}
const code = `<lucide-icon${attrs.join(' ')}></lucide-icon>`
navigator.clipboard.writeText(code)
}
</script>
<template>
<ButtonMenu
:buttonClass="`confetti-button ${animate ? 'animate' : ''}`"
id="copy-code-button"
callOptionOnClick
@click="confetti"
@optionClick="confetti"
data-confetti-text="Copied!"
:popoverPosition="popoverPosition"
:options="[
{ text: 'Copy JSX' , onClick: copyJSX },
{ text: 'Copy Vue' , onClick: copyVue },
{ text: 'Copy Svelte' , onClick: copyJSX },
{ text: 'Copy Angular' , onClick: copyAngular },
]"
/>
</template>
<style src="./confetti.css" />
@@ -0,0 +1,123 @@
<script setup lang="ts">
import { ref } from 'vue';
import ButtonMenu from '../base/ButtonMenu.vue'
import { useIconStyleContext } from '../../composables/useIconStyle';
import useConfetti from '../../composables/useConfetti';
const allowedAttrs = [
'xmlns',
'width',
'height',
'viewBox',
'fill',
'stroke',
'stroke-width',
'stroke-linecap',
'stroke-linejoin',
'class',
]
const downloadText = 'Download!'
const copiedText = 'Copied!'
const confettiText = ref(copiedText)
const props = defineProps<{
name: string
popoverPosition?: 'top' | 'bottom'
}>()
const { size } = useIconStyleContext()
const { animate, confetti } = useConfetti()
function getSVGIcon() {
const svg = document.querySelector('#previewer svg')
if (!svg) return
const clonedSvg = svg.cloneNode(true) as SVGElement
// Filter out attributes that are not allowed in SVGs
for (const attr of Array.from(clonedSvg.attributes)) {
if (!allowedAttrs.includes(attr.name)) {
clonedSvg.removeAttribute(attr.name)
}
}
const svgString = new XMLSerializer().serializeToString(clonedSvg)
return svgString
}
function copySVG() {
confettiText.value = copiedText
const svgString = getSVGIcon()
navigator.clipboard.writeText(svgString)
confetti()
}
function copyDataUrl() {
confettiText.value = copiedText
const svgString = getSVGIcon()
// Create SVG data url
const dataUrl = `data:image/svg+xml;base64,${btoa(svgString)}`
navigator.clipboard.writeText(dataUrl)
confetti()
}
function downloadSVG() {
confettiText.value = downloadText
const svgString = getSVGIcon()
const link = document.createElement('a');
link.download = `${props.name}.svg`;
link.href = `data:image/svg+xml;base64,${btoa(svgString)}`
link.click();
confetti()
}
function downloadPNG() {
confettiText.value = downloadText
const svgString = getSVGIcon()
const canvas = document.createElement('canvas');
canvas.width = size.value;
canvas.height = size.value;
const ctx = canvas.getContext("2d");
const image = new Image();
image.src = `data:image/svg+xml;base64,${btoa(svgString)}`;
image.onload = function() {
ctx.drawImage(image, 0, 0);
const link = document.createElement('a');
link.download = `${props.name}.png`;
link.href = canvas.toDataURL('image/png')
link.click();
confetti()
}
}
</script>
<template>
<ButtonMenu
:buttonClass="`confetti-button ${animate ? 'animate' : ''}`"
callOptionOnClick
id="copy-svg-button"
:data-confetti-text="confettiText"
:popoverPosition="popoverPosition"
:options="[
{ text: 'Copy SVG' , onClick: copySVG },
{ text: 'Copy Data URL' , onClick: copyDataUrl },
{ text: 'Download SVG' , onClick: downloadSVG },
{ text: 'Download PNG' , onClick: downloadPNG },
]"
/>
</template>
<style src="./confetti.css" />
@@ -0,0 +1,88 @@
<script setup lang="ts">
import {IconEntity} from "../../types";
import Label from "../base/Label.vue";
const props = defineProps<{
icon: IconEntity
}>()
</script>
<template>
<div class="contributors" v-if="props.icon.contributors?.length>0">
<Label>Contributors:</Label>
<div class="avatar-group">
<a class="avatar"
v-for="contributor in props.icon.contributors"
:key="contributor"
:href="`https://github.com/${contributor}`"
target="_blank"
:data-name="contributor"
rel="noreferrer noopener"
>
<img class="avatar-image" :alt="contributor" :src="`https://github.com/${contributor}.png?size=128`" />
</a>
</div>
</div>
</template>
<style scoped>
.contributors {
display: flex;
flex-direction: row;
align-items: center;
/* justify-content: flex-end; */
gap: 16px;
}
.avatar-group {
display: flex;
}
.avatar:not(:first-child) {
margin-left: -24px;
}
.avatar {
position: relative;
}
.avatar:before {
content: attr(data-name);
display: block;
font-size: 12px;
line-height: 20px;
transform: translateX(-50%) scale(0.9);
font-weight: 400;
position: absolute;
top: -28px;
left: 50%;
background: var(--vp-c-brand-dark);
color: white;
z-index: 10;
padding: 2px 8px;
border-radius: 4px;
box-shadow: var(--vp-shadow-1);
opacity: 0;
pointer-events: none;
transition: cubic-bezier(0.19, 1, 0.22, 1) .2s;
transition-property: opacity, transform;
overflow: hidden;
white-space: nowrap;
word-break: break-word;
}
.avatar:hover:before {
opacity: 1;
transform: translateX(-50%) scale(1);
}
.avatar-image {
width: 48px;
height: 48px;
border-radius: 50%;
border: 2px solid var(--vp-c-bg-elv);
background-color: var(--vp-c-neutral);
}
.avatar:hover .avatar-image {
border: 2px solid var(--vp-c-bg-soft-mute);
}
.avatar:hover {
z-index: 2;
}
</style>
@@ -0,0 +1,56 @@
<script setup>
import { onMounted, computed } from 'vue'
import { useData } from 'vitepress'
import IconPreview from './IconPreview.vue'
const { params } = useData()
onMounted(() => {
console.log(params, 'data')
})
const tags = computed(() => {
if (!params.tags) return []
return params.tags.join(' • ')
})
</script>
<template>
<!-- <PageContainer class="overview"> -->
<IconPreview
:name="$params.name"
:iconNode="$params.iconNode"
class="preview"
customizable
/>
<div class="details">
<h1 class="title">
{{ $params.name }}
</h1>
</div>
<!-- </PageContainer> -->
</template>
<style scoped>
.overview {
display: flex;
gap: 32px;
}
.preview {
flex: 1;
}
.details {
flex: 2
}
.title {
font-size: 32px;
margin: 0;
margin-bottom: 16px;
line-height: 1.2;
color: var(--vp-text-color);
font-family: monospace;
}
</style>
@@ -0,0 +1,74 @@
<script setup lang="ts">
import { computed, useSlots } from 'vue';
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon'
import { copy } from '../../../data/iconNodes'
import useConfetti from '../../composables/useConfetti';
const { animate, confetti } = useConfetti()
const slots = useSlots()
const copiedText = computed(() => slots.default?.()[0].children)
function copyText() {
navigator.clipboard.writeText(copiedText.value)
confetti()
}
const Copy = createLucideIcon('ChevronUp', copy)
</script>
<template>
<h1
class="icon-name confetti-button"
:class="{animate}"
data-confetti-text="Copied!"
@click="copyText"
>
<slot />
<Copy :size="20" class="copy-icon"/>
</h1>
</template>
<style scoped>
@import './confetti.css';
.icon-name {
font-size: 24px;
font-weight: 500;
line-height: 32px;
transition: background ease-in .15s;;
padding: 2px 8px;
border-radius: 8px;
width: auto;
display: inline-flex;
margin-left: -8px;
}
.icon-name:hover {
background-color: var(--vp-c-bg-alt);
}
.icon-name:hover .copy-icon {
opacity: .9;
}
.icon-name:before,
.icon-name:after {
left: unset !important;
right: -20%;
}
.icon-name:before {
text-align: center;
}
.copy-icon {
opacity: 0;
margin-left: 12px;
margin-top: 6px;
transition:ease .3s opacity;
}
.icon-name:hover .copy-icon:hover {
opacity: .6;
}
</style>
@@ -0,0 +1,154 @@
<script setup lang="ts">
import type { IconEntity } from '../../types'
import { computed, ref, watch } from 'vue'
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon';
import IconButton from '../base/IconButton.vue';
import IconContributors from './IconContributors.vue';
import IconPreview from './IconPreview.vue';
import { x, expand } from '../../../data/iconNodes'
import { useRouter } from 'vitepress';
import IconInfo from './IconInfo.vue';
import Badge from '../base/Badge.vue';
const props = defineProps<{
icon: IconEntity
}>()
const emit = defineEmits(['close'])
const isOpen = computed(() => !!props.icon)
function onClose() {
emit('close')
}
const { go } = useRouter()
const CloseIcon = createLucideIcon('Close', x)
const Expand = createLucideIcon('Expand', expand)
</script>
<template>
<Transition name="drawer" appear>
<div class="overlay-container" v-if="icon">
<div class="overlay-panel">
<nav class="overlay-menu">
<Badge
v-if="icon.createdRelease"
class="version"
:href="`https://github.com/lucide-icons/lucide/releases/tag/v${icon.createdRelease.version}`"
target="_blank"
rel="noreferrer noopener"
>v{{ icon.createdRelease.version }}</Badge>
<IconButton @click="go(`/icons/${icon.name}`)">
<component :is="Expand" />
</IconButton>
<IconButton @click="onClose">
<component :is="CloseIcon" />
</IconButton>
</nav>
<IconPreview
id="previewer"
:name="icon.name"
:iconNode="icon.iconNode"
customizable
/>
<IconInfo :icon="icon" popoverPosition="top">
<template v-slot:footer>
<IconContributors :icon="icon" class="contributors" />
</template>
</IconInfo>
</div>
</div>
</Transition>
</template>
<style scoped>
.overlay-container {
position: fixed;
top: 0;
left: var(--left, 0);
right: var(--right, 0);
bottom: 0;
pointer-events: none;
height: 100%;
width: 100%;
display: flex;
align-items: flex-end;
width: auto;
}
@media (min-width: 960px) {
.overlay-container {
--left: var(--vp-sidebar-width);
}
}
@media (min-width: 1440px) {
.overlay-container {
--left: calc((100% - (var(--vp-layout-max-width) - 64px)) / 2 + var(--vp-sidebar-width) - 32px);
--right: calc(((100% - (var(--vp-layout-max-width) - var(--vp-sidebar-width))) - 272px) / 2);
}
.overlay-panel {
border-top-right-radius: 8px;
}
}
.overlay-panel {
position: relative;
width: 100%;
margin: 0 auto;
padding: 1rem;
background-color: var(--vp-c-bg-elv);
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
will-change: transform;
pointer-events: all;
height: 288px;
padding: 24px;
display: flex;
box-shadow: var(--vp-shadow-5);
}
.icon-info {
padding: 0 24px;
flex-basis: 100%;
}
.icon-tags {
font-size: 16px;
color: var(--vp-c-text-2);
font-weight: 500;
}
.overlay-menu {
position: absolute;
top: 24px;
right: 24px;
display: flex;
gap: 8px;
align-items: center;
}
.drawer-enter-active {
transition: all 0.2s cubic-bezier(.21,.8,.46,.9);
}
.drawer-leave-active {
transition: all 0.4s cubic-bezier(1, 0.5, 0.8, 1);
}
.drawer-enter-from,
.drawer-leave-to {
transform: translateY(100%);
opacity: 0;
}
.version {
margin-right: 24px;
}
.contributors {
justify-content: flex-end;
}
</style>
@@ -0,0 +1,47 @@
<script setup lang="ts">
import type { IconEntity } from '../../types'
import IconItem from './IconItem.vue'
const emit = defineEmits(['setActiveIcon'])
const props = defineProps<{
icons: IconEntity[]
activeIcon?: string
overlayMode?: boolean
hideIcons?: boolean
}>()
function setActiveIcon(name: string) {
emit('setActiveIcon', name)
}
</script>
<template>
<div class="icons">
<div class="icon" v-for="icon in icons" :key="icon.name">
<IconItem
v-bind="icon"
@setActiveIcon="setActiveIcon(icon.name)"
:active="activeIcon === icon.name"
customizable
:overlayMode="overlayMode"
:hideIcon="hideIcons"
/>
</div>
</div>
</template>
<style>
.icons {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(56px, 1fr));
/* padding: 32px 32px 96px; */
gap: 8px;
width: 100%;
}
.icon {
aspect-ratio: 1/1;
}
</style>
@@ -0,0 +1,83 @@
<script setup lang="ts">
import { IconEntity } from '../../types';
import IconDetailName from './IconDetailName.vue';
import Badge from '../base/Badge.vue';
import CopySVGButton from './CopySVGButton.vue';
import CopyCodeButton from './CopyCodeButton.vue';
import VPButton from 'vitepress/dist/client/theme-default/components/VPButton.vue';
import {useData, useRouter} from 'vitepress';
import { computed } from 'vue';
const props = defineProps<{
icon: IconEntity
popoverPosition?: 'top' | 'bottom'
}>()
const { go } = useRouter()
const { page } = useData()
const tags = computed(() => {
if (!props.icon) return []
return props.icon.tags.join(' • ')
})
</script>
<template>
<div class="icon-info">
<IconDetailName class="icon-name">
{{ icon.name }}
</IconDetailName>
<p class="icon-tags">
{{ tags }}
</p>
<div class="group">
<Badge
v-for="category in icon.categories"
class="category"
:href="`/icons/categories#${category}`"
>
{{ category }}
</Badge>
</div>
<div class="group buttons">
<VPButton
v-if="!page?.relativePath?.startsWith?.(`icons/${icon.name}`)"
:href="`/icons/${icon.name}`"
text="See in action"
@click="go(`/icons/${icon.name}`)"
/>
<CopySVGButton :name="icon.name" :popoverPosition="popoverPosition"/>
<CopyCodeButton :name="icon.name" :popoverPosition="popoverPosition"/>
</div>
<slot name="footer" />
</div>
</template>
<style scoped>
.group {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 16px;
}
.category {
text-transform: capitalize;
}
.icon-name {
margin-bottom: 4px;
}
.icon-tags {
font-size: 16px;
color: var(--vp-c-text-2);
font-weight: 500;
margin-top: 0;;
margin-bottom: 16px;
line-height: 28px;
}
.buttons {
margin-top: 24px;
}
</style>
@@ -0,0 +1,154 @@
<script setup lang="ts">
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon';
import { useMediaQuery } from '@vueuse/core';
import { useRouter } from 'vitepress';
export type IconNode = [elementName: string, attrs: Record<string, string>][]
const props = defineProps<{
name: string;
// tags: string[];
// categories: string[];
iconNode: IconNode;
active: boolean;
customizable?: boolean;
overlayMode?: boolean
hideIcon?: boolean
}>()
const emit = defineEmits(['setActiveIcon'])
const { go } = useRouter()
const showOverlay = useMediaQuery('(min-width: 860px)');
const icon = createLucideIcon(props.name, props.iconNode)
function navigateToIcon() {
if(props.overlayMode && showOverlay.value) {
window.history.pushState({}, '', `/icons/${props.name}`)
emit('setActiveIcon', props.name)
}
else {
go(`/icons/${props.name}`)
}
}
</script>
<template>
<button
class="icon-button"
@click="navigateToIcon"
:class="{ 'active' : active }"
:data-title="name"
:aria-label="name"
:href="`/icons/${props.name}`"
>
<KeepAlive>
<component
v-if="!hideIcon"
:is="icon"
class="lucide-icon"
:class="{ customizable }"
/>
</KeepAlive>
</button>
</template>
<style scoped>
.icon-button {
display: inline-block;
border: 1px solid transparent;
text-align: center;
font-weight: 600;
border-radius: 4px;
white-space: nowrap;
transition: color 0.25s, border-color 0.25s, background-color 0.25s;
border-radius: 6px;
background-color: var(--vp-c-bg-alt);
display: inline-flex;
width: 56px;
height: 56px;
font-size: 24px;
color: var(--vp-c-text-1);
}
.icon-button:hover:before {
opacity: 1;
transform: translate(-50%, 48px) scale(1);
}
.icon-button:before {
content: attr(data-title);
display: block;
font-size: 12px;
line-height: 20px;
margin-left: 27px;
transform: translate(-50%, 48px) scale(0.9);
font-weight: 400;
position: absolute;
background: var(--vp-c-brand-dark);
color: white;
z-index: 10;
white-space: nowrap;
padding: 2px 8px;
border-radius: 4px;
box-shadow: var(--vp-shadow-1);
opacity: 0;
pointer-events: none;
transition: cubic-bezier(0.19, 1, 0.22, 1) .2s;
transition-property: opacity, transform;
/* max-width: calc((32px * 2) + 56px); */
overflow: hidden;
white-space: pre-wrap;
word-break: break-word;
}
.icon-button:active {
transition: color 0.1s, border-color 0.1s, background-color 0.1s;
}
.icon-button.medium {
border-radius: 20px;
padding: 0 20px;
line-height: 38px;
font-size: 14px;
}
.icon-button.big {
border-radius: 24px;
padding: 0 24px;
line-height: 46px;
font-size: 16px;
}
.icon-button:hover {
border-color: var(--vp-button-alt-hover-border);
color: var(--vp-button-alt-hover-text);
background-color: var(--vp-button-alt-hover-bg);
}
.icon-button:active {
border-color: var(--vp-button-alt-active-border);
color: var(--vp-button-alt-active-text);
background-color: var(--vp-button-alt-active-bg);
}
.icon-button.active {
border-color: var(--vp-c-brand);
}
.lucide-icon {
margin: auto;
}
.lucide-icon.customizable {
will-change: width, height, stroke-width, stroke;
color: var(--customize-color, currentColor);
stroke-width: var(--customize-strokeWidth, 2);
width: calc(var(--customize-size, 24) * 1px);
height: calc(var(--customize-size, 24) * 1px);
}
html.absolute-stroke-width .lucide-icon.customizable {
stroke-width: calc(var(--customize-strokeWidth, 2) * 24 / var(--customize-size, 24));
}
</style>
@@ -0,0 +1,73 @@
<script setup lang="ts">
import type { IconEntity } from '../../types'
import { computed, ref } from 'vue'
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon';
import { useIconStyleContext } from '../../composables/useIconStyle';
const props = defineProps<{
name: IconEntity['name']
iconNode: IconEntity['iconNode']
customizable?: boolean
}>()
const { size, color, strokeWidth, absoluteStrokeWidth } = useIconStyleContext()
const previewIcon = ref()
const gridLines = computed(() => Array.from({ length:(size.value - 1) }))
const iconComponent = computed(() => {
if (!props.name || !props.iconNode) return null
return createLucideIcon(props.name, props.iconNode)
})
</script>
<template>
<div class="icon-container">
<component
ref="previewIcon"
:is="iconComponent"
:width="size"
:height="size"
:stroke="color"
:stroke-width="absoluteStrokeWidth ? Number(strokeWidth) * 24 / Number(size) : strokeWidth"
/>
<svg class="icon-grid" :viewBox="`0 0 ${size} ${size}`" fill="none" stroke-width="0.1" xmlns="http://www.w3.org/2000/svg">
<g :key="`grid-${i}`" v-for="(_, i) in gridLines">
<line :key="`horizontal-${i}`" :x1="0" :y1="i + 1" :x2="size" :y2="i + 1" />
<line :key="`vertical-${i}`" :x1="i + 1" y1="0" :x2="i + 1" :y2="size" />
</g>
</svg>
</div>
</template>
<style scoped>
.icon-grid {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
stroke: var(--vp-c-divider);
}
.icon-container {
height: 100%;
aspect-ratio: 1/1;
position: relative;
background: var(--vp-c-bg-alt);
border-radius: 8px;
}
.icon-container > :deep(svg:not(.icon-grid)) {
width: 100%;
height: 100%;
position: relative;
z-index: 1;
color: var(--vp-c-neutral);
opacity: 0.8;
}
.icon-component.customizable {
will-change: width, height, stroke-width, stroke;
/* color: var(--customize-color, currentColor);
stroke-width: var(--customize-strokeWidth, 2); */
}
</style>
@@ -0,0 +1,69 @@
<script setup lang="ts">
import type { IconEntity } from '../../types'
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon';
const props = defineProps<{
name: IconEntity['name']
iconNode: IconEntity['iconNode']
customizable?: boolean
}>()
const Icon = createLucideIcon(props.name, props.iconNode)
</script>
<template>
<div class="icons-small-preview">
<div class="icon-wrapper">
<Icon :size="48" class="lucide-icon"/>
</div>
<div class="icon-wrapper">
<Icon :size="32" class="lucide-icon"/>
</div>
<div class="icon-wrapper">
<Icon class="lucide-icon"/>
</div>
</div>
</template>
<style scoped>
.icons-small-preview {
display: flex;
width: 100%;
height: 100%;
/* align-items: center; */
justify-content: center;
gap: 24px;
/* margin-top: 24px; */
}
.icon-wrapper {
border: 1px solid transparent;
text-align: center;
font-weight: 600;
border-radius: 4px;
white-space: nowrap;
transition: color 0.25s, border-color 0.25s, background-color 0.25s;
border-radius: 6px;
background-color: var(--vp-c-bg-alt);
display: inline-flex;
width: 64px;
height: 64px;
font-size: 24px;
color: var(--vp-c-text-1);
align-items: center;
justify-content: center;
}
.lucide-icon {
will-change: width, height, stroke-width, stroke;
color: var(--customize-color, currentColor);
stroke-width: var(--customize-strokeWidth, 2);
/* Not sure if this is logical for 100% previews */
/* width: calc(var(--customize-size, 24) * 1px);
height: calc(var(--customize-size, 24) * 1px); */
}
html.absolute-stroke-width .lucide-icon {
stroke-width: calc(var(--customize-strokeWidth, 2) * 24 / var(--customize-size, 24));
}
</style>
@@ -0,0 +1,56 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Category } from '../../types';
import IconGrid from './IconGrid.vue'
import { vIntersectionObserver } from '@vueuse/components'
defineProps<{
activeIconName: string
category: Category
}>()
const emit = defineEmits(['setActiveIcon'])
const showIcons = ref(false)
// Added intersection observer to improve performance
const onIntersectionObserver: IntersectionObserverCallback = ([{ isIntersecting }]) => {
showIcons.value = isIntersecting
}
</script>
<template>
<section
class="category"
:key="category.name"
:id="category.name"
v-intersection-observer="onIntersectionObserver"
>
<h2 class="title" >
<a class="header-anchor" :href="`#${category.name}`" :aria-label="`Permalink to &quot;${category.title}&quot;`">&ZeroWidthSpace;</a>
{{ category.title }}
</h2>
<IconGrid
:activeIcon="activeIconName"
:icons="category.icons"
@setActiveIcon="$event => $emit('setActiveIcon', $event)"
overlayMode
:hideIcons="!showIcons"
/>
</section>
</template>
<style scoped>
.title {
margin-bottom: 24px;
font-size: 19px;
font-weight: 500;
padding-top: 86px;
/* scroll-padding-top: 240px; */
}
.category {
margin-bottom: calc(-86px + 32px);
}
</style>
@@ -0,0 +1,103 @@
<script setup lang="ts">
import { ref, computed, defineAsyncComponent } from 'vue'
import type { IconEntity, Category } from '../../types'
import useSearch from '../../composables/useSearch'
import InputSearch from '../base/InputSearch.vue'
import useSearchInput from '../../composables/useSearchInput'
import StickyBar from './StickyBar.vue'
import IconsCategory from './IconsCategory.vue'
const props = defineProps<{
icons: IconEntity[]
categories: Category[]
iconCategories: Record<string, string[]>
}>()
const activeIconName = ref(null)
const { searchInput, searchQuery, searchQueryThrottled } = useSearchInput()
const isSearching = computed(() => !!searchQuery.value)
function setActiveIconName(name: string) {
activeIconName.value = name
}
const searchResults = useSearch(searchQuery, props.icons, [
{ name: 'name', weight: 2 },
{ name: 'tags', weight: 1 },
])
const categories = computed(() => {
if( !props.categories?.length || !props.icons?.length ) return []
return props.categories.map(({ name, title }) => {
const categoryIcons = props.icons.filter((icon) => {
const iconCategories = props.iconCategories[icon.name]
return iconCategories?.includes(name)
})
const searchedCategoryIcons = isSearching
? categoryIcons.filter(icon => searchResults.value.some((item) => item?.name === icon?.name))
: categoryIcons;
return {
title,
name,
icons: searchedCategoryIcons,
};
})
.filter(({ icons }) => icons.length)
})
const activeIcon = computed(() =>
props.icons?.find((icon) => icon.name === activeIconName.value)
)
const NoResults = defineAsyncComponent(() =>
import('./NoResults.vue')
)
const IconDetailOverlay = defineAsyncComponent(() =>
import('./IconDetailOverlay.vue')
)
</script>
<template>
<StickyBar class="search-bar category-search">
<InputSearch
:placeholder="`Search ${icons.length} icons ...`"
v-model="searchQuery"
class="input-wrapper"
ref="searchInput"
/>
</StickyBar>
<NoResults
v-if="categories.length === 0"
:searchQuery="searchQuery"
@clear="searchQuery = ''"
/>
<IconsCategory
v-for="category in categories"
:key="category.name"
:category="category"
:activeIconName="activeIconName"
@setActiveIcon="setActiveIconName"
/>
<IconDetailOverlay
v-if="activeIconName != null"
:icon="activeIcon"
@close="setActiveIconName('')"
/>
</template>
<style scoped>
.input-wrapper {
width: 100%;
}
.search-bar.category-search {
margin-bottom: -54px;
}
</style>
@@ -0,0 +1,124 @@
<script setup lang="ts">
import { ref, computed, watch, defineAsyncComponent } from 'vue'
import type { IconEntity } from '../../types'
import { useMediaQuery, useOffsetPagination } from '@vueuse/core'
import IconGrid from './IconGrid.vue'
import InputSearch from '../base/InputSearch.vue'
import useSearch from '../../composables/useSearch'
import EndOfPage from '../base/EndOfPage.vue'
import useSearchInput from '../../composables/useSearchInput'
import StickyBar from './StickyBar.vue'
const props = defineProps<{
icons: IconEntity[]
}>()
const activeIconName = ref(null)
const isExtraLargeScreen = useMediaQuery('(min-width: 1440px)');
const isLargeScreen = useMediaQuery('(min-width: 1280px)');
const isMediumScreen = useMediaQuery('(min-width: 960px)');
const isSmallScreen = useMediaQuery('(min-width: 640px)');
const pageSize = computed(() => {
if(isExtraLargeScreen.value) {
return 16 * 16;
}
if(isLargeScreen.value) {
return 16 * 12;
}
if(isMediumScreen.value) {
return 13 * 12;
}
if(isSmallScreen.value) {
return 10 * 10;
}
return 10 * 5;
})
const { searchInput, searchQuery, searchQueryThrottled } = useSearchInput()
const searchResults = useSearch(searchQueryThrottled, props.icons, [
{ name: 'name', weight: 3 },
{ name: 'tags', weight: 2 },
{ name: 'categories', weight: 1 },
])
const { next, currentPage } = useOffsetPagination( { pageSize })
const paginatedIcons = computed(() => {
const end = pageSize.value * currentPage.value
return searchResults.value.slice(0, end)
})
function setActiveIconName(name: string) {
activeIconName.value = name
}
const activeIcon = computed(() => props.icons.find((icon) => icon.name === activeIconName.value))
watch(searchQueryThrottled, (searchString) => {
currentPage.value = 1
})
const NoResults = defineAsyncComponent(() =>
import('./NoResults.vue')
)
const IconDetailOverlay = defineAsyncComponent(() =>
import('./IconDetailOverlay.vue')
)
</script>
<template>
<StickyBar>
<InputSearch
:placeholder="`Search ${icons.length} icons ...`"
v-model="searchQuery"
ref="searchInput"
class="input-wrapper"
/>
</StickyBar>
<NoResults
v-if="paginatedIcons.length === 0"
:searchQuery="searchQuery"
@clear="searchQuery = ''"
/>
<IconGrid
overlayMode
:activeIcon="activeIconName"
:icons="paginatedIcons"
@setActiveIcon="setActiveIconName"
/>
<EndOfPage @end-of-page="next" class="bottom-page"/>
<IconDetailOverlay
v-if="activeIconName != null"
:icon="activeIcon"
@close="setActiveIconName('')"
/>
</template>
<style>
.icons {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(56px, 1fr));
gap: 8px;
width: 100%;
}
.icon {
aspect-ratio: 1/1;
}
.input-wrapper {
width: 100%;
}
.bottom-page {
height: 288px;
}
</style>
@@ -0,0 +1,77 @@
<script setup lang="ts">
import {ref} from 'vue'
import {bird} from '../../../data/iconNodes'
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon'
import {useEventListener} from '@vueuse/core'
import VPButton from 'vitepress/dist/client/theme-default/components/VPButton.vue'
defineProps<{
searchQuery: string
}>()
defineEmits(['clear'])
const birdIcon = ref<HTMLElement>()
const Bird = createLucideIcon('bird', bird)
const flip = ref(false)
useEventListener(document, 'mousemove', (mouseEvent) => {
const {width, height, x, y} = birdIcon.value.getBoundingClientRect()
const centerX = (width / 2) + x
flip.value = mouseEvent.x < centerX
})
</script>
<template>
<div class="no-results">
<Bird class="bird-icon" ref="birdIcon" :class="{ flip }" :strokeWidth="1"/>
<h2 class="no-results-text">
No icons found for '{{ searchQuery }}'
</h2>
<VPButton text="Clear your search and try again" theme="alt" @click="$emit('clear')"/>
or
<VPButton text="Check if someone has already requested this icon"
theme="alt"
:href="`https://github.com/lucide-icons/lucide/issues?q=is%3Aopen+${searchQuery}`"
target="_blank"
/>
</div>
</template>
<style scoped>
.no-results {
display: flex;
flex-direction: column;
align-items: center;
}
.bird-icon {
width: 160px;
height: 160px;
color: var(--vp-c-neutral);
opacity: 0.8;
margin-top: 72px;
}
.bird-icon.flip {
transform: rotateY(180deg);
}
@media (min-width: 960px) {
.bird-icon {
width: 240px;
height: 240px;
}
}
.no-results-text {
line-height: 40px;
font-size: 24px;
margin-top: 24px;
margin-bottom: 32px;
text-align: center;
}
</style>
@@ -0,0 +1,30 @@
<script setup lang="ts">
import type { IconEntity } from '../../types'
import IconGrid from './IconGrid.vue'
defineProps<{
icons: IconEntity[]
}>()
</script>
<template>
<section class="related-icons">
<h2 class="title">
Related Icons
</h2>
<IconGrid
:icons="icons"
/>
</section>
</template>
<style scoped>
.title {
margin-bottom: 24px;
font-size: 19px;
font-weight: 500;
}
.related-icons {
margin-bottom: 32px;
}
</style>
@@ -0,0 +1,155 @@
<script setup lang="ts">
import { shallowRef, type Ref, watch } from 'vue'
import { useCssVar, syncRef } from '@vueuse/core'
import { useIconStyleContext } from '../../composables/useIconStyle'
import RangeSlider from '../base/RangeSlider.vue'
import InputField from '../base/InputField.vue'
import ColorPicker from '../base/ColorPicker.vue'
import ResetButton from '../base/ResetButton.vue'
import Switch from '../base/Switch.vue'
const props = defineProps<{
rootEl?: Ref<HTMLElement>
}>()
const { color, strokeWidth, size, absoluteStrokeWidth } = useIconStyleContext()
const documentRef = shallowRef<HTMLElement | undefined>(typeof document !== 'undefined' ? document?.documentElement : undefined)
const colorCssVar = useCssVar(
'--customize-color',
props.rootEl?.value ?? documentRef.value,
{
initialValue: 'default'
}
)
const strokeWidthCssVar = useCssVar(
'--customize-strokeWidth',
props.rootEl?.value ?? documentRef.value,
{
initialValue: '2'
}
)
const sizeCssVar = useCssVar(
'--customize-size',
props.rootEl?.value ?? documentRef.value,
{
initialValue: '24'
}
)
syncRef(color, colorCssVar, { direction: 'ltr' })
syncRef(strokeWidth, strokeWidthCssVar, { direction: 'ltr' })
syncRef(size, sizeCssVar, { direction: 'ltr' })
function resetStyle () {
color.value = 'currentColor'
strokeWidth.value = 2
size.value = 24
}
watch(absoluteStrokeWidth, (enabled) => {
const htmlEl = document.documentElement
htmlEl.classList.toggle('absolute-stroke-width', enabled)
})
</script>
<template>
<div class="customizer-card">
<div class="card-header">
<h2 class="card-title">
Customizer
</h2>
<ResetButton @click="resetStyle"></ResetButton>
</div>
<InputField
id="icon-color"
label="Color"
>
<template #display>
<ColorPicker v-model="color" id="icon-color" class="color-picker"/>
</template>
</InputField>
<InputField
id="stroke-width"
label="Stroke width"
>
<template #display>
<span class="customize-label">{{ strokeWidth }}px</span>
</template>
<RangeSlider
id="stroke-width"
name="stroke-width"
v-model="strokeWidth"
:min="0.5"
:max="3"
:step="0.25"
/>
</InputField>
<InputField
id="size"
label="Size"
>
<template #display>
<span class="customize-label">{{ size }}px</span>
</template>
<RangeSlider
id="size"
name="size"
v-model="size"
:min="16"
:max="48"
:step="4"
/>
</InputField>
<InputField
id="absolute-stroke-width"
label="Absolute Stroke width"
>
<Switch
id="size"
name="size"
v-model="absoluteStrokeWidth"
/>
</InputField>
</div>
</template>
<style scoped>
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.card-title {
font-weight: 700;
color: var(--vp-c-text-1);
line-height: 32px;
font-size: 16px;
/* margin-bottom: 12px; */
}
.customizer-card {
background: var(--vp-c-bg);
padding: 12px 24px 24px;
border-radius: 12px;
margin-bottom: 24px;
position: relative;
z-index: 0;
}
.color-picker {
margin-left: auto;
}
#absolute-stroke-width {
flex-direction: row-reverse;
}
</style>
@@ -0,0 +1,28 @@
<template>
<div class="search-bar">
<slot />
</div>
</template>
<style scoped>
.search-bar {
margin-bottom: 16px;
position: sticky;
z-index: 10;
top: 48px;
padding-top: 24px;
margin-top: -32px;
width: 100%;
display: flex;
margin-bottom: 32px;
background: var(--vp-c-bg);
box-shadow: 0 16px 24px var(--vp-c-bg);
}
@media (min-width: 960px) {
.search-bar {
padding-top: 32px;
top: 64px;
}
}
</style>
@@ -0,0 +1,104 @@
.confetti-button {
cursor: pointer;
position: relative;
--confetti-color: var(--vp-c-brand);
--text-color: 0 0 0;
}
.dark .confetti-button {
--confetti-color: var(--vp-c-brand-dark);
--text-color: 255 255 255;
}
.confetti-button:before,
.confetti-button:after {
position: absolute;
content: "";
display: block;
width: 140%;
max-width: 160px;
height: 100%;
left: -20%;
z-index: -1000;
transition: all ease-in-out 0.5s;
background-repeat: no-repeat;
font-size: 14px;
}
.confetti-button:before {
content: attr(data-confetti-text);
letter-spacing: 1px;
font-weight: bold;
transform: rotate(-8deg);
color: rgb(var(--text-color) / 1);
display: none;
top: -85%;
background-image: radial-gradient(circle, var(--confetti-color) 20%, transparent 20%),
radial-gradient(circle, transparent 20%, var(--confetti-color) 20%, transparent 30%),
radial-gradient(circle, var(--confetti-color) 20%, transparent 20%),
radial-gradient(circle, var(--confetti-color) 20%, transparent 20%),
radial-gradient(circle, transparent 10%, var(--confetti-color) 15%, transparent 20%),
radial-gradient(circle, var(--confetti-color) 20%, transparent 20%),
radial-gradient(circle, var(--confetti-color) 20%, transparent 20%),
radial-gradient(circle, var(--confetti-color) 20%, transparent 20%),
radial-gradient(circle, var(--confetti-color) 20%, transparent 20%);
background-size: 10% 10%, 20% 20%, 15% 15%, 20% 20%, 18% 18%, 10% 10%, 15% 15%,
10% 10%, 18% 18%;
}
.confetti-button:after {
display: none;
bottom: -75%;
background-image: radial-gradient(circle, var(--confetti-color) 20%, transparent 20%),
radial-gradient(circle, var(--confetti-color) 20%, transparent 20%),
radial-gradient(circle, transparent 10%, var(--confetti-color) 15%, transparent 20%),
radial-gradient(circle, var(--confetti-color) 20%, transparent 20%),
radial-gradient(circle, var(--confetti-color) 20%, transparent 20%),
radial-gradient(circle, var(--confetti-color) 20%, transparent 20%),
radial-gradient(circle, var(--confetti-color) 20%, transparent 20%);
background-size: 15% 15%, 20% 20%, 18% 18%, 20% 20%, 15% 15%, 10% 10%, 20% 20%;
}
.confetti-button.animate:before {
display: block;
animation: topBubbles ease-in-out 1s forwards;
}
.confetti-button.animate:after {
display: block;
animation: bottomBubbles ease-in-out 1s forwards;
}
@keyframes topBubbles {
0% {
color: rgb(var(--text-color) / 0);
background-position: 5% 90%, 10% 90%, 10% 90%, 15% 90%, 25% 90%, 25% 90%,
40% 90%, 55% 90%, 70% 90%;
}
30% {
color: rgb(var(--text-color) / 1);
}
50% {
background-position: 0% 80%, 0% 20%, 10% 40%, 20% 0%, 30% 30%, 22% 50%,
50% 50%, 65% 20%, 90% 30%;
}
100% {
background-position: 0% 70%, 0% 10%, 10% 30%, 20% -10%, 30% 20%, 22% 40%,
50% 40%, 65% 10%, 90% 20%;
background-size: 0% 0%, 0% 0%, 0% 0%, 0% 0%, 0% 0%, 0% 0%;
color: rgb(var(--text-color) / 0);
}
}
@keyframes bottomBubbles {
0% {
background-position: 10% -10%, 30% 10%, 55% -10%, 70% -10%, 85% -10%,
70% -10%, 70% 0%;
}
50% {
background-position: 0% 80%, 20% 80%, 45% 60%, 60% 100%, 75% 70%, 95% 60%,
105% 0%;
}
100% {
background-position: 0% 90%, 20% 90%, 45% 70%, 60% 110%, 75% 80%, 95% 70%,
110% 10%;
background-size: 0% 0%, 0% 0%, 0% 0%, 0% 0%, 0% 0%, 0% 0%;
}
}
@@ -0,0 +1,108 @@
<script setup lang="ts">
import { useData } from 'vitepress'
import { useSidebar } from 'vitepress/dist/client/theme-default/composables/sidebar'
import VPLink from 'vitepress/dist/client/theme-default/components/VPLink.vue'
import { computed } from 'vue'
const { theme } = useData()
const { hasSidebar } = useSidebar()
const githubLink = computed(() => theme.value.socialLinks.find(({icon}) => icon === 'github').link)
const links = computed(() => [
{
text: 'License',
href: '/license'
},
{
text: 'Contribute',
href: '/contributing'
},
{
text: 'Changelog',
href: `${githubLink.value}/releases`
},
{
text: 'Github',
href: `${githubLink.value}/issues`
},
{
text: 'Issues',
href: `${githubLink.value}/issues`
}
])
</script>
<template>
<footer v-if="theme.footer" class="VPFooter" :class="{ 'has-sidebar': hasSidebar }">
<div class="container">
<p v-if="theme.footer.message" class="message" v-html="theme.footer.message"></p>
<p v-if="theme.footer.copyright" class="copyright" v-html="theme.footer.copyright"></p>
<div class="links">
<VPLink v-for="link in links" :href="link.href" :key="link.text" :rel="link.href.startsWith('http') ? 'noreferrer noopener': undefined">
{{ link.text }}
</VPLink>
<a href="https://vercel.com?utm_source=lucide&utm_campaign=oss" rel="noreferrer noopener">
<img src="/vercel.svg" alt="Powered by Vercel" width="200" />
</a>
</div>
</div>
</footer>
</template>
<style scoped>
.VPFooter {
position: relative;
z-index: var(--vp-z-index-footer);
border-top: 1px solid var(--vp-c-gutter);
padding: 32px 24px;
background-color: var(--vp-c-bg);
}
.VPFooter.has-sidebar {
display: none;
}
.container {
margin: 0 auto;
max-width: var(--vp-layout-max-width);
display: flex;
flex-direction: column;
align-items: center;
gap: 32px;
}
.message,
.copyright {
line-height: 24px;
font-size: 14px;
font-weight: 500;
color: var(--vp-c-text-2);
}
.message { order: 2; }
.copyright { order: 1; }
.links {
display: flex;
gap: 32px;
align-items: center;
flex-wrap: wrap;
justify-content: center;
}
@media (min-width: 1152px) {
.VPFooter {
padding: 32px;
}
.container {
flex-direction: row-reverse;
}
.links {
margin-left: auto;
}
}
</style>
@@ -0,0 +1,10 @@
<script setup lang="ts">
import { menu } from '../../../data/iconNodes'
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon'
const Menu = createLucideIcon('menu', menu)
</script>
<template>
<Menu />
</template>
@@ -0,0 +1,21 @@
import packageData from '../../../data/packageData.json';
import thirdPartyPackages from '../../../data/packageData.thirdParty.json';
import fetchPackages from "../../../lib/fetchPackages";
export default {
async load() {
const packages = await fetchPackages();
return {
packages: packages
.filter(p => p.name in packageData)
.map((pData) => ({
...pData,
...packageData[pData.name],
documentation: `/guide/packages/${pData.name}`,
source: `https://github.com/lucide-icons/lucide/tree/main/packages/${pData.name}`,
icon: `/framework-logos/${packageData[pData.name].icon}.svg`,
})).sort((a, b) => a.order - b.order),
thirdPartyPackages,
};
}
}
@@ -0,0 +1,64 @@
<script setup lang="ts">
import {data} from './PackageList.data'
import PackageListItem from "./PackageListItem.vue";</script>
<template>
<section class="package-group">
<h1 class="name">Packages</h1>
<div class="grid package-list" ref="container">
<div v-for="packageData in data.packages" class="item">
<PackageListItem :packageData="packageData"/>
</div>
</div>
</section>
<section class="package-group">
<h2 class="name">Third-party packages</h2>
<div class="grid package-list" ref="container">
<div v-for="packageData in data.thirdPartyPackages" class="item">
<PackageListItem :packageData="packageData"/>
</div>
</div>
</section>
</template>
<style scoped>
.name {
font-size: 32px;
font-weight: bold;
text-align: center;
margin-bottom: 24px;
}
.package-group {
margin-bottom: 96px;
}
.grid {
display: flex;
flex-wrap: wrap;
align-items: stretch;
justify-content: center;
align-content: space-evenly;
box-sizing: border-box;
margin: -8px;
}
.grid > * {
flex-basis: 100%;
box-sizing: border-box;
padding: 8px;
}
@media (min-width: 960px) {
.grid > * {
flex-basis: 50%;
}
}
@media (min-width: 1280px) {
.grid > * {
flex-basis: 33.33%;
}
}
</style>
@@ -0,0 +1,110 @@
<script setup lang="ts">
import { useRouter } from 'vitepress';
import {PackageItem} from "../../types";
import VPButton from 'vitepress/dist/client/theme-default/components/VPButton.vue';
const { go } = useRouter()
const props = defineProps<{
packageData: PackageItem,
}>()
</script>
<template>
<article class="package">
<header class="package-header">
<div class="package-icon-well">
<img :src="packageData.icon" alt="" class="package-icon" :class="{[packageData.iconClass]: true, light: packageData.iconDark}" />
<img v-if="packageData.iconDark" :src="packageData.iconDark" alt="" class="package-icon dark" :class="packageData.iconClass" />
</div>
<div class="package-title">
<h2 class="title">{{ props.packageData.name }}</h2>
<a v-for="shield in props.packageData.shields" :href="shield.href" class="package-shield" rel="noreferrer noopener">
<img :src="shield.src" :alt="shield.href" />
</a>
</div>
</header>
<div class="package-details">
{{ packageData.description }}
</div>
<footer class="package-footer">
<VPButton
:href="packageData.documentation"
text="Guide"
theme="brand"
@click="go(packageData.documentation)"
/>
<VPButton
:href="packageData.source"
text="Source"
theme="alt"
@click="go(packageData.source)"
/>
</footer>
</article>
</template>
<style scoped>
.package {
border: 1px solid var(--vp-c-bg-soft);
border-radius: 12px;
background-color: var(--vp-c-bg-soft);
display: flex;
flex-direction: column;
padding: 24px;
}
.package {
display: flex;
flex-direction: column;
gap: 24px;
height: 100%;
}
.package-header {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 24px;
}
@media screen and (min-width: 480px) {
.package-header {
flex-direction: row;
}
}
.package-icon-well {
padding: 16px;
border-radius: 12px;
background-color: var(--vp-c-bg);
}
.package-icon {
width: 64px;
height: 64px;
}
h2.title {
margin: 0;
padding: 0;
border: 0;
font-size: 18px;
font-weight: bold;
}
.package-title {
display: flex;
flex-direction: column;
gap: 8px;
}
.package-details {
flex-grow: 1;
}
.package-footer {
display: flex;
flex-direction: row;
gap: 12px;
}
html.dark .package-icon-invert {
filter: invert(1) hue-rotate(180deg);
}
html.dark .package-icon.light {
display: none;
}
html:not(.dark) .package-icon.dark {
display: none;
}
</style>
@@ -0,0 +1,87 @@
import { onMounted, onUpdated, onUnmounted } from 'vue';
import { throttleAndDebounce } from 'vitepress/dist/client/theme-default/support/utils'
/*
* This file is compied and adjusted from vitepress/dist/client/theme-default/composables/useActiveAnchor.ts
*/
export function useActiveAnchor(container, marker) {
const onScroll = throttleAndDebounce(setActiveLink, 100);
let prevActiveLink = null;
onMounted(() => {
requestAnimationFrame(setActiveLink);
window.addEventListener('scroll', onScroll);
});
onUpdated(() => {
// sidebar update means a route change
activateLink(location.hash);
});
onUnmounted(() => {
window.removeEventListener('scroll', onScroll);
});
function setActiveLink() {
const links = [].slice.call(container.value.querySelectorAll('.outline-link'));
const anchors = [].slice
.call(document.querySelectorAll('.content .header-anchor'))
.filter((anchor) => {
return links.some((link) => {
return link.hash === anchor.hash && anchor.offsetParent !== null;
});
});
const scrollY = window.scrollY;
const innerHeight = window.innerHeight;
const offsetHeight = document.body.offsetHeight;
const isBottom = Math.abs(scrollY + innerHeight - offsetHeight) < 1;
// page bottom - highlight last one
if (anchors.length && isBottom) {
activateLink(anchors[anchors.length - 1].hash);
return;
}
for (let i = 0; i < anchors.length; i++) {
const anchor = anchors[i];
const nextAnchor = anchors[i + 1];
const [isActive, hash] = isAnchorActive(i, anchor, nextAnchor);
if (isActive) {
activateLink(hash);
return;
}
}
}
function activateLink(hash) {
if (prevActiveLink) {
prevActiveLink.classList.remove('active');
}
if (hash !== null) {
prevActiveLink = container.value.querySelector(`a[href="${decodeURIComponent(hash)}"]`);
}
const activeLink = prevActiveLink;
if (activeLink) {
activeLink.classList.add('active');
marker.value.style.top = activeLink.offsetTop + 5 + 'px';
marker.value.style.opacity = '1';
}
else {
marker.value.style.top = '33px';
marker.value.style.opacity = '0';
}
}
}
const PAGE_OFFSET = 64;
function getAnchorTop(anchor) {
return anchor.parentElement.offsetTop - PAGE_OFFSET;
}
function isAnchorActive(index, anchor, nextAnchor) {
const scrollTop = window.scrollY;
if (index === 0 && scrollTop === 0) {
return [true, null];
}
if (scrollTop < getAnchorTop(anchor)) {
return [false, null];
}
if (!nextAnchor || scrollTop < getAnchorTop(nextAnchor)) {
return [true, anchor.hash];
}
return [false, null];
}
@@ -0,0 +1,25 @@
import {
ref, inject, Ref
} from 'vue';
export const CATEGORY_VIEW_CONTEXT = Symbol('categoryView');
interface CategoryViewContext {
selectedCategory: Ref<string>
categoryCounts: Ref<Record<string, number>>
}
export const categoryViewContext = {
selectedCategory: ref(''),
categoryCounts: ref({}),
};
export function useCategoryView(): CategoryViewContext {
const context = inject<CategoryViewContext>(CATEGORY_VIEW_CONTEXT);
if (!context) {
throw new Error('useCategoryView must be used with categoryView context');
}
return context;
}
@@ -0,0 +1,18 @@
import { ref } from "vue";
export default function useConfetti() {
const animate = ref(false)
function confetti() {
animate.value = true;
setTimeout(function () {
animate.value = false;
}, 1000);
}
return {
animate,
confetti
}
}
@@ -0,0 +1,30 @@
/* eslint-disable no-console */
import {
ref, inject, Ref
} from 'vue';
export const ICON_STYLE_CONTEXT = Symbol('size');
interface IconSizeContext {
size: Ref<number>
strokeWidth: Ref<number>
color: Ref<string>
}
export const iconStyleContext = {
size: ref(24),
strokeWidth: ref(2),
color: ref('currentColor'),
absoluteStrokeWidth: ref(false),
};
export function useIconStyleContext(): IconSizeContext{
const context = inject<IconSizeContext>(ICON_STYLE_CONTEXT);
if (!context) {
throw new Error('useIconStyleContext must be used with useIconStyleProvider');
}
return context;
}
@@ -0,0 +1,23 @@
import Fuse from 'fuse.js';
import { shallowRef, computed, Ref } from 'vue';
const useSearch = <T>(query: Ref<string>, collection: T[], keys: Fuse.FuseOptionKey<T>[] = []) => {
const index = shallowRef(
new Fuse(collection, {
threshold: 0.2,
keys,
})
)
const results = computed(() => {
if (query.value) {
return index.value.search(query.value).map((result) => result.item);
}
return collection;
});
return results;
};
export default useSearch;
@@ -0,0 +1,40 @@
import { refThrottled } from '@vueuse/core';
import { nextTick, onMounted, ref, watch } from 'vue';
const useSearchInput = () => {
const searchInput = ref()
const searchQuery = ref(
typeof window === 'undefined'
? ''
: (
new URLSearchParams(window.location.search).get('search')
|| ''
)
)
const searchQueryThrottled = refThrottled(searchQuery, 200)
watch(searchQueryThrottled, (searchString) => {
const newUrl = new URL(window.location.href);
newUrl.searchParams.set('search', searchString);
nextTick(() => {
window.history.replaceState({}, '', newUrl)
})
})
onMounted(() => {
const searchParams = new URLSearchParams(window.location.search);
if(searchParams.has('focus')) {
searchInput.value.focus()
}
})
return {
searchInput,
searchQuery,
searchQueryThrottled
};
};
export default useSearchInput;
+26
View File
@@ -0,0 +1,26 @@
import { h } from 'vue'
import DefaultTheme from 'vitepress/theme'
import './style.css'
import { Theme } from 'vitepress'
import IconsSidebarNavAfter from './layouts/IconsSidebarNavAfter.vue'
import HomeHeroIconsCard from './components/home/HomeHeroIconsCard.vue'
import HomeHeroBefore from "./components/home/HomeHeroBefore.vue";
import { ICON_STYLE_CONTEXT, iconStyleContext } from './composables/useIconStyle'
import { CATEGORY_VIEW_CONTEXT, categoryViewContext } from './composables/useCategoryView'
const theme: Partial<Theme> = {
extends: DefaultTheme,
Layout() {
return h(DefaultTheme.Layout, null, {
'home-hero-before': () => h(HomeHeroBefore),
'sidebar-nav-after': () => h(IconsSidebarNavAfter),
'home-hero-image': () => h(HomeHeroIconsCard),
})
},
enhanceApp({ app }) {
app.provide(ICON_STYLE_CONTEXT, iconStyleContext)
app.provide(CATEGORY_VIEW_CONTEXT, categoryViewContext)
}
}
export default theme
@@ -0,0 +1,16 @@
<script setup>
import { useData } from 'vitepress'
import CategoryList from '../components/icons/CategoryList.vue'
import SidebarIconCustomizer from '../components/icons/SidebarIconCustomizer.vue'
const { page } = useData()
</script>
<template>
<div>
<SidebarIconCustomizer v-if="page?.relativePath?.startsWith?.('icons')"/>
<CategoryList v-if="page?.relativePath?.startsWith?.('icons')"/>
</div>
</template>
+128
View File
@@ -0,0 +1,128 @@
:root {
--vp-c-brand: #F56565;
--vp-c-brand-light: #F67373;
--vp-c-brand-lighter: #F89191;
--vp-c-brand-dark: #DC5A5A;
--vp-c-brand-darker: #C45050;
--vp-c-bg-alt-up: #fff;
--vp-c-bg-alt-down: #fff;
}
.dark {
--vp-c-bg-alt-up: #1B1B1D;
--vp-c-bg-alt-down: #0F0F10;
}
.VPNavBarTitle .logo {
height: 36px;
width: 36px;
}
.VPNavBarTitle .title {
font-size: 21px;
}
.VPHomeHero > .container {
gap: 24px;
}
.VPHomeHero .image-container {
transform: none;
width: 100%;
/* padding: 0 24px; */
}
/* .VPHomeHero .container {
flex-direction: column-reverse;
} */
.VPHomeHero .container .main {
/* flex:1; */
flex-shirk: 0;
}
.VPHomeHero .container .main h1.name {
color: var(--vp-c-text);
}
.VPHomeHero .container .main h1.name .clip {
color: inherit;
-webkit-text-fill-color: unset;
color: var(--vp-c-text);
font-size: 36px;
}
.VPHomeHero .container .main h1::first-line {
color: var(--vp-c-brand);
}
/* */
.VPHomeHero .container .image {
margin: 0;
order: 2;
/* flex: 1; */
margin-top: 32px;
}
.VPHomeHero .container .image-container {
height: auto;
justify-content: flex-end;
}
.VPHomeHero .container .image-bg {
display: none;
}
.VPFeature .icon {
background-color: var(--vp-c-bg);;
}
.vp-doc[class*=" _icons_"] > div {
max-width: 100%;
}
.VPDoc:has(.vp-doc[class*=" _icons_"]) > .container > .content{
padding-right: 0;
padding-left: 0;
}
@media (min-width: 640px) {
.VPHomeHero .container .main h1.name .clip {
font-size: unset;
}
}
@media (min-width: 960px) {
.VPHomeHero .container .image {
order: 1;
margin-bottom: auto;
margin-top: 0;
position: relative;
}
.VPHomeHero .container .main {
width: 50%;
}
.VPHomeHero .container .image {
width: 50%;
}
.VPHomeHero .container .image-container {
display: block;
}
.VPHomeHero .container .main h1.name {
}
}
.VPNavBarHamburger .container > span {
border-radius: 2px;
}
/*
html:has(* .outline-link:target) {
scroll-behavior: smooth;
} */
+46
View File
@@ -0,0 +1,46 @@
export type IconNode = [elementName: string, attrs: Record<string, string>][]
export type IconNodeWithKeys = [elementName: string, attrs: Record<string, string>, key: string][]
export interface IconEntity {
name: string;
tags: string[];
categories: string[];
contributors: string[];
aliases?: string[];
iconNode: IconNode;
createdRelease?: Release;
changedRelease?: Release;
}
export interface Category {
name: string
title: string
icon?: string
iconCount: number
icons?: IconEntity[]
}
interface Shield {
alt: string
src: string
href: string
}
export interface PackageItem {
name: string
description: string
icon: string
iconDark: string
shields: Shield[]
source: string
documentation: string
order?: number
private?: boolean
flutter?: object
}
export interface Release {
version: string
date: string
}
+10
View File
@@ -0,0 +1,10 @@
declare module "*.vue" {
import Vue from "vue";
export default Vue;
}
declare module "*.data.ts" {
const data: any;
export { data };
}
+37
View File
@@ -0,0 +1,37 @@
# Lucide Docs website
The Lucide docs website is built with Vitepress: https://vitepress.dev/
This is Markdown-based documentation powered by Vue.
This is why this file is in txt format.
## Development
```sh
# Install dependencies
pnpm install
```
```sh
# Start docs dev server
pnpm docs:dev
# Start api dev server
pnpm dev
```
## Build
```sh
# Build docs
pnpm docs:build
```
```sh
# Build api
pnpm build:api
```
## Components
See .vitepress directory.
+5
View File
@@ -0,0 +1,5 @@
---
aside: false
---
<!--@include: ../CODE_OF_CONDUCT.md -->
-25
View File
@@ -1,25 +0,0 @@
---
title: Comparison
---
# Comparison
## Lucide vs Feather Icons
Lucide is a community-run fork of [Feather Icons](https://github.com/feathericons/feather).
It began after growing disaffection of the [Feather Icons](https://github.com/feathericons/feather) project moderation. With over 300+ open issues and over 100+ open PRs, the Feather Icons project has been abandoned and not maintained actively. This unfortunately means that hundreds of developers and designers wasted their time contributing to Feather Icons with no chance of PRs being accepted.
Lucide is trying to expand the icon set as much as possible while staying faithful to the original simplistic design language. We do this as a community of devs and designers.
### Why should I choose Lucide over Feather Icons?
- Lucide already expended the icon set by 130+ in less then a year. Lucide has over 500+ icon, feather sticks around 286 icons.
- Well maintained code base.
- Active community.
### Should I migrate to Lucide?
That depends if you're fine with the icons from feather icons. If that is the case, it is maybe not the effort worth it.
But if you keep wrestling and feel limited by the icons Feather provides you can consider to migrate.
We didn't remove any icons when we forked, but there are some icons renamed.
+5
View File
@@ -0,0 +1,5 @@
---
aside: false
---
<!--@include: ../CONTRIBUTING.md -->
+25
View File
@@ -0,0 +1,25 @@
---
title: Comparison
---
# Comparison
## Lucide vs Feather Icons
Lucide is a community-driven fork of [Feather Icons](https://github.com/feathericons/feather).
The decision to create Lucide arose from growing dissatisfaction with the moderation of the Feather Icons project. With more than 300 open issues and over 100 open PRs, the Feather Icons project has been abandoned and is no longer actively maintained. Unfortunately, this means that numerous developers and designers have invested their time in contributing to Feather Icons without the possibility of their PRs being accepted.
In an effort to expand the icon set while remaining true to the original minimalist design language, Lucide is driven by a community of developers and designers. We strive to grow together and maintain a faithful continuation of the project.
### Why should I choose Lucide over Feather Icons?
- Lucide has expanded its icon set by 500+ in the last few years. Lucide now has over 1000 icons, while Feather has around 287 icons.
- Well maintained code base.
- Active community.
### Should I migrate to Lucide?
That depends on whether you're satisfied with the icons from Feather Icons. If that is the case, it may not be worth the effort.
However, if you find yourself struggling and feeling limited by the icons provided by Feather, you can consider migrating.
When we forked, we didn't remove any icons, but some icons have been renamed.
@@ -28,7 +28,7 @@ Set the following:
1. Stroke width: 2px
2. Stroke alignment: center
![Figma Stroke Options](images/figma-stroke-options.png)
![Figma Stroke Options](../../images/figma-stroke-options.png)
## Export Or Copy Your Icon
Once you have completed your icon, you can export it.
@@ -21,35 +21,35 @@ Here are rules that should be followed to keep quality and consistency when maki
### 1. Icons must be designed on a 24 by 24 pixels canvas.
![24px-24px](images/24px-24px.png?raw=true "24px-24px")
![24px-24px](../../images/24px-24px.svg?raw=true "24px-24px")
### 2. Icons must have at least 1 pixel padding within the canvas.
![1px-padding](images/1px-padding.png?raw=true "1px-padding")
![1px-padding](../../images/1px-padding.svg?raw=true "1px-padding")
### 3. Icons must have a stroke width of 2 pixels.
![2px-stroke](images/2px-stroke.png?raw=true "2px-stroke")
![2px-stroke](../../images/2px-stroke.svg?raw=true "2px-stroke")
### 4. Icons must use round joins.
![round-joints](images/round-joints.png?raw=true "round-joints")
![round-joints](../../images/round-joints.svg?raw=true "round-joints")
### 5. Icons must use round caps.
![round-caps](images/round-caps.png?raw=true "round-caps")
![round-caps](../../images/round-caps.svg?raw=true "round-caps")
### 6. Icons must use centered strokes.
![centered-strokes](images/centered-strokes.png?raw=true "centered-strokes")
![centered-strokes](../../images/centered-strokes.svg?raw=true "centered-strokes")
### 7. Shapes (such as squares) in icons must have border radius of 2 pixels.
![2px-border-radius](images/2px-border-radius.png?raw=true "2px-border-radius")
![2px-border-radius](../../images/2px-border-radius.svg?raw=true "2px-border-radius")
### 8. Distinct elements must have 2 pixels of spacing between each other.
![2px-element-spacing](images/2px-element-spacing.png?raw=true "2px-element-spacing")
![2px-element-spacing](../../images/2px-element-spacing.svg?raw=true "2px-element-spacing")
## Code Conventions
@@ -24,7 +24,7 @@ The Illustrator template is created following guidelines from the [Icon Design G
5. Export the file with the export menu under: `Export > Export As..` than safe the file as SVG. Select the following options in the SVG Options dialog:
![SVG export options in Illustrator](images/illustrator-svg-options.png?raw=true "Setting Page Size")
![SVG export options in Illustrator](../../images/illustrator-svg-options.png?raw=true "Setting Page Size")
After that, double check that the [code conventions and SVG global attributes](icon-design-guide.md#code-conventions) are correct.
@@ -13,11 +13,11 @@ When opening a new document, Inkscape will create a canvas of a default size. T
1. Open the Document Properties dialog (File -> Document Properties).
2. On the “Page Size” tab, under “Custom Size” set the Units to `px` and set both Height and Width to 24.
![Setting Page Size](images/page-size.png?raw=true "Setting Page Size")
![Setting Page Size](../../images/page-size.png?raw=true "Setting Page Size")
3. On the “Grid” tab, select `Rectangular Grid` and click “New Grid”.
![Setting Grid Properties](images/grid-1.png?raw=true "Setting Grid Properties")
![Setting Grid Properties](../../images/grid-1.png?raw=true "Setting Grid Properties")
4. Set the Grid Units to `px` and set Spacing X and Spacing Y both to 1.
![Setting Grid Properties](images/grid-2.png?raw=true "Setting Grid Properties")
![Setting Grid Properties](../../images/grid-2.png?raw=true "Setting Grid Properties")
5. Close the Document Properties dialog.
6. To center the canvas in the viewport, select View -> Zoom -> Drawing.
@@ -25,17 +25,17 @@ When opening a new document, Inkscape will create a canvas of a default size. T
1. Create a path or shape.
2. With the path selected, open the Stroke and Fill panel by pressing `Ctrl+Shift+F` on your keyboard.
![Stroke Style Properties](images/strokes.png?raw=true "Setting Grid Properties")
![Stroke Style Properties](../../images/strokes.png?raw=true "Setting Grid Properties")
3. On the “Stroke Style” tab:
* Set Stroke Width to `2px`.
* Select the rounded join type.
* Select the rounded cap type.
4. If the shape is a rectangle, select the rectangle and in the top of the screen below the menu bar, set `Rx` and `Ry` to `2px`.
![Rectangle Radius Properties](images/corner-radius.png?raw=true "Rectangle Radius Properties")
![Rectangle Radius Properties](../../images/corner-radius.png?raw=true "Rectangle Radius Properties")
## Saving A File
1. When ready to save the file, click Save As and select “Optimized SVG” as the file type.
![Save As](images/save-as.png?raw=true "Save as")
![Save As](../../images/save-as.png?raw=true "Save as")
2. After clicking Save, to conform with the other icons in the package, set Pretty Printing to use spaces and set the indentation depth to 2.
![Optimize](images/optimize-settings.png?raw=true "Optimize")
![Optimize](../../images/optimize-settings.png?raw=true "Optimize")
+28
View File
@@ -0,0 +1,28 @@
---
title: What is Lucide?
nextPage:
- comparison
- installation
---
# What is Lucide?
Lucide is an open-source icon library that provides 1000+ vector (svg) files for displaying icons and symbols in digital and non-digital projects. The library aims to make it easier for designers and developers to incorporate icons into their projects by providing several official [packages](/packages) to make it easier to use these icons in your project.
## Available Icons
Lucide contains icons with different variants and states, allowing users to choose the most suitable icon for their needs. And if a desired icon isn't available yet, users can open a design request, and the Lucide community contributors will help provide new icons. With more icons to choose from, users have more options to work with in their projects.
Complete Set of Icons
As new applications with specific features arise, Lucide aims to provide a complete set of icons for every project. The community follows a set of design rules when designing new icons. These rules maintain standards for the icons, such as recognizability, consistency in style, and readability at all sizes. While creativity is valued in new icons, recognizable design conventions are important to ensure that the icons are easily identifiable by users.
## Code Optimization
In addition to design, code is also important. Assets like icons can significantly increase bandwidth usage in web projects. With the growing internet, Lucide has a responsibility to keep their assets as small as possible. To achieve this, Lucide uses SVG compression and specific code architecture for tree-shaking abilities. After tree-shaking, you only ship the icons you used, which helps to keep software distribution size to a minimum.
## Official Packages
Lucide's official packages are designed to work on different platforms, making it easier for users to integrate icons into their projects. The packages are available for various technologies, including [Web (Vanilla)](https://lucide.dev/guide/packages/lucide), [React](https://lucide.dev/guide/packages/lucide-react), [React Native](https://lucide.dev/guide/packages/lucide-react-native), [Vue](https://lucide.dev/guide/packages/lucide-vue), [Vue 3](https://lucide.dev/guide/packages/lucide-vue-next), [Svelte](https://lucide.dev/guide/packages/lucide-svelte),[Preact](https://lucide.dev/guide/packages/lucide-preact), [Solid](https://lucide.dev/guide/packages/lucide-solid), [Angular](https://lucide.dev/guide/packages/lucide-angular), [NodeJS](https://lucide.dev/guide/packages/lucide-static#nodejs) and [Flutter](https://lucide.dev/guide/packages/lucide-flutter).
## Community
If you have any questions about Lucide, feel free to reach out to the community. You can find them on [GitHub](https://github.com/lucide-icons/lucide) and [Discord](https://discord.gg/EH6nSts).
@@ -8,15 +8,21 @@ title: Installation
Implementation of the lucide icon library for web applications.
```bash
::: code-group
```sh [pnpm]
pnpm install lucide
```
```sh [yarn]
yarn add lucide
```
```sh [npm]
npm install lucide
```
or
```sh
yarn add lucide
```
:::
For more details, see the [documentation](packages/lucide.md).
@@ -24,16 +30,22 @@ For more details, see the [documentation](packages/lucide.md).
Implementation of the lucide icon library for react applications.
```bash
::: code-group
```sh [pnpm]
pnpm install lucide-react
```
```sh [yarn]
yarn add lucide-react
```
or
```sh
```sh [npm]
npm install lucide-react
```
:::
For more details, see the [documentation](packages/lucide-react.md).
## Vue 2
+109
View File
@@ -0,0 +1,109 @@
# Lucide Angular
Implementation of the lucide icon library for Angular applications.
## Installation
```bash
yarn add lucide-angular
```
or
```bash
npm install lucide-angular
```
## How to use
### Step 1: Import `LucideAngularModule`
In any Angular module you wish to use Lucide icons in, you have to import `LucideAngularModule`, and pick any icons you wish to use:
```js
import { LucideAngularModule, File, Home, Menu, UserCheck } from 'lucide-angular';
@NgModule({
imports: [
LucideAngularModule.pick({File, Home, Menu, UserCheck})
]
})
export class AppModule { }
```
### Step 2: Use the icons in templates
Within your templates you may now use one of the following component tags to insert an icon:
```html
<lucide-angular name="file" class="my-icon"></lucide-angular>
<lucide-icon name="home" class="my-icon"></lucide-icon>
<i-lucide name="menu" class="my-icon"></i-lucide>
<span-lucide name="user-check" class="my-icon"></span-lucide>
```
### Props
You can pass additional props to adjust the icon appearance.
| name | type | default |
| --------------------- | --------- | ------------ |
| `size` | *number* | 24 |
| `color` | *string* | currentColor |
| `strokeWidth` | *number* | 2 |
| `absoluteStrokeWidth` | *boolean* | false |
```html
<i-lucide name="home" [size]="48" color="red" [strokeWidth]="1"></i-lucide>
```
### Global configuration
You can inject the `LucideIconConfig` service in your root component to globally configure the default property values as defined above.
### Styling using a custom CSS class
Any extra HTML attribute is ignored, but the `class` attribute
is passed onto the internal SVG image element and it can be used to style it:
```css
svg.my-icon {
width: 12px;
height: 12px;
stroke-width: 3;
}
```
## Injecting multiple icon providers
You may provide additional icons using the `LUCIDE_ICONS` injection token,
which accepts multiple providers of the interface `LucideIconsProviderInterface`
with the utility class `LucideIconsProvider` available for easier usage:
```js
import { LUCIDE_ICONS, LucideIconProvider } from 'lucide-angular';
import { MyIcon } from './icons/my-icon';
const myIcons = {MyIcon};
@NgModule({
providers: [
{provide: LUCIDE_ICONS, multi: true, useValue: new LucideIconProvider(myIcons)},
]
})
export class AppModule { }
```
To add custom icons, you will first need to convert them to an [svgson format](https://github.com/elrumordelaluz/svgson).
## Loading all icons
> :warning: You may also opt to import all icons if necessary using the following format but be aware that this will significantly increase your application build size.
```js
import { icons } from 'lucide-angular';
...
LucideAngularModule.pick(icons)
```

Some files were not shown because too many files have changed in this diff Show More