Compare commits

...

86 Commits

Author SHA1 Message Date
Daniel Bayley
6916aebee4 Add generic shapes icon (#1239)
* Add generic `shapes` icon

* Update icons/shapes.svg

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

---------

Co-authored-by: Karsa <contact@karsa.org>
2023-06-12 21:03:39 +02:00
Jakob Guddas
65d213264f Changed binary (#1188)
* Changed `binary`

Felt to aggressive.

* feat: rounded radius to zero in file-digit
2023-06-11 20:51:26 +02:00
Daniel Bayley
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 Bayley
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
Guillermo Angeles
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 Bayley
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 Guddas
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 Bayley
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
Daniel Bayley
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 Fennis
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 Guddas
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 Bayley
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 Bayley
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 Bayley
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
locness3
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 Bayley
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
Daniel Bayley
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 Bayley
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 Bayley
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
Karsa
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 Bayley
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
Daniel Bayley
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 Bayley
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
Karsa
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 Guddas
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
Neutron
3449097f77 Add sparkle icon (#1220)
Co-authored-by: Karsa <contact@karsa.org>
2023-05-17 21:32:39 +02:00
Daniel Bayley
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
Karsa
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 Bayley
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
Karsa
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
Karsa
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
Karsa
e50b03f316 Adds leafy-green icon (#1168)
Co-authored-by: Karsa <karsa@karsa.org>
2023-05-05 13:27:35 +02:00
Karsa
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
Daniel Bayley
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
Daniel Bayley
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
Daniel Bayley
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 Bayley
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 Bayley
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 Bayley
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 Bayley
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 Bayley
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 Bayley
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 Bayley
f0529b9ef7 Add combine icon (#1069)
Co-authored-by: Eric Fennis <eric.fennis@gmail.com>
2023-05-03 15:19:54 +02:00
Karsa
0c216b41c5 optimizes sun and moon icons (#1210)
Co-authored-by: Karsa <karsa@karsa.org>
2023-05-03 14:11:30 +02:00
Daniel Bayley
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
1602 changed files with 19332 additions and 12081 deletions

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
.gitignore vendored
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

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.

View File

@@ -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
})

View File

@@ -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
})

View File

@@ -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
}, {})
})

View File

@@ -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
})

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 ])
)
})

View File

@@ -0,0 +1,3 @@
export default eventHandler(() => {
return { nitro: 'Is Awesome! asda' }
})

160
docs/.vitepress/config.ts Normal file
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))
},
})

View File

@@ -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"
}
]

View File

@@ -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;

View File

@@ -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>

View File

@@ -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];
};

View File

@@ -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] };
};

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)
}))
}

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);
}

View File

@@ -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)

View File

@@ -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]
})

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
docs/.vitepress/sidebar.ts Normal file
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

View File

@@ -0,0 +1,11 @@
<template>
<div class="container">
<slot />
</div>
</template>
<style scoped>
.container {
padding: 32px;
}
</style>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
}
}
}

View File

@@ -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>

View File

@@ -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,
}
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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',
},
]
}
}
}

View File

@@ -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>

View File

@@ -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,
}
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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" />

View File

@@ -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" />

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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%;
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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,
};
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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];
}

View File

@@ -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;
}

View File

@@ -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
}
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;

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

View File

@@ -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>

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;
} */

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
docs/.vitepress/vue-shim.d.ts vendored Normal file
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
docs/README.txt Normal file
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
docs/code-of-conduct.md Normal file
View File

@@ -0,0 +1,5 @@
---
aside: false
---
<!--@include: ../CODE_OF_CONDUCT.md -->

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
docs/contributing.md Normal file
View File

@@ -0,0 +1,5 @@
---
aside: false
---
<!--@include: ../CONTRIBUTING.md -->

25
docs/guide/comparison.md Normal file
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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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.

View File

@@ -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
docs/guide/index.md Normal file
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).

View File

@@ -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

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