docs(icons): External Lucide icons like from lab on lucide.dev (#2194)

* Add section title

* Add external libs list in sidebar

* Make external lib work

* Adds external lib to detail view

* fix lint issues

* Update to https

authored by Eric Fennis and committed by GitHub e11fa135 f980863f

+4
docs/.vitepress/config.ts
··· 28 28 new URL('./theme/components/overrides/VPFooter.vue', import.meta.url), 29 29 ), 30 30 }, 31 + { 32 + find: '~/.vitepress', 33 + replacement: fileURLToPath(new URL('./', import.meta.url)), 34 + }, 31 35 ], 32 36 }, 33 37 },
+186
docs/.vitepress/data/categoriesData.json
··· 1 + [ 2 + { 3 + "name": "accessibility", 4 + "title": "Accessibility" 5 + }, 6 + { 7 + "name": "account", 8 + "title": "Accounts & access" 9 + }, 10 + { 11 + "name": "animals", 12 + "title": "Animals" 13 + }, 14 + { 15 + "name": "arrows", 16 + "title": "Arrows" 17 + }, 18 + { 19 + "name": "brands", 20 + "title": "Brands" 21 + }, 22 + { 23 + "name": "buildings", 24 + "title": "Buildings" 25 + }, 26 + { 27 + "name": "charts", 28 + "title": "Charts" 29 + }, 30 + { 31 + "name": "communication", 32 + "title": "Communication" 33 + }, 34 + { 35 + "name": "connectivity", 36 + "title": "Connectivity" 37 + }, 38 + { 39 + "name": "currency", 40 + "title": "Currency" 41 + }, 42 + { 43 + "name": "cursors", 44 + "title": "Cursors" 45 + }, 46 + { 47 + "name": "design", 48 + "title": "Design" 49 + }, 50 + { 51 + "name": "development", 52 + "title": "Coding & development" 53 + }, 54 + { 55 + "name": "devices", 56 + "title": "Devices" 57 + }, 58 + { 59 + "name": "emoji", 60 + "title": "Emoji" 61 + }, 62 + { 63 + "name": "files", 64 + "title": "File icons" 65 + }, 66 + { 67 + "name": "food-beverage", 68 + "title": "Food & beverage" 69 + }, 70 + { 71 + "name": "furniture", 72 + "title": "Furniture" 73 + }, 74 + { 75 + "name": "gaming", 76 + "title": "Gaming" 77 + }, 78 + { 79 + "name": "home", 80 + "title": "Home" 81 + }, 82 + { 83 + "name": "layout", 84 + "title": "Layout" 85 + }, 86 + { 87 + "name": "mail", 88 + "title": "Mail" 89 + }, 90 + { 91 + "name": "maps", 92 + "title": "Maps" 93 + }, 94 + { 95 + "name": "maths", 96 + "title": "Maths" 97 + }, 98 + { 99 + "name": "medical", 100 + "title": "Medical" 101 + }, 102 + { 103 + "name": "money", 104 + "title": "Money" 105 + }, 106 + { 107 + "name": "multimedia", 108 + "title": "Multimedia" 109 + }, 110 + { 111 + "name": "nature", 112 + "title": "Nature" 113 + }, 114 + { 115 + "name": "navigation", 116 + "title": "Navigation" 117 + }, 118 + { 119 + "name": "notifications", 120 + "title": "Notifications" 121 + }, 122 + { 123 + "name": "people", 124 + "title": "People" 125 + }, 126 + { 127 + "name": "photography", 128 + "title": "Photography" 129 + }, 130 + { 131 + "name": "science", 132 + "title": "Science" 133 + }, 134 + { 135 + "name": "seasons", 136 + "title": "Seasons" 137 + }, 138 + { 139 + "name": "security", 140 + "title": "Security" 141 + }, 142 + { 143 + "name": "shapes", 144 + "title": "Shapes" 145 + }, 146 + { 147 + "name": "shopping", 148 + "title": "Shopping" 149 + }, 150 + { 151 + "name": "social", 152 + "title": "Social" 153 + }, 154 + { 155 + "name": "sports", 156 + "title": "Sports" 157 + }, 158 + { 159 + "name": "sustainability", 160 + "title": "Sustainability" 161 + }, 162 + { 163 + "name": "text", 164 + "title": "Text formatting" 165 + }, 166 + { 167 + "name": "time", 168 + "title": "Time & calendar" 169 + }, 170 + { 171 + "name": "tools", 172 + "title": "Tools" 173 + }, 174 + { 175 + "name": "transportation", 176 + "title": "Transportation" 177 + }, 178 + { 179 + "name": "travel", 180 + "title": "Travel" 181 + }, 182 + { 183 + "name": "weather", 184 + "title": "Weather" 185 + } 186 + ]
+161
docs/.vitepress/lib/codeExamples/createLabCodeExamples.ts
··· 1 + import { bundledLanguages, type ThemeRegistration } from 'shikiji'; 2 + import { getHighlighter } from 'shikiji'; 3 + 4 + type CodeExampleType = { 5 + title: string; 6 + language: string; 7 + code: string; 8 + }[]; 9 + 10 + const getIconCodes = (): CodeExampleType => { 11 + return [ 12 + { 13 + language: 'js', 14 + title: 'Vanilla', 15 + code: `\ 16 + import { createIcons, icons } from 'lucide'; 17 + import { $Name } from '@lucide/lab'; 18 + 19 + createIcons({ 20 + icons: { 21 + $Name 22 + } 23 + }); 24 + 25 + document.body.append('<i data-lucide="$Name"></i>');\ 26 + `, 27 + }, 28 + { 29 + language: 'tsx', 30 + title: 'React', 31 + code: `import { Icon } from 'lucide-react'; 32 + import { $Name } from '@lucide/lab'; 33 + 34 + const App = () => { 35 + return ( 36 + <Icon iconNode={$Name} /> 37 + ); 38 + }; 39 + 40 + export default App; 41 + `, 42 + }, 43 + { 44 + language: 'vue', 45 + title: 'Vue', 46 + code: `<script setup> 47 + import { Icon } from 'lucide-vue-next'; 48 + import { $Name } from '@lucide/lab'; 49 + </script> 50 + 51 + <template> 52 + <Icon :iconNode="burger" /> 53 + </template> 54 + `, 55 + }, 56 + { 57 + language: 'svelte', 58 + title: 'Svelte', 59 + code: `<script> 60 + import { Icon } from 'lucide-svelte'; 61 + import { $Name } from '@lucide/lab'; 62 + </script> 63 + 64 + <Icon iconNode={burger} /> 65 + `, 66 + }, 67 + { 68 + language: 'tsx', 69 + title: 'Preact', 70 + code: `import { Icon } from 'lucide-preact'; 71 + import { $Name } from '@lucide/lab'; 72 + 73 + const App = () => { 74 + return ( 75 + <Icon iconNode={$Name} /> 76 + ); 77 + }; 78 + 79 + export default App; 80 + `, 81 + }, 82 + { 83 + language: 'tsx', 84 + title: 'Solid', 85 + code: `import { Icon } from 'lucide-solid'; 86 + import { $Name } from '@lucide/lab'; 87 + 88 + const App = () => { 89 + return ( 90 + <Icon iconNode={$Name} /> 91 + ); 92 + }; 93 + 94 + export default App; 95 + `, 96 + }, 97 + { 98 + language: 'tsx', 99 + title: 'Angular', 100 + code: `// app.module.ts 101 + import { LucideAngularModule, $PascalCase } from 'lucide-angular'; 102 + import { $Name } from '@lucide/lab'; 103 + 104 + @NgModule({ 105 + imports: [ 106 + LucideAngularModule.pick({ $Name }) 107 + ], 108 + }) 109 + 110 + // app.component.html 111 + <lucide-icon name="$Name"></lucide-icon> 112 + `, 113 + }, 114 + ]; 115 + }; 116 + 117 + export type ThemeOptions = 118 + | ThemeRegistration 119 + | { light: ThemeRegistration; dark: ThemeRegistration }; 120 + 121 + const highLightCode = async (code: string, lang: string, active?: boolean) => { 122 + const highlighter = await getHighlighter({ 123 + themes: ['github-light', 'github-dark'], 124 + langs: Object.keys(bundledLanguages), 125 + }); 126 + 127 + const highlightedCode = highlighter 128 + .codeToHtml(code, { 129 + lang, 130 + themes: { 131 + light: 'github-light', 132 + dark: 'github-dark', 133 + }, 134 + defaultColor: false, 135 + }) 136 + .replace('shiki-themes', 'shiki-themes vp-code'); 137 + 138 + return `<div class="language-${lang} ${active ? 'active' : ''}"> 139 + <button title="Copy Code" class="copy"></button> 140 + <span class="lang">${lang}</span> 141 + ${highlightedCode} 142 + </div>`; 143 + }; 144 + 145 + export default async function createCodeExamples() { 146 + const codes = getIconCodes(); 147 + 148 + const codeExamplePromises = codes.map(async ({ title, language, code }, index) => { 149 + const isFirst = index === 0; 150 + 151 + const codeString = await highLightCode(code, language, isFirst); 152 + 153 + return { 154 + title, 155 + language: language, 156 + code: codeString, 157 + }; 158 + }); 159 + 160 + return Promise.all(codeExamplePromises); 161 + }
+32
docs/.vitepress/lib/codeExamples/highLightCode.ts
··· 1 + import { bundledLanguages, type ThemeRegistration } from 'shikiji'; 2 + import { getHighlighter } from 'shikiji'; 3 + 4 + export type ThemeOptions = 5 + | ThemeRegistration 6 + | { light: ThemeRegistration; dark: ThemeRegistration }; 7 + 8 + const highLightCode = async (code: string, lang: string, active?: boolean) => { 9 + const highlighter = await getHighlighter({ 10 + themes: ['github-light', 'github-dark'], 11 + langs: Object.keys(bundledLanguages), 12 + }); 13 + 14 + const highlightedCode = highlighter 15 + .codeToHtml(code, { 16 + lang, 17 + themes: { 18 + light: 'github-light', 19 + dark: 'github-dark', 20 + }, 21 + defaultColor: false, 22 + }) 23 + .replace('shiki-themes', 'shiki-themes vp-code'); 24 + 25 + return `<div class="language-${lang} ${active ? 'active' : ''}"> 26 + <button title="Copy Code" class="copy"></button> 27 + <span class="lang">${lang}</span> 28 + ${highlightedCode} 29 + </div>`; 30 + }; 31 + 32 + export default highLightCode;
+5
docs/.vitepress/lib/codeExamples/types.ts
··· 1 + export type CodeExampleType = { 2 + title: string; 3 + language: string; 4 + code: string; 5 + }[];
+23 -17
docs/.vitepress/lib/createCodeExamples.ts docs/.vitepress/lib/codeExamples/createCodeExamples.ts
··· 10 10 const getIconCodes = (): CodeExampleType => { 11 11 return [ 12 12 { 13 - language: 'html', 14 - title: 'HTML', 15 - code: `<i data-lucide="Name"></i>`, 13 + language: 'js', 14 + title: 'Vanilla', 15 + code: `\ 16 + import { createIcons, icons } from 'lucide'; 17 + 18 + createIcons({ icons }); 19 + 20 + document.body.append('<i data-lucide="$Name"></i>');\ 21 + `, 16 22 }, 17 23 { 18 24 language: 'tsx', 19 25 title: 'React', 20 - code: `import { PascalCase } from 'lucide-react'; 26 + code: `import { $PascalCase } from 'lucide-react'; 21 27 22 28 const App = () => { 23 29 return ( 24 - <PascalCase /> 30 + <$PascalCase /> 25 31 ); 26 32 }; 27 33 ··· 32 38 language: 'vue', 33 39 title: 'Vue', 34 40 code: `<script setup> 35 - import { PascalCase } from 'lucide-vue-next'; 41 + import { $PascalCase } from 'lucide-vue-next'; 36 42 </script> 37 43 38 44 <template> 39 - <PascalCase /> 45 + <$PascalCase /> 40 46 </template> 41 47 `, 42 48 }, ··· 44 50 language: 'svelte', 45 51 title: 'Svelte', 46 52 code: `<script> 47 - import { PascalCase } from 'lucide-svelte'; 53 + import { $PascalCase } from 'lucide-svelte'; 48 54 </script> 49 55 50 - <PascalCase /> 56 + <$PascalCase /> 51 57 `, 52 58 }, 53 59 { 54 60 language: 'tsx', 55 61 title: 'Preact', 56 - code: `import { PascalCase } from 'lucide-preact'; 62 + code: `import { $PascalCase } from 'lucide-preact'; 57 63 58 64 const App = () => { 59 65 return ( 60 - <PascalCase /> 66 + <$PascalCase /> 61 67 ); 62 68 }; 63 69 ··· 67 73 { 68 74 language: 'tsx', 69 75 title: 'Solid', 70 - code: `import { PascalCase } from 'lucide-solid'; 76 + code: `import { $PascalCase } from 'lucide-solid'; 71 77 72 78 const App = () => { 73 79 return ( 74 - <PascalCase /> 80 + <$PascalCase /> 75 81 ); 76 82 }; 77 83 ··· 82 88 language: 'tsx', 83 89 title: 'Angular', 84 90 code: `// app.module.ts 85 - import { LucideAngularModule, PascalCase } from 'lucide-angular'; 91 + import { LucideAngularModule, $PascalCase } from 'lucide-angular'; 86 92 87 93 @NgModule({ 88 94 imports: [ 89 - LucideAngularModule.pick({ PascalCase }) 95 + LucideAngularModule.pick({ $PascalCase }) 90 96 ], 91 97 }) 92 98 93 99 // app.component.html 94 - <lucide-icon name="Name"></lucide-icon> 100 + <lucide-icon name="$Name"></lucide-icon> 95 101 `, 96 102 }, 97 103 { ··· 101 107 @import ('~lucide-static/font/Lucide.css'); 102 108 </style> 103 109 104 - <div class="icon-Name"></div> 110 + <div class="icon-$Name"></div> 105 111 `, 106 112 }, 107 113 ];
+90
docs/.vitepress/theme/components/base/Checkbox.vue
··· 1 + <script setup lang="ts"> 2 + import { computed } from 'vue'; 3 + 4 + const props = defineProps<{ 5 + label: string 6 + id: string 7 + value: string 8 + modelValue: string | string[] 9 + }>() 10 + 11 + const emit = defineEmits(['change', 'input', 'update:modelValue']) 12 + 13 + const model = computed({ 14 + get: () => { 15 + if (Array.isArray(props.modelValue)) { 16 + return props.modelValue.includes(props.value) 17 + } 18 + return props.modelValue === props.value 19 + 20 + }, 21 + set: (value: string) => { 22 + if (Array.isArray(props.modelValue)) { 23 + const newValue = [...props.modelValue] 24 + const index = newValue.indexOf(props.value) 25 + if (value) { 26 + if (index === -1) { 27 + newValue.push(props.value) 28 + } 29 + } else { 30 + if (index !== -1) { 31 + newValue.splice(index, 1) 32 + } 33 + } 34 + emit('update:modelValue', newValue) 35 + } else { 36 + emit('update:modelValue', value) 37 + } 38 + } 39 + }) 40 + </script> 41 + 42 + <template> 43 + <div class="checkbox-wrapper"> 44 + <input 45 + type="checkbox" 46 + class="checkbox" 47 + ref="input" 48 + :id="id" 49 + v-model="model" 50 + v-bind="$attrs" 51 + /> 52 + <label :for="id" class="checkbox-label"> 53 + {{ label }} 54 + </label> 55 + </div> 56 + </template> 57 + 58 + <style scoped> 59 + .checkbox-wrapper { 60 + display: flex; 61 + align-items: center; 62 + gap: 8px; 63 + } 64 + 65 + .checkbox-label { 66 + line-height: 20px; 67 + font-size: 13px; 68 + color: var(--vt-c-text-1); 69 + transition: color .5s; 70 + display: block; 71 + } 72 + 73 + .checkbox { 74 + -webkit-appearance: none; 75 + appearance: none; 76 + width: 16px; 77 + height: 16px; 78 + cursor: pointer; 79 + border: 1px solid var(--vp-input-border-color); 80 + background-color: var(--vp-input-switch-bg-color); 81 + border-radius: 4px; 82 + } 83 + 84 + .checkbox:checked { 85 + border-color: transparent; 86 + background: url("data:image/svg+xml,%3Csvg width='12px' height='12px' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='4' stroke-linecap='round' stroke-linejoin='round' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E") 87 + center no-repeat var(--vp-c-brand);; 88 + } 89 + 90 + </style>
+1 -1
docs/.vitepress/theme/components/home/TeamMemberCard.vue
··· 3 3 name: string 4 4 title: string 5 5 image: string 6 - sponsor: string 6 + sponsor?: string 7 7 socialLinks: DefaultTheme.SocialLink[] 8 8 } 9 9 </script>
+14 -7
docs/.vitepress/theme/components/icons/CategoryList.vue
··· 6 6 import { useActiveAnchor } from '../../composables/useActiveAnchor' 7 7 import { data } from './CategoryList.data' 8 8 import CategoryListItem from './CategoryListItem.vue' 9 + import SidebarTitle from './SidebarTitle.vue' 9 10 import { useCategoryView } from '../../composables/useCategoryView' 10 11 11 12 const { page } = useData() ··· 46 47 47 48 <template> 48 49 <div class="category-list" ref="container"> 49 - <VPLink class="sidebar-title" href="/icons/" :class="{ 'active': overviewIsActive } "> 50 + <SidebarTitle> 51 + View 52 + </SidebarTitle> 53 + <VPLink class="sidebar-link sidebar-text" href="/icons/" :class="{ 'active': overviewIsActive } "> 50 54 All 51 55 </VPLink> 52 - <VPLink class="sidebar-title" href="/icons/categories" :class="{ 'active': categoriesIsActive } "> 56 + <VPLink class="sidebar-link sidebar-text" href="/icons/categories" :class="{ 'active': categoriesIsActive } "> 53 57 Categories 54 58 </VPLink> 55 59 <div class="content"> ··· 62 66 </template> 63 67 64 68 <style scoped> 65 - .sidebar-title { 66 - font-weight: 500; 67 - color: var(--vp-c-text-2); 68 - margin-bottom: 6px; 69 + .sidebar-text { 69 70 line-height: 24px; 70 71 font-size: 14px; 71 72 display: block; 72 73 transition: color 0.25s; 74 + padding: 4px 0; 73 75 } 74 76 75 - .sidebar-title:hover, .sidebar-title.active { 77 + .sidebar-link { 78 + font-weight: 500; 79 + color: var(--vp-c-text-2); 80 + } 81 + 82 + .sidebar-link:hover, .sidebar-link.active { 76 83 color: var(--vp-c-brand); 77 84 } 78 85 .content {
+14 -3
docs/.vitepress/theme/components/icons/IconDetailOverlay.vue
··· 11 11 import Badge from '../base/Badge.vue'; 12 12 import { computedAsync } from '@vueuse/core'; 13 13 import { satisfies } from 'semver'; 14 + import { useExternalLibs } from '../../composables/useExternalLibs'; 14 15 15 16 const props = defineProps<{ 16 17 iconName: string | null 17 18 }>() 18 19 20 + const { externalIconNodes } = useExternalLibs() 21 + 19 22 const { go } = useRouter() 20 23 21 24 const icon = computedAsync<IconEntity | null>(async () => { 22 25 if (props.iconName) { 23 26 try { 24 - return (await import(`../../../data/iconDetails/${props.iconName}.ts`)).default as IconEntity 27 + if (props.iconName.includes(':')) { 28 + const [library, name] = props.iconName.split(':') 29 + 30 + return externalIconNodes.value[library].find((icon) => icon.name === name) 31 + } else { 32 + return (await import(`../../../data/iconDetails/${props.iconName}.ts`)).default as IconEntity 33 + } 25 34 } catch (err) { 26 - go(`/icons/${props.iconName}`) 35 + if (!props.iconName.includes(':')) { 36 + go(`/icons/${props.iconName}`) 37 + } 27 38 } 28 39 } 29 40 return null ··· 56 67 class="version" 57 68 :href="releaseTagLink(icon.createdRelease.version)" 58 69 >v{{ icon.createdRelease.version }}</Badge> 59 - <IconButton @click="go(`/icons/${icon.name}`)"> 70 + <IconButton @click="go(icon.externalLibrary ? `/icons/${icon.externalLibrary}/${icon.name}` : `/icons/${icon.name}`)"> 60 71 <component :is="Expand" /> 61 72 </IconButton> 62 73 <IconButton @click="onClose">
+4 -2
docs/.vitepress/theme/components/icons/IconGrid.vue
··· 25 25 :key="icon.name" 26 26 > 27 27 <IconItem 28 - v-bind="icon" 29 - @setActiveIcon="setActiveIcon(icon.name)" 28 + :iconNode="icon.iconNode" 29 + :name="icon.name" 30 + :externalLibrary="icon.externalLibrary" 31 + @setActiveIcon="setActiveIcon" 30 32 :active="activeIcon === icon.name" 31 33 customizable 32 34 :overlayMode="overlayMode"
+34 -6
docs/.vitepress/theme/components/icons/IconInfo.vue
··· 7 7 import VPButton from 'vitepress/dist/client/theme-default/components/VPButton.vue'; 8 8 import {useData, useRouter} from 'vitepress'; 9 9 import { computed } from 'vue'; 10 + import createLucideIcon from 'lucide-vue-next/src/createLucideIcon'; 11 + import { diamond } from '../../../data/iconNodes' 10 12 11 13 const props = defineProps<{ 12 14 icon: IconEntity ··· 20 22 if (!props.icon || !props?.icon?.tags) return [] 21 23 return props.icon.tags.join(' • ') 22 24 }) 25 + 26 + const DiamondIcon = createLucideIcon('Diamond', diamond) 23 27 </script> 24 28 25 29 <template> 26 30 <div class="icon-info"> 27 - <IconDetailName class="icon-name"> 28 - {{ icon.name }} 29 - </IconDetailName> 31 + <div class="icon-name-wrapper"> 32 + <IconDetailName class="icon-name"> 33 + {{ icon.name }} 34 + </IconDetailName> 35 + <div v-if="icon.externalLibrary" class="icon-external-lib"> 36 + <DiamondIcon fill="currentColor" :size="12"/> 37 + {{ icon.externalLibrary }} 38 + </div> 39 + </div> 30 40 <div class="tags-scroller" v-if="tags.length"> 31 41 <p class="icon-tags horizontal-scroller"> 32 42 {{ tags }} ··· 44 54 45 55 <div class="group buttons"> 46 56 <VPButton 47 - v-if="!page?.relativePath?.startsWith?.(`icons/${icon.name}`)" 48 - :href="`/icons/${icon.name}`" 57 + v-if="!page?.relativePath?.startsWith?.(icon.externalLibrary ? `icons/${icon.externalLibrary}/${icon.name}`: `icons/${icon.name}`)" 58 + :href="icon.externalLibrary ? `/icons/${icon.externalLibrary}/${icon.name}`: `/icons/${icon.name}`" 49 59 text="See in action" 50 - @click="go(`/icons/${icon.name}`)" 60 + @click="go(icon.externalLibrary ? `/icons/${icon.externalLibrary}/${icon.name}`: `/icons/${icon.name}`)" 51 61 /> 52 62 <CopySVGButton :name="icon.name" :popoverPosition="popoverPosition"/> 53 63 <CopyCodeButton :name="icon.name" :popoverPosition="popoverPosition"/> ··· 67 77 text-transform: capitalize; 68 78 } 69 79 .icon-name { 80 + margin-right: -36px; 81 + } 82 + 83 + .icon-name-wrapper { 84 + display: flex; 85 + align-items: center; 86 + gap: 2px; 70 87 margin-bottom: 4px; 88 + } 89 + 90 + .icon-external-lib { 91 + color: var(--vp-c-brand-dark); 92 + padding: 4px 12px; 93 + font-size: 16px; 94 + font-weight: 600; 95 + line-height: 28px; 96 + display: flex; 97 + gap: 8px; 98 + align-items: center; 71 99 } 72 100 73 101 .icon-tags {
+26 -5
docs/.vitepress/theme/components/icons/IconItem.vue
··· 6 6 import getSVGIcon from '../../utils/getSVGIcon'; 7 7 import useConfetti from '../../composables/useConfetti'; 8 8 import Tooltip from '../base/Tooltip.vue'; 9 + import { diamond } from '../../../data/iconNodes' 9 10 10 11 const downloadText = 'Download!' 11 12 const copiedText = 'Copied!' ··· 16 17 name: string; 17 18 iconNode: IconNode; 18 19 active: boolean; 20 + externalLibrary?: string; 19 21 customizable?: boolean; 20 22 overlayMode?: boolean 21 23 hideIcon?: boolean ··· 33 35 return createLucideIcon(props.name, props.iconNode) 34 36 }) 35 37 36 - async function navigateToIcon(event) { 38 + const href = computed(() => props.externalLibrary ? `/icons/${props.externalLibrary}/${props.name}` : `/icons/${props.name}`) 37 39 40 + async function navigateToIcon(event) { 38 41 if (event.shiftKey) { 39 42 event.preventDefault() 40 43 const svgString = getSVGIcon(event.target.firstChild, { ··· 50 53 51 54 if(props.overlayMode && showOverlay.value) { 52 55 event.preventDefault() 53 - window.history.pushState({}, '', `/icons/${props.name}`) 54 - emit('setActiveIcon', props.name) 56 + 57 + window.history.pushState({}, '', props.externalLibrary ? `/icons/${props.externalLibrary}/${props.name}` : `/icons/${props.name}`) 58 + emit('setActiveIcon', props.externalLibrary ? `${props.externalLibrary}:${props.name}`: props.name) 55 59 } else { 56 60 event.preventDefault() 57 - go(`/icons/${props.name}`) 61 + go(props.externalLibrary ? `/icons/${props.externalLibrary}/${props.name}` : `/icons/${props.name}`) 58 62 } 59 63 } 64 + 65 + const DiamondIcon = createLucideIcon('Diamond', diamond) 60 66 </script> 61 67 62 68 <template> ··· 66 72 @click="navigateToIcon" 67 73 :class="{ active, animate }" 68 74 :aria-label="name" 69 - :href="`/icons/${props.name}`" 75 + 70 76 :data-confetti-text="confettiText" 71 77 ref="ref" 72 78 > ··· 80 86 }" 81 87 /> 82 88 </KeepAlive> 89 + <div 90 + v-if="externalLibrary" 91 + class="floating-diamond" 92 + aria-hidden="true" 93 + > 94 + <DiamondIcon fill="currentColor" :size="8"/> 95 + </div> 83 96 </a> 84 97 </Tooltip> 85 98 </template> ··· 88 101 89 102 <style scoped> 90 103 .icon-button { 104 + position: relative; 91 105 display: inline-block; 92 106 border: 1px solid transparent; 93 107 text-align: center; ··· 102 116 height: 56px; 103 117 font-size: 24px; 104 118 color: var(--vp-c-text-1); 119 + } 120 + 121 + .floating-diamond { 122 + position: absolute; 123 + top: 4px; 124 + right: 4px; 125 + color: var(--vp-c-brand); 105 126 } 106 127 107 128 .confetti-button:before,
+1 -1
docs/.vitepress/theme/components/icons/IconsCategory.vue
··· 19 19 import IconGrid from './IconGrid.vue' 20 20 21 21 defineProps<{ 22 - activeIconName: string 22 + activeIconName: string | null 23 23 categoryRow: CategoryRow 24 24 }>() 25 25
+8 -2
docs/.vitepress/theme/components/icons/IconsCategoryOverview.vue
··· 1 1 <script setup lang="ts"> 2 - import { ref, computed, defineAsyncComponent, onMounted } from 'vue'; 2 + import { ref, computed, defineAsyncComponent, onMounted, watch, watchEffect } from 'vue'; 3 3 import type { IconEntity, Category } from '../../types'; 4 4 import useSearch from '../../composables/useSearch'; 5 5 import InputSearch from '../base/InputSearch.vue'; ··· 69 69 return props.categories 70 70 .map(({ name, title }) => { 71 71 const categoryIcons = props.icons.filter((icon) => { 72 - const iconCategories = props.iconCategories[icon.name]; 72 + const iconCategories = icon?.externalLibrary ? icon.categories : props.iconCategories[icon.name] 73 73 74 74 return iconCategories?.includes(name); 75 75 }); ··· 140 140 141 141 window.history.pushState({}, '', '/icons/categories'); 142 142 } 143 + 144 + watchEffect(() => { 145 + 146 + console.log(props.icons.find((icon) => icon.name === 'burger')); 147 + 148 + }); 143 149 </script> 144 150 145 151 <template>
+47
docs/.vitepress/theme/components/icons/SidebarExternalLibrarySelect.vue
··· 1 + <script setup lang="ts"> 2 + import Checkbox from '../base/Checkbox.vue' 3 + import SidebarTitle from './SidebarTitle.vue' 4 + import { useExternalLibs } from '../../composables/useExternalLibs'; 5 + import { ExternalLibs } from '../../types'; 6 + 7 + interface ExternalLibrary { 8 + name: string; 9 + value: ExternalLibs; 10 + } 11 + 12 + const externalLibraries: ExternalLibrary[] = [ 13 + { 14 + name: 'Lab', 15 + value: 'lab' 16 + }, 17 + ]; 18 + 19 + const { selectedLibs } = useExternalLibs(); 20 + </script> 21 + 22 + <template> 23 + <div class="external-library-select"> 24 + <SidebarTitle> 25 + Include external libs 26 + </SidebarTitle> 27 + <ul> 28 + <li 29 + v-for="library in externalLibraries" 30 + :key="library.name" 31 + > 32 + <Checkbox 33 + :label="library.name" 34 + :id="library.name" 35 + v-model="selectedLibs" 36 + :value="library.value" 37 + /> 38 + </li> 39 + </ul> 40 + </div> 41 + </template> 42 + 43 + <style scoped> 44 + .external-library-select { 45 + margin-bottom: 24px; 46 + } 47 + </style>
+19
docs/.vitepress/theme/components/icons/SidebarTitle.vue
··· 1 + <template> 2 + <h2 class="sidebar-title sidebar-text"> 3 + <slot /> 4 + </h2> 5 + </template> 6 + 7 + <style scoped> 8 + .sidebar-title { 9 + font-weight: 700; 10 + color: var(--vp-c-text-1); 11 + } 12 + .sidebar-text { 13 + line-height: 24px; 14 + font-size: 14px; 15 + display: block; 16 + transition: color 0.25s; 17 + padding: 4px 0; 18 + } 19 + </style>
+57
docs/.vitepress/theme/composables/useExternalLibs.ts
··· 1 + import { ref, inject, Ref, watch } from 'vue'; 2 + import { ExternalLibs, IconEntity } from '../types'; 3 + 4 + export const EXTERNAL_LIBS_CONTEXT = Symbol('externalLibs'); 5 + 6 + type ExternalIconNodes = Partial<Record<ExternalLibs, IconEntity[]>>; 7 + 8 + interface ExternalLibContext { 9 + selectedLibs: Ref<[ExternalLibs]>; 10 + externalIconNodes: Ref<ExternalIconNodes>; 11 + } 12 + 13 + export const externalLibContext = { 14 + selectedLibs: ref([]), 15 + externalIconNodes: ref({}), 16 + }; 17 + 18 + const externalLibIconNodesAPI = { 19 + lab: 'https://lab.lucide.dev/api/icon-details', 20 + }; 21 + 22 + export function useExternalLibs(): ExternalLibContext { 23 + const context = inject<ExternalLibContext>(EXTERNAL_LIBS_CONTEXT); 24 + 25 + watch(context?.selectedLibs, async (selectedLibs) => { 26 + const savedIconNodes = { ...context?.externalIconNodes.value }; 27 + const newExternalIconNodes: ExternalIconNodes = {}; 28 + 29 + try { 30 + for (const lib of selectedLibs) { 31 + if (savedIconNodes[lib]) { 32 + newExternalIconNodes[lib] = savedIconNodes[lib]; 33 + } else { 34 + const response = await fetch(externalLibIconNodesAPI[lib]); 35 + const iconNodes = await response.json(); 36 + 37 + if (iconNodes) { 38 + newExternalIconNodes[lib] = Object.values(iconNodes).map((iconEntity: IconEntity) => ({ 39 + ...iconEntity, 40 + externalLibrary: lib, 41 + })); 42 + } 43 + } 44 + } 45 + 46 + context.externalIconNodes.value = newExternalIconNodes; 47 + } catch (error) { 48 + console.error(error); 49 + } 50 + }); 51 + 52 + if (!context) { 53 + throw new Error('useExternalLibs must be used with externalLibs context'); 54 + } 55 + 56 + return context; 57 + }
+29
docs/.vitepress/theme/composables/useIconsWithExternalLibs.ts
··· 1 + import { computed } from 'vue'; 2 + import { useExternalLibs } from '~/.vitepress/theme/composables/useExternalLibs'; 3 + import { IconEntity } from '../types'; 4 + 5 + const useIconsWithExternalLibs = (initialIcons?: IconEntity[]) => { 6 + const { externalIconNodes } = useExternalLibs(); 7 + 8 + return computed(() => { 9 + let icons = []; 10 + 11 + if (initialIcons) { 12 + icons = icons.concat(initialIcons); 13 + } 14 + 15 + const externalIconNodesArray = Object.values(externalIconNodes.value); 16 + 17 + if (externalIconNodesArray?.length) { 18 + externalIconNodesArray.forEach((iconNodes) => { 19 + if (iconNodes?.length) { 20 + icons = icons.concat(iconNodes); 21 + } 22 + }); 23 + } 24 + 25 + return icons; 26 + }); 27 + }; 28 + 29 + export default useIconsWithExternalLibs;
+2
docs/.vitepress/theme/index.ts
··· 7 7 import HomeHeroBefore from './components/home/HomeHeroBefore.vue'; 8 8 import { ICON_STYLE_CONTEXT, iconStyleContext } from './composables/useIconStyle'; 9 9 import { CATEGORY_VIEW_CONTEXT, categoryViewContext } from './composables/useCategoryView'; 10 + import { EXTERNAL_LIBS_CONTEXT, externalLibContext } from './composables/useExternalLibs'; 10 11 11 12 const theme: Partial<Theme> = { 12 13 extends: DefaultTheme, ··· 20 21 enhanceApp({ app }) { 21 22 app.provide(ICON_STYLE_CONTEXT, iconStyleContext); 22 23 app.provide(CATEGORY_VIEW_CONTEXT, categoryViewContext); 24 + app.provide(EXTERNAL_LIBS_CONTEXT, externalLibContext); 23 25 }, 24 26 }; 25 27
+2
docs/.vitepress/theme/layouts/IconsSidebarNavAfter.vue
··· 3 3 4 4 import CategoryList from '../components/icons/CategoryList.vue' 5 5 import SidebarIconCustomizer from '../components/icons/SidebarIconCustomizer.vue' 6 + import ExternalLibrarySelect from '../components/icons/SidebarExternalLibrarySelect.vue' 6 7 7 8 const { page } = useData() 8 9 ··· 11 12 <template> 12 13 <div> 13 14 <SidebarIconCustomizer v-if="page?.relativePath?.startsWith?.('icons')"/> 15 + <ExternalLibrarySelect v-if="page?.relativePath?.startsWith?.('icons')"/> 14 16 <CategoryList v-if="page?.relativePath?.startsWith?.('icons')"/> 15 17 </div> 16 18 </template>
+7 -2
docs/.vitepress/theme/types.ts
··· 1 1 export type IconNode = [elementName: string, attrs: Record<string, string>][]; 2 2 export type IconNodeWithKeys = [elementName: string, attrs: Record<string, string>, key: string][]; 3 3 4 - export interface IconEntity { 5 - name: string; 4 + export interface IconMetaData { 6 5 tags: string[]; 7 6 categories: string[]; 8 7 contributors: string[]; 9 8 aliases?: string[]; 9 + } 10 + 11 + export type ExternalLibs = 'lab'; 12 + export interface IconEntity extends IconMetaData { 13 + name: string; 10 14 iconNode: IconNode; 15 + externalLibrary?: ExternalLibs; 11 16 createdRelease?: Release; 12 17 changedRelease?: Release; 13 18 }
+1 -1
docs/guide/packages/lucide-vue-next.md
··· 87 87 </script> 88 88 89 89 <template> 90 - <Icon :iconNode={burger} /> 90 + <Icon :iconNode="burger" /> 91 91 </template> 92 92 ``` 93 93
+13 -10
docs/icons/[name].md
··· 10 10 <script setup> 11 11 import { computed } from 'vue' 12 12 import { useData } from 'vitepress' 13 - import IconPreview from '../.vitepress/theme/components/icons/IconPreview.vue' 14 - import IconPreviewSmall from '../.vitepress/theme/components/icons/IconPreviewSmall.vue' 15 - import IconInfo from '../.vitepress/theme/components/icons/IconInfo.vue' 16 - import IconContributors from '../.vitepress/theme/components/icons/IconContributors.vue' 17 - import RelatedIcons from '../.vitepress/theme/components/icons/RelatedIcons.vue' 18 - import CodeGroup from '../.vitepress/theme/components/base/CodeGroup.vue' 19 - import Badge from '../.vitepress/theme/components/base/Badge.vue' 20 - import Label from '../.vitepress/theme/components/base/Label.vue' 13 + import IconPreview from '~/.vitepress/theme/components/icons/IconPreview.vue' 14 + import IconPreviewSmall from '~/.vitepress/theme/components/icons/IconPreviewSmall.vue' 15 + import IconInfo from '~/.vitepress/theme/components/icons/IconInfo.vue' 16 + import IconContributors from '~/.vitepress/theme/components/icons/IconContributors.vue' 17 + import RelatedIcons from '~/.vitepress/theme/components/icons/RelatedIcons.vue' 18 + import CodeGroup from '~/.vitepress/theme/components/base/CodeGroup.vue' 19 + import Badge from '~/.vitepress/theme/components/base/Badge.vue' 20 + import Label from '~/.vitepress/theme/components/base/Label.vue' 21 21 import VPButton from 'vitepress/dist/client/theme-default/components/VPButton.vue'; 22 22 import { data } from './codeExamples.data' 23 23 import { camelCase, startCase } from 'lodash-es' ··· 32 32 const codeExample = computed(() => data.codeExamples?.map( 33 33 (codeExample) => { 34 34 const pascalCase = startCase(camelCase( params.value.name)).replace(/\s/g, '') 35 - return codeExample.code.replace(/PascalCase/g, pascalCase).replace(/Name/g, params.value.name) 35 + return codeExample.code.replace(/\$PascalCase/g, pascalCase).replace(/\$Name/g, params.value.name) 36 36 } 37 37 ).join('') ?? [] 38 38 ) ··· 100 100 </div> 101 101 </div> 102 102 103 - <RelatedIcons :icons="params.relatedIcons" /> 103 + <RelatedIcons 104 + v-if="params.relatedIcons" 105 + :icons="params.relatedIcons" 106 + /> 104 107 105 108 <style module> 106 109 .preview {
+4 -1
docs/icons/categories.md
··· 10 10 import { data as categoriesData } from './categories.data.ts' 11 11 import PageContainer from '../.vitepress/theme/components/PageContainer.vue' 12 12 import IconsCategoryOverview from '../.vitepress/theme/components/icons/IconsCategoryOverview.vue' 13 + import useIconsWithExternalLibs from '~/.vitepress/theme/composables/useIconsWithExternalLibs' 14 + 15 + const icons = useIconsWithExternalLibs(data.icons) 13 16 </script> 14 17 15 18 <div class="VPDoc content"> 16 19 <PageContainer> 17 20 <IconsCategoryOverview 18 21 :categories="categoriesData.categories" 19 - :icons="data.icons" 22 + :icons="icons" 20 23 :iconCategories="categoriesData.iconCategories" 21 24 /> 22 25 </PageContainer>
+1 -3
docs/icons/codeExamples.data.ts
··· 1 - import createCodeExamples from '../.vitepress/lib/createCodeExamples'; 1 + import createCodeExamples from '../.vitepress/lib/codeExamples/createCodeExamples'; 2 2 3 3 export default { 4 4 async load() { 5 5 const codeExamples = await createCodeExamples(); 6 - 7 - // const randomIcons = Array.from({ length: 200 }, () => getRandomItem(icons)) 8 6 9 7 return { 10 8 codeExamples,
+7 -3
docs/icons/index.md
··· 10 10 --- 11 11 12 12 <script setup> 13 + import { computed } from 'vue' 13 14 import { data } from './icons.data.ts' 14 - import IconsOverview from '../.vitepress/theme/components/icons/IconsOverview.vue' 15 - import PageContainer from '../.vitepress/theme/components/PageContainer.vue' 15 + import IconsOverview from '~/.vitepress/theme/components/icons/IconsOverview.vue' 16 + import PageContainer from '~/.vitepress/theme/components/PageContainer.vue' 17 + import useIconsWithExternalLibs from '~/.vitepress/theme/composables/useIconsWithExternalLibs' 18 + 19 + const icons = useIconsWithExternalLibs(data.icons) 16 20 </script> 17 21 18 22 <div class="VPDoc content"> 19 23 <PageContainer> 20 - <IconsOverview :icons="data.icons" /> 24 + <IconsOverview :icons="icons" /> 21 25 </PageContainer> 22 26 </div>
+10
docs/icons/lab/[name].md
··· 1 + --- 2 + layout: doc 3 + footer: false 4 + aside: false 5 + editLink: false 6 + next: false 7 + prev: false 8 + sidebar: true 9 + --- 10 + <!--@include: ../[name].md -->
+19
docs/icons/lab/[name].paths.ts
··· 1 + import { IconEntity } from '../../.vitepress/theme/types'; 2 + 3 + export default { 4 + paths: async () => { 5 + const iconDetailsResponse = await fetch('https://lab.lucide.dev/api/icon-details'); 6 + const iconDetails = (await iconDetailsResponse.json()) as Record<string, IconEntity>; 7 + 8 + return Object.values(iconDetails).map((iconEntity) => { 9 + const params = { 10 + externalLibrary: 'lab', 11 + ...iconEntity, 12 + }; 13 + 14 + return { 15 + params, 16 + }; 17 + }); 18 + }, 19 + };
+11
docs/icons/lab/codeExamples.data.ts
··· 1 + import createCodeExamples from '../../.vitepress/lib/codeExamples/createLabCodeExamples'; 2 + 3 + export default { 4 + async load() { 5 + const codeExamples = await createCodeExamples(); 6 + 7 + return { 8 + codeExamples, 9 + }; 10 + }, 11 + };
+3
docs/tsconfig.json
··· 5 5 "allowImportingTsExtensions": true, 6 6 "allowSyntheticDefaultImports": true, 7 7 "noEmit": true, 8 + "paths": { 9 + "~/.vitepress/*": ["./.vitepress/*"], 10 + }, 8 11 }, 9 12 }