feat(user components): icon

This commit is contained in:
HPCesia 2025-03-28 14:55:40 +08:00
parent 597ab70be5
commit f0f9457f9e
4 changed files with 164 additions and 4 deletions

View File

@ -0,0 +1,34 @@
---
import { Icon as AstroIcon } from 'astro-icon/components';
interface Props {
name: string;
size?:
| string
| {
width: string;
height: string;
};
}
const { name, size } = Astro.props;
let width = '1.25em';
let height = '1.25em';
if (size) {
if (typeof size === 'string') {
width = size;
height = size;
} else {
width = size.width;
height = size.height;
}
}
---
<AstroIcon
name={name}
class="inline align-text-bottom"
width={width}
height={height}
is:inline
/>

View File

@ -1,4 +1,5 @@
export { default as Collapse } from './Collapse.astro';
export { default as Icon } from './Icon.astro';
export { default as LinkCard } from './LinkCard.astro';
export { default as Repl } from './Repl.astro';
export { default as RepoCard } from './RepoCard.astro';

View File

@ -9,7 +9,7 @@ tags:
published: 2025-02-10T21:23:23+08:00
---
import { Collapse, LinkCard, Repl, RepoCard, TabItem, Tabs, Tooltip } from '@components/user';
import { Collapse, Icon, LinkCard, Repl, RepoCard, TabItem, Tabs, Tooltip } from '@components/user';
Components let you easily reuse a piece of UI or styling consistently. You can use them not just in `.astro` files, but also in `.mdx` files.
@ -145,6 +145,7 @@ Components let you easily reuse a piece of UI or styling consistently. You can u
## Inline Containers
### Tooltip
<Repl>
<div class="flex flex-col gap-2 w-fit mx-auto">
<Tooltip tip="I'm here!" position="top">
@ -190,6 +191,34 @@ Components let you easily reuse a piece of UI or styling consistently. You can u
</Fragment>
</Repl>
### Icon
<Repl>
<div class="flex flex-col gap-2 w-fit mx-auto">
<span>You can get this template in <Icon name="mdi:github" />[GitHub](https://github.com/HPCesia/astral-halo)</span>
<span>This is an open source project <Icon name="mdi:open-source-initiative" /></span>
<span>A huge icon is here: <Icon name="mdi:alert-octagon" size="5em" /></span>
</div>
<Fragment slot="desc">
<Tabs>
<TabItem label="mdx" active>
```jsx
<span>You can get this template in <Icon name="mdi:github" />[GitHub](https://github.com/HPCesia/astral-halo)</span>
<span>This is an open source project <Icon name="mdi:open-source-initiative" /></span>
<span>A huge icon is here: <Icon name="mdi:alert-octagon" size="5em" /></span>
```
</TabItem>
<TabItem label="md">
```md
:icon{name="mdi:github"}
:icon{name="mdi:open-source-initiative"}
:icon{name="mdi:alert-octagon" size="5em"}
```
</TabItem>
</Tabs>
</Fragment>
</Repl>
## Web Contents
### RepoCard

View File

@ -1,10 +1,52 @@
/**
* All components in this file should sync with the components in `src/components/user`
*/
import { icons as MaterialSymbols } from '@iconify-json/material-symbols';
import { getIconData, iconToHTML, iconToSVG } from '@iconify/utils';
import type { IconifyJSON } from '@iconify/types';
import { getIconData, iconToHTML, iconToSVG, stringToIcon } from '@iconify/utils';
import { h } from 'hastscript';
import type { Child } from 'hastscript';
import { readFile } from 'node:fs/promises';
import path from 'node:path';
async function detectInstalledCollections(root: string) {
try {
const packages = [];
const text = await readFile(path.resolve(root, './package.json'), {
encoding: 'utf8',
});
const { dependencies = {}, devDependencies = {} } = JSON.parse(text);
packages.push(...Object.keys(dependencies));
packages.push(...Object.keys(devDependencies));
const collections = packages
.filter((name) => name.startsWith('@iconify-json/'))
.map((name) => name.replace('@iconify-json/', ''));
return collections;
} catch (err) {
console.error(err);
}
return [];
}
const iconSets = await detectInstalledCollections(process.cwd());
async function loadCollection(name: string) {
if (!iconSets.find((it) => it === name)) return;
const icons: IconifyJSON = JSON.parse(
await readFile(
path.resolve(process.cwd(), `./node_modules/@iconify-json/${name}/icons.json`),
{
encoding: 'utf8',
}
)
);
return icons;
}
const collections: Record<string, IconifyJSON> = {};
iconSets.forEach(async (set) => {
const icons = await loadCollection(set);
if (icons) collections[set] = icons;
});
const Collapse = function (
props: {
@ -28,6 +70,54 @@ const Collapse = function (
return h('div', { class: wrapperClassName }, [inputNode, titleNode, contentNode]);
};
const Icon = function (props: {
name: string;
size?:
| string
| {
width: string;
height: string;
};
}) {
const { name, size } = props;
let width = '1.25em';
let height = '1.25em';
if (size) {
if (typeof size === 'string') {
width = size;
height = size;
} else {
width = size.width;
height = size.height;
}
}
const className = 'inline align-middle';
const { prefix, name: iconName } = stringToIcon(name, true)!;
const collection = collections[prefix];
if (!collection) {
console.error(`'Icon set not found: '${prefix}'`);
return h('span', `'Icon set not found: '${prefix}'`);
}
const iconData = getIconData(collection, iconName);
if (!iconData) {
console.error(`Icon "${iconName}" not found in icon set '${prefix}'`);
return h('span', `Icon "${iconName}" not found in icon set '${prefix}'`);
}
const { attributes, body } = iconToSVG(iconData);
attributes.width = width;
attributes.height = height;
const iconHtml = iconToHTML(body, attributes);
return h(
'span',
{ class: className },
{
type: 'raw',
value: iconHtml,
}
);
};
const LinkCard = function (props: { title: string; description: string; url: string }) {
const { title, description, url } = props;
const wrapperClassName = 'card border-base-content/25 my-4 overflow-hidden border';
@ -39,7 +129,12 @@ const LinkCard = function (props: { title: string; description: string; url: str
const descNode = h('div', { class: descClassName }, description);
const contentNode = h('div', null, [titleNode, descNode]);
const iconData = getIconData(MaterialSymbols, 'arrow-right-alt-rounded');
const collection = collections['material-symbols'];
if (!collection) {
console.error('LinkCard icon set found: material-symbols');
return h('a', { class: wrapperClassName, href: url, title }, 'Link card error');
}
const iconData = getIconData(collection, 'arrow-right-alt-rounded');
if (!iconData) {
console.error('LinkCard icon not found: material-symbols:arrow-right-alt-rounded');
return h('a', { class: wrapperClassName, href: url, title }, 'Link card error');
@ -75,6 +170,7 @@ const Tooltip = function (
export const rehypeComponentsList = {
collapse: Collapse,
icon: Icon,
linkcard: LinkCard,
tooltip: Tooltip,
};