├── .eslintrc.json ├── .github └── FUNDING.yml ├── .npmrc ├── LICENSE ├── _animations.scss ├── _icon-park.scss ├── _material-icons.scss ├── _material-symbols.scss ├── _transformations.scss ├── bower.json ├── demo.html ├── last-icon.css ├── last-icon.css.map ├── last-icon.js ├── last-icon.min.css ├── last-icon.min.css.map ├── last-icon.min.js ├── last-icon.min.js.map ├── last-icon.scss ├── package.json └── readme.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "parserOptions": { 8 | "ecmaVersion": 12, 9 | "sourceType": "module" 10 | }, 11 | "rules": { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: lekoala 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Thomas Portelange 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /_animations.scss: -------------------------------------------------------------------------------- 1 | $default-duration: 1.5s !default; 2 | 3 | @-webkit-keyframes spin { 4 | 0% { 5 | transform: rotate(0); 6 | } 7 | 100% { 8 | transform: rotate(359deg); 9 | } 10 | } 11 | @keyframes spin { 12 | 0% { 13 | transform: rotate(0); 14 | } 15 | 100% { 16 | transform: rotate(359deg); 17 | } 18 | } 19 | @-webkit-keyframes burst { 20 | 0% { 21 | transform: scale(1); 22 | opacity: 1; 23 | } 24 | 90% { 25 | transform: scale(1.5); 26 | opacity: 0; 27 | } 28 | } 29 | @keyframes burst { 30 | 0% { 31 | transform: scale(1); 32 | opacity: 1; 33 | } 34 | 90% { 35 | transform: scale(1.5); 36 | opacity: 0; 37 | } 38 | } 39 | @-webkit-keyframes flashing { 40 | 0% { 41 | opacity: 1; 42 | } 43 | 45% { 44 | opacity: 0; 45 | } 46 | 90% { 47 | opacity: 1; 48 | } 49 | } 50 | @keyframes flashing { 51 | 0% { 52 | opacity: 1; 53 | } 54 | 45% { 55 | opacity: 0; 56 | } 57 | 90% { 58 | opacity: 1; 59 | } 60 | } 61 | @-webkit-keyframes fade-left { 62 | 0% { 63 | transform: translateX(0); 64 | opacity: 1; 65 | } 66 | 75% { 67 | transform: translateX(-20px); 68 | opacity: 0; 69 | } 70 | } 71 | @keyframes fade-left { 72 | 0% { 73 | transform: translateX(0); 74 | opacity: 1; 75 | } 76 | 75% { 77 | transform: translateX(-20px); 78 | opacity: 0; 79 | } 80 | } 81 | @-webkit-keyframes fade-right { 82 | 0% { 83 | transform: translateX(0); 84 | opacity: 1; 85 | } 86 | 75% { 87 | transform: translateX(20px); 88 | opacity: 0; 89 | } 90 | } 91 | @keyframes fade-right { 92 | 0% { 93 | transform: translateX(0); 94 | opacity: 1; 95 | } 96 | 75% { 97 | transform: translateX(20px); 98 | opacity: 0; 99 | } 100 | } 101 | @-webkit-keyframes fade-up { 102 | 0% { 103 | transform: translateY(0); 104 | opacity: 1; 105 | } 106 | 75% { 107 | transform: translateY(-20px); 108 | opacity: 0; 109 | } 110 | } 111 | @keyframes fade-up { 112 | 0% { 113 | transform: translateY(0); 114 | opacity: 1; 115 | } 116 | 75% { 117 | transform: translateY(-20px); 118 | opacity: 0; 119 | } 120 | } 121 | @-webkit-keyframes fade-down { 122 | 0% { 123 | transform: translateY(0); 124 | opacity: 1; 125 | } 126 | 75% { 127 | transform: translateY(20px); 128 | opacity: 0; 129 | } 130 | } 131 | @keyframes fade-down { 132 | 0% { 133 | transform: translateY(0); 134 | opacity: 1; 135 | } 136 | 75% { 137 | transform: translateY(20px); 138 | opacity: 0; 139 | } 140 | } 141 | @-webkit-keyframes tada { 142 | from { 143 | transform: scale3d(1, 1, 1); 144 | } 145 | 146 | 10%, 147 | 20% { 148 | transform: scale3d(0.95, 0.95, 0.95) rotate3d(0, 0, 1, -10deg); 149 | } 150 | 151 | 30%, 152 | 50%, 153 | 70%, 154 | 90% { 155 | transform: scale3d(1, 1, 1) rotate3d(0, 0, 1, 10deg); 156 | } 157 | 158 | 40%, 159 | 60%, 160 | 80% { 161 | transform: scale3d(1, 1, 1) rotate3d(0, 0, 1, -10deg); 162 | } 163 | 164 | to { 165 | transform: scale3d(1, 1, 1); 166 | } 167 | } 168 | @keyframes tada { 169 | from { 170 | transform: scale3d(1, 1, 1); 171 | } 172 | 173 | 10%, 174 | 20% { 175 | transform: scale3d(0.95, 0.95, 0.95) rotate3d(0, 0, 1, -10deg); 176 | } 177 | 178 | 30%, 179 | 50%, 180 | 70%, 181 | 90% { 182 | transform: scale3d(1, 1, 1) rotate3d(0, 0, 1, 10deg); 183 | } 184 | 185 | 40%, 186 | 60%, 187 | 80% { 188 | transform: rotate3d(0, 0, 1, -10deg); 189 | } 190 | 191 | to { 192 | transform: scale3d(1, 1, 1); 193 | } 194 | } 195 | .spin { 196 | animation: spin $default-duration linear infinite; 197 | } 198 | .spin-hover:hover { 199 | animation: spin $default-duration linear infinite; 200 | } 201 | .tada { 202 | animation: tada $default-duration ease infinite; 203 | } 204 | .tada-hover:hover { 205 | animation: tada $default-duration ease infinite; 206 | } 207 | .flashing { 208 | animation: flashing $default-duration infinite linear; 209 | } 210 | .flashing-hover:hover { 211 | animation: flashing $default-duration infinite linear; 212 | } 213 | .burst { 214 | animation: burst $default-duration infinite linear; 215 | } 216 | .burst-hover:hover { 217 | animation: burst $default-duration infinite linear; 218 | } 219 | .fade-up { 220 | animation: fade-up $default-duration infinite linear; 221 | } 222 | .fade-up-hover:hover { 223 | animation: fade-up $default-duration infinite linear; 224 | } 225 | .fade-down { 226 | animation: fade-down $default-duration infinite linear; 227 | } 228 | .fade-down-hover:hover { 229 | animation: fade-down $default-duration infinite linear; 230 | } 231 | .fade-left { 232 | animation: fade-left $default-duration infinite linear; 233 | } 234 | .fade-left-hover:hover { 235 | animation: fade-left $default-duration infinite linear; 236 | } 237 | .fade-right { 238 | animation: fade-right $default-duration infinite linear; 239 | } 240 | .fade-right-hover:hover { 241 | animation: fade-right $default-duration infinite linear; 242 | } 243 | -------------------------------------------------------------------------------- /_icon-park.scss: -------------------------------------------------------------------------------- 1 | l-i[theme="multi-color"] { 2 | // outer fill color 3 | path[fill="#2F88FF"] { 4 | fill: var(--color); 5 | } 6 | // inner fill color 7 | path[fill="#43CCF8"] { 8 | fill: var(--color-secondary); 9 | stroke: var(--color-bg); 10 | } 11 | path[stroke="white"] { 12 | stroke: white; 13 | } 14 | path[stroke="black"] { 15 | stroke: #333; 16 | } 17 | } 18 | l-i[theme="two-tone"] { 19 | path[fill="#2F88FF"] { 20 | fill: var(--color); 21 | } 22 | path[fill="#43CCF8"] { 23 | fill: var(--color); 24 | } 25 | path[stroke="white"], 26 | path[stroke="black"] { 27 | stroke: #333; 28 | } 29 | } 30 | l-i[theme="filled"] { 31 | path[fill] { 32 | fill: currentColor; 33 | } 34 | path[stroke="black"] { 35 | stroke: white; 36 | } 37 | } 38 | l-i[theme="outline"] { 39 | path[fill] { 40 | fill: none; 41 | } 42 | path[stroke="black"] { 43 | stroke: currentColor; 44 | } 45 | path[fill="#43CCF8"] { 46 | fill: currentColor; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /_material-icons.scss: -------------------------------------------------------------------------------- 1 | .material-icons-two-tone { 2 | background-color: currentColor; 3 | -webkit-background-clip: text; 4 | background-clip: text; 5 | -webkit-text-fill-color: transparent; 6 | } 7 | -------------------------------------------------------------------------------- /_material-symbols.scss: -------------------------------------------------------------------------------- 1 | $default-symbols-duration: 0.5s !default; 2 | 3 | @keyframes symbols-pulse { 4 | 0% { 5 | font-variation-settings: "wght" 100; 6 | } 7 | 8 | 50% { 9 | font-variation-settings: "wght" 700; 10 | } 11 | 12 | 100% { 13 | font-variation-settings: "wght" 100; 14 | } 15 | } 16 | 17 | @keyframes symbols-fill { 18 | 0% { 19 | font-variation-settings: "FILL" 0; 20 | } 21 | 22 | 100% { 23 | font-variation-settings: "FILL" 1; 24 | } 25 | } 26 | 27 | @keyframes symbols-empty { 28 | 0% { 29 | font-variation-settings: "FILL" 1; 30 | } 31 | 32 | 100% { 33 | font-variation-settings: "FILL" 0; 34 | } 35 | } 36 | 37 | .symbols-pulse i, 38 | .symbols-pulse-hover:hover i, 39 | .symbols-fill i, 40 | .symbols-fill-hover:hover i, 41 | .symbols-empty i, 42 | .symbols-empty-hover:hover i { 43 | animation-timing-function: cubic-bezier(0.45, 0, 0.55, 1); 44 | animation-duration: var(--duration, $default-symbols-duration); 45 | } 46 | 47 | .symbols-pulse i, 48 | .symbols-pulse-hover:hover i { 49 | --duration: #{$default-symbols-duration * 3}; 50 | animation-name: symbols-pulse; 51 | animation-iteration-count: infinite; 52 | } 53 | 54 | .symbols-fill i, 55 | .symbols-fill-hover:hover i { 56 | animation-name: symbols-fill; 57 | animation-fill-mode: forwards; 58 | } 59 | 60 | .symbols-empty i, 61 | .symbols-empty-hover:hover i { 62 | animation-name: symbols-empty; 63 | animation-fill-mode: forwards; 64 | } 65 | 66 | .material-symbols-rounded, 67 | .material-symbols-outlined, 68 | .material-symbols-sharp { 69 | font-variation-settings: "FILL" var(--fill, 0), "wght" var(--weight, 400), "GRAD" var(--grad, 0), "OPSZ" var(--opsz, 24); 70 | } 71 | 72 | .dark { 73 | --grad: -25; 74 | background: black; 75 | color: rgba(255, 255, 255, 1); 76 | 77 | &[disabled], 78 | &.disabled { 79 | color: rgba(255, 255, 255, 0.3); 80 | } 81 | } -------------------------------------------------------------------------------- /_transformations.scss: -------------------------------------------------------------------------------- 1 | .rotate-90 { 2 | transform: rotate(90deg); 3 | } 4 | .rotate-180 { 5 | transform: rotate(180deg); 6 | } 7 | .rotate-270 { 8 | transform: rotate(270deg); 9 | } 10 | .flip-horizontal { 11 | transform: scaleX(-1); 12 | } 13 | .flip-vertical { 14 | transform: scaleY(-1); 15 | } 16 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "last-icon", 3 | "version": "2.0.0", 4 | "description": "One custom icon element to rule them all", 5 | "authors": ["LeKoala"], 6 | "keywords": ["DOM", "icons", "icon", "custom", "element", "component", "es6"], 7 | "main": "last-icon.js" 8 | } 9 | -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Last Icon Demo 7 | 9 | 10 | 11 | 43 | 44 | 45 | 46 | 65 | 66 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 107 | 109 | 111 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 133 | 134 | 135 | 136 |
137 |

138 | 139 | 140 | 142 | 143 | 144 | Last Icon 145 |

146 |

One custom icon element to rule them all

147 |
148 |
149 |

Basic usage

150 |

Simply use <l-i></l-i>

151 |

Default icons

152 |

153 | Set the name with name="your-icon" and, optionnaly, set a custom size with size="xx" (which is just a 154 | shortcut for setting style="--size:{xx}px") 155 |

156 | 157 | 158 | 159 |

160 | Icons are using vertical-align:middle by default and a vertical-align:0.125em offset when contained in a 161 | p, button, a or span 162 |

163 | 164 |

165 | Bootstrap Icons 166 | 167 | 168 | 169 |

170 |

Use set="bootstrap" or set="bs"

171 | 172 | 173 | 174 | 175 | 176 |

177 | Tabler Icons 178 | 179 | 180 | 181 |

182 |

This is the default set

183 |

Use set="tabler" or set="tb"

184 | 185 | 186 | 187 | 188 | 189 |

190 | You can also set various widths using stroke attribute (it will NOT use the font) ! It can also be set globally with 191 | defaultStroke. 192 |

193 | 194 | 195 | 196 | 197 |

198 | Material Icons 199 | 200 | 201 | 202 |

203 |

Use set="material" or set="mi"

204 | 205 | 206 | 207 | 208 |

209 | You can specify the following types: filled, outlined, round, sharp, 210 | two-tone 211 |

212 | 213 | 214 | 215 | 216 | 217 | 218 |

You can also use the font as an icon if needed!

219 | 220 |

221 | Material Symbols 222 | 223 | 224 | 225 |

226 |

Use set="symbols" or set="ms". Works best with font icons if you intend to use options.

227 | 230 | 231 |

Weight (note how search and star behave differently, the star becomes much smaller with lower weights)

232 | 233 | 234 | 235 | 236 | 237 | 238 |

You can specify the following types: outlined, rounded, sharp

239 | 240 | 241 | 242 |

Click to toggle fill. Note that we don't see much difference between outlined and sharp ;-)

243 | 244 | 245 |

Animate them! (only works with font icons)

246 | 247 | 248 |

Animate on hover (only works with font icons)

249 | 250 | 251 | 252 |

Make sure size is perfect 253 |

254 | 255 | 256 | 257 | 258 | 259 |

Dark background

260 | 261 | 262 | 263 | 264 |

265 | Super Tiny Icons 266 | 267 | 268 | 269 |

270 |

Use set="supertiny" or set="st"

271 |

Super useful if you need the social icons with proper colors 👌

272 | 273 | 274 | 275 |

276 | Flags Icons 277 | 278 | 279 | 280 |

281 |

Use set="flags" or set="fl"

282 | 283 | 284 | 285 |

286 | Emoji Icons 287 | 288 | 289 | 290 |

291 |

Use set="emojicc" or set="em"

292 | 293 | 294 |

295 | Iconoir 296 | 297 | 298 | 299 |

300 |

Use set="iconoir" or set="in"

301 |

Stroke only works with svg

302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 |

311 | Feather 312 | 313 | 314 | 315 |

316 |

Use set="feather" or set="ft"

317 |

You can also set various widths using stroke attribute !

318 | 319 | 320 | 321 | 322 | 323 |

324 | Lucide 325 | 326 | 327 | 328 |

329 |

Use set="lucide" or set="lu"

330 |

You can also set various widths using stroke attribute !

331 | 332 | 333 | 334 | 335 | 336 |

337 | IconPark 338 | 339 | 340 | 341 |

342 |

Use set="iconpark" or set="ip"

343 |

You can also set various widths using stroke attribute !

344 |

You can set 4 themes (outline, filled, two-tone, multi-color): these need to be set in CSS.

345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 356 | 357 | 358 |

359 | Phosphor 360 | 361 | 362 | 363 |

364 |

Use set="phosphor" or set="ph"

365 |

You can set 6 types (bold, duotone, fill, light, regular, thin)

366 | 367 | 368 | 369 | 370 | 371 | 374 | 375 | 376 |

Invalid icons

377 |

Invalid icons are logged to the console and display a warning emoji

378 | 379 | 380 |

Animations and transformations

381 | 382 |

Use the same icon and rotate

383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 |

Use the animation classes or they -hover variant

392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 |

Or use svg animation

411 | 412 | 413 | 414 | 415 | 416 | 417 | 419 | 420 | 421 | 422 | 423 | 424 | 425 |

Advanced usage

426 |

Host your own icons

427 |

You can customize or add your own iconSets.

428 |
429 | customElements.whenDefined("l-i").then(() => {
430 |   // Access through registry
431 |   customElements.get("l-i").configure({
432 |     debug: true,
433 |     // Specify our own loading path
434 |     sets: {
435 |       bootstrap: {
436 |         svgPath: () => "./vendor/bootstrap",
437 |       }
438 |     },
439 |   });
440 | });
441 |       
442 |
443 | 447 | 448 | 449 | -------------------------------------------------------------------------------- /last-icon.css: -------------------------------------------------------------------------------- 1 | l-i { 2 | --size: 1em; 3 | display: inline-flex; 4 | width: var(--size); 5 | height: var(--size); 6 | vertical-align: middle; 7 | contain: strict; 8 | justify-content: center; 9 | align-items: center; 10 | } 11 | l-i span, 12 | l-i svg, 13 | l-i i { 14 | pointer-events: none; 15 | } 16 | l-i span { 17 | font-size: 0.8em; 18 | } 19 | l-i svg { 20 | display: block; 21 | width: 100%; 22 | height: 100%; 23 | } 24 | l-i i { 25 | font-size: var(--size) !important; 26 | color: currentColor; 27 | } 28 | 29 | p l-i:not([size]), 30 | button l-i:not([size]), 31 | a l-i:not([size]), 32 | span l-i:not([size]) { 33 | vertical-align: -0.125em; 34 | } 35 | 36 | .material-icons-two-tone { 37 | background-color: currentColor; 38 | -webkit-background-clip: text; 39 | background-clip: text; 40 | -webkit-text-fill-color: transparent; 41 | } 42 | 43 | @-webkit-keyframes symbols-pulse { 44 | 0% { 45 | font-variation-settings: "wght" 100; 46 | } 47 | 50% { 48 | font-variation-settings: "wght" 700; 49 | } 50 | 100% { 51 | font-variation-settings: "wght" 100; 52 | } 53 | } 54 | 55 | @keyframes symbols-pulse { 56 | 0% { 57 | font-variation-settings: "wght" 100; 58 | } 59 | 50% { 60 | font-variation-settings: "wght" 700; 61 | } 62 | 100% { 63 | font-variation-settings: "wght" 100; 64 | } 65 | } 66 | @-webkit-keyframes symbols-fill { 67 | 0% { 68 | font-variation-settings: "FILL" 0; 69 | } 70 | 100% { 71 | font-variation-settings: "FILL" 1; 72 | } 73 | } 74 | @keyframes symbols-fill { 75 | 0% { 76 | font-variation-settings: "FILL" 0; 77 | } 78 | 100% { 79 | font-variation-settings: "FILL" 1; 80 | } 81 | } 82 | @-webkit-keyframes symbols-empty { 83 | 0% { 84 | font-variation-settings: "FILL" 1; 85 | } 86 | 100% { 87 | font-variation-settings: "FILL" 0; 88 | } 89 | } 90 | @keyframes symbols-empty { 91 | 0% { 92 | font-variation-settings: "FILL" 1; 93 | } 94 | 100% { 95 | font-variation-settings: "FILL" 0; 96 | } 97 | } 98 | .symbols-pulse i, 99 | .symbols-pulse-hover:hover i, 100 | .symbols-fill i, 101 | .symbols-fill-hover:hover i, 102 | .symbols-empty i, 103 | .symbols-empty-hover:hover i { 104 | -webkit-animation-timing-function: cubic-bezier(0.45, 0, 0.55, 1); 105 | animation-timing-function: cubic-bezier(0.45, 0, 0.55, 1); 106 | -webkit-animation-duration: var(--duration, 0.5s); 107 | animation-duration: var(--duration, 0.5s); 108 | } 109 | 110 | .symbols-pulse i, 111 | .symbols-pulse-hover:hover i { 112 | --duration: 1.5s; 113 | -webkit-animation-name: symbols-pulse; 114 | animation-name: symbols-pulse; 115 | -webkit-animation-iteration-count: infinite; 116 | animation-iteration-count: infinite; 117 | } 118 | 119 | .symbols-fill i, 120 | .symbols-fill-hover:hover i { 121 | -webkit-animation-name: symbols-fill; 122 | animation-name: symbols-fill; 123 | -webkit-animation-fill-mode: forwards; 124 | animation-fill-mode: forwards; 125 | } 126 | 127 | .symbols-empty i, 128 | .symbols-empty-hover:hover i { 129 | -webkit-animation-name: symbols-empty; 130 | animation-name: symbols-empty; 131 | -webkit-animation-fill-mode: forwards; 132 | animation-fill-mode: forwards; 133 | } 134 | 135 | .material-symbols-rounded, 136 | .material-symbols-outlined, 137 | .material-symbols-sharp { 138 | font-variation-settings: "FILL" var(--fill, 0), "wght" var(--weight, 400), "GRAD" var(--grad, 0), "OPSZ" var(--opsz, 24); 139 | } 140 | 141 | .dark { 142 | --grad: -25; 143 | background: black; 144 | color: rgb(255, 255, 255); 145 | } 146 | .dark[disabled], .dark.disabled { 147 | color: rgba(255, 255, 255, 0.3); 148 | } 149 | 150 | l-i[theme=multi-color] path[fill="#2F88FF"] { 151 | fill: var(--color); 152 | } 153 | l-i[theme=multi-color] path[fill="#43CCF8"] { 154 | fill: var(--color-secondary); 155 | stroke: var(--color-bg); 156 | } 157 | l-i[theme=multi-color] path[stroke=white] { 158 | stroke: white; 159 | } 160 | l-i[theme=multi-color] path[stroke=black] { 161 | stroke: #333; 162 | } 163 | 164 | l-i[theme=two-tone] path[fill="#2F88FF"] { 165 | fill: var(--color); 166 | } 167 | l-i[theme=two-tone] path[fill="#43CCF8"] { 168 | fill: var(--color); 169 | } 170 | l-i[theme=two-tone] path[stroke=white], 171 | l-i[theme=two-tone] path[stroke=black] { 172 | stroke: #333; 173 | } 174 | 175 | l-i[theme=filled] path[fill] { 176 | fill: currentColor; 177 | } 178 | l-i[theme=filled] path[stroke=black] { 179 | stroke: white; 180 | } 181 | 182 | l-i[theme=outline] path[fill] { 183 | fill: none; 184 | } 185 | l-i[theme=outline] path[stroke=black] { 186 | stroke: currentColor; 187 | } 188 | l-i[theme=outline] path[fill="#43CCF8"] { 189 | fill: currentColor; 190 | } 191 | 192 | @-webkit-keyframes spin { 193 | 0% { 194 | transform: rotate(0); 195 | } 196 | 100% { 197 | transform: rotate(359deg); 198 | } 199 | } 200 | @keyframes spin { 201 | 0% { 202 | transform: rotate(0); 203 | } 204 | 100% { 205 | transform: rotate(359deg); 206 | } 207 | } 208 | @-webkit-keyframes burst { 209 | 0% { 210 | transform: scale(1); 211 | opacity: 1; 212 | } 213 | 90% { 214 | transform: scale(1.5); 215 | opacity: 0; 216 | } 217 | } 218 | @keyframes burst { 219 | 0% { 220 | transform: scale(1); 221 | opacity: 1; 222 | } 223 | 90% { 224 | transform: scale(1.5); 225 | opacity: 0; 226 | } 227 | } 228 | @-webkit-keyframes flashing { 229 | 0% { 230 | opacity: 1; 231 | } 232 | 45% { 233 | opacity: 0; 234 | } 235 | 90% { 236 | opacity: 1; 237 | } 238 | } 239 | @keyframes flashing { 240 | 0% { 241 | opacity: 1; 242 | } 243 | 45% { 244 | opacity: 0; 245 | } 246 | 90% { 247 | opacity: 1; 248 | } 249 | } 250 | @-webkit-keyframes fade-left { 251 | 0% { 252 | transform: translateX(0); 253 | opacity: 1; 254 | } 255 | 75% { 256 | transform: translateX(-20px); 257 | opacity: 0; 258 | } 259 | } 260 | @keyframes fade-left { 261 | 0% { 262 | transform: translateX(0); 263 | opacity: 1; 264 | } 265 | 75% { 266 | transform: translateX(-20px); 267 | opacity: 0; 268 | } 269 | } 270 | @-webkit-keyframes fade-right { 271 | 0% { 272 | transform: translateX(0); 273 | opacity: 1; 274 | } 275 | 75% { 276 | transform: translateX(20px); 277 | opacity: 0; 278 | } 279 | } 280 | @keyframes fade-right { 281 | 0% { 282 | transform: translateX(0); 283 | opacity: 1; 284 | } 285 | 75% { 286 | transform: translateX(20px); 287 | opacity: 0; 288 | } 289 | } 290 | @-webkit-keyframes fade-up { 291 | 0% { 292 | transform: translateY(0); 293 | opacity: 1; 294 | } 295 | 75% { 296 | transform: translateY(-20px); 297 | opacity: 0; 298 | } 299 | } 300 | @keyframes fade-up { 301 | 0% { 302 | transform: translateY(0); 303 | opacity: 1; 304 | } 305 | 75% { 306 | transform: translateY(-20px); 307 | opacity: 0; 308 | } 309 | } 310 | @-webkit-keyframes fade-down { 311 | 0% { 312 | transform: translateY(0); 313 | opacity: 1; 314 | } 315 | 75% { 316 | transform: translateY(20px); 317 | opacity: 0; 318 | } 319 | } 320 | @keyframes fade-down { 321 | 0% { 322 | transform: translateY(0); 323 | opacity: 1; 324 | } 325 | 75% { 326 | transform: translateY(20px); 327 | opacity: 0; 328 | } 329 | } 330 | @-webkit-keyframes tada { 331 | from { 332 | transform: scale3d(1, 1, 1); 333 | } 334 | 10%, 20% { 335 | transform: scale3d(0.95, 0.95, 0.95) rotate3d(0, 0, 1, -10deg); 336 | } 337 | 30%, 50%, 70%, 90% { 338 | transform: scale3d(1, 1, 1) rotate3d(0, 0, 1, 10deg); 339 | } 340 | 40%, 60%, 80% { 341 | transform: scale3d(1, 1, 1) rotate3d(0, 0, 1, -10deg); 342 | } 343 | to { 344 | transform: scale3d(1, 1, 1); 345 | } 346 | } 347 | @keyframes tada { 348 | from { 349 | transform: scale3d(1, 1, 1); 350 | } 351 | 10%, 20% { 352 | transform: scale3d(0.95, 0.95, 0.95) rotate3d(0, 0, 1, -10deg); 353 | } 354 | 30%, 50%, 70%, 90% { 355 | transform: scale3d(1, 1, 1) rotate3d(0, 0, 1, 10deg); 356 | } 357 | 40%, 60%, 80% { 358 | transform: rotate3d(0, 0, 1, -10deg); 359 | } 360 | to { 361 | transform: scale3d(1, 1, 1); 362 | } 363 | } 364 | .spin { 365 | -webkit-animation: spin 1.5s linear infinite; 366 | animation: spin 1.5s linear infinite; 367 | } 368 | 369 | .spin-hover:hover { 370 | -webkit-animation: spin 1.5s linear infinite; 371 | animation: spin 1.5s linear infinite; 372 | } 373 | 374 | .tada { 375 | -webkit-animation: tada 1.5s ease infinite; 376 | animation: tada 1.5s ease infinite; 377 | } 378 | 379 | .tada-hover:hover { 380 | -webkit-animation: tada 1.5s ease infinite; 381 | animation: tada 1.5s ease infinite; 382 | } 383 | 384 | .flashing { 385 | -webkit-animation: flashing 1.5s infinite linear; 386 | animation: flashing 1.5s infinite linear; 387 | } 388 | 389 | .flashing-hover:hover { 390 | -webkit-animation: flashing 1.5s infinite linear; 391 | animation: flashing 1.5s infinite linear; 392 | } 393 | 394 | .burst { 395 | -webkit-animation: burst 1.5s infinite linear; 396 | animation: burst 1.5s infinite linear; 397 | } 398 | 399 | .burst-hover:hover { 400 | -webkit-animation: burst 1.5s infinite linear; 401 | animation: burst 1.5s infinite linear; 402 | } 403 | 404 | .fade-up { 405 | -webkit-animation: fade-up 1.5s infinite linear; 406 | animation: fade-up 1.5s infinite linear; 407 | } 408 | 409 | .fade-up-hover:hover { 410 | -webkit-animation: fade-up 1.5s infinite linear; 411 | animation: fade-up 1.5s infinite linear; 412 | } 413 | 414 | .fade-down { 415 | -webkit-animation: fade-down 1.5s infinite linear; 416 | animation: fade-down 1.5s infinite linear; 417 | } 418 | 419 | .fade-down-hover:hover { 420 | -webkit-animation: fade-down 1.5s infinite linear; 421 | animation: fade-down 1.5s infinite linear; 422 | } 423 | 424 | .fade-left { 425 | -webkit-animation: fade-left 1.5s infinite linear; 426 | animation: fade-left 1.5s infinite linear; 427 | } 428 | 429 | .fade-left-hover:hover { 430 | -webkit-animation: fade-left 1.5s infinite linear; 431 | animation: fade-left 1.5s infinite linear; 432 | } 433 | 434 | .fade-right { 435 | -webkit-animation: fade-right 1.5s infinite linear; 436 | animation: fade-right 1.5s infinite linear; 437 | } 438 | 439 | .fade-right-hover:hover { 440 | -webkit-animation: fade-right 1.5s infinite linear; 441 | animation: fade-right 1.5s infinite linear; 442 | } 443 | 444 | .rotate-90 { 445 | transform: rotate(90deg); 446 | } 447 | 448 | .rotate-180 { 449 | transform: rotate(180deg); 450 | } 451 | 452 | .rotate-270 { 453 | transform: rotate(270deg); 454 | } 455 | 456 | .flip-horizontal { 457 | transform: scaleX(-1); 458 | } 459 | 460 | .flip-vertical { 461 | transform: scaleY(-1); 462 | } 463 | /*# sourceMappingURL=last-icon.css.map */ -------------------------------------------------------------------------------- /last-icon.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["last-icon.scss","last-icon.css","_material-icons.scss","_material-symbols.scss","_icon-park.scss","_animations.scss","_transformations.scss"],"names":[],"mappings":"AAAA;EACE,WAAA;EACA,oBAAA;EACA,kBAAA;EACA,mBAAA;EACA,sBAAA;EACA,eAAA;EACA,uBAAA;EACA,mBAAA;ACCF;ADCE;;;EAGE,oBAAA;ACCJ;ADIE;EACE,gBAAA;ACFJ;ADKE;EACE,cAAA;EACA,WAAA;EACA,YAAA;ACHJ;ADME;EACE,iCAAA;EACA,mBAAA;ACJJ;;ADWE;;;;EACE,wBAAA;ACLJ;;ACjCA;EACE,8BAAA;EACA,6BAAA;EACA,qBAAA;EACA,oCAAA;ADoCF;;AEtCA;EACE;IACE,mCAAA;EFyCF;EEtCA;IACE,mCAAA;EFwCF;EErCA;IACE,mCAAA;EFuCF;AACF;;AElDA;EACE;IACE,mCAAA;EFyCF;EEtCA;IACE,mCAAA;EFwCF;EErCA;IACE,mCAAA;EFuCF;AACF;AEpCA;EACE;IACE,iCAAA;EFsCF;EEnCA;IACE,iCAAA;EFqCF;AACF;AE5CA;EACE;IACE,iCAAA;EFsCF;EEnCA;IACE,iCAAA;EFqCF;AACF;AElCA;EACE;IACE,iCAAA;EFoCF;EEjCA;IACE,iCAAA;EFmCF;AACF;AE1CA;EACE;IACE,iCAAA;EFoCF;EEjCA;IACE,iCAAA;EFmCF;AACF;AEhCA;;;;;;EAME,iEAAA;UAAA,yDAAA;EACA,iDAAA;UAAA,yCAAA;AFkCF;;AE/BA;;EAEE,gBAAA;EACA,qCAAA;UAAA,6BAAA;EACA,2CAAA;UAAA,mCAAA;AFkCF;;AE/BA;;EAEE,oCAAA;UAAA,4BAAA;EACA,qCAAA;UAAA,6BAAA;AFkCF;;AE/BA;;EAEE,qCAAA;UAAA,6BAAA;EACA,qCAAA;UAAA,6BAAA;AFkCF;;AE/BA;;;EAGE,wHAAA;AFkCF;;AE/BA;EACE,WAAA;EACA,iBAAA;EACA,yBAAA;AFkCF;AEhCE;EAEE,+BAAA;AFiCJ;;AG7GE;EACE,kBAAA;AHgHJ;AG7GE;EACE,4BAAA;EACA,uBAAA;AH+GJ;AG7GE;EACE,aAAA;AH+GJ;AG7GE;EACE,YAAA;AH+GJ;;AG3GE;EACE,kBAAA;AH8GJ;AG5GE;EACE,kBAAA;AH8GJ;AG5GE;;EAEE,YAAA;AH8GJ;;AG1GE;EACE,kBAAA;AH6GJ;AG3GE;EACE,aAAA;AH6GJ;;AGzGE;EACE,UAAA;AH4GJ;AG1GE;EACE,oBAAA;AH4GJ;AG1GE;EACE,kBAAA;AH4GJ;;AIvJA;EACE;IACE,oBAAA;EJ0JF;EIxJA;IACE,yBAAA;EJ0JF;AACF;AIxJA;EACE;IACE,oBAAA;EJ0JF;EIxJA;IACE,yBAAA;EJ0JF;AACF;AIxJA;EACE;IACE,mBAAA;IACA,UAAA;EJ0JF;EIxJA;IACE,qBAAA;IACA,UAAA;EJ0JF;AACF;AIxJA;EACE;IACE,mBAAA;IACA,UAAA;EJ0JF;EIxJA;IACE,qBAAA;IACA,UAAA;EJ0JF;AACF;AIxJA;EACE;IACE,UAAA;EJ0JF;EIxJA;IACE,UAAA;EJ0JF;EIxJA;IACE,UAAA;EJ0JF;AACF;AIxJA;EACE;IACE,UAAA;EJ0JF;EIxJA;IACE,UAAA;EJ0JF;EIxJA;IACE,UAAA;EJ0JF;AACF;AIxJA;EACE;IACE,wBAAA;IACA,UAAA;EJ0JF;EIxJA;IACE,4BAAA;IACA,UAAA;EJ0JF;AACF;AIxJA;EACE;IACE,wBAAA;IACA,UAAA;EJ0JF;EIxJA;IACE,4BAAA;IACA,UAAA;EJ0JF;AACF;AIxJA;EACE;IACE,wBAAA;IACA,UAAA;EJ0JF;EIxJA;IACE,2BAAA;IACA,UAAA;EJ0JF;AACF;AIxJA;EACE;IACE,wBAAA;IACA,UAAA;EJ0JF;EIxJA;IACE,2BAAA;IACA,UAAA;EJ0JF;AACF;AIxJA;EACE;IACE,wBAAA;IACA,UAAA;EJ0JF;EIxJA;IACE,4BAAA;IACA,UAAA;EJ0JF;AACF;AIxJA;EACE;IACE,wBAAA;IACA,UAAA;EJ0JF;EIxJA;IACE,4BAAA;IACA,UAAA;EJ0JF;AACF;AIxJA;EACE;IACE,wBAAA;IACA,UAAA;EJ0JF;EIxJA;IACE,2BAAA;IACA,UAAA;EJ0JF;AACF;AIxJA;EACE;IACE,wBAAA;IACA,UAAA;EJ0JF;EIxJA;IACE,2BAAA;IACA,UAAA;EJ0JF;AACF;AIxJA;EACE;IACE,2BAAA;EJ0JF;EIvJA;IAEE,8DAAA;EJwJF;EIrJA;IAIE,oDAAA;EJoJF;EIjJA;IAGE,qDAAA;EJiJF;EI9IA;IACE,2BAAA;EJgJF;AACF;AI9IA;EACE;IACE,2BAAA;EJgJF;EI7IA;IAEE,8DAAA;EJ8IF;EI3IA;IAIE,oDAAA;EJ0IF;EIvIA;IAGE,oCAAA;EJuIF;EIpIA;IACE,2BAAA;EJsIF;AACF;AIpIA;EACE,4CAAA;UAAA,oCAAA;AJsIF;;AIpIA;EACE,4CAAA;UAAA,oCAAA;AJuIF;;AIrIA;EACE,0CAAA;UAAA,kCAAA;AJwIF;;AItIA;EACE,0CAAA;UAAA,kCAAA;AJyIF;;AIvIA;EACE,gDAAA;UAAA,wCAAA;AJ0IF;;AIxIA;EACE,gDAAA;UAAA,wCAAA;AJ2IF;;AIzIA;EACE,6CAAA;UAAA,qCAAA;AJ4IF;;AI1IA;EACE,6CAAA;UAAA,qCAAA;AJ6IF;;AI3IA;EACE,+CAAA;UAAA,uCAAA;AJ8IF;;AI5IA;EACE,+CAAA;UAAA,uCAAA;AJ+IF;;AI7IA;EACE,iDAAA;UAAA,yCAAA;AJgJF;;AI9IA;EACE,iDAAA;UAAA,yCAAA;AJiJF;;AI/IA;EACE,iDAAA;UAAA,yCAAA;AJkJF;;AIhJA;EACE,iDAAA;UAAA,yCAAA;AJmJF;;AIjJA;EACE,kDAAA;UAAA,0CAAA;AJoJF;;AIlJA;EACE,kDAAA;UAAA,0CAAA;AJqJF;;AKrYA;EACE,wBAAA;ALwYF;;AKtYA;EACE,yBAAA;ALyYF;;AKvYA;EACE,yBAAA;AL0YF;;AKxYA;EACE,qBAAA;AL2YF;;AKzYA;EACE,qBAAA;AL4YF","file":"last-icon.css"} -------------------------------------------------------------------------------- /last-icon.js: -------------------------------------------------------------------------------- 1 | const JSDELIVR = "https://cdn.jsdelivr.net/"; 2 | const CACHE = {}; 3 | 4 | /** 5 | * @typedef IconSet 6 | * @property {String} alias Short two letters alias 7 | * @property {Function} svgPath The svg path 8 | * @property {Boolean} [fixFill] Does this set needs fixing fill:currentColor ? 9 | * @property {String} [useStroke] Add stroke to svg 10 | * @property {String} [defaultStroke] Default stroke to use (if supports stroke) 11 | * @property {String} [defaultType] Default type to use (when there are multiple types) 12 | * @property {Function} [fontClass] Font class 13 | * @property {Boolean} [opticalFont] Is an optical font? 14 | * @property {String} [name] Full name (injected automatically) 15 | */ 16 | 17 | /** 18 | * @typedef Options 19 | * @property {Boolean} debug Should we output messages to console 20 | * @property {Boolean} lazy Load icons lazily 21 | * @property {Object} replaceName Transparently replace icons with other values 22 | * @property {Array} fonts Icon sets using font icons rather than svg 23 | * @property {String} defaultSet Default icon set 24 | * @property {Object.} sets Available iconsets 25 | */ 26 | const options = { 27 | debug: false, 28 | lazy: true, 29 | replaceName: {}, 30 | fonts: [], 31 | defaultSet: "tabler", 32 | defaultStroke: 2, 33 | sets: { 34 | bootstrap: { 35 | alias: "bs", 36 | svgPath: () => `${JSDELIVR}npm/bootstrap-icons@1/icons/{icon}.svg`, 37 | }, 38 | flags: { 39 | alias: "fl", 40 | // types: ["4x3", "1x1"], 41 | defaultType: "4x3", 42 | svgPath: () => 43 | `${JSDELIVR}npm/flag-svg-collection@1/flags/{type}/{icon}.svg`, 44 | }, 45 | iconoir: { 46 | alias: "in", 47 | svgPath: () => `${JSDELIVR}gh/lucaburgio/iconoir/icons/{icon}.svg`, 48 | fontClass: () => "iconoir-{icon}", 49 | useStroke: true, 50 | }, 51 | iconpark: { 52 | alias: "ip", 53 | types: [], // see full list here https://github.com/bytedance/IconPark/tree/master/source 54 | svgPath: () => 55 | `${JSDELIVR}gh/bytedance/IconPark/source/{type}/{icon}.svg`, 56 | useStroke: true, 57 | }, 58 | lucide: { 59 | alias: "lu", 60 | svgPath: () => `${JSDELIVR}npm/lucide-static/icons/{icon}.svg`, 61 | }, 62 | material: { 63 | alias: "mi", 64 | // types: ["filled", "outlined", "round", "sharp", "two-tone"], 65 | defaultType: "filled", 66 | svgPath: () => 67 | `${JSDELIVR}npm/@material-design-icons/svg/{type}/{icon}.svg`, 68 | fontClass: (type) => { 69 | if (type === "filled") { 70 | return "material-icons"; 71 | } 72 | return "material-icons-{type}"; 73 | }, 74 | }, 75 | phosphor: { 76 | alias: "ph", 77 | // types: ["regular", "bold", "duotone", "fill", "light", "thin"], 78 | defaultType: "regular", 79 | svgPath: (type) => { 80 | if (type === "regular") { 81 | return `${JSDELIVR}npm/@phosphor-icons/core@2/assets/{type}/{icon}.svg`; 82 | } 83 | return `${JSDELIVR}npm/@phosphor-icons/core@2/assets/{type}/{icon}-{type}.svg`; 84 | }, 85 | fontClass: (type) => { 86 | if (type === "regular") { 87 | return "ph ph-{icon}"; 88 | } 89 | return "ph-{type} ph-{icon}"; 90 | }, 91 | }, 92 | supertiny: { 93 | alias: "st", 94 | svgPath: () => `${JSDELIVR}npm/super-tiny-icons/images/svg/{icon}.svg`, 95 | }, 96 | symbols: { 97 | alias: "ms", 98 | // types: ["outlined", "rounded", "sharp"], 99 | defaultType: "outlined", 100 | svgPath: () => 101 | `${JSDELIVR}npm/@material-symbols/svg-400@0.5/{type}/{icon}.svg`, 102 | fixFill: true, 103 | fontClass: () => "material-symbols-{type}", 104 | opticalFont: true, 105 | }, 106 | tabler: { 107 | alias: "ti", 108 | svgPath: () => `${JSDELIVR}npm/@tabler/icons@2/icons/{icon}.svg`, 109 | useStroke: true, 110 | fontClass: () => "ti ti-{icon}", 111 | }, 112 | }, 113 | }; 114 | 115 | /** 116 | * @var {IntersectionObserver} 117 | */ 118 | const observer = new window.IntersectionObserver((entries, observerRef) => { 119 | for (const entry of entries) { 120 | if (entry.isIntersecting) { 121 | observerRef.unobserve(entry.target); 122 | entry.target.init(); 123 | } 124 | } 125 | }); 126 | 127 | /** 128 | * @param {string} value 129 | * @param {string} iconName 130 | * @param {string} iconType 131 | * @return {string} 132 | */ 133 | function replacePlaceholders(value, iconName, iconType) { 134 | let v = value; 135 | v = v.replace("{icon}", iconName); 136 | if (iconType) { 137 | v = v.replaceAll("{type}", iconType); 138 | } else { 139 | // Maybe we want to remove the type like in material icons 140 | v = v.replace("-{type}", ""); 141 | } 142 | return v; 143 | } 144 | 145 | function log(message) { 146 | if (options.debug) { 147 | console.log(`[l-i] ${message}`); 148 | } 149 | } 150 | 151 | /** 152 | * @param {string} iconName 153 | * @param {IconSet} iconSet 154 | * @param {string} iconType 155 | * @return {Promise} 156 | */ 157 | function getIconSvg(iconName, iconSet, iconType) { 158 | let iconUrl = iconSet.svgPath(iconType); 159 | if (!iconUrl) { 160 | throw Error(`Icon set ${iconSet} does not exists`); 161 | } 162 | const cacheKey = `${iconSet.name}-${iconName}-${iconType || "base"}`; 163 | iconUrl = replacePlaceholders(iconUrl, iconName, iconType); 164 | 165 | // If we have it in cache 166 | if (iconUrl && CACHE[cacheKey]) { 167 | log(`Fetching ${cacheKey} from cache`); 168 | return CACHE[cacheKey]; 169 | } 170 | 171 | // Or resolve 172 | log(`Fetching ${cacheKey} from url ${iconUrl}`); 173 | CACHE[cacheKey] = fetch(iconUrl).then((response) => { 174 | if (response.ok) { 175 | return response.text(); 176 | } 177 | throw Error(response.status); 178 | }); 179 | return CACHE[cacheKey]; 180 | } 181 | 182 | /** 183 | * @param {LastIcon} inst 184 | * @param {string} name 185 | * @param {IconSet} iconSet 186 | * @param {string} type 187 | */ 188 | function refreshIcon(inst, name, iconSet, type) { 189 | let iconName = name; 190 | let iconType = type; 191 | // Replace name 192 | if (options.replaceName[iconName]) { 193 | iconName = options.replaceName[iconName]; 194 | } 195 | // Set default type if any 196 | if (!iconType && iconSet.defaultType) { 197 | iconType = iconSet.defaultType; 198 | } 199 | 200 | // Use font (if not using a specific stroke) 201 | if (options.fonts.includes(iconSet.name) && !inst.hasAttribute("stroke")) { 202 | log(`Using font for ${iconName}`); 203 | let iconClass = iconSet.fontClass(iconType); 204 | const nameAsClass = iconClass.includes("{icon}"); 205 | iconClass = replacePlaceholders(iconClass, iconName, iconType); 206 | if (nameAsClass) { 207 | inst.innerHTML = ``; 208 | } else { 209 | inst.innerHTML = `${iconName}`; 210 | } 211 | if (inst.stroke && iconSet.opticalFont) { 212 | inst.style.setProperty("--weight", inst.stroke * 100); 213 | } 214 | return; // Return early 215 | } 216 | 217 | getIconSvg(iconName, iconSet, iconType) 218 | .then((data) => { 219 | let iconData = data; 220 | // Strip class attribute as it may be affected by css 221 | if (iconData.includes("class=")) { 222 | iconData = iconData.replace(/ class="([a-z- ]*)"/g, ""); 223 | } 224 | // Add and/or fix stroke 225 | if (inst.stroke || iconSet.useStroke) { 226 | iconData = iconData.replace( 227 | /stroke-width="([0-9\.]*)"/g, 228 | `stroke-width="${inst.stroke}"`, 229 | ); 230 | } 231 | // Fix fill to currentColor 232 | if (iconSet.fixFill) { 233 | iconData = iconData.replace(/(/, '$1 fill="currentColor">'); 234 | } 235 | // If we have some html, pass it along (useful for svg anim) 236 | if (inst.defaultHTML) { 237 | iconData = iconData.replace("", `${inst.defaultHTML}`); 238 | } 239 | inst.innerHTML = iconData; 240 | }) 241 | .catch((error) => { 242 | inst.innerHTML = "⚠️"; 243 | console.error(`Failed to load icon ${iconName} (error ${error})`); 244 | }); 245 | } 246 | 247 | /** 248 | * @param {HTMLElement} element 249 | * @returns {Boolean} 250 | */ 251 | function isInViewport(element) { 252 | const rect = element.getBoundingClientRect(); 253 | return ( 254 | rect.top >= 0 && 255 | rect.left >= 0 && 256 | rect.bottom <= 257 | (window.innerHeight || document.documentElement.clientHeight) && 258 | rect.right <= (window.innerWidth || document.documentElement.clientWidth) 259 | ); 260 | } 261 | 262 | /** 263 | * Performs a deep merge of objects and returns new object. Does not modify 264 | * objects (immutable) and merges arrays via concatenation. 265 | * 266 | * @param {...object} objects - Objects to merge 267 | * @returns {object} New object with merged key/values 268 | */ 269 | function mergeDeep(...objects) { 270 | const isObject = (obj) => obj && typeof obj === "object"; 271 | const isArray = Array.isArray; 272 | 273 | return objects.reduce((prev, obj) => { 274 | for (const key of Object.keys(obj)) { 275 | const pVal = prev[key]; 276 | const oVal = obj[key]; 277 | 278 | if (isArray(pVal) && isArray(oVal)) { 279 | prev[key] = pVal.concat(...oVal); 280 | } else if (isObject(pVal) && isObject(oVal)) { 281 | prev[key] = mergeDeep(pVal, oVal); 282 | } else { 283 | prev[key] = oVal; 284 | } 285 | } 286 | return prev; 287 | }, {}); 288 | } 289 | 290 | const aliases = {}; 291 | function processIconSets() { 292 | for (const [key, set] of Object.entries(options.sets)) { 293 | // List aliases for easy retrieval 294 | aliases[set.alias] = key; 295 | // Include full name in iconset definition 296 | set.name = key; 297 | } 298 | } 299 | processIconSets(); 300 | 301 | class LastIcon extends HTMLElement { 302 | /** 303 | * @param {object} opts 304 | * @returns {Options} The updated option object 305 | */ 306 | static configure(opts = {}) { 307 | for (const k in opts) { 308 | if (typeof options[k] === "undefined") { 309 | console.error(`Invalid option key ${k}`); 310 | return; 311 | } 312 | if (Array.isArray(opts[k])) { 313 | options[k] = options[k].concat(opts[k]); 314 | } else if (typeof opts[k] === "object") { 315 | options[k] = mergeDeep(options[k], opts[k]); 316 | } else { 317 | options[k] = opts[k]; 318 | } 319 | } 320 | processIconSets(); 321 | // Log after we had the opportunity to change debug flag 322 | log("configuring options"); 323 | return options; 324 | } 325 | 326 | /** 327 | * @return {String|null} 328 | */ 329 | get type() { 330 | return this.getAttribute("type") || null; 331 | } 332 | 333 | /** 334 | * @return {String} 335 | */ 336 | get set() { 337 | return aliases[this.getAttribute("set")] || options.defaultSet; 338 | } 339 | 340 | /** 341 | * @return {IconSet} 342 | */ 343 | get iconSet() { 344 | return options.sets[this.set]; 345 | } 346 | 347 | /** 348 | * @return {Number} 349 | */ 350 | get stroke() { 351 | return this.getAttribute("stroke") || options.defaultStroke; 352 | } 353 | 354 | static get observedAttributes() { 355 | return ["name", "stroke", "size", "set", "type"]; 356 | } 357 | 358 | connectedCallback() { 359 | // innerHTML is not available because not parsed yet 360 | // setTimeout also allows whenDefined to kick in before init 361 | setTimeout(() => { 362 | if (options.lazy && !isInViewport(this)) { 363 | // observer will call init when element is visible 364 | observer.observe(this); 365 | } else { 366 | // init directly 367 | this.init(); 368 | } 369 | }); 370 | } 371 | 372 | init() { 373 | // Store default content as we will inject it back later 374 | this.defaultHTML = this.innerHTML; 375 | this.loadIcon(); 376 | } 377 | 378 | loadIcon() { 379 | const name = this.getAttribute("name"); 380 | if (!name) { 381 | return; 382 | } 383 | const iconSet = this.iconSet; 384 | // Clear icon 385 | this.innerHTML = ""; 386 | // Useful for customizing size in css 387 | const size = this.getAttribute("size"); 388 | if (size) { 389 | this.setSize(size); 390 | } 391 | refreshIcon(this, name, iconSet, this.type); 392 | } 393 | 394 | setSize(size) { 395 | this.style.setProperty("--size", `${size}px`); 396 | if (this.iconSet.opticalFont) { 397 | this.style.setProperty("--opsz", size); 398 | } 399 | } 400 | 401 | attributeChangedCallback(attr, oldVal, newVal) { 402 | // Wait until properly loaded for the first time 403 | if (typeof this.defaultHTML !== "string") { 404 | return; 405 | } 406 | log(`Attr ${attr} changed from ${oldVal} to ${newVal}`); 407 | if (attr === "size") { 408 | this.setSize(newVal); 409 | } else if (newVal) { 410 | log("attribute changed"); 411 | this.loadIcon(); 412 | } 413 | } 414 | } 415 | 416 | customElements.define("l-i", LastIcon); 417 | -------------------------------------------------------------------------------- /last-icon.min.css: -------------------------------------------------------------------------------- 1 | l-i{--size: 1em;display:inline-flex;width:var(--size);height:var(--size);vertical-align:middle;contain:strict;justify-content:center;align-items:center}l-i span,l-i svg,l-i i{pointer-events:none}l-i span{font-size:.8em}l-i svg{display:block;width:100%;height:100%}l-i i{font-size:var(--size) !important;color:currentColor}p l-i:not([size]),button l-i:not([size]),a l-i:not([size]),span l-i:not([size]){vertical-align:-0.125em}.material-icons-two-tone{background-color:currentColor;-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:rgba(0,0,0,0)}@-webkit-keyframes symbols-pulse{0%{font-variation-settings:"wght" 100}50%{font-variation-settings:"wght" 700}100%{font-variation-settings:"wght" 100}}@keyframes symbols-pulse{0%{font-variation-settings:"wght" 100}50%{font-variation-settings:"wght" 700}100%{font-variation-settings:"wght" 100}}@-webkit-keyframes symbols-fill{0%{font-variation-settings:"FILL" 0}100%{font-variation-settings:"FILL" 1}}@keyframes symbols-fill{0%{font-variation-settings:"FILL" 0}100%{font-variation-settings:"FILL" 1}}@-webkit-keyframes symbols-empty{0%{font-variation-settings:"FILL" 1}100%{font-variation-settings:"FILL" 0}}@keyframes symbols-empty{0%{font-variation-settings:"FILL" 1}100%{font-variation-settings:"FILL" 0}}.symbols-pulse i,.symbols-pulse-hover:hover i,.symbols-fill i,.symbols-fill-hover:hover i,.symbols-empty i,.symbols-empty-hover:hover i{-webkit-animation-timing-function:cubic-bezier(0.45, 0, 0.55, 1);animation-timing-function:cubic-bezier(0.45, 0, 0.55, 1);-webkit-animation-duration:var(--duration, 0.5s);animation-duration:var(--duration, 0.5s)}.symbols-pulse i,.symbols-pulse-hover:hover i{--duration: 1.5s;-webkit-animation-name:symbols-pulse;animation-name:symbols-pulse;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.symbols-fill i,.symbols-fill-hover:hover i{-webkit-animation-name:symbols-fill;animation-name:symbols-fill;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}.symbols-empty i,.symbols-empty-hover:hover i{-webkit-animation-name:symbols-empty;animation-name:symbols-empty;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}.material-symbols-rounded,.material-symbols-outlined,.material-symbols-sharp{font-variation-settings:"FILL" var(--fill, 0),"wght" var(--weight, 400),"GRAD" var(--grad, 0),"OPSZ" var(--opsz, 24)}.dark{--grad: -25;background:#000;color:#fff}.dark[disabled],.dark.disabled{color:rgba(255,255,255,.3)}l-i[theme=multi-color] path[fill="#2F88FF"]{fill:var(--color)}l-i[theme=multi-color] path[fill="#43CCF8"]{fill:var(--color-secondary);stroke:var(--color-bg)}l-i[theme=multi-color] path[stroke=white]{stroke:#fff}l-i[theme=multi-color] path[stroke=black]{stroke:#333}l-i[theme=two-tone] path[fill="#2F88FF"]{fill:var(--color)}l-i[theme=two-tone] path[fill="#43CCF8"]{fill:var(--color)}l-i[theme=two-tone] path[stroke=white],l-i[theme=two-tone] path[stroke=black]{stroke:#333}l-i[theme=filled] path[fill]{fill:currentColor}l-i[theme=filled] path[stroke=black]{stroke:#fff}l-i[theme=outline] path[fill]{fill:none}l-i[theme=outline] path[stroke=black]{stroke:currentColor}l-i[theme=outline] path[fill="#43CCF8"]{fill:currentColor}@-webkit-keyframes spin{0%{transform:rotate(0)}100%{transform:rotate(359deg)}}@keyframes spin{0%{transform:rotate(0)}100%{transform:rotate(359deg)}}@-webkit-keyframes burst{0%{transform:scale(1);opacity:1}90%{transform:scale(1.5);opacity:0}}@keyframes burst{0%{transform:scale(1);opacity:1}90%{transform:scale(1.5);opacity:0}}@-webkit-keyframes flashing{0%{opacity:1}45%{opacity:0}90%{opacity:1}}@keyframes flashing{0%{opacity:1}45%{opacity:0}90%{opacity:1}}@-webkit-keyframes fade-left{0%{transform:translateX(0);opacity:1}75%{transform:translateX(-20px);opacity:0}}@keyframes fade-left{0%{transform:translateX(0);opacity:1}75%{transform:translateX(-20px);opacity:0}}@-webkit-keyframes fade-right{0%{transform:translateX(0);opacity:1}75%{transform:translateX(20px);opacity:0}}@keyframes fade-right{0%{transform:translateX(0);opacity:1}75%{transform:translateX(20px);opacity:0}}@-webkit-keyframes fade-up{0%{transform:translateY(0);opacity:1}75%{transform:translateY(-20px);opacity:0}}@keyframes fade-up{0%{transform:translateY(0);opacity:1}75%{transform:translateY(-20px);opacity:0}}@-webkit-keyframes fade-down{0%{transform:translateY(0);opacity:1}75%{transform:translateY(20px);opacity:0}}@keyframes fade-down{0%{transform:translateY(0);opacity:1}75%{transform:translateY(20px);opacity:0}}@-webkit-keyframes tada{from{transform:scale3d(1, 1, 1)}10%,20%{transform:scale3d(0.95, 0.95, 0.95) rotate3d(0, 0, 1, -10deg)}30%,50%,70%,90%{transform:scale3d(1, 1, 1) rotate3d(0, 0, 1, 10deg)}40%,60%,80%{transform:scale3d(1, 1, 1) rotate3d(0, 0, 1, -10deg)}to{transform:scale3d(1, 1, 1)}}@keyframes tada{from{transform:scale3d(1, 1, 1)}10%,20%{transform:scale3d(0.95, 0.95, 0.95) rotate3d(0, 0, 1, -10deg)}30%,50%,70%,90%{transform:scale3d(1, 1, 1) rotate3d(0, 0, 1, 10deg)}40%,60%,80%{transform:rotate3d(0, 0, 1, -10deg)}to{transform:scale3d(1, 1, 1)}}.spin{-webkit-animation:spin 1.5s linear infinite;animation:spin 1.5s linear infinite}.spin-hover:hover{-webkit-animation:spin 1.5s linear infinite;animation:spin 1.5s linear infinite}.tada{-webkit-animation:tada 1.5s ease infinite;animation:tada 1.5s ease infinite}.tada-hover:hover{-webkit-animation:tada 1.5s ease infinite;animation:tada 1.5s ease infinite}.flashing{-webkit-animation:flashing 1.5s infinite linear;animation:flashing 1.5s infinite linear}.flashing-hover:hover{-webkit-animation:flashing 1.5s infinite linear;animation:flashing 1.5s infinite linear}.burst{-webkit-animation:burst 1.5s infinite linear;animation:burst 1.5s infinite linear}.burst-hover:hover{-webkit-animation:burst 1.5s infinite linear;animation:burst 1.5s infinite linear}.fade-up{-webkit-animation:fade-up 1.5s infinite linear;animation:fade-up 1.5s infinite linear}.fade-up-hover:hover{-webkit-animation:fade-up 1.5s infinite linear;animation:fade-up 1.5s infinite linear}.fade-down{-webkit-animation:fade-down 1.5s infinite linear;animation:fade-down 1.5s infinite linear}.fade-down-hover:hover{-webkit-animation:fade-down 1.5s infinite linear;animation:fade-down 1.5s infinite linear}.fade-left{-webkit-animation:fade-left 1.5s infinite linear;animation:fade-left 1.5s infinite linear}.fade-left-hover:hover{-webkit-animation:fade-left 1.5s infinite linear;animation:fade-left 1.5s infinite linear}.fade-right{-webkit-animation:fade-right 1.5s infinite linear;animation:fade-right 1.5s infinite linear}.fade-right-hover:hover{-webkit-animation:fade-right 1.5s infinite linear;animation:fade-right 1.5s infinite linear}.rotate-90{transform:rotate(90deg)}.rotate-180{transform:rotate(180deg)}.rotate-270{transform:rotate(270deg)}.flip-horizontal{transform:scaleX(-1)}.flip-vertical{transform:scaleY(-1)} 2 | /*# sourceMappingURL=last-icon.min.css.map */ -------------------------------------------------------------------------------- /last-icon.min.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["last-icon.scss","_material-icons.scss","_material-symbols.scss","_icon-park.scss","_animations.scss","_transformations.scss"],"names":[],"mappings":"AAAA,IACE,WAAA,CACA,mBAAA,CACA,iBAAA,CACA,kBAAA,CACA,qBAAA,CACA,cAAA,CACA,sBAAA,CACA,kBAAA,CAEA,uBAGE,mBAAA,CAKF,SACE,cAAA,CAGF,QACE,aAAA,CACA,UAAA,CACA,WAAA,CAGF,MACE,gCAAA,CACA,kBAAA,CAOF,gFACE,uBAAA,CCtCJ,yBACE,6BAAA,CACA,4BAAA,CACA,oBAAA,CACA,qCAAA,CCFF,iCACE,GACE,kCAAA,CAGF,IACE,kCAAA,CAGF,KACE,kCAAA,CAAA,CAVJ,yBACE,GACE,kCAAA,CAGF,IACE,kCAAA,CAGF,KACE,kCAAA,CAAA,CAIJ,gCACE,GACE,gCAAA,CAGF,KACE,gCAAA,CAAA,CANJ,wBACE,GACE,gCAAA,CAGF,KACE,gCAAA,CAAA,CAIJ,iCACE,GACE,gCAAA,CAGF,KACE,gCAAA,CAAA,CANJ,yBACE,GACE,gCAAA,CAGF,KACE,gCAAA,CAAA,CAIJ,wIAME,gEAAA,CAAA,wDAAA,CACA,gDAAA,CAAA,wCAAA,CAGF,8CAEE,gBAAA,CACA,oCAAA,CAAA,4BAAA,CACA,0CAAA,CAAA,kCAAA,CAGF,4CAEE,mCAAA,CAAA,2BAAA,CACA,oCAAA,CAAA,4BAAA,CAGF,8CAEE,oCAAA,CAAA,4BAAA,CACA,oCAAA,CAAA,4BAAA,CAGF,6EAGE,oHAAA,CAGF,MACE,WAAA,CACA,eAAA,CACA,UAAA,CAEA,+BAEE,0BAAA,CC5EF,4CACE,iBAAA,CAGF,4CACE,2BAAA,CACA,sBAAA,CAEF,0CACE,WAAA,CAEF,0CACE,WAAA,CAIF,yCACE,iBAAA,CAEF,yCACE,iBAAA,CAEF,8EAEE,WAAA,CAIF,6BACE,iBAAA,CAEF,qCACE,WAAA,CAIF,8BACE,SAAA,CAEF,sCACE,mBAAA,CAEF,wCACE,iBAAA,CC3CJ,wBACE,GACE,mBAAA,CAEF,KACE,wBAAA,CAAA,CAGJ,gBACE,GACE,mBAAA,CAEF,KACE,wBAAA,CAAA,CAGJ,yBACE,GACE,kBAAA,CACA,SAAA,CAEF,IACE,oBAAA,CACA,SAAA,CAAA,CAGJ,iBACE,GACE,kBAAA,CACA,SAAA,CAEF,IACE,oBAAA,CACA,SAAA,CAAA,CAGJ,4BACE,GACE,SAAA,CAEF,IACE,SAAA,CAEF,IACE,SAAA,CAAA,CAGJ,oBACE,GACE,SAAA,CAEF,IACE,SAAA,CAEF,IACE,SAAA,CAAA,CAGJ,6BACE,GACE,uBAAA,CACA,SAAA,CAEF,IACE,2BAAA,CACA,SAAA,CAAA,CAGJ,qBACE,GACE,uBAAA,CACA,SAAA,CAEF,IACE,2BAAA,CACA,SAAA,CAAA,CAGJ,8BACE,GACE,uBAAA,CACA,SAAA,CAEF,IACE,0BAAA,CACA,SAAA,CAAA,CAGJ,sBACE,GACE,uBAAA,CACA,SAAA,CAEF,IACE,0BAAA,CACA,SAAA,CAAA,CAGJ,2BACE,GACE,uBAAA,CACA,SAAA,CAEF,IACE,2BAAA,CACA,SAAA,CAAA,CAGJ,mBACE,GACE,uBAAA,CACA,SAAA,CAEF,IACE,2BAAA,CACA,SAAA,CAAA,CAGJ,6BACE,GACE,uBAAA,CACA,SAAA,CAEF,IACE,0BAAA,CACA,SAAA,CAAA,CAGJ,qBACE,GACE,uBAAA,CACA,SAAA,CAEF,IACE,0BAAA,CACA,SAAA,CAAA,CAGJ,wBACE,KACE,0BAAA,CAGF,QAEE,6DAAA,CAGF,gBAIE,mDAAA,CAGF,YAGE,oDAAA,CAGF,GACE,0BAAA,CAAA,CAGJ,gBACE,KACE,0BAAA,CAGF,QAEE,6DAAA,CAGF,gBAIE,mDAAA,CAGF,YAGE,mCAAA,CAGF,GACE,0BAAA,CAAA,CAGJ,MACE,2CAAA,CAAA,mCAAA,CAEF,kBACE,2CAAA,CAAA,mCAAA,CAEF,MACE,yCAAA,CAAA,iCAAA,CAEF,kBACE,yCAAA,CAAA,iCAAA,CAEF,UACE,+CAAA,CAAA,uCAAA,CAEF,sBACE,+CAAA,CAAA,uCAAA,CAEF,OACE,4CAAA,CAAA,oCAAA,CAEF,mBACE,4CAAA,CAAA,oCAAA,CAEF,SACE,8CAAA,CAAA,sCAAA,CAEF,qBACE,8CAAA,CAAA,sCAAA,CAEF,WACE,gDAAA,CAAA,wCAAA,CAEF,uBACE,gDAAA,CAAA,wCAAA,CAEF,WACE,gDAAA,CAAA,wCAAA,CAEF,uBACE,gDAAA,CAAA,wCAAA,CAEF,YACE,iDAAA,CAAA,yCAAA,CAEF,wBACE,iDAAA,CAAA,yCAAA,CChPF,WACE,uBAAA,CAEF,YACE,wBAAA,CAEF,YACE,wBAAA,CAEF,iBACE,oBAAA,CAEF,eACE,oBAAA","file":"last-icon.min.css"} -------------------------------------------------------------------------------- /last-icon.min.js: -------------------------------------------------------------------------------- 1 | (()=>{var o="https://cdn.jsdelivr.net/",g={},n={debug:!1,lazy:!0,replaceName:{},fonts:[],defaultSet:"tabler",defaultStroke:2,sets:{bootstrap:{alias:"bs",svgPath:()=>o+"npm/bootstrap-icons@1/icons/{icon}.svg"},boxicons:{alias:"bx",defaultType:"solid",svgPath:()=>o+"npm/boxicons@2/svg/{type}/{prefix}-{icon}.svg",fixFill:!0,fontClass:()=>"bx {prefix}-{icon}",prefixes:{solid:"bxs",regular:"bx",logos:"bxl"}},bytesize:{alias:"by",svgPath:()=>o+"npm/bytesize-icons@1/dist/icons/{icon}.svg",useStroke:!0},cssgg:{alias:"gg",svgPath:()=>o+"npm/css.gg@2/icons/svg/{icon}.svg"},emojicc:{alias:"em",svgPath:()=>o+"npm/emoji-cc@1/svg/{icon}.svg"},eos:{alias:"eo",defaultType:"solid",svgPath:()=>o+"gh/lekoala/eos-icons-mirror/{type}/{icon}.svg",fixFill:!0},feather:{alias:"ft",svgPath:()=>o+"npm/feather-icons@4/dist/icons/{icon}.svg"},flags:{alias:"fl",defaultType:"4x3",svgPath:()=>o+"npm/flag-svg-collection@1/flags/{type}/{icon}.svg"},fontawesome:{alias:"fa",defaultType:"solid",svgPath:()=>o+"npm/@fortawesome/fontawesome-free@5/svgs/{type}/{icon}.svg",fixFill:!0,fontClass:()=>"{prefix} fa-{icon}",prefixes:{solid:"fas",regular:"far",light:"fal",duotone:"fad",brands:"fab"}},iconoir:{alias:"in",svgPath:()=>o+"gh/lucaburgio/iconoir/icons/{icon}.svg",fontClass:()=>"iconoir-{icon}",useStroke:!0},iconpark:{alias:"ip",types:[],svgPath:()=>o+"gh/bytedance/IconPark/source/{type}/{icon}.svg",useStroke:!0},lucide:{alias:"lu",svgPath:()=>o+"npm/lucide-static/icons/{icon}.svg"},material:{alias:"mi",defaultType:"filled",svgPath:()=>o+"npm/@material-design-icons/svg/{type}/{icon}.svg",fontClass:t=>t==="filled"?"material-icons":"material-icons-{type}"},phosphor:{alias:"ph",defaultType:"regular",svgPath:t=>t==="regular"?o+"npm/@phosphor-icons/core@2/assets/{type}/{icon}.svg":o+"npm/@phosphor-icons/core@2/assets/{type}/{icon}-{type}.svg",fontClass:t=>t==="regular"?"ph ph-{icon}":"ph-{type} ph-{icon}"},supertiny:{alias:"st",svgPath:()=>o+"npm/super-tiny-icons/images/svg/{icon}.svg"},symbols:{alias:"ms",defaultType:"outlined",svgPath:()=>o+"npm/@material-symbols/svg-400@0.5/{type}/{icon}.svg",fixFill:!0,fontClass:()=>"material-symbols-{type}",opticalFont:!0},tabler:{alias:"ti",svgPath:()=>o+"npm/@tabler/icons@2/icons/{icon}.svg",useStroke:!0,fontClass:()=>"ti ti-{icon}"}}},b=new window.IntersectionObserver((t,e)=>{t.forEach(async s=>{s.isIntersecting&&(e.unobserve(s.target),s.target.init())})});function u(t,e,s,i){return t=t.replace("{icon}",e),i?t=t.replaceAll("{type}",i):t=t.replace("-{type}",""),s.prefixes&&s.prefixes[i]&&(t=t.replace("{prefix}",s.prefixes[i])),t}function a(t){n.debug&&console.log(`[l-i] ${t}`)}function y(t,e,s){let i=e.svgPath(s);if(!i)throw Error(`Icon set ${e} does not exists`);let r=`${e.name}-${t}-${s||"base"}`;return i=u(i,t,e,s),i&&g[r]?(a(`Fetching ${r} from cache`),g[r]):(a(`Fetching ${r} from url ${i}`),g[r]=fetch(i).then(function(l){if(l.ok)return l.text();throw Error(l.status)}),g[r])}function v(t,e,s,i){if(n.replaceName[e]&&(e=n.replaceName[e]),!i&&s.defaultType&&(i=s.defaultType),n.fonts.includes(s.name)&&!t.hasAttribute("stroke")){a(`Using font for ${e}`);let r=s.fontClass(i),l=r.includes("{icon}");r=u(r,e,s,i),l?t.innerHTML=``:t.innerHTML=`${e}`,t.stroke&&s.opticalFont&&t.style.setProperty("--weight",t.stroke*100);return}y(e,s,i).then(r=>{r.includes("class=")&&(r=r.replace(/ class="([a-z- ]*)"/g,"")),(t.stroke||s.useStroke)&&(r=r.replace(/stroke-width="([0-9\.]*)"/g,`stroke-width="${t.stroke}"`)),s.fixFill&&(r=r.replace(/(/,'$1 fill="currentColor">')),t.defaultHTML&&(r=r.replace("",`${t.defaultHTML}`)),t.innerHTML=r}).catch(r=>{t.innerHTML="\u26A0\uFE0F",console.error(`Failed to load icon ${e} (error ${r})`)})}function x(t){let e=t.getBoundingClientRect();return e.top>=0&&e.left>=0&&e.bottom<=(window.innerHeight||document.documentElement.clientHeight)&&e.right<=(window.innerWidth||document.documentElement.clientWidth)}function h(...t){let e=i=>i&&typeof i=="object",s=Array.isArray;return t.reduce((i,r)=>(Object.keys(r).forEach(l=>{let f=i[l],c=r[l];s(f)&&s(c)?i[l]=f.concat(...c):e(f)&&e(c)?i[l]=h(f,c):i[l]=c}),i),{})}var p={};function d(){for(let[t,e]of Object.entries(n.sets))p[e.alias]=t,e.name=t}d();var m=class extends HTMLElement{static configure(e={}){for(let s in e){if(typeof n[s]>"u"){console.error(`Invalid option key ${s}`);return}Array.isArray(e[s])?n[s]=n[s].concat(e[s]):typeof e[s]=="object"?n[s]=h(n[s],e[s]):n[s]=e[s]}return d(),a("configuring options"),n}get type(){return this.getAttribute("type")||null}get set(){let e=this.getAttribute("set");return p[e]||n.defaultSet}get iconSet(){return n.sets[this.set]}get stroke(){return this.getAttribute("stroke")||n.defaultStroke}static get observedAttributes(){return["name","stroke","size","set","type"]}connectedCallback(){setTimeout(()=>{n.lazy&&!x(this)?b.observe(this):this.init()})}init(){this.defaultHTML=this.innerHTML,this.loadIcon()}loadIcon(){let e=this.getAttribute("name");if(!e)return;let s=this.iconSet;this.innerHTML="";let i=this.getAttribute("size");i&&this.setSize(i),v(this,e,s,this.type)}setSize(e){this.style.setProperty("--size",`${e}px`),this.iconSet.opticalFont&&this.style.setProperty("--opsz",e)}attributeChangedCallback(e,s,i){typeof this.defaultHTML=="string"&&(a(`Attr ${e} changed from ${s} to ${i}`),e==="size"?this.setSize(i):i&&(a("attribute changed"),this.loadIcon()))}};customElements.define("l-i",m);})(); 2 | //# sourceMappingURL=last-icon.min.js.map 3 | -------------------------------------------------------------------------------- /last-icon.min.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "sources": ["last-icon.js"], 4 | "sourcesContent": ["const JSDELIVR = \"https://cdn.jsdelivr.net/\";\r\nconst CACHE = {};\r\n\r\n/**\r\n * @typedef IconSet\r\n * @property {String} alias Short two letters alias\r\n * @property {Function} svgPath The svg path\r\n * @property {Boolean} [fixFill] Does this set needs fixing fill:currentColor ?\r\n * @property {String} [useStroke] Add stroke to svg\r\n * @property {String} [defaultStroke] Default stroke to use (if supports stroke)\r\n * @property {String} [defaultType] Default type to use (when there are multiple types)\r\n * @property {Object.} [prefixes] Types to prefixes\r\n * @property {Function} [fontClass] Font class\r\n * @property {Boolean} [opticalFont] Is an optical font?\r\n * @property {String} [name] Full name (injected automatically)\r\n */\r\n\r\n/**\r\n * @typedef Options\r\n * @property {Boolean} debug Should we output messages to console\r\n * @property {Boolean} lazy Load icons lazily\r\n * @property {Object} replaceName Transparently replace icons with other values\r\n * @property {Array} fonts Icon sets using font icons rather than svg\r\n * @property {String} defaultSet Default icon set\r\n * @property {Object.} sets Available iconsets\r\n */\r\nconst options = {\r\n debug: false,\r\n lazy: true,\r\n replaceName: {},\r\n fonts: [],\r\n defaultSet: \"tabler\",\r\n defaultStroke: 2,\r\n sets: {\r\n bootstrap: {\r\n alias: \"bs\",\r\n svgPath: () => JSDELIVR + \"npm/bootstrap-icons@1/icons/{icon}.svg\",\r\n },\r\n boxicons: {\r\n alias: \"bx\",\r\n // types: [\"solid\", \"regular\", \"logos\"],\r\n defaultType: \"solid\",\r\n svgPath: () => JSDELIVR + \"npm/boxicons@2/svg/{type}/{prefix}-{icon}.svg\",\r\n fixFill: true,\r\n fontClass: () => \"bx {prefix}-{icon}\",\r\n prefixes: {\r\n solid: \"bxs\",\r\n regular: \"bx\",\r\n logos: \"bxl\",\r\n },\r\n },\r\n bytesize: {\r\n alias: \"by\",\r\n svgPath: () => JSDELIVR + \"npm/bytesize-icons@1/dist/icons/{icon}.svg\",\r\n useStroke: true,\r\n },\r\n cssgg: {\r\n alias: \"gg\",\r\n svgPath: () => JSDELIVR + \"npm/css.gg@2/icons/svg/{icon}.svg\",\r\n },\r\n emojicc: {\r\n alias: \"em\",\r\n svgPath: () => JSDELIVR + \"npm/emoji-cc@1/svg/{icon}.svg\",\r\n },\r\n eos: {\r\n alias: \"eo\",\r\n // types: [\"solid\", \"outlined\", \"animated\"],\r\n defaultType: \"solid\",\r\n svgPath: () => JSDELIVR + \"gh/lekoala/eos-icons-mirror/{type}/{icon}.svg\",\r\n fixFill: true,\r\n },\r\n feather: {\r\n alias: \"ft\",\r\n svgPath: () => JSDELIVR + \"npm/feather-icons@4/dist/icons/{icon}.svg\",\r\n },\r\n flags: {\r\n alias: \"fl\",\r\n // types: [\"4x3\", \"1x1\"],\r\n defaultType: \"4x3\",\r\n svgPath: () => JSDELIVR + \"npm/flag-svg-collection@1/flags/{type}/{icon}.svg\",\r\n },\r\n fontawesome: {\r\n alias: \"fa\",\r\n // types: [\"solid\", \"regular\", \"brands\", \"light\", \"duotone\"],\r\n defaultType: \"solid\",\r\n svgPath: () => JSDELIVR + \"npm/@fortawesome/fontawesome-free@5/svgs/{type}/{icon}.svg\",\r\n fixFill: true,\r\n fontClass: () => \"{prefix} fa-{icon}\",\r\n prefixes: {\r\n solid: \"fas\",\r\n regular: \"far\",\r\n light: \"fal\",\r\n duotone: \"fad\",\r\n brands: \"fab\",\r\n },\r\n },\r\n iconoir: {\r\n alias: \"in\",\r\n svgPath: () => JSDELIVR + \"gh/lucaburgio/iconoir/icons/{icon}.svg\",\r\n fontClass: () => \"iconoir-{icon}\",\r\n useStroke: true,\r\n },\r\n iconpark: {\r\n alias: \"ip\",\r\n types: [], // see full list here https://github.com/bytedance/IconPark/tree/master/source\r\n svgPath: () => JSDELIVR + \"gh/bytedance/IconPark/source/{type}/{icon}.svg\",\r\n useStroke: true,\r\n },\r\n lucide: {\r\n alias: \"lu\",\r\n svgPath: () => JSDELIVR + \"npm/lucide-static/icons/{icon}.svg\",\r\n },\r\n material: {\r\n alias: \"mi\",\r\n // types: [\"filled\", \"outlined\", \"round\", \"sharp\", \"two-tone\"],\r\n defaultType: \"filled\",\r\n svgPath: () => JSDELIVR + \"npm/@material-design-icons/svg/{type}/{icon}.svg\",\r\n fontClass: (type) => {\r\n if (type === \"filled\") {\r\n return \"material-icons\";\r\n }\r\n return \"material-icons-{type}\";\r\n },\r\n },\r\n phosphor: {\r\n alias: \"ph\",\r\n // types: [\"regular\", \"bold\", \"duotone\", \"fill\", \"light\", \"thin\"],\r\n defaultType: \"regular\",\r\n svgPath: (type) => {\r\n if (type === \"regular\") {\r\n return JSDELIVR + \"npm/@phosphor-icons/core@2/assets/{type}/{icon}.svg\";\r\n }\r\n return JSDELIVR + \"npm/@phosphor-icons/core@2/assets/{type}/{icon}-{type}.svg\";\r\n },\r\n fontClass: (type) => {\r\n if (type === \"regular\") {\r\n return \"ph ph-{icon}\";\r\n }\r\n return \"ph-{type} ph-{icon}\";\r\n },\r\n },\r\n supertiny: {\r\n alias: \"st\",\r\n svgPath: () => JSDELIVR + \"npm/super-tiny-icons/images/svg/{icon}.svg\",\r\n },\r\n symbols: {\r\n alias: \"ms\",\r\n // types: [\"outlined\", \"rounded\", \"sharp\"],\r\n defaultType: \"outlined\",\r\n svgPath: () => JSDELIVR + \"npm/@material-symbols/svg-400@0.5/{type}/{icon}.svg\",\r\n fixFill: true,\r\n fontClass: () => \"material-symbols-{type}\",\r\n opticalFont: true,\r\n },\r\n tabler: {\r\n alias: \"ti\",\r\n svgPath: () => JSDELIVR + \"npm/@tabler/icons@2/icons/{icon}.svg\",\r\n useStroke: true,\r\n fontClass: () => \"ti ti-{icon}\",\r\n },\r\n },\r\n};\r\n\r\n/**\r\n * @var {IntersectionObserver}\r\n */\r\nconst observer = new window.IntersectionObserver((entries, observerRef) => {\r\n entries.forEach(async (entry) => {\r\n if (entry.isIntersecting) {\r\n observerRef.unobserve(entry.target);\r\n entry.target.init();\r\n }\r\n });\r\n});\r\n\r\n/**\r\n * @param {string} value\r\n * @param {string} iconName\r\n * @param {IconSet} iconSet\r\n * @param {string} iconType\r\n * @return {string}\r\n */\r\nfunction replacePlaceholders(value, iconName, iconSet, iconType) {\r\n value = value.replace(\"{icon}\", iconName);\r\n if (iconType) {\r\n value = value.replaceAll(\"{type}\", iconType);\r\n } else {\r\n // Maybe we want to remove the type like in material icons\r\n value = value.replace(\"-{type}\", \"\");\r\n }\r\n if (iconSet.prefixes && iconSet.prefixes[iconType]) {\r\n value = value.replace(\"{prefix}\", iconSet.prefixes[iconType]);\r\n }\r\n return value;\r\n}\r\n\r\nfunction log(message) {\r\n if (options.debug) {\r\n console.log(`[l-i] ${message}`);\r\n }\r\n}\r\n\r\n/**\r\n * @param {string} iconName\r\n * @param {IconSet} iconSet\r\n * @param {string} iconType\r\n * @return {Promise}\r\n */\r\nfunction getIconSvg(iconName, iconSet, iconType) {\r\n let iconUrl = iconSet.svgPath(iconType);\r\n if (!iconUrl) {\r\n throw Error(`Icon set ${iconSet} does not exists`);\r\n }\r\n const cacheKey = `${iconSet.name}-${iconName}-${iconType || \"base\"}`;\r\n iconUrl = replacePlaceholders(iconUrl, iconName, iconSet, iconType);\r\n\r\n // If we have it in cache\r\n if (iconUrl && CACHE[cacheKey]) {\r\n log(`Fetching ${cacheKey} from cache`);\r\n return CACHE[cacheKey];\r\n }\r\n\r\n // Or resolve\r\n log(`Fetching ${cacheKey} from url ${iconUrl}`);\r\n CACHE[cacheKey] = fetch(iconUrl).then(function (response) {\r\n if (response.ok) {\r\n return response.text();\r\n }\r\n throw Error(response.status);\r\n });\r\n return CACHE[cacheKey];\r\n}\r\n\r\n/**\r\n * @param {LastIcon} inst\r\n * @param {string} iconName\r\n * @param {IconSet} iconSet\r\n * @param {string} iconType\r\n */\r\nfunction refreshIcon(inst, iconName, iconSet, iconType) {\r\n // Replace name\r\n if (options.replaceName[iconName]) {\r\n iconName = options.replaceName[iconName];\r\n }\r\n // Set default type if any\r\n if (!iconType && iconSet.defaultType) {\r\n iconType = iconSet.defaultType;\r\n }\r\n\r\n // Use font (if not using a specific stroke)\r\n if (options.fonts.includes(iconSet.name) && !inst.hasAttribute(\"stroke\")) {\r\n log(`Using font for ${iconName}`);\r\n let iconClass = iconSet.fontClass(iconType);\r\n let nameAsClass = iconClass.includes(\"{icon}\");\r\n iconClass = replacePlaceholders(iconClass, iconName, iconSet, iconType);\r\n if (nameAsClass) {\r\n inst.innerHTML = ``;\r\n } else {\r\n inst.innerHTML = `${iconName}`;\r\n }\r\n if (inst.stroke && iconSet.opticalFont) {\r\n inst.style.setProperty(\"--weight\", inst.stroke * 100);\r\n }\r\n return; // Return early\r\n }\r\n\r\n getIconSvg(iconName, iconSet, iconType)\r\n .then((iconData) => {\r\n // Strip class attribute as it may be affected by css\r\n if (iconData.includes(\"class=\")) {\r\n iconData = iconData.replace(/ class=\"([a-z- ]*)\"/g, \"\");\r\n }\r\n // Add and/or fix stroke\r\n if (inst.stroke || iconSet.useStroke) {\r\n iconData = iconData.replace(/stroke-width=\"([0-9\\.]*)\"/g, `stroke-width=\"${inst.stroke}\"`);\r\n }\r\n // Fix fill to currentColor\r\n if (iconSet.fixFill) {\r\n iconData = iconData.replace(/(/, '$1 fill=\"currentColor\">');\r\n }\r\n // If we have some html, pass it along (useful for svg anim)\r\n if (inst.defaultHTML) {\r\n iconData = iconData.replace(\"\", `${inst.defaultHTML}`);\r\n }\r\n inst.innerHTML = iconData;\r\n })\r\n .catch((error) => {\r\n inst.innerHTML = \"\u26A0\uFE0F\";\r\n console.error(`Failed to load icon ${iconName} (error ${error})`);\r\n });\r\n}\r\n\r\n/**\r\n * @param {HTMLElement} element\r\n * @returns {Boolean}\r\n */\r\nfunction isInViewport(element) {\r\n const rect = element.getBoundingClientRect();\r\n return (\r\n rect.top >= 0 &&\r\n rect.left >= 0 &&\r\n rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\r\n rect.right <= (window.innerWidth || document.documentElement.clientWidth)\r\n );\r\n}\r\n\r\n/**\r\n * Performs a deep merge of objects and returns new object. Does not modify\r\n * objects (immutable) and merges arrays via concatenation.\r\n *\r\n * @param {...object} objects - Objects to merge\r\n * @returns {object} New object with merged key/values\r\n */\r\nfunction mergeDeep(...objects) {\r\n const isObject = (obj) => obj && typeof obj === \"object\";\r\n const isArray = Array.isArray;\r\n\r\n return objects.reduce((prev, obj) => {\r\n Object.keys(obj).forEach((key) => {\r\n const pVal = prev[key];\r\n const oVal = obj[key];\r\n\r\n if (isArray(pVal) && isArray(oVal)) {\r\n prev[key] = pVal.concat(...oVal);\r\n } else if (isObject(pVal) && isObject(oVal)) {\r\n prev[key] = mergeDeep(pVal, oVal);\r\n } else {\r\n prev[key] = oVal;\r\n }\r\n });\r\n\r\n return prev;\r\n }, {});\r\n}\r\n\r\nlet aliases = {};\r\nfunction processIconSets() {\r\n for (const [key, set] of Object.entries(options.sets)) {\r\n // List aliases for easy retrieval\r\n aliases[set.alias] = key;\r\n // Include full name in iconset definition\r\n set.name = key;\r\n }\r\n}\r\nprocessIconSets();\r\n\r\nclass LastIcon extends HTMLElement {\r\n /**\r\n * @param {object} opts\r\n * @returns {Options} The updated option object\r\n */\r\n static configure(opts = {}) {\r\n for (const k in opts) {\r\n if (typeof options[k] === \"undefined\") {\r\n console.error(`Invalid option key ${k}`);\r\n return;\r\n }\r\n if (Array.isArray(opts[k])) {\r\n options[k] = options[k].concat(opts[k]);\r\n } else if (typeof opts[k] === \"object\") {\r\n options[k] = mergeDeep(options[k], opts[k]);\r\n } else {\r\n options[k] = opts[k];\r\n }\r\n }\r\n processIconSets();\r\n // Log after we had the opportunity to change debug flag\r\n log(\"configuring options\");\r\n return options;\r\n }\r\n\r\n /**\r\n * @return {String|null}\r\n */\r\n get type() {\r\n return this.getAttribute(\"type\") || null;\r\n }\r\n\r\n /**\r\n * @return {String}\r\n */\r\n get set() {\r\n let v = this.getAttribute(\"set\");\r\n return aliases[v] || options.defaultSet;\r\n }\r\n\r\n /**\r\n * @return {IconSet}\r\n */\r\n get iconSet() {\r\n return options.sets[this.set];\r\n }\r\n\r\n /**\r\n * @return {Number}\r\n */\r\n get stroke() {\r\n return this.getAttribute(\"stroke\") || options.defaultStroke;\r\n }\r\n\r\n static get observedAttributes() {\r\n return [\"name\", \"stroke\", \"size\", \"set\", \"type\"];\r\n }\r\n\r\n connectedCallback() {\r\n // innerHTML is not available because not parsed yet\r\n // setTimeout also allows whenDefined to kick in before init\r\n setTimeout(() => {\r\n if (options.lazy && !isInViewport(this)) {\r\n // observer will call init when element is visible\r\n observer.observe(this);\r\n } else {\r\n // init directly\r\n this.init();\r\n }\r\n });\r\n }\r\n\r\n init() {\r\n // Store default content as we will inject it back later\r\n this.defaultHTML = this.innerHTML;\r\n this.loadIcon();\r\n }\r\n\r\n loadIcon() {\r\n const name = this.getAttribute(\"name\");\r\n if (!name) {\r\n return;\r\n }\r\n const iconSet = this.iconSet;\r\n // Clear icon\r\n this.innerHTML = \"\";\r\n // Useful for customizing size in css\r\n const size = this.getAttribute(\"size\");\r\n if (size) {\r\n this.setSize(size);\r\n }\r\n refreshIcon(this, name, iconSet, this.type);\r\n }\r\n\r\n setSize(size) {\r\n this.style.setProperty(\"--size\", `${size}px`);\r\n if (this.iconSet.opticalFont) {\r\n this.style.setProperty(\"--opsz\", size);\r\n }\r\n }\r\n\r\n attributeChangedCallback(attr, oldVal, newVal) {\r\n // Wait until properly loaded for the first time\r\n if (typeof this.defaultHTML !== \"string\") {\r\n return;\r\n }\r\n log(`Attr ${attr} changed from ${oldVal} to ${newVal}`);\r\n if (attr === \"size\") {\r\n this.setSize(newVal);\r\n } else if (newVal) {\r\n log(\"attribute changed\");\r\n this.loadIcon();\r\n }\r\n }\r\n}\r\n\r\ncustomElements.define(\"l-i\", LastIcon);\r\n"], 5 | "mappings": "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", 6 | "names": [] 7 | } 8 | -------------------------------------------------------------------------------- /last-icon.scss: -------------------------------------------------------------------------------- 1 | l-i { 2 | --size: 1em; 3 | display: inline-flex; 4 | width: var(--size); 5 | height: var(--size); 6 | vertical-align: middle; 7 | contain: strict; 8 | justify-content: center; 9 | align-items: center; 10 | 11 | span, 12 | svg, 13 | i { 14 | pointer-events: none; 15 | } 16 | 17 | // This is needed for ⚠️ icon as it goes outside of the limits of the div 18 | // And it would be cut due to contain: strict usage 19 | span { 20 | font-size: 0.8em; 21 | } 22 | 23 | svg { 24 | display: block; 25 | width: 100%; 26 | height: 100%; 27 | } 28 | // When rendering font icons (eg: material, symbols...) 29 | i { 30 | font-size: var(--size) !important; 31 | color: currentColor; 32 | } 33 | } 34 | p, 35 | button, 36 | a, 37 | span { 38 | l-i:not([size]) { 39 | vertical-align: -0.125em; 40 | } 41 | } 42 | 43 | // Optional features 44 | @import "material-icons"; 45 | @import "material-symbols"; 46 | @import "icon-park"; 47 | @import "animations"; 48 | @import "transformations"; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "last-icon", 3 | "version": "2.2.0", 4 | "description": "One custom icon element to rule them all", 5 | "main": "last-icon.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "esbuild --bundle --minify --format=iife last-icon.js --outfile=last-icon.min.js", 9 | "build-min": "npm run build && git add -A && git commit -m \"build files\"", 10 | "start": "npm run build -- --servedir=.", 11 | "watch": "npm run build -- --watch", 12 | "bump": "npm run build-min && npm version patch", 13 | "jsdoc": "npx jsdoc-to-markdown last-icon.js" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/lekoala/last-icon" 18 | }, 19 | "keywords": [ 20 | "DOM", 21 | "icons", 22 | "icon", 23 | "custom", 24 | "element", 25 | "component", 26 | "es6" 27 | ], 28 | "author": "LeKoala", 29 | "license": "MIT" 30 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # last-icon 2 | 3 | [![NPM](https://nodei.co/npm/last-icon.png?mini=true)](https://nodei.co/npm/last-icon/) 4 | [![Downloads](https://img.shields.io/npm/dt/last-icon.svg)](https://www.npmjs.com/package/last-icon) 5 | 6 | The last icon library you will ever need. 7 | 8 | Key features: 9 | 10 | - Load as svg or font icons 11 | - Mix & match icon sets if needed 12 | - Bring your own icons 13 | - Fix iconsistencies 14 | - Lazy load your icons 15 | 16 | ## How to use 17 | 18 | Simply include the library 19 | 20 | ```html 21 | 22 | ``` 23 | 24 | **NOTE:** It is recommended to define this as early as possible so that all icons are resolved as soon 25 | as possible. Otherwise, you might see a delay before your icons are being displayed. 26 | Even when doing this, you might still see a very small delay as opposed as a font icon or an embedded SVG. 27 | 28 | If you are fine with a little more delay, you can use this instead (which will be ignored on browsers 29 | that don’t support modules): 30 | 31 | ```html 32 | 33 | 34 | 35 | ``` 36 | 37 | And call your icons! 38 | 39 | ```html 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | ``` 48 | 49 | The following CSS is recommended: 50 | 51 | ```css 52 | l-i { 53 | --size: 1em; 54 | display: inline-flex; 55 | width: var(--size); 56 | height: var(--size); 57 | vertical-align: middle; 58 | } 59 | l-i svg { 60 | display: block; 61 | width: 100%; 62 | height: 100%; 63 | } 64 | p l-i, 65 | button l-i, 66 | a l-i, 67 | span l-i { 68 | vertical-align: -0.125em; 69 | } 70 | ``` 71 | 72 | ## Configuring 73 | 74 | You can set any options using `LastIcon.configure`. The recommended way to call it is this way: 75 | 76 | ```js 77 | customElements.whenDefined("l-i").then(() => { 78 | // Access through registry 79 | customElements.get("l-i").configure({ 80 | debug: true, 81 | // Transform stars to trash 82 | // replaceName: { 83 | // star: "trash" 84 | // }, 85 | // Use font icon 86 | // fonts: [ 87 | // "material", 88 | // "phosphor", 89 | // ], 90 | // Change default set 91 | defaultSet: "tabler", 92 | // Change default stroke 93 | // defaultStroke: 1, 94 | }); 95 | }); 96 | ``` 97 | 98 | All available options: 99 | 100 | | Name | Type | Description | 101 | |-------------|---------------------------------------------|-----------------------------------------------| 102 | | debug | Boolean | Should we output messages to console | 103 | | lazy | Boolean | Load icons lazily | 104 | | replaceName | Object | Transparently replace icons with other values | 105 | | fonts | Array | Icon sets using font icons rather than SVG | 106 | | defaultSet | String | Default icon set | 107 | | sets | Object.<string, IconSet> | Available iconsets | 108 | 109 | ## Supported icon sets 110 | 111 | | Icon Set | Name | Alias | Types | Stroke | Count | Website | 112 | |-------------------|-------------|-------|:-----:|:------:|:------|--------------------------------------------------------------------------| 113 | | Bootstrap Icons | bootstrap | bs | 1 | x | 1800+ | [bootstrap](https://icons.getbootstrap.com/) | 114 | | Flags | flags | fl | 1 | x | ? | [flags](https://github.com/lipis/flag-icons/) | 115 | | Fontawesome | fontawesome | fa | 5 | x | 1600+ | [fontawesome](https://fontawesome.com/cheatsheet) | 116 | | Iconoir | iconoir | in | 1 | x | 1600+ | [iconoir](https://iconoir.com/) | 117 | | IconPark | iconpark | ip | 4 | v | 2400+ | [iconpark](https://iconpark.oceanengine.com/official) | 118 | | Lucide | lucide | lu | 1 | v | 1800+ | [lucide](https://lucide.dev/) | 119 | | Materials Icons | material | mi | 5 | x | 1100+ | [material icons](https://fonts.google.com/icons?selected=Material+Icons) | 120 | | Phosphor | phosphor | ph | 6 | x | 9000+ | [phosphor](https://phosphoricons.com/) | 121 | | Super Tiny Icons | supertiny | st | 1 | x | 386 | [supertiny](https://github.com/edent/SuperTinyIcons) | 122 | | Materials Symbols | symbols | ms | 3 | v | 2500+ | [material symbols](https://fonts.google.com/icons) | 123 | | Tabler Icons | tabler | ti | 1 | v | 5700+ | [tabler](https://tabler-icons.io/) | 124 | 125 | --- 126 | 127 | ## Adding or updating an icon set 128 | 129 | You can update any option for an icon set 130 | 131 | | Name | Type | Description | 132 | |-----------------|--------------------------------------------|-----------------------------------------------------| 133 | | alias | String | Short two letters alias | 134 | | svgPath | function | The SVG path | 135 | | [fixFill] | Boolean | Does this set needs fixing `fill:currentColor`? | 136 | | [useStroke] | String | Add stroke to svg | 137 | | [defaultStroke] | String | Default stroke to use (if supports stroke) | 138 | | [defaultType] | String | Default type to use (when there are multiple types) | 139 | | [prefixes] | Object.<string, string> | Types to prefixes | 140 | | [fontClass] | function | Font class | 141 | | [opticalFont] | Boolean | Is an optical font? | 142 | | [name] | String | Full name (injected automatically) | 143 | 144 | ## Fill 145 | 146 | Some icon sets include a default `fill="currentColor"` and some don't. In order 147 | to have all icon sets behave consistently, we apply a `fill="currentColor"` to all 148 | icon sets. This fix apply to: 'material', 'fontawesome'. 149 | 150 | ## Why a custom element 151 | 152 | - External sprite or font is loading all the icons which lead to extra load time 153 | - Including SVG is leading to really super long HTML and not very developer friendly 154 | - No need for custom JS inliner, it feels cleaner overall 155 | 156 | ## Why external CSS 157 | 158 | A custom element CSS is not loaded until the component itself is loaded, which 159 | can lead to FOUC and things moving around as the icon appear. 160 | The solution I've found so far is to apply some global CSS rules than are known 161 | before the component is loaded. 162 | 163 | You can check any extra SCSS that might be useful for you as well. 164 | 165 | ## Using fonts 166 | 167 | Sometimes it is easier to use an icon font. Indeed, it is fully cached by the browser and will not have any display glitch. 168 | Obviously, the downside is that you have to load the whole font, but it's cached after the first load. 169 | The advantage of using LastIcon over regular icons is that is allows you to switch easily between one way or the other. 170 | 171 | First of all, load any relevant fonts style 172 | 173 | ```html 174 | 175 | 176 | 177 | 178 | 179 | ``` 180 | 181 | And after that, use the font config to tell Last Icon to use the font over the SVG icons 182 | 183 | ```js 184 | customElements.whenDefined("l-i").then(() => { 185 | // Access through registry 186 | customElements.get("l-i").configure({ 187 | debug: true, 188 | fonts: ["material"], 189 | material: { 190 | defaultType: "two-tone", 191 | }, 192 | }); 193 | }); 194 | ``` 195 | 196 | And then, update your styles: 197 | 198 | ```css 199 | l-i { 200 | --size: 1em; 201 | display: inline-flex; 202 | width: var(--size); 203 | height: var(--size); 204 | vertical-align: middle; 205 | 206 | svg { 207 | display: block; 208 | width: 100%; 209 | height: 100%; 210 | } 211 | i { 212 | font-size: var(--size) !important; 213 | color: currentColor; 214 | } 215 | } 216 | p, 217 | button, 218 | a, 219 | span { 220 | l-i { 221 | vertical-align: -0.125em; 222 | } 223 | } 224 | 225 | .material-icons-two-tone { 226 | background-color: currentColor; 227 | -webkit-background-clip: text; 228 | -webkit-text-fill-color: transparent; 229 | } 230 | ``` 231 | 232 | ## Demo 233 | 234 | See `demo.html` or the following pen https://codepen.io/lekoalabe/pen/eYvdjqY 235 | 236 | ## Worth looking at 237 | 238 | You might also be interested in https://icon-sets.iconify.design/ 239 | --------------------------------------------------------------------------------