Commit f0f9457

HPCesia <me@hpcesia.com>
2025-03-28 06:55:40
feat(user components): icon
1 parent 597ab70
Changed files (4)
src/components/user/Icon.astro
@@ -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
+/>
src/components/user/index.ts
@@ -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';
src/content/posts/components.mdx
@@ -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
src/plugins/rehype-components-list.ts
@@ -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,
 };