mirror of
https://github.com/lucide-icons/lucide.git
synced 2026-11-14 13:58:21 +00:00
Merge branch 'docs-vitepress' of https://github.com/lucide-icons/lucide into docs-vitepress
This commit is contained in:
@@ -28,5 +28,6 @@ docs/.vitepress/dist
|
||||
docs/.vitepress/.temp
|
||||
docs/.vitepress/data/iconNodes
|
||||
docs/.vitepress/data/iconMetaData.ts
|
||||
docs/.vitepress/data/releaseMetaData.json
|
||||
docs/.vercel
|
||||
docs/.nitro
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
import crypto from 'crypto';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { Contributor } from '../theme/types';
|
||||
import fetch, { Headers } from 'node-fetch'
|
||||
|
||||
const IGNORE_COMMIT_MESSAGES = ['fork', 'optimize'];
|
||||
|
||||
function getContentHashOfFile(path) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const hash = crypto.createHash('sha256');
|
||||
const stream = fs.createReadStream(path);
|
||||
stream.on('error', err => reject(err));
|
||||
stream.on('data', chunk => hash.update(chunk));
|
||||
stream.on('end', () => resolve(hash.digest('hex')));
|
||||
});
|
||||
}
|
||||
|
||||
const fetchCommitsOfIcon = async (name) =>{
|
||||
try {
|
||||
const headers = new Headers();
|
||||
const token = process.env.GITHUB_TOKEN;
|
||||
const username = process.env.GITHUB_USERNAME;
|
||||
const password = process.env.GITHUB_API_KEY;
|
||||
headers.set(
|
||||
'Authorization',
|
||||
token ? `Bearer ${token}` : `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`,
|
||||
);
|
||||
|
||||
const res = await fetch(
|
||||
`https://api.github.com/repos/lucide-icons/lucide/commits?path=icons/${name}.svg`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
},
|
||||
);
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
return {
|
||||
name,
|
||||
commits: data,
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
throw new Error(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const filterCommits = (commits) =>
|
||||
commits.filter(({ commit }) =>
|
||||
!IGNORE_COMMIT_MESSAGES.some(ignoreItem =>
|
||||
commit.message.toLowerCase().includes(ignoreItem),
|
||||
))
|
||||
.map(({ author }) => ({
|
||||
author: author && author.login ? author.login : null,
|
||||
}))
|
||||
.filter(({ author }, index, self) => self.findIndex((commit) => commit.author === author) === index);
|
||||
|
||||
const getIconHash = async (icon) => await getContentHashOfFile(path.join(process.cwd(), "../icons", `${icon}.svg`))
|
||||
const iconCacheDir = path.join(process.cwd(),'.vitepress/cache/github-api');
|
||||
const iconCache = (hash) => path.join(iconCacheDir, `${hash}.json`);
|
||||
|
||||
export async function checkIconCache(icon) {
|
||||
const hash = await getIconHash(icon);
|
||||
|
||||
const cachePath = iconCache(hash);
|
||||
|
||||
if(fs.existsSync( cachePath )) {
|
||||
const iconCache = fs.readFileSync(cachePath, "utf8");
|
||||
|
||||
return JSON.parse(iconCache)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
async function writeIconCache(icon, content) {
|
||||
const hash = await getIconHash(icon);
|
||||
|
||||
const iconCachePath = iconCache(hash);
|
||||
|
||||
if (!fs.existsSync(iconCacheDir)){
|
||||
fs.mkdirSync(iconCacheDir);
|
||||
}
|
||||
|
||||
fs.writeFileSync(iconCachePath, JSON.stringify(content), 'utf-8');
|
||||
}
|
||||
|
||||
export async function getContributors(icon): Promise<Contributor[]> {
|
||||
try {
|
||||
let iconCommits
|
||||
const iconCache = await checkIconCache(icon);
|
||||
|
||||
if (iconCache) {
|
||||
iconCommits = iconCache
|
||||
} else {
|
||||
const { commits } = await fetchCommitsOfIcon(icon);
|
||||
|
||||
writeIconCache(icon, commits)
|
||||
|
||||
iconCommits = commits
|
||||
}
|
||||
|
||||
if (iconCommits && iconCommits.length) {
|
||||
return filterCommits(iconCommits);
|
||||
}
|
||||
|
||||
return [];
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAllContributors(icons) {
|
||||
try {
|
||||
const AllIconCommits = await Promise.all(icons.map(fetchCommitsOfIcon));
|
||||
|
||||
const filteredCommits = AllIconCommits.reduce((acc, { name, commits }) => {
|
||||
if (commits && commits.length) {
|
||||
acc[name] = filterCommits(commits)
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return filteredCommits
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,8 @@ import path from "path";
|
||||
import { parseSync } from "svgson";
|
||||
import { IconNode } from "../../../packages/lucide-react/src/createLucideIcon";
|
||||
import { IconEntity } from "../theme/types";
|
||||
import { getContributors } from "./fetchAllContributors";
|
||||
import { generateHashedKey } from "./helpers";
|
||||
import releaseMeta from "../data/releaseMetaData.json";
|
||||
|
||||
const directory = path.join(process.cwd(), "../icons");
|
||||
|
||||
@@ -25,7 +25,7 @@ export async function getData(name: string, { withChildKeys = false }: GetDataOp
|
||||
const svgContent = fs.readFileSync(svgPath, "utf8");
|
||||
const jsonPath = path.join(directory, `${name}.json`);
|
||||
const jsonContent = fs.readFileSync(jsonPath, "utf8");
|
||||
const { tags, categories } = JSON.parse(jsonContent);
|
||||
const { tags, categories, contributors } = JSON.parse(jsonContent);
|
||||
|
||||
const iconNode = parseSync(svgContent).children.map(
|
||||
(child) => {
|
||||
@@ -39,14 +39,14 @@ export async function getData(name: string, { withChildKeys = false }: GetDataOp
|
||||
}
|
||||
) as IconNode
|
||||
|
||||
// const contributors = await getContributors(name);
|
||||
|
||||
return {
|
||||
name,
|
||||
tags,
|
||||
categories,
|
||||
// contributors,
|
||||
iconNode,
|
||||
//contributors,
|
||||
contributors: ['ericfennis', 'karsa-mistmere'],
|
||||
...(releaseMeta[name] ?? {})
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
padding: 32px 32px 96px;
|
||||
padding: 32px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -41,14 +41,14 @@ const component = computed(() => props.href ? 'a' : 'div')
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.badge:hover, a.badge:hover {
|
||||
.badge[href]:hover, a.badge:hover {
|
||||
border-color: var(--vp-button-alt-hover-border);
|
||||
color: var(--vp-button-alt-hover-text);
|
||||
background-color: var(--vp-button-alt-hover-bg);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.badge:active {
|
||||
.badge[href]:active {
|
||||
border-color: var(--vp-button-alt-active-border);
|
||||
color: var(--vp-button-alt-active-text);
|
||||
background-color: var(--vp-button-alt-active-bg);
|
||||
|
||||
@@ -85,4 +85,8 @@ const value = computed({
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-bg-alt);
|
||||
}
|
||||
|
||||
.color-input[value="currentColor"] {
|
||||
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import { rotateCw } from '../../../data/iconNodes'
|
||||
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon'
|
||||
import IconButton from "./IconButton.vue";
|
||||
|
||||
const RotateIcon = createLucideIcon('RotateIcon', rotateCw)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<IconButton class="reset-button">
|
||||
<RotateIcon :size="20"/>
|
||||
</IconButton>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.reset-button {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.reset-button .lucide {
|
||||
transition: ease-in-out 0.1s transform;
|
||||
}
|
||||
|
||||
.reset-button:hover {
|
||||
background: none;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
/* a rotate css animation keyframes */
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(359deg);
|
||||
}
|
||||
}
|
||||
|
||||
.reset-button:active .lucide {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import Badge from '../base/Badge.vue';
|
||||
import lucidePackage from '../../../../../packages/lucide/package.json';
|
||||
const currentVersion = lucidePackage.version;
|
||||
</script>
|
||||
<template>
|
||||
<div class="hero-badge">
|
||||
<Badge>v{{ currentVersion }}</Badge>
|
||||
</div>
|
||||
<h1>
|
||||
<span class="name">Beautiful &</span><br>
|
||||
<span class="text">consistent icons</span>
|
||||
</h1>
|
||||
<p class="tagline">Made by the community.</p>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
h1 {
|
||||
line-height: 64px;
|
||||
font-size: 56px;
|
||||
max-width: 576px;
|
||||
font-weight: 700;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.name {
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,11 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { syncRef, useCssVar } from '@vueuse/core'
|
||||
import HomeContainer from './HomeContainer.vue'
|
||||
import { rotateCw } from '../../../data/iconNodes'
|
||||
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon'
|
||||
import RangeSlider from '../base/RangeSlider.vue'
|
||||
import InputField from '../base/InputField.vue'
|
||||
import ColorPicker from '../base/ColorPicker.vue'
|
||||
import ResetButton from '../base/ResetButton.vue'
|
||||
import HomeIconCustomizerIcons from './HomeIconCustomizerIcons.vue'
|
||||
|
||||
import { ref } from 'vue'
|
||||
@@ -43,14 +42,11 @@ syncRef(color, colorCssVar)
|
||||
syncRef(strokeWidth, strokeWidthCssVar)
|
||||
syncRef(size, sizeCssVar)
|
||||
|
||||
// TODO Add Reset button
|
||||
// const RotateIcon = createLucideIcon('RotateIcon', rotateCw)
|
||||
|
||||
// function resetStyle () {
|
||||
// color.value = 'currentColor'
|
||||
// strokeWidth.value = 2
|
||||
// size.value = 24
|
||||
// }
|
||||
function resetStyle () {
|
||||
color.value = 'currentColor'
|
||||
strokeWidth.value = 2
|
||||
size.value = 24
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@@ -60,6 +56,7 @@ syncRef(size, sizeCssVar)
|
||||
<div class="card-column">
|
||||
<h2 class="title">
|
||||
Style as you please
|
||||
<ResetButton @click="resetStyle"></ResetButton>
|
||||
</h2>
|
||||
<p class="copy">
|
||||
Lucide has a lot of customization options to match the icons with you UI.
|
||||
@@ -89,7 +86,7 @@ syncRef(size, sizeCssVar)
|
||||
v-model="strokeWidth"
|
||||
:min="1"
|
||||
:max="3"
|
||||
:steps="0.25"
|
||||
:step="0.25"
|
||||
/>
|
||||
</InputField>
|
||||
|
||||
@@ -134,6 +131,11 @@ syncRef(size, sizeCssVar)
|
||||
line-height: 32px;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.copy {
|
||||
@@ -170,6 +172,6 @@ syncRef(size, sizeCssVar)
|
||||
}
|
||||
|
||||
.color-picker-field:deep(.display-value) {
|
||||
width: 130px;
|
||||
width: 138px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -33,11 +33,11 @@ const onIntersectionObserver: IntersectionObserverCallback = ([{ isIntersecting
|
||||
.icon-grid {
|
||||
display: grid;
|
||||
gap: 1px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(56px, 1fr));
|
||||
grid-template-rows: repeat(auto-fill, minmax(56px, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(68px, 1fr));
|
||||
grid-template-rows: repeat(auto-fill, minmax(68px, 1fr));
|
||||
width: 100%;
|
||||
height:100%;
|
||||
max-height: 300px;
|
||||
max-height: 360px;
|
||||
gap: 1px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
@@ -54,7 +54,28 @@ function copyVue() {
|
||||
navigator.clipboard.writeText(code)
|
||||
}
|
||||
|
||||
// TODO: This function should be moved to a composable, together with the styling
|
||||
function copyAngular() {
|
||||
let attrs = ['']
|
||||
|
||||
attrs.push(`name="${props.name}"`)
|
||||
|
||||
if (size.value && size.value !== 24) {
|
||||
attrs.push(`[size]="${size.value}"`)
|
||||
}
|
||||
|
||||
if (color.value && color.value !== 'currentColor') {
|
||||
attrs.push(`color="${color.value}"`)
|
||||
}
|
||||
|
||||
if (strokeWidth.value && strokeWidth.value !== 2) {
|
||||
attrs.push(`[strokeWidth]="${strokeWidth.value}"`)
|
||||
}
|
||||
|
||||
const code = `<lucide-icon${attrs.join(' ')}></lucide-icon>`
|
||||
|
||||
navigator.clipboard.writeText(code)
|
||||
}
|
||||
|
||||
function confetti(e) {
|
||||
animate.value = true;
|
||||
|
||||
@@ -77,6 +98,7 @@ function confetti(e) {
|
||||
{ text: 'Copy JSX' , onClick: copyJSX },
|
||||
{ text: 'Copy Vue' , onClick: copyVue },
|
||||
{ text: 'Copy Svelte' , onClick: copyJSX },
|
||||
{ text: 'Copy Angular' , onClick: copyAngular },
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<script setup lang="ts">
|
||||
import {IconEntity} from "../../types";
|
||||
|
||||
const props = defineProps<{
|
||||
icon: IconEntity
|
||||
}>()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="contributors" v-if="props.icon.contributors?.length>0">
|
||||
<h2 class="label">Contributors</h2>
|
||||
<div class="avatar-group">
|
||||
<a class="avatar" v-for="contributor in props.icon.contributors" :href="`https://github.com/${contributor}`" target="_blank" :data-name="contributor">
|
||||
<img class="avatar-image" :alt="contributor" :src="`https://github.com/${contributor}.png?size=128`" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.contributors {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 24px;
|
||||
}
|
||||
h2.label {
|
||||
font-size: 20px;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.avatar-group {
|
||||
display: flex;
|
||||
}
|
||||
.avatar:not(:first-child) {
|
||||
margin-left: -24px;
|
||||
}
|
||||
.avatar {
|
||||
position: relative;
|
||||
}
|
||||
.avatar:before {
|
||||
content: attr(data-name);
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
transform: translateX(-50%) scale(0.9);
|
||||
font-weight: 400;
|
||||
position: absolute;
|
||||
top: -28px;
|
||||
left: 50%;
|
||||
background: var(--vp-c-brand-dark);
|
||||
color: white;
|
||||
z-index: 10;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
box-shadow: var(--vp-shadow-1);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: cubic-bezier(0.19, 1, 0.22, 1) .2s;
|
||||
transition-property: opacity, transform;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
.avatar:hover:before {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) scale(1);
|
||||
}
|
||||
.avatar-image {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid var(--vp-c-bg-elv);
|
||||
}
|
||||
.avatar:hover .avatar-image {
|
||||
border: 3px solid var(--vp-c-bg-soft-mute);
|
||||
}
|
||||
</style>
|
||||
@@ -9,7 +9,6 @@
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
line-height: 32px;
|
||||
margin-bottom: 12px;
|
||||
transition: background ease-in .15s;;
|
||||
padding: 2px 8px;
|
||||
border-radius: 8px;
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon';
|
||||
import IconButton from '../base/IconButton.vue';
|
||||
import IconContributors from './IconContributors.vue';
|
||||
import IconPreview from './IconPreview.vue';
|
||||
import { x, expand } from '../../../data/iconNodes'
|
||||
import { useRouter } from 'vitepress';
|
||||
import IconInfo from './IconInfo.vue';
|
||||
import Badge from '../base/Badge.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
icon: IconEntity
|
||||
@@ -35,9 +37,7 @@
|
||||
<div class="overlay-container" v-if="props.icon">
|
||||
<div class="overlay-panel">
|
||||
<nav class="overlay-menu">
|
||||
<IconButton @click="go(`/icons/${icon.name}`)">
|
||||
<component :is="Expand" />
|
||||
</IconButton>
|
||||
<Badge>v{{ icon.createdRelease.version }}</Badge>
|
||||
<IconButton @click="onClose">
|
||||
<component :is="CloseIcon" />
|
||||
</IconButton>
|
||||
@@ -48,7 +48,11 @@
|
||||
:iconNode="props.icon.iconNode"
|
||||
customizable
|
||||
/>
|
||||
<IconInfo :icon="props.icon" popoverPosition="top" />
|
||||
<IconInfo :icon="props.icon" popoverPosition="top">
|
||||
<template v-slot:footer>
|
||||
<IconContributors :icon="props.icon" />
|
||||
</template>
|
||||
</IconInfo>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
@@ -94,8 +98,8 @@
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
will-change: transform;
|
||||
pointer-events: all;
|
||||
height: 240px;
|
||||
padding: 24px 32px;
|
||||
height: 288px;
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
box-shadow: var(--vp-shadow-5);
|
||||
}
|
||||
@@ -118,6 +122,7 @@
|
||||
right: 24px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.drawer-enter-active {
|
||||
@@ -134,6 +139,4 @@
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
@@ -41,7 +41,7 @@ const { page } = useData()
|
||||
<CopySVGButton :name="icon.name" :popoverPosition="popoverPosition"/>
|
||||
<CopyCodeButton :name="icon.name" :popoverPosition="popoverPosition"/>
|
||||
</div>
|
||||
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -52,8 +52,10 @@ const { page } = useData()
|
||||
gap: 8px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.category {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.icon-name {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -95,6 +95,6 @@ watch(searchQueryThrottled, (searchString) => {
|
||||
}
|
||||
|
||||
.bottom-page {
|
||||
height: 200px;
|
||||
height: 288px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,11 +3,9 @@ import { shallowRef, type Ref } from 'vue'
|
||||
import { useCssVar, syncRef } from '@vueuse/core'
|
||||
import { useIconStyleContext } from '../../composables/useIconStyle'
|
||||
import RangeSlider from '../base/RangeSlider.vue'
|
||||
import IconButton from '../base/IconButton.vue'
|
||||
import InputField from '../base/InputField.vue'
|
||||
import ColorPicker from '../base/ColorPicker.vue'
|
||||
import { rotateCw } from '../../../data/iconNodes'
|
||||
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon'
|
||||
import ResetButton from '../base/ResetButton.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
rootEl?: Ref<HTMLElement>
|
||||
@@ -44,8 +42,6 @@ syncRef(color, colorCssVar)
|
||||
syncRef(strokeWidth, strokeWidthCssVar)
|
||||
syncRef(size, sizeCssVar)
|
||||
|
||||
const RotateIcon = createLucideIcon('RotateIcon', rotateCw)
|
||||
|
||||
function resetStyle () {
|
||||
color.value = 'currentColor'
|
||||
strokeWidth.value = 2
|
||||
@@ -59,9 +55,7 @@ function resetStyle () {
|
||||
<h2 class="card-title">
|
||||
Customizer
|
||||
</h2>
|
||||
<IconButton class="reset-button" @click="resetStyle">
|
||||
<RotateIcon :size="20"/>
|
||||
</IconButton>
|
||||
<ResetButton @click="resetStyle"></ResetButton>
|
||||
</div>
|
||||
<InputField
|
||||
id="icon-color"
|
||||
@@ -136,30 +130,4 @@ function resetStyle () {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.reset-button {
|
||||
background: none;
|
||||
padding: 0;
|
||||
transition: ease-in-out 0.1s transform;
|
||||
}
|
||||
|
||||
.reset-button:hover {
|
||||
background: none;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
/* a rotate css animation keyframes */
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(359deg);
|
||||
}
|
||||
}
|
||||
|
||||
.reset-button:active {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
@@ -4,6 +4,7 @@ import './style.css'
|
||||
import { Theme } from 'vitepress'
|
||||
import IconsSidebarNavAfter from './layouts/IconsSidebarNavAfter.vue'
|
||||
import HomeHeroIconsCard from './components/home/HomeHeroIconsCard.vue'
|
||||
import HomeHeroInfo from "./components/home/HomeHeroInfo.vue";
|
||||
import { ICON_STYLE_CONTEXT, iconStyleContext } from './composables/useIconStyle'
|
||||
import { CATEGORY_VIEW_CONTEXT, categoryViewContext } from './composables/useCategoryView'
|
||||
|
||||
@@ -11,6 +12,7 @@ const theme: Partial<Theme> = {
|
||||
extends: DefaultTheme,
|
||||
Layout() {
|
||||
return h(DefaultTheme.Layout, null, {
|
||||
'home-hero-info': () => h(HomeHeroInfo),
|
||||
'sidebar-nav-after': () => h(IconsSidebarNavAfter),
|
||||
'home-hero-image': () => h(HomeHeroIconsCard)
|
||||
})
|
||||
|
||||
@@ -4,12 +4,10 @@ export interface IconEntity {
|
||||
name: string;
|
||||
tags: string[];
|
||||
categories: string[];
|
||||
contributors: Contributor[];
|
||||
contributors: string[];
|
||||
iconNode: IconNode;
|
||||
}
|
||||
|
||||
export interface Contributor {
|
||||
author: string;
|
||||
createdRelease?: Release;
|
||||
changedRelease?: Release;
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
@@ -36,3 +34,9 @@ export interface PackageItem {
|
||||
private?: boolean
|
||||
flutter?: object
|
||||
}
|
||||
|
||||
|
||||
export interface Release {
|
||||
version: string
|
||||
date: string
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@ import { computed } from 'vue'
|
||||
import { useData } from 'vitepress'
|
||||
import IconPreview from '../.vitepress/theme/components/icons/IconPreview.vue'
|
||||
import IconInfo from '../.vitepress/theme/components/icons/IconInfo.vue'
|
||||
import IconContributors from '../.vitepress/theme/components/icons/IconContributors.vue'
|
||||
import CodeGroup from '../.vitepress/theme/components/base/CodeGroup.vue'
|
||||
import Badge from '../.vitepress/theme/components/base/Badge.vue'
|
||||
import VPButton from 'vitepress/dist/client/theme-default/components/VPButton.vue';
|
||||
import { data } from './codeExamples.data'
|
||||
import { camelCase, startCase } from 'lodash-es'
|
||||
@@ -50,6 +52,11 @@ const codeExample = computed(() => data.codeExamples?.map(
|
||||
/>
|
||||
</CodeGroup>
|
||||
</div>
|
||||
<div class="release-info">
|
||||
<Badge><strong>Created:</strong> v{{$params.createdRelease.version}}</Badge>
|
||||
<Badge><strong>Last changed:</strong> v{{$params.changedRelease.version}}</Badge>
|
||||
</div>
|
||||
<IconContributors :icon="$params" />
|
||||
</div>
|
||||
|
||||
<style module>
|
||||
@@ -75,3 +82,14 @@ const codeExample = computed(() => data.codeExamples?.map(
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style scoped>
|
||||
.release-info {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.badge {
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -21,7 +21,6 @@ hero:
|
||||
link: https://github.com/lucide-icons/lucide
|
||||
|
||||
features:
|
||||
|
||||
- title: Lightweight & Scalable
|
||||
details: Icons are lightweight, highly optimized scalable vector graphics (SVG).
|
||||
icon: |
|
||||
|
||||
+4
-1
@@ -10,10 +10,11 @@
|
||||
"docs:preview": "vitepress preview",
|
||||
"build:iconNodes": "node ../scripts/writeIconNodes.mjs",
|
||||
"build:metaJson": "node ../scripts/writeIconMetaIndex.mjs",
|
||||
"build:releaseJson": "node ../scripts/writeReleaseMetadata.mjs",
|
||||
"build:vercelJson": "node ../scripts/writeVercelOutput.mjs",
|
||||
"dev": "npx nitropack dev",
|
||||
"build:api": "npx nitropack build",
|
||||
"build": "pnpm build:iconNodes && pnpm build:metaJson && pnpm build:api && pnpm docs:build && pnpm build:vercelJson",
|
||||
"build": "pnpm build:iconNodes && pnpm build:metaJson && pnpm build:releaseJson && pnpm build:api && pnpm docs:build && pnpm build:vercelJson",
|
||||
"preview": "node .output/server/index.mjs"
|
||||
},
|
||||
"keywords": [],
|
||||
@@ -39,8 +40,10 @@
|
||||
"lucide-vue-next": "workspace:*",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"semver": "^7.5.0",
|
||||
"shiki": "^0.14.2",
|
||||
"shiki-processor": "^0.1.3",
|
||||
"simple-git": "^3.18.0",
|
||||
"svg-pathdata": "^6.0.3",
|
||||
"vue": "^3.2.47"
|
||||
}
|
||||
|
||||
@@ -41,6 +41,8 @@
|
||||
"minimist": "^1.2.8",
|
||||
"node-fetch": "^3.3.1",
|
||||
"prettier": "2.7.1",
|
||||
"semver": "^7.5.0",
|
||||
"simple-git": "^3.18.0",
|
||||
"svgo": "^3.0.2",
|
||||
"svgson": "^5.2.1"
|
||||
},
|
||||
|
||||
Generated
+311
-218
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,94 @@
|
||||
import {CleanOptions, simpleGit} from 'simple-git';
|
||||
import semver from 'semver';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import {readSvgDirectory} from './helpers.mjs';
|
||||
|
||||
const currentDir = process.cwd();
|
||||
const ICONS_DIR = path.resolve(currentDir, '../icons');
|
||||
const iconJsonFiles = readSvgDirectory(ICONS_DIR, '.json');
|
||||
|
||||
const location = path.resolve(currentDir, '.vitepress/data', 'releaseMetaData.json');
|
||||
|
||||
if (fs.existsSync(location)) {
|
||||
fs.unlinkSync(location);
|
||||
}
|
||||
|
||||
export const releaseMin = (a, b) => {
|
||||
if (!a) return b;
|
||||
if (!b) return a;
|
||||
return semver.gt(a.version, b.version) ? b : a;
|
||||
}
|
||||
|
||||
export const releaseMax = (a, b) => {
|
||||
if (!a) return b;
|
||||
if (!b) return a;
|
||||
return semver.gt(a.version, b.version) ? a : b;
|
||||
}
|
||||
|
||||
export const updateReleaseMetadataWithCommit = (metadata, date, release) => {
|
||||
metadata = metadata || {};
|
||||
metadata.createdRelease = releaseMin(metadata.createdRelease, release);
|
||||
metadata.changedRelease = releaseMax(metadata.changedRelease, release);
|
||||
}
|
||||
|
||||
export const fetchAllReleases = async () => {
|
||||
return (await Promise.all((await simpleGit().raw('show-ref', '--tags', '-d')).trim().split(/\n/)
|
||||
.map(async (line) => {
|
||||
const [commit, ref] = line.split(/ /);
|
||||
const version = semver.coerce(ref.replace('refs/tags/', '')).version;
|
||||
const date = (await simpleGit().show(['-s', '--format=%cI', commit])).trim();
|
||||
return {version, date};
|
||||
}))).filter(({version}) => semver.valid(version))
|
||||
}
|
||||
|
||||
simpleGit().clean(CleanOptions.FORCE);
|
||||
|
||||
const findRelease = (date, releases) => {
|
||||
let closestRelease = null;
|
||||
for (const release of releases) {
|
||||
if (release.date >= date && (closestRelease === null || release.date < closestRelease.date)) {
|
||||
closestRelease = release;
|
||||
}
|
||||
}
|
||||
return closestRelease;
|
||||
}
|
||||
|
||||
const fetchCommits = async (name) => {
|
||||
const file = `../icons/${name}.svg`;
|
||||
const {all: commits} = await simpleGit().log(['--reverse', '--follow', '--', file]);
|
||||
return commits;
|
||||
}
|
||||
|
||||
export const getReleaseMetadata = async (name, aliases, releases) => {
|
||||
const metadata = {name};
|
||||
for (let alias of [name, ...(aliases ?? [])]) {
|
||||
const commits = await fetchCommits(alias);
|
||||
for (const commit of commits) {
|
||||
const date = new Date(commit.date).toISOString();
|
||||
const release = findRelease(date, releases)
|
||||
updateReleaseMetadataWithCommit(metadata, date, release);
|
||||
}
|
||||
}
|
||||
return metadata;
|
||||
};
|
||||
|
||||
|
||||
const releases = await fetchAllReleases();
|
||||
const releaseMetaData = (await Promise.all(iconJsonFiles.map(async (iconJsonFile) => {
|
||||
const iconName = path.basename(iconJsonFile, '.json');
|
||||
const aliases = JSON.parse(fs.readFileSync(path.join(ICONS_DIR, iconJsonFile))).aliases;
|
||||
return await getReleaseMetadata(iconName, aliases, releases);
|
||||
}))).reduce((acc, {name, ...rest}) => {
|
||||
acc[name] = rest;
|
||||
return acc;
|
||||
}, {})
|
||||
|
||||
fs.promises
|
||||
.writeFile(location, JSON.stringify(releaseMetaData, null, 2), 'utf-8')
|
||||
.then(() => {
|
||||
console.log('Successfully written icon release meta cache file');
|
||||
})
|
||||
.catch((error) => {
|
||||
throw new Error(`Something went wrong generating icon release meta cache file,\n ${error}`);
|
||||
});
|
||||
Reference in New Issue
Block a user