diff --git a/88x31.gif b/88x31.gif new file mode 100644 index 0000000..aa5ae64 Binary files /dev/null and b/88x31.gif differ diff --git a/README.md b/README.md index 0670a7b..bf6a6c2 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,12 @@ -![Astro](https://img.shields.io/badge/Built%20with-Astro-dee2e6?logo=astro&logoColor=white&style=for-the-badge) ![Eidolon](https://img.shields.io/badge/theme-eidolon-yellow?style=for-the-badge) +Eidolon 是一款轻量、简洁且美观的 Astro 主题。基于 MIT License 开源。 ---- - -这里是 **⌈路明笔记⌋** 的代码仓库。 - -[GitHub](https://github.com/virelyx258/Blog) | [云仓](https://src.luming.cool/riseforever2026/Blog) - -之所以在自己的服务器上托管一份,是因为 GitHub 直连很不稳定,导致我无法愉快地更新博客。自托管后,无需挂代理即可轻松更新。 +[GitHub](https://github.com/virelyx258/Astro-Theme-Eidolon) | [云仓](https://src.luming.cool/riseforever2026/Eidolon) 两个仓库互为镜像关系,同步更新。 -## 主题 +本主题的设计思想参照了 Typecho 主题 [Mirages](https://get233.com/archives/mirages-intro.html)。 -Eidolon,基于 [Astro](https://astro.build) 进行构建的主题,设计思想参照了 Typecho 主题 [Mirages](https://get233.com/archives/mirages-intro.html)。 - -我曾是 Mirages 主题用户,后为追求极致速度,便产生了移植主题到 Astro 的想法。 +我自己曾是 Mirages 主题用户,后为追求极致速度,便产生了移植主题到 Astro 的想法。 一款外观相似、免费的主题,如果开源,可能会对原付费主题的销售造成影响。但细想想,我们其实不在一个赛道。喜欢使用 Typecho 博客系统、追求简约的用户,一定还会选择 Mirages 的。本主题的开源,只是为 Astro 用户提供另一种选择。 @@ -28,10 +20,4 @@ Eidolon,基于 [Astro](https://astro.build) 进行构建的主题,设计思 - 支持多样的短代码; - 原生支持友链卡片。 -欢迎使用。如果能帮助到你,欢迎去 [GitHub 仓库](https://github.com/virelyx258/astro_theme_) 给我点个 Star。 - -## 版权协议 - -本站的文章基于 [CC-BY-NC-ND 4.0](https://creativecommons.org/licenses/by-nc-nd/4.0/deed.zh-hans) 协议公开。 - -转载请注明原文链接。[协议](https://creativecommons.org/licenses/by-nc-nd/4.0/deed.zh-hans)不允许对本站文章的再混合、转换、或者二次创作。若认为有必要二创,请邮件沟通。 +欢迎使用。如果能帮助到你,欢迎去 [GitHub 仓库](https://github.com/virelyx258/Astro-Theme-Eidolon) 给我点个 Star。 diff --git a/astro.config.mjs b/astro.config.mjs index 4409a04..9bb0b54 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -6,9 +6,23 @@ import remarkMath from 'remark-math'; import rehypeKatex from 'rehype-katex'; import shortcodes from './src/lib/shortcodes.ts'; import externalLinks from './src/lib/external-links.ts'; +import responsiveTables from './src/lib/responsive-tables.ts'; +import noticeShortcode from './src/lib/notice-shortcode.ts'; import { siteConfig } from './src/site.config.ts'; -import { readFileSync } from 'node:fs'; +import { copyFileSync, readFileSync } from 'node:fs'; import { resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const rootGif = resolve(process.cwd(), '88x31.gif'); + +const copyRootGif = { + name: 'copy-root-gif', + hooks: { + 'astro:build:done': ({ dir }) => { + copyFileSync(rootGif, resolve(fileURLToPath(dir), '88x31.gif')); + } + } +}; let githubData = {}; try { @@ -20,7 +34,7 @@ try { export default defineConfig({ site: siteConfig.site.url, - integrations: [sitemap()], + integrations: [sitemap(), copyRootGif], vite: { plugins: [tailwindcss()] }, @@ -34,7 +48,7 @@ export default defineConfig({ }, processor: unified({ remarkPlugins: [remarkMath, [shortcodes, { githubData, siteUrl: siteConfig.site.url }]], - rehypePlugins: [rehypeKatex, [externalLinks, { siteUrl: siteConfig.site.url }]] + rehypePlugins: [rehypeKatex, noticeShortcode, responsiveTables, [externalLinks, { siteUrl: siteConfig.site.url }]] }) } }); diff --git a/public/assets/meta/7f3c91a2e64b.txt b/public/assets/meta/7f3c91a2e64b.txt new file mode 100644 index 0000000..e5cf546 --- /dev/null +++ b/public/assets/meta/7f3c91a2e64b.txt @@ -0,0 +1,2 @@ +RiseForever +RSV diff --git a/public/media/cache/c4e29b718fd5.txt b/public/media/cache/c4e29b718fd5.txt new file mode 100644 index 0000000..e5cf546 --- /dev/null +++ b/public/media/cache/c4e29b718fd5.txt @@ -0,0 +1,2 @@ +RiseForever +RSV diff --git a/public/resources/data/f17a8c46d293.txt b/public/resources/data/f17a8c46d293.txt new file mode 100644 index 0000000..e5cf546 --- /dev/null +++ b/public/resources/data/f17a8c46d293.txt @@ -0,0 +1,2 @@ +RiseForever +RSV diff --git a/src/components/Footer.astro b/src/components/Footer.astro index bea881f..3b11218 100644 --- a/src/components/Footer.astro +++ b/src/components/Footer.astro @@ -1,17 +1,33 @@ --- import { siteConfig } from '@/site.config'; +import lightSticker from '@/images/Written-By-Human-white.svg'; +import darkSticker from '@/images/Written-By-Human-black.svg'; const containsChinese = (value: string) => /[\u3400-\u9fff]/.test(value); +const blogDomain = new URL(siteConfig.site.url).hostname; +const blogsClubBadge = `https://www.blogsclub.org/badge/${blogDomain}/theme`; --- + + diff --git a/src/components/Masthead.astro b/src/components/Masthead.astro index 257f8d4..b778440 100644 --- a/src/components/Masthead.astro +++ b/src/components/Masthead.astro @@ -122,8 +122,8 @@ const position = banner?.position ?? siteConfig.banner.position; .masthead-separator { font-family: Consolas, Menlo, Monaco, 'lucida_console', 'Liberation_Mono', 'Courier_New', 'andale_mono', monospaceX, monospace, sans-serif; } .masthead-date strong { font-family: var(--mirages-font-ui); font-weight: 400; } .masthead-category { font-family: var(--mirages-font-ui); } - .about-avatar { animation: avatar-rotate 8s ease-in-out infinite alternate; } - @keyframes avatar-rotate { from { transform: rotate(-2deg); } to { transform: rotate(2deg); } } + .about-avatar { transform: rotate(0deg); transition: transform 0.5s cubic-bezier(0.33, 1, 0.68, 1); } + .about-avatar:hover { transform: rotate(-32deg); } @media (min-width: 48rem) { section { height: var(--desktop-height); } } - @media (prefers-reduced-motion: reduce) { .about-avatar { animation: none; transform: none; } } + @media (prefers-reduced-motion: reduce) { .about-avatar { transition: none; transform: none; } .about-avatar:hover { transform: none; } } diff --git a/src/components/Navbar.astro b/src/components/Navbar.astro index e643034..905465e 100644 --- a/src/components/Navbar.astro +++ b/src/components/Navbar.astro @@ -168,7 +168,6 @@ const { overlay = false } = Astro.props; .navbar-over-banner { border-color: color-mix(in srgb, var(--color-border) 70%, transparent); background: color-mix(in srgb, var(--color-bg) 82%, transparent); - box-shadow: 0 2px 10px color-mix(in srgb, var(--color-text) 12%, transparent); } .navbar-over-banner [data-navbar-tools] > [data-toolbar-item] > a:hover, diff --git a/src/components/NavigationProgress.astro b/src/components/NavigationProgress.astro index d4a6f17..49c3356 100644 --- a/src/components/NavigationProgress.astro +++ b/src/components/NavigationProgress.astro @@ -114,7 +114,6 @@ min-height: 3px; max-height: 3px; background: var(--color-accent); - box-shadow: 0 3px 8px rgb(0 0 0 / 35%); transform: scaleX(0); transform-origin: left center; transition: transform 240ms linear; diff --git a/src/components/WritingHeatmap.astro b/src/components/WritingHeatmap.astro index 4fc7260..f583520 100644 --- a/src/components/WritingHeatmap.astro +++ b/src/components/WritingHeatmap.astro @@ -187,7 +187,7 @@ const totalWordsLabel = totalWords >= 10_000 const availableCellSize = (heatmapScroll.clientWidth - 3.25 * baseRootFontSize) / 53; if (scale <= 1.01 || baseCellSize === 0) { - baseCellSize = Math.min(12, Math.max(8, availableCellSize)); + baseCellSize = Math.max(8, availableCellSize); } heatmapGrid.style.setProperty('--heatmap-cell-size', `${baseCellSize * scale}px`); heatmapViewport?.classList.toggle('has-overflow', heatmapGrid.getBoundingClientRect().width > heatmapScroll.clientWidth + 1); diff --git a/src/content/pages/about.md b/src/content/pages/about.md new file mode 100644 index 0000000..5c400e7 --- /dev/null +++ b/src/content/pages/about.md @@ -0,0 +1,188 @@ +--- +cover: "https://images.unsplash.com/photo-1519681393784-d120267933ba?auto=format&fit=crop&w=1600&q=80" +title: "关于" +pubDate: "2026-07-22T01:28:00.000Z" +updatedDate: "2026-07-27T05:26:00.000Z" +categories: +tags: +slug: "about" +comments: false +--- + +## 关于⌈我⌋ + +我是 RiseForever,生于河南。 + +准高二了。喜欢开发软件、科技数码。是一名 Xiaomi Vela 快应用开发者。 + +性格偏多疑敏感,善于察觉身边人的微小情绪。平时喜欢独处,不喜欢与任何人一起逛街、看电影,因为我感觉受到束缚。 + +如果你想知道我是一个怎样的人,请随便翻翻[我的文章](/archives),里面写的比我说的更清楚。 + +### 联系 + +电子邮箱:hi[at]riseforever.cn(请将“[at]”替换为“@”) + +GitHub:[virelyx258](https://github.com/virelyx258) + +请不要试图添加我的即时通讯账户。无论有什么想探讨的,随时欢迎电邮交流。 + +### 使用的工具 + +[tools title = "设计"] +[Canva](https://www.canva.cn)+(https://image.luming.cool/i/2026/08/07/6a75ba231df9b.webp)/(图像设计) +[即时设计](https://js.design)+(https://image.luming.cool/i/2026/08/07/6a75c0b9bbf29.webp)/(UI 设计) +[Microsoft 365](https://m365.cloud.microsoft/)+(https://image.luming.cool/i/2026/08/07/6a75bd0318e8e.webp)/(文档处理) +[剪映](https://www.capcut.cn/)+(https://image.luming.cool/i/2026/08/07/6a75bf6a337d1.webp)/(视频剪辑) +[/tools] + +[tools title = "生产力"] +[Chrome](https://google.cn/chrome/)+(https://image.luming.cool/i/2026/08/07/6a75c029d170b.webp)/(网页浏览) +[Edge](https://explore.microsoft.com/zh-cn/edge?ep=2185&form=MA14LR&es=375&cs=2324297850)+(https://image.luming.cool/i/2026/08/07/6a75c06af3bd2.webp)/(网页浏览) +[Typora](https://typoraio.cn/)+(https://image.luming.cool/i/2026/08/07/6a75bfad4d232.webp)/(Markdown 编辑) + +[/tools] + +[tools title = "开发"] +[AIoT IDE](https://iot.mi.com/vela/quickapp/)+(https://image.luming.cool/i/2026/08/07/6a75bd500971b.webp)/(快应用编写 & 调试) +[VS Code](https://code.visualstudio.com/)+(https://image.luming.cool/i/2026/08/07/6a75c0ba31e58.webp)/(全能 IDE) +[记事本](#)+(https://image.luming.cool/i/2026/08/07/6a75c16e34a58.webp)/(你懂的,真的便捷) +[/tools] + +[tools title = "环境"] +[Windows 11](https://news.microsoft.com/windows11-general-availability/)+(https://image.luming.cool/i/2026/08/07/6a75c1d50d47d.webp)/(主力系统) +[Debian 13](https://www.debian.org/index.zh-cn.html)+(https://image.luming.cool/i/2026/08/07/6a75c2db6a6e9.webp)/(备用系统) +[/tools] + +## 关于⌈这个网站⌋ + +严格来说,这个网站的建立时间是在 2024 年 10 月。因为我在那时开始转型记录生活。 + +有些话我没法对任何人说,于是就在这里写下。 + +### 域名 + +起初用的是 riseforever.cn,因为初中时我很想要一个有美好寓意的域名,想了很多,结果只有这个域名可注册。于是我选了它,网名也跟着取成了“RiseForever”。 + +初三时,我觉得 riseforever.cn 太长了,且 .CN 当时仍不支持隐私保护,便想着换个域名。当时朋友买了一个“mnb.cool”,让我看到了 .cool 这个后缀。过了几天,我在学校灵机一动——用自己的名字拼写“luming”做前缀,使用“.cool”后缀,这样搭配在视觉上非常和谐。 + +那周周末,我回家一搜 luming.cool ——未注册,于是赶紧注册了它。 + +| 注册商 | 帝思普(腾讯云) | +| -------------: | :------------------------------------------------------------------------------------------------------------- | +| 首次购入价格 | ¥48 | +| 续费价格 | ¥65 | +| 域名年龄 | ![域名年龄徽章](https://yisi.yun/api/badge/domain-age/luming.cool?theme=blue&lang=zh&size=medium&mode=light) | + +[collapse title="riseforever.cn 信息"] + +| 注册商 | 云讯(腾讯云) | +| -------------: | :---------------------------------------------------------------------------------------------------------------- | +| 首次购入价格 | ¥33 | +| 续费价格 | ¥38 | +| 域名年龄 | ![域名年龄徽章](https://yisi.yun/api/badge/domain-age/riseforever.cn?theme=blue&lang=zh&size=medium&mode=light) | + +[/collapse] + +目前保持着 riseforever.cn 和 luming.cool 双域名持有,每年续费大约需要¥103。 + +### 服务器 + +于 2026 年 7 月 21 日搬迁至 007IDC 的香港 BGP 线路服务器,经实测晚高峰也能跑满带宽 100 Mbps,三网回程经过优化,表现优异,河南联通 ping 延迟仅 31ms。 + +首月 9 元,后续叠加优惠,¥17.5/月。 + +| 厂商 | [007IDC](https://www.007idc.cn/aff/ZFIKNXMX) | +| -----: | :--------------------------------------------- | +| CPU | 2H Platinum 8272CL | +| 内存 | 2G DDR4 | +| 硬盘 | 30GB SSD | +| 带宽 | 100 Mbps(峰值) | + +[collapse title="2026 年 1 月 - 6 月"] +7iNet 香港特惠秒杀机,¥9.9/月,炸夸响。后因其性能太过诡异而被更换。 + +| CPU | 2H E5-2698 v4 | +| -----: | :-------------------------------------- | +| 内存 | 2G | +| 硬盘 | 40GB SSD(写入 43MB/s,读取 156MB/s) | +| 带宽 | 10 Mbps(不限流量) | + +[/collapse] + +[collapse title="2026 年以前"] +腾讯云新加坡服务器,¥99/年,买了两年,参加活动又送了三个月。后因 IP 线路更换导致延迟过高而被更换。 + +| CPU | 2H Platinum 8255C | +| -----: | :--------------------------------------- | +| 内存 | 2G | +| 硬盘 | 50GB SSD(写入 338MB/s,读取 6.2GB/s) | +| 带宽 | 30 Mbps(月限流 1TB) | + +[/collapse] + +### CDN + +使用[毅融盾](https://rcdn.hydun.com/)进行图床加速。 + +毅融盾是融合了网宿与白山 CDN 资源的企业级 CDN,针对未备案域名进行特调,提供卓越的中国大陆访问速度。且没有三六九等的套餐配置,仅按流量计费,出售的是流量包。 + +毅融盾是我所在公司的产品,内部团队可免费使用,不限流量。如果你是外部用户,也可以通过[赞助计划](https://blog.hydun.com/9.html)免费获取 CDN 服务。 + +### 主题 + +使用自己制作的 Astro 主题 Eidolon。 + +它极大程度上复刻了 Typecho 主题 Mirages 的风格和功能,又在此基础上使用了更先进的 TailWind CSS,使得站点响应速度更加优秀。 + +~~因为 Mirages 是一款付费主题,如果贸然开源会影响到原主题的销售,且本站主题仍处于开发状态,所以本主题暂不开源。~~ + +仔细想想,发现我们其实不是同一个赛道。我做的是 Astro 主题,受众是 Astro 用户,不耽误 Typecho 主题的销售。况且,并不是所有 Typecho 用户都能无感切换到静态博客。 + +所以,本主题准备开源。 + +[collapse title="在此之前"] + +使用 [Mirages](https://get233.com/archives/mirages-intro.html),一款阅读体验优秀的单栏 Typecho 主题。 + +我是在[白熊阿丸的小屋](https://blog.bxaw.name)首次体验这款主题,发现它支持用户手动切换衬线/非衬线字体、字体大小,这能使不同口味的用户都获得最适合自己的阅读体验。并且,这款主题在我的 MacBook Pro 2013 上表现得非常流畅。 + +于是在 2026 年 4 月,我选择购入并使用它。 + +[/collapse] + +### 使用的第三方服务 + +使用 [WeAvatar](https://weavatar.com/) 作为评论区头像系统。 + +使用 [LUM ALBUM](https://image.luming.cool) 作为图床。(~~这也算第三方服务嘛~~) + +### 备案 + +本站永远都不会考虑备案。 + +## 关于⌈我的作品⌋ + +[tools title = "Xiaomi Vela 快应用"] + +[真智学](https://astrobox.online/open?source=resv2&id=cn.seedsoft.realzhixue&provider=OfficialV2)+(https://image.luming.cool/i/2026/08/07/6a75c6781929d.webp)/(免费 · 智学网客户端) + +[极物流](https://astrobox.online/open?source=resv2&id=cn.seedsoft.express&provider=OfficialV2)+(https://image.luming.cool/i/2026/08/07/6a75c6910d03b.webp)/(订阅制 · 快递查询工具) + +[Poster](https://astrobox.online/open?source=resv2&id=com.virelyx258.poster&provider=OfficialV2)+(https://image.luming.cool/i/2026/08/07/6a75c7021f80e.webp)/(免费 · 网络请求工具) + +[/tools] + +[tools title = "开源项目"] + +[RStatus](https://github.com/virelyx258/RStatus)+(https://image.luming.cool/i/2026/08/08/6a7729950adcd.webp)/(公开你的互联网在线状态) + +[火苗调试器](https://github.com/virelyx258/Flame-Debugger)+(https://image.luming.cool/i/2026/08/08/6a772a0218e74.webp)/(适用于网课软件的调试工具) + +[/tools] + +## 关于⌈版权协议⌋ + +如果没有特殊说明,本站文章都是原创,基于 [CC-BY-NC-ND 4.0](https://creativecommons.org/licenses/by-nc-nd/4.0/deed.zh-hans) 协议公开。 + +转载请注明原文链接。[协议](https://creativecommons.org/licenses/by-nc-nd/4.0/deed.zh-hans)原则上不允许对本站文章的再混合、转换、或者二次创作。如果有必要二创,请邮件沟通。 diff --git a/src/content/pages/links.md b/src/content/pages/links.md new file mode 100644 index 0000000..b179e49 --- /dev/null +++ b/src/content/pages/links.md @@ -0,0 +1,11 @@ +--- +title: "友人" +description: "本站收藏的一些精品中文博客" +pubDate: "2026-07-22T01:28:00.000Z" +cover: "https://images.unsplash.com/photo-1490730141103-6cac27aaab94?auto=format&fit=crop&w=1600&q=80" +slug: "links" +--- + +## 单向链接 + +[路明笔记](https://www.luming.cool)+(https://image.luming.cool/i/2026/05/10/6a001b4129893.png)/(一名高中生的技术和生活博客。) \ No newline at end of file diff --git a/src/content/pages/test-page.md b/src/content/pages/test-page.md deleted file mode 100644 index 1279e28..0000000 --- a/src/content/pages/test-page.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -title: "测试页面" -description: "主题初始化后创建的测试页面。" -pubDate: "2026-08-11T00:00:00.000Z" -categories: [] -tags: [] -slug: "test-page" -draft: false -comments: false -toc: true ---- - -## 测试页面 - -这是主题初始化后创建的测试页面。 - -你可以在这里检查独立页面的标题、正文、链接和列表排版: - -- [Markdown 排版测试文章](/posts/markdown-test/) -- [Astro 官方网站](https://astro.build/) diff --git a/src/content/posts/eidolon-basic-guide.md b/src/content/posts/eidolon-basic-guide.md new file mode 100644 index 0000000..ea44f22 --- /dev/null +++ b/src/content/posts/eidolon-basic-guide.md @@ -0,0 +1,254 @@ +--- +title: "Eidolon 主题基本使用教程" +pubDate: "2026-08-13T00:25:27.180Z" +categories: ['主题'] +tags: ['主题'] +draft: false +--- + +欢迎使用 Eidolon,本文将带着您快速入手,使用 Eidolon 轻松定制您的个人博客。 + +--- + +## 站点基础信息 + +欲修改站点基础信息(如站点标题、Banner 大标题、副标题、背景图等),您需要编辑 `/src/site.config.ts`。 + +其中,`site`块代表站点总体配置。示例格式如下: + +```ts +site: { + title: 'Eidolon', + description: '一款轻量、简洁且美观的 Astro 主题', + author: { + name: 'Eidolon', + avatar: 'https://weavatar.com/avatar/302380667bdaf4e1390800e62494d4af?s=512&r=G', + bio: '一款轻量、简洁且美观的 Astro 主题', + rotateAvatar: true + }, + locale: 'zh-CN', + url: 'https://www.luming.cool', + favicon: 'https://image.luming.cool/i/2026/05/10/6a001b4129893.png' + }, +``` + +每个字段的含义如下表: + +| 字段 | 含义 | 类型 | +| :------------------: | :----------------------------------------------: | :----: | +| title | 站点名称,也作为显示在浏览器标签页的标题 | 文本型 | +| description | 站点简介,也用于 SEO 描述 | 文本型 | +| author: name | 博主昵称 | 文本型 | +| author: avatar | 博主头像,填写图片链接 | 文本型 | +| author: bio | 博主的个性签名 | 文本型 | +| author: rotateAvatar | 是否开启关于页面的头像旋转动画 | 布尔型 | +| locale | 站点语言 | 文本型 | +| url | 站点的域名 | 文本型 | +| favicon | 站点图标,同时作为浏览器标签页图标,填写图片链接 | 文本型 | + +## 导航栏设置 + +`navigation`块代表导航项目。示例格式如下: + +```ts +navigation: [ + { label: '首页', href: '/', external: false }, + { label: '归档', href: '/archives/', external: false }, + { label: '友人', href: '/links/', external: false }, + { label: '关于', href: '/about/', external: false } + ], +``` + +每个字段的含义如下表: + +| 字段 | 含义 | 类型 | +| :------: | :----------------------------------------------: | :----: | +| label | 对外显示的导航名称 | 文本型 | +| href | 点击后跳转的链接,支持站内相对地址和站外绝对地址 | 文本型 | +| external | 是否为外部链接,它决定链接是否在新标签页打开 | 布尔型 | + +[hint type="info" title="提示"]无论是顶栏还是侧栏,都有一个“分类”选项,以展示博客里存在的所有分类。该项目无法删除。[/hint] + +## Pjax 无刷新加载 + +`pjax`块代表无刷新加载开关。示例代码: + +```ts + pjax: { + enabled: true + }, +``` + +本主题默认开启 Pjax 以获得更好的浏览体验。如果存在兼容性问题,可将 `enabled` 属性设置为 false 以关闭。 + +## 颜色设置 + +`appearance`块代表着主题的样式设置。包括深浅色模式和强调色。 + +示例代码如下: + +```ts + appearance: { + defaultTheme: 'auto', + accentColor: '#1abc9c' + }, +``` + +每个字段的含义如下表: + +| 字段 | 含义 | 类型 | +| :----------: | :----------------------------------------------------------: | :-----------------: | +| defaultTheme | 用户首次访问博客时,默认采用的主题类型。`auto`代表自动,`light`代表浅色主题,`sunset`代表日落主题,`dark`代表深色主题。 | 文本型 | +| accentColor | 强调色。会影响部分手机浏览器,决定系统状态栏的氛围颜色。 | HEX颜色值(文本型) | + +## Banner 设置 + +`Banner`块代表首页的 Banner 设置。示例代码如下: + +```ts + banner: { + enabled: true, + title: 'Eidolon', + subtitle: '一款轻量、简洁且美观的 Astro 主题', + image: 'https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=2000&q=85', + position: 'center center', + desktopHeightVh: 55, + mobileHeightVh: 40, + overlay: 0.25, + textTone: 'auto' + }, +``` + +各字段含义如下表: + +| 字段 | 含义 | 类型 | +| :-------------: | :----------------------------------------------------------: | :----: | +| enabled | 是否启用 Banner。如果设为 false,则下方所有项目都不起作用。 | 布尔型 | +| title | 大标题 | 文本型 | +| subtitle | 小标题 | 文本型 | +| image | 背景图片,填写图片链接 | 文本型 | +| position | 图片的对齐方式,填写格式为 “方向 方向”(方向 空格 方向) | 文本型 | +| desktopHeightVh | 桌面端 Banner 高度占比,譬如上例代表 Banner 高度占页面可视高度的 55% | 数值型 | +| mobileHeightVh | 移动端 Banner 高度占比 | 数值型 | +| overlay | 横幅上方黑色遮罩的透明度。此功能是为了防止 Banner 图过亮导致遮挡文字。0 为完全没有遮罩,1 为暗到纯黑。 | 小数型 | +| textTone | Banner 文字的色调。推荐保持 `auto`,这样 Banner 文字就能够根据图片的亮暗来调整文字的颜色。如果实在需要,可选 `light`、`Dark`。 | 文本型 | + +## 文章卡片默认封面图 + +`cards`块代表文章卡片设置。示例代码如下: + +```ts + cards: { + defaultCovers: ['https://images.unsplash.com/photo-1519681393784-d120267933ba?auto=format&fit=crop&w=1600&q=80'] + }, +``` + +其中,defaultCovers 是一个 JSON 数组,你可以往里面填写一个多个图片的地址。当有文章未设置封面,且文章里没有图片可被自动设为封面时,系统会从 defaultCovers 里随机抽选一个图片作为封面。 + +## 页脚 + +`footer`块代表页脚部分。示例代码如下: + +```ts + footer: { + copyright: `© 2023-${new Date().getFullYear()} 你的名字`, // 记得把"你的名字"改成你自己的名字。 + // 外链(如果有) + links: [ + { label: 'BlogsClub', href: 'https://blogs.club', external: true } + ] + }, +``` + +首先,你需要把 Copyright 中的“你的名字”改为你自己的名字。 + +其次,如果你有外链需求(比如加入了某博客组织),你可以在 links 这个 JSON 数组里添加外链。`external`代表是否在新标签页打开。 + +## 评论系统 + +Eidolon 支持使用 Twikoo 作为评论系统。你只需要自行部署 Twikoo 云函数并获取 envID,将其填写到 comments: envID 中即可。 + +如果你不需要评论系统,只需将 comments: provider 改为 `none`,然后删除 comments 里剩余的子项即可。 + +## 顶栏/侧栏按钮自定义 + +Eidolon 支持自定义按钮。该按钮项目会在顶栏和侧栏同步显示。 + +示例代码如下: + +```ts + toolbarItems: [ + { type: 'search', icon: 'search', name: '搜索文章' }, + { type: 'rss', icon: 'rss', name: 'RSS 订阅', href: '/rss.xml' }, + { type: 'settings', icon: 'settings', name: '阅读设置' } + ] +``` + +其中,`search`、`rss`、`settings`这三个 type 都是系统自带的按钮类型,分别对应搜索按钮、RSS 按钮、阅读设置按钮。其分别具有独立的功能,不建议删除。 + +如果你要新增带图标的外链按钮,需要经历以下这几步: + +### 选择图标 + +Eidolon 使用 [Lucide](https://lucide.dev/icons/) 图标库。先前往该站点选择你要使用的图标,并将其名称复制下来。 + +### 注册图标名称 + +编辑 `src/types/config.ts`,第 18 行,有一个 + +```ts +export type ToolbarIcon = 'search' | 'rss' | 'settings' | 'link' | 'tram-front'; +``` + +只需要将你要添加的图标名称,规范地加在 ToolbarIcon 后面即可。 + +譬如我新增了一个名为 check 的图标,那么我修改后的代码就是: + +```ts +export type ToolbarIcon = 'search' | 'rss' | 'settings' | 'link' | 'tram-front' | 'check'; +``` + +### 导入并建立映射 + +编辑 `/src/components/ToolbarIcon.astro`,第 2 行 + +```ts +import { Link, Rss, Search, TramFront, Type } from '@lucide/astro'; +``` + +在这个数组的最前方添加你的图标名称。同上例,如果我要添加名为 check 的图标,那么实际代码就是: + +```ts +import { Check, Link, Rss, Search, TramFront, Type } from '@lucide/astro'; +``` + +是的,在第 2 行添加图标名时,需要将图标名首字母大写。 + +接下来看到第 7 行: + +```ts +const icons = { link: Link, rss: Rss, search: Search, settings: Type, 'tram-front': TramFront } as const; +``` + +只需要在数组的末端加入`图标名(首字母小写): 图标名(首字母大写)`,即可。 + +同上例,加入 check 这个图标后,代码应为: + +```ts +const icons = { link: Link, rss: Rss, search: Search, settings: Type, 'tram-front': TramFront, check: Check } as const; +``` + +### 添加按钮 + +回到 site.config.ts,在 toolbarItems 里加入自定义链接按钮。 + +譬如我添加的是开往,图标名是`tram-front`,那么我的代码就是这样的: + +```json +{ type: 'link', icon: 'tram-front', name: '开往', href: 'https://www.travellings.cn/plain.html', external: true }, +``` + +其中,type: 'link' 定义了这是一个自定义链接;icon 制定了我想要的图标;name 就是鼠标悬浮之上时显示的气泡标题;href 即自定义外链跳转的的地址;external 即是否在新标签页中打开。 + +--- + +至此,Eidolon 的自定义教程结束。 diff --git a/src/content/posts/markdown-test.md b/src/content/posts/markdown-test.md deleted file mode 100644 index f112a6b..0000000 --- a/src/content/posts/markdown-test.md +++ /dev/null @@ -1,167 +0,0 @@ ---- -title: "Markdown 排版测试" -description: "用于检查主题中常见 Markdown 元素的完整排版效果。" -pubDate: "2026-08-11T00:00:00.000Z" -categories: ['测试分类'] -tags: ['测试标签'] -slug: "markdown-test" -draft: false -comments: false -math: true -mermaid: true -toc: true ---- - -# 一级标题 - -这是一篇用于检查主题排版的测试文章,涵盖常用 Markdown、GFM、数学公式和 Mermaid 图表。 - -## 二级标题 - -### 三级标题 - -#### 四级标题 - -##### 五级标题 - -###### 六级标题 - -## 文本样式 - -普通文本、**粗体文本**、*斜体文本*、***粗斜体文本***、~~删除线文本~~、`行内代码`。 - -这是第一行,行末使用两个空格强制换行。 -这是第二行。 - -特殊字符可以转义:\*星号\*、\# 井号、\[方括号\]。 - -## 链接与图片 - -这是[行内链接](https://astro.build/),这是使用引用的[参考链接][astro],也可以自动识别 。 - -![Astro 标志测试图](/favicon.svg "图片标题") - -[astro]: https://astro.build/ "Astro" - -## 引用 - -> 这是一段引用。 -> -> > 这是嵌套引用,其中包含 **粗体** 和 `代码`。 - -## 列表 - -- 无序列表第一项 -- 无序列表第二项 - - 二级列表 - - 三级列表 - -1. 有序列表第一项 -2. 有序列表第二项 - 1. 二级有序列表 - 2. 另一项 - -- [x] 已完成任务 -- [ ] 未完成任务 - -## 代码 - -行内代码示例:`const message = 'Hello, Astro';`。 - -```ts -interface Post { - title: string; - published: boolean; -} - -const post: Post = { - title: 'Markdown 排版测试', - published: true -}; - -console.log(post.title); -``` - -```diff -- const theme = 'old'; -+ const theme = 'Eidolon'; -``` - - 这是使用四个空格缩进的代码块。 - -## 表格 - -| 左对齐 | 居中 | 右对齐 | -| :--- | :---: | ---: | -| 文本 | **粗体** | 100 | -| `code` | [链接](https://astro.build/) | 200 | - -## 分隔线 - ---- - -分隔线前后的正文用于检查垂直间距。 - -## 脚注 - -这句话包含一个脚注。[^note] - -[^note]: 这是脚注内容,其中也可以包含 **Markdown**。 - -## 数学公式 - -行内公式:$E = mc^2$。 - -块级公式: - -$$ -\int_{-\infty}^{\infty} e^{-x^2}\,dx = \sqrt{\pi} -$$ - -## Mermaid 图表 - -```mermaid -flowchart LR - A[Markdown] --> B{渲染成功?} - B -->|是| C[完成] - B -->|否| D[检查样式] -``` - -## HTML 元素 - -
- 展开原生 HTML 内容 -

这是一段位于 details 元素中的内容。

-
- -Ctrl + K - -## 主题短代码 - -[hint type="info" title="信息提示"] -这是一个信息提示块,包含 **Markdown 文本**。 -[/hint] - -[collapse title="折叠内容"] -这里是折叠区域中的内容。 - -- 列表项目 -- 另一项目 -[/collapse] - -[tabs title="选项卡测试"] -[tab name="选项一" selected] -第一个选项卡的内容。 -[/tab] -[tab name="选项二"] -第二个选项卡的内容。 -[/tab] -[/tabs] - -[button href="https://astro.build/"]访问 Astro[/button] - -[tag type="success"]成功标签[/tag] - -## 段落收尾 - -最后一段用于观察文章底部留白、正文行高以及中英文混排效果。The quick brown fox jumps over the lazy dog. 0123456789。 diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 765c616..b18ef18 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -10,12 +10,11 @@ import NavigationProgress from '@/components/NavigationProgress.astro'; import { ClientRouter } from 'astro:transitions'; import '../styles/global.css'; -interface Props { title?: string; description?: string; image?: string; type?: 'website' | 'article'; noindex?: boolean; math?: boolean; navbarOverlay?: boolean; } -const { title, description, image, type, noindex, math, navbarOverlay = false } = Astro.props; -const hasMasthead = Astro.slots.has('masthead'); +interface Props { title?: string; description?: string; image?: string; type?: 'website' | 'article'; noindex?: boolean; math?: boolean; navbarOverlay?: boolean; hasMasthead?: boolean; } +const { title, description, image, type, noindex, math, navbarOverlay = false, hasMasthead = Astro.slots.has('masthead') } = Astro.props; const appearanceScript = `(function(){var r=document.documentElement,t='mirages-theme',s='mirages-font-scale',f='mirages-font-family',themes=['auto','light','sunset','dark'],fonts=['serif','sans'],stored=Number(localStorage.getItem(s)),scale=stored>=.8&&stored<=1.5?stored*100:stored>=80&&stored<=150?stored:100,theme=localStorage.getItem(t),font=localStorage.getItem(f);scale=Math.round(scale/5)*5;r.dataset.theme=themes.indexOf(theme)>-1?theme:'${siteConfig.appearance.defaultTheme}';r.dataset.font=fonts.indexOf(font)>-1?font:'sans';r.dataset.platform=/Win/.test(navigator.userAgentData?.platform||navigator.platform)?'windows':'other';r.style.fontSize=scale+'%'})();`; - const tabsScript = `document.addEventListener('click',function(event){var button=event.target.closest('[data-shortcode-tab]');if(!button)return;var group=button.closest('[data-shortcode-tabs]');var id=button.dataset.shortcodeTab;group.querySelectorAll('[data-shortcode-tab="'+id+'"]').forEach(function(tab){var active=tab===button;tab.setAttribute('aria-selected',String(active));tab.tabIndex=active?0:-1;var panel=group.querySelector('#'+tab.getAttribute('aria-controls'));if(panel)panel.hidden=!active;});});`; - const collapseScript = `document.querySelectorAll('.shortcode-collapse').forEach(function(details){details.dataset.collapseReady='';details.classList.toggle('is-expanded',details.open);var body=details.querySelector(':scope>.shortcode-body');if(body instanceof HTMLElement&&details.open)body.style.height='auto';});document.addEventListener('click',function(event){var summary=event.target.closest('.shortcode-collapse>summary');if(!summary)return;var details=summary.parentElement;if(!(details instanceof HTMLDetailsElement))return;event.preventDefault();var body=details.querySelector(':scope>.shortcode-body');if(!(body instanceof HTMLElement))return;var expand=!details.classList.contains('is-expanded');details.classList.toggle('is-expanded',expand);if(matchMedia('(prefers-reduced-motion: reduce)').matches){details.open=expand;body.style.height=expand?'auto':'';delete details.dataset.animating;return;}var height=body.getBoundingClientRect().height;details.open=true;body.style.height=height+'px';void body.offsetHeight;details.dataset.animating='true';body.style.height=(expand?body.scrollHeight:0)+'px';});document.addEventListener('transitionend',function(event){var body=event.target;if(!(body instanceof HTMLElement)||!body.classList.contains('shortcode-body')||event.propertyName!=='height')return;var details=body.parentElement;if(!(details instanceof HTMLDetailsElement))return;var expanded=details.classList.contains('is-expanded');details.open=expanded;body.style.height=expanded?'auto':'';delete details.dataset.animating;});`; + const tabsScript = `if(!window.__tabsInit){window.__tabsInit=true;document.addEventListener('click',function(event){var button=event.target.closest('[data-shortcode-tab]');if(!button)return;var group=button.closest('[data-shortcode-tabs]');var id=button.dataset.shortcodeTab;group.querySelectorAll('[data-shortcode-tab=\"'+id+'\"]').forEach(function(tab){var active=tab===button;tab.setAttribute('aria-selected',String(active));tab.tabIndex=active?0:-1;var panel=group.querySelector('#'+tab.getAttribute('aria-controls'));if(panel)panel.hidden=!active;});});}`; + const collapseScript = `function initCollapse(){document.querySelectorAll('.shortcode-collapse').forEach(function(details){details.dataset.collapseReady='';details.classList.toggle('is-expanded',details.open);var body=details.querySelector(':scope>.shortcode-body');if(body instanceof HTMLElement&&details.open)body.style.height='auto';});}initCollapse();document.addEventListener('astro:page-load',initCollapse);document.addEventListener('click',function(event){var summary=event.target.closest('.shortcode-collapse>summary');if(!summary)return;var details=summary.parentElement;if(!(details instanceof HTMLDetailsElement))return;event.preventDefault();var body=details.querySelector(':scope>.shortcode-body');if(!(body instanceof HTMLElement))return;var expand=!details.classList.contains('is-expanded');details.classList.toggle('is-expanded',expand);if(matchMedia('(prefers-reduced-motion: reduce)').matches){details.open=expand;body.style.height=expand?'auto':'';delete details.dataset.animating;return;}var height=body.getBoundingClientRect().height;details.open=true;body.style.height=height+'px';void body.offsetHeight;details.dataset.animating='true';body.style.height=(expand?body.scrollHeight:0)+'px';});document.addEventListener('transitionend',function(event){var body=event.target;if(!(body instanceof HTMLElement)||!body.classList.contains('shortcode-body')||event.propertyName!=='height')return;var details=body.parentElement;if(!(details instanceof HTMLDetailsElement))return;var expanded=details.classList.contains('is-expanded');details.open=expanded;body.style.height=expanded?'auto':'';delete details.dataset.animating;});`; --- @@ -51,10 +50,35 @@ const hasMasthead = Astro.slots.has('masthead'); diff --git a/src/lib/notice-shortcode.ts b/src/lib/notice-shortcode.ts new file mode 100644 index 0000000..c9b4181 --- /dev/null +++ b/src/lib/notice-shortcode.ts @@ -0,0 +1,52 @@ +const NOTICE_ICON = { + type: 'element', + tagName: 'span', + properties: { className: ['shortcode-notice'], ariaLabel: '警告' }, + children: [{ + type: 'element', + tagName: 'svg', + properties: { + ariaHidden: 'true', + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + strokeWidth: 2, + strokeLinecap: 'round', + strokeLinejoin: 'round', + }, + children: [ + { type: 'element', tagName: 'circle', properties: { cx: 12, cy: 12, r: 10 }, children: [] }, + { type: 'element', tagName: 'path', properties: { d: 'M12 8v4' }, children: [] }, + { type: 'element', tagName: 'path', properties: { d: 'M12 16h.01' }, children: [] }, + ], + }], +}; + +function cloneNoticeIcon() { + return structuredClone(NOTICE_ICON); +} + +export function applyNoticeShortcodes(tree: any) { + const transform = (node: any) => { + if (!Array.isArray(node.children) || node.tagName === 'code' || node.tagName === 'pre') return; + const output: any[] = []; + for (const child of node.children) { + if (child.type !== 'text' || !child.value.includes('[!/]')) { + transform(child); + output.push(child); + continue; + } + const parts = child.value.split('[!/]'); + parts.forEach((part: string, index: number) => { + if (part) output.push({ type: 'text', value: part }); + if (index < parts.length - 1) output.push({ type: 'text', value: '\u00a0' }, cloneNoticeIcon(), { type: 'text', value: '\u00a0' }); + }); + } + node.children = output; + }; + transform(tree); +} + +export default function noticeShortcode() { + return (tree: any) => applyNoticeShortcodes(tree); +} diff --git a/src/lib/responsive-tables.ts b/src/lib/responsive-tables.ts new file mode 100644 index 0000000..961d800 --- /dev/null +++ b/src/lib/responsive-tables.ts @@ -0,0 +1,25 @@ +import { SKIP, visit } from 'unist-util-visit'; + +export function applyResponsiveTables(tree: any) { + visit(tree, 'element', (node: any, index: number | undefined, parent: any) => { + if (node.tagName !== 'table' || index === undefined || !parent) return; + const classes = Array.isArray(node.properties?.className) ? node.properties.className : []; + if (classes.includes('responsive-table')) return; + + const section = node.children?.find((child: any) => child.tagName === 'thead' || child.tagName === 'tbody'); + const row = section?.children?.find((child: any) => child.tagName === 'tr'); + const columns = Math.max(1, row?.children?.filter((child: any) => child.tagName === 'th' || child.tagName === 'td').length ?? 1); + node.properties = { ...node.properties, className: [...classes, 'responsive-table'] }; + parent.children[index] = { + type: 'element', + tagName: 'div', + properties: { className: ['table-scroll'], style: `--table-columns:${columns}` }, + children: [node], + }; + return SKIP; + }); +} + +export default function responsiveTables() { + return (tree: any) => applyResponsiveTables(tree); +} diff --git a/src/lib/shortcodes.ts b/src/lib/shortcodes.ts index d7b798e..dcf60c1 100644 --- a/src/lib/shortcodes.ts +++ b/src/lib/shortcodes.ts @@ -5,10 +5,18 @@ import { toHast } from 'mdast-util-to-hast'; import { gfm } from 'micromark-extension-gfm'; import { visit } from 'unist-util-visit'; import { applyExternalLinks } from './external-links'; +import { applyResponsiveTables } from './responsive-tables'; +import { applyNoticeShortcodes } from './notice-shortcode'; const URL_SCHEME = /^(https?:|mailto:|tel:)/i; const TAG_TYPES = new Set(['primary', 'success', 'warning', 'danger', 'info', 'default']); const HINT_TYPES = new Set(['warn', 'warning', 'error', 'danger', 'success', 'info']); +const HINT_ICONS: Record = { + info: '', + warning: '', + danger: '', + success: '', +}; const BLOCK_NAMES = new Set(['hint', 'tip', 'collapse', 'tabs', 'tools']); const INLINE_NAMES = new Set(['button', 'btn', 'file', 'tag', 'label']); type GithubData = { stars?: string; forks?: string; description?: string; commitDate?: string; htmlUrl?: string; defaultBranch?: string }; @@ -61,12 +69,44 @@ function normalizeMarkdownHeadings(source: string): string { }).join('\n'); } -function markdown(source: string, preserveHtml = false): string { +function markdown(source: string, preserveHtml = true, lineNumbers = true): string { const normalized = normalizeMarkdownHeadings(source); const tree = fromMarkdown(normalized, { extensions: [gfm()], mdastExtensions: [gfmFromMarkdown()] }); transform(tree, normalized); if (!preserveHtml) visit(tree, 'html', (node: any) => { if (!node.data?.shortcode) node.value = ''; }); const hast = toHast(tree, { allowDangerousHtml: true }) as any; + if (lineNumbers) { + visit(hast, 'element', (node: any) => { + if (node.tagName !== 'pre') return; + const code = node.children?.find((child: any) => child.type === 'element' && child.tagName === 'code'); + if (!code) return; + const classes = Array.isArray(node.properties?.className) ? node.properties.className : []; + if (!classes.includes('astro-code')) classes.push('astro-code'); + if (!classes.includes('shortcode-code')) classes.push('shortcode-code'); + if (!classes.includes('no-pangu-spacing')) classes.push('no-pangu-spacing'); + node.properties = { ...node.properties, className: classes }; + if (code.children?.every((child: any) => child.type === 'text')) { + const value = code.children.map((child: any) => child.value).join(''); + const text = value.endsWith('\n') ? value.slice(0, -1) : value; + const lines = text.split('\n'); + code.children = lines.map((line: string) => ({ + type: 'element', + tagName: 'span', + properties: { className: ['line'] }, + children: [ + { + type: 'element', + tagName: 'span', + properties: { className: ['line-content'] }, + children: line ? [{ type: 'text', value: line }] : [{ type: 'text', value: '\n' }], + } + ], + })); + } + }); + } + applyNoticeShortcodes(hast); + applyResponsiveTables(hast); if (siteUrl) applyExternalLinks(hast, siteUrl); return toHtml(hast, { allowDangerousHtml: true }); } @@ -77,33 +117,41 @@ export function renderMarkdown(source: string): string { function renderBlock(name: string, args: Record, body = ''): string { const label = args.title ?? args.name ?? args.text ?? body.trim().split('\n')[0] ?? ''; - if (name === 'button' || name === 'btn' || name === 'file') { + if (name === 'button' || name === 'btn') { const url = safeUrl(args.href ?? args.url ?? args.link); if (!url) return `${escape(label || '链接不可用')}`; return `${inline(label || url)}`; } + if (name === 'file') { + const url = safeUrl(args.href ?? args.url ?? args.link); + if (!url) return `${escape(label || '文件链接不可用')}`; + const icon = ''; + return `${icon}${inline(label || url)}`; + } if (name === 'tag' || name === 'label') { const type = TAG_TYPES.has(args.type ?? '') ? args.type : 'default'; const outline = args.outline !== undefined ? ' shortcode-tag-outline' : ''; return `${inline(label)}`; } if (name === 'hint' || name === 'tip') { - const type = HINT_TYPES.has(args.type ?? '') ? args.type : 'info'; - const title = args.title ? `${inline(args.title)}` : ''; - return ``; + const requestedType = args.type ?? [...HINT_TYPES].find((candidate) => args[candidate] !== undefined); + const type = requestedType === 'warn' ? 'warning' : requestedType === 'error' ? 'danger' : HINT_TYPES.has(requestedType ?? '') ? requestedType : 'info'; + const icon = ``; + const heading = args.title ? `
${icon}

${inline(args.title)}

` : icon; + return ``; } if (name === 'collapse') { const open = args.open !== undefined || args.expanded !== undefined ? ' open' : ''; - return `
${inline(label || '展开内容')}
${markdown(body, true)}
`; + return `
${inline(label || '展开内容')}
${markdown(body, true, true)}
`; } if (name === 'tabs') { const parsed = parseBlocks(body); const tabs = parsed.filter((item) => item.name === 'tab'); - if (!tabs.length) return `
${inline(label || '展开内容')}
${markdown(body)}
`; + if (!tabs.length) return `
${inline(label || '展开内容')}
${markdown(body, true, true)}
`; const id = `shortcode-tabs-${tabs.length}-${Math.random().toString(36).slice(2, 8)}`; const selected = Math.max(0, tabs.findIndex((tab) => tab.args.selected !== undefined)); const buttons = tabs.map((tab, index) => ``).join(''); - const panels = tabs.map((tab, index) => `
${markdown(tab.body)}
`).join(''); + const panels = tabs.map((tab, index) => `
${markdown(tab.body, true, true)}
`).join(''); return `
${buttons}
${panels}
`; } if (name === 'tools') { @@ -151,8 +199,28 @@ function renderBlock(name: string, args: Record, body = ''): str function parseBlocks(source: string): Array<{ name: string; args: Record; body: string }> { const result: Array<{ name: string; args: Record; body: string }> = []; - const pattern = /^\[tab([^\]]*)\]\s*\n?([\s\S]*?)^\[\/tab\]\s*$/gm; - for (const match of source.matchAll(pattern)) result.push({ name: 'tab', args: attrs(match[1]), body: match[2] }); + let fence: string | null = null; + let opening: { args: Record; bodyStart: number } | null = null; + for (const line of source.matchAll(/^[^\n]*(?:\n|$)/gm)) { + const text = line[0].trim(); + const start = line.index!; + const fenceMatch = text.match(/^(`{3,}|~{3,})/); + if (fenceMatch) { + if (!fence) fence = fenceMatch[1][0]; + else if (fenceMatch[1][0] === fence) fence = null; + continue; + } + if (fence) continue; + + const closing = text.match(/^\[\/tab\]\s*$/i); + if (closing && opening) { + result.push({ name: 'tab', args: opening.args, body: source.slice(opening.bodyStart, start) }); + opening = null; + continue; + } + const openingMatch = text.match(/^\[tab([^\]]*)\]\s*$/i); + if (openingMatch && !opening) opening = { args: attrs(openingMatch[1]), bodyStart: start + line[0].length }; + } return result; } @@ -220,15 +288,9 @@ function findBlocks(source: string): Array<{ name: string; args: Record { - replacedNotice = true; - return `${inline(body)}`; - }); - const shortcode = /\[(button|btn|file|tag|label)([^\]]*)\]([\s\S]*?)\[\/\1\]/gi; + const shortcode = /\[(button|btn|file|tag|label|hint|tip)([^\]]*)\]([\s\S]*?)\[\/\1\]/gi; const replacedShortcode = shortcode.test(source); - if (!replacedNotice && !replacedShortcode) return null; + if (!replacedShortcode) return null; return source.replace(shortcode, (_match, name, rawArgs, body) => renderBlock(name.toLowerCase(), attrs(rawArgs), body)); } @@ -268,6 +330,7 @@ function transform(tree: any, raw = '') { const output: any[] = []; for (const node of root.children) { if (node.type !== 'paragraph' && node.type !== 'html') { output.push(node); continue; } + if (node.type === 'html' && node.data?.shortcode) { output.push(node); continue; } const source = node.type === 'html' ? node.value : node.position ? raw.slice(node.position.start.offset, node.position.end.offset) : ''; const replaced = replaceInline(source.trim()); output.push(replaced ? { type: 'html', value: replaced, data: { shortcode: true } } : node); diff --git a/src/pages/archives/index.astro b/src/pages/archives/index.astro index d50160e..2926ba6 100644 --- a/src/pages/archives/index.astro +++ b/src/pages/archives/index.astro @@ -12,7 +12,7 @@ const tags = deriveTaxonomy(posts, 'tags') const visibleTags = tagLimit === 0 ? tags : tags.slice(0, Math.max(0, tagLimit)); --- - +
diff --git a/src/pages/index.astro b/src/pages/index.astro index c3355a7..35532f6 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -8,7 +8,7 @@ const posts = await getPublicPosts(); const total = Math.max(1, Math.ceil(posts.length / POSTS_PER_PAGE)); --- - - + + {siteConfig.banner.enabled && } diff --git a/src/pages/links/index.astro b/src/pages/links/index.astro new file mode 100644 index 0000000..b14b5c9 --- /dev/null +++ b/src/pages/links/index.astro @@ -0,0 +1,90 @@ +--- +import { getCollection } from 'astro:content'; +import BaseLayout from '@/layouts/BaseLayout.astro'; +import Masthead from '@/components/Masthead.astro'; +import { renderMarkdown } from '@/lib/shortcodes'; + +const page = (await getCollection('pages')).find((entry) => entry.data.slug === 'links'); +if (!page) throw new Error('Missing links page content.'); +type Link = { name: string; url: string; avatar: string; description: string }; +type Section = { name: string; links: Link[]; contentLines: string[] }; +const sections: Section[] = [{ name: '其他', links: [], contentLines: [] }]; +let section = sections[0]; +let shortcodeDepth = 0; +const escapeHtml = (value: string) => value.replace(/[&<>"']/g, (character) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[character]!); +const renderNestedCard = (link: Link) => ``; +for (const line of (page.body ?? '').split(/\r?\n/)) { + if (/^\s*\[(?:collapse|hint|tip|tabs)\b[^\]]*\]\s*$/i.test(line)) { + shortcodeDepth += 1; + section.contentLines.push(line); + continue; + } + if (/^\s*\[\/(?:collapse|hint|tip|tabs)\]\s*$/i.test(line)) { + shortcodeDepth = Math.max(0, shortcodeDepth - 1); + section.contentLines.push(line); + continue; + } + const heading = line.match(/^##\s+(.+?)\s*#*$/); + if (heading && shortcodeDepth === 0) { + section = { name: heading[1].trim(), links: [], contentLines: [] }; + sections.push(section); + continue; + } + const link = line.match(/^\s*\[([^\]]+)\]\(([^)]+)\)\+\(([^)]+)\)\/\((.*)\)\s*$/) + ?? line.match(/^\s*\[([^\]]+)\]\(([^)]+)\)\+\(([^)]+)\)\/(.*?)\s*\)?$/); + if (link && shortcodeDepth === 0) { + section.links.push({ name: link[1], url: link[2], avatar: link[3], description: link[4] }); + continue; + } + section.contentLines.push(shortcodeDepth > 0 && link ? renderNestedCard({ name: link[1], url: link[2], avatar: link[3], description: link[4] }) : line); +} +if (!sections[0].links.length && !sections[0].contentLines.some((line) => line.trim())) sections.shift(); +--- + + + + + + + diff --git a/src/pages/page/[page].astro b/src/pages/page/[page].astro index 557345d..6054fc7 100644 --- a/src/pages/page/[page].astro +++ b/src/pages/page/[page].astro @@ -14,7 +14,7 @@ export async function getStaticPaths() { } const { posts, page, total } = Astro.props; --- - - + + {siteConfig.banner.enabled && } diff --git a/src/site.config.ts b/src/site.config.ts index 4c85d25..e50dd96 100644 --- a/src/site.config.ts +++ b/src/site.config.ts @@ -2,12 +2,12 @@ import type { SiteConfig } from './types/config'; export const siteConfig: SiteConfig = { site: { - title: '路明笔记', - description: '一名高中生的技术与生活博客', + title: 'Eidolon', + description: '一款轻量、简洁且美观的 Astro 主题', author: { - name: 'RiseForever', - avatar: 'https://weavatar.com/avatar/302380667bdaf4e1390800e62494d4af?s=512&r=G', - bio: '不慌张,不绝望,不狂妄,不投降。', + name: 'Eidolon', + avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&w=400&h=400&q=85', + bio: '一款轻量、简洁且美观的 Astro 主题', rotateAvatar: true }, locale: 'zh-CN', @@ -17,7 +17,8 @@ export const siteConfig: SiteConfig = { navigation: [ { label: '首页', href: '/', external: false }, { label: '归档', href: '/archives/', external: false }, - { label: '测试页面', href: '/test-page/', external: false } + { label: '友人', href: '/links/', external: false }, + { label: '关于', href: '/about/', external: false } ], pjax: { enabled: true @@ -28,9 +29,9 @@ export const siteConfig: SiteConfig = { }, banner: { enabled: true, - title: '路明笔记', - subtitle: '一名高中生的技术与生活博客', - image: 'https://image.luming.cool/i/2026/07/26/6a6628967fe03.webp', + title: 'Eidolon', + subtitle: '一款轻量、简洁且美观的 Astro 主题', + image: 'https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=2000&q=85', position: 'center center', desktopHeightVh: 55, mobileHeightVh: 40, @@ -38,34 +39,31 @@ export const siteConfig: SiteConfig = { textTone: 'auto' }, cards: { - defaultCovers: ['https://image.luming.cool/i/2026/08/03/6a6f79cc091ac.webp'] + defaultCovers: ['https://images.unsplash.com/photo-1519681393784-d120267933ba?auto=format&fit=crop&w=1600&q=80'] }, archives: { tagLimit: 30 }, footer: { - copyright: `© 2023-${new Date().getFullYear()} RiseForever`, + copyright: `© 2023-${new Date().getFullYear()} 你的名字`, // 记得把"你的名字"改成你自己的名字。 + // 外链(如果有) links: [ - { label: 'BlogsClub', href: 'https://blogs.quest/luming', external: true }, - { label: '笔墨迹', href: 'https://blogscn.fun/blogs/01k7zk4mhndsr6atqd1wm11f7b', external: true }, - { label: '十年之约', href: 'https://www.foreverblog.cn/blog/6506.html', external: true }, - { label: '博友圈', href: 'https://www.boyouquan.com/blogs/luming.cool', external: true }, - { label: '好站网', href: 'https://haozhan.wang/site_detail.php?id=176', external: true }, - { label: '集博栈', href: 'https://www.heyblog.net/site/019eff04-b025-76c9-b8f2-388deb7195cd', external: true }, - { label: '博客大联盟', href: 'https://bo.ke/luming.cool/', external: true } + { label: 'BlogsClub', href: 'https://blogs.club', external: true } ] }, + // 评论系统采用 Twikoo,请自行部署云函数后,并在下方填写你的云函数 URL comments: { provider: 'twikoo', - envId: 'https://twokii.luming.cool/', + envId: 'https://link.to.your.twikoo.service/', region: 'cn', lang: 'zh-CN' }, + // 搜索服务默认使用 PageFind,无需理会 search: { provider: 'pagefind', placeholder: '搜索文章' }, + // 这是顶栏右上角及侧栏底部的工具按钮,可自定义。下方示例已涵盖了站内功能、RSS 复制、外链跳转等场景。 toolbarItems: [ { type: 'search', icon: 'search', name: '搜索文章' }, { type: 'rss', icon: 'rss', name: 'RSS 订阅', href: '/rss.xml' }, - { type: 'link', icon: 'tram-front', name: '开往', href: 'https://www.travellings.cn/plain.html', external: true }, { type: 'settings', icon: 'settings', name: '阅读设置' } ] }; diff --git a/src/styles/content.css b/src/styles/content.css index 7ba1a59..cab4417 100644 --- a/src/styles/content.css +++ b/src/styles/content.css @@ -131,54 +131,88 @@ font-family: var(--mirages-font-code); font-size: .875rem; line-height: 1.45; + white-space: pre; +} + +.prose .astro-code { + --code-line-digits: 1; + --code-gutter-background: color-mix(in srgb, var(--color-code) 65%, var(--color-border)); + padding: 0; + overflow: hidden; + position: relative; + background: var(--color-code); } .prose .astro-code code { - --code-line-digits: 1; display: block; + position: relative; + overflow-x: auto; + overflow-y: hidden; + overscroll-behavior-x: none; + width: 100%; + min-width: 0; padding-block: 1rem; counter-reset: code-line; - font-size: 0; + font-size: .875rem; + line-height: 1.45; + scrollbar-color: color-mix(in srgb, var(--color-muted) 60%, transparent) transparent; + scrollbar-width: thin; + white-space: normal; } -.prose .astro-code code:has(.line:nth-child(10)) { --code-line-digits: 2; } -.prose .astro-code code:has(.line:nth-child(100)) { --code-line-digits: 3; } -.prose .astro-code code:has(.line:nth-child(1000)) { --code-line-digits: 4; } +.prose .astro-code code::-webkit-scrollbar { height: 8px; } +.prose .astro-code code::-webkit-scrollbar-thumb { border-radius: 4px; background: color-mix(in srgb, var(--color-muted) 60%, transparent); } +.prose .astro-code code::-webkit-scrollbar-thumb:hover { background: color-mix(in srgb, var(--color-muted) 78%, transparent); } + +.prose .astro-code:has(.line:nth-child(10)) { --code-line-digits: 2; } +.prose .astro-code:has(.line:nth-child(100)) { --code-line-digits: 3; } +.prose .astro-code:has(.line:nth-child(1000)) { --code-line-digits: 4; } + +.prose .astro-code .code-line-numbers { + position: absolute; + left: 0; + top: 0; + bottom: 0; + z-index: 20; + display: block; + width: calc(var(--code-line-digits) * 1ch + 2rem); + padding-block: 1rem; + font-family: var(--mirages-font-code); + font-size: .875rem; + line-height: 1.45; + background: var(--code-gutter-background); + border-right: 1px solid color-mix(in srgb, var(--color-border) 70%, transparent); + box-sizing: border-box; + color: var(--color-muted); + font-variant-numeric: tabular-nums; + text-align: right; + user-select: none; + pointer-events: none; +} + +.prose .astro-code .code-line-numbers > span { + display: block; + height: 1.45em; + padding-inline: 1rem; + box-sizing: border-box; + color: inherit; +} .prose .astro-code .line { - display: flex; + display: block; + width: max-content; + min-width: 100%; min-height: 1.45em; + padding-left: calc(var(--code-line-digits) * 1ch + 3rem); + box-sizing: border-box; counter-increment: code-line; font-size: .875rem; line-height: 1.45; + white-space: pre; } .prose .astro-code .line::before { - display: inline-block; - width: calc(var(--code-line-digits) * 1ch + 2rem); - flex: 0 0 calc(var(--code-line-digits) * 1ch + 2rem); - margin-right: 1rem; - padding-inline: 1rem; - background: var(--color-code-gutter); - box-sizing: border-box; - color: var(--color-muted); - content: counter(code-line); - font-variant-numeric: tabular-nums; - opacity: 0.72; - text-align: right; - user-select: none; -} - -.prose .astro-code .line:first-child::before { - box-shadow: 0 -1rem var(--color-code-gutter); -} - -.prose .astro-code .line:last-child::before { - box-shadow: 0 1rem var(--color-code-gutter); -} - -.prose .astro-code .line:only-child::before { - box-shadow: 0 -1rem var(--color-code-gutter), 0 1rem var(--color-code-gutter); + content: none; } .prose .astro-code span { @@ -186,6 +220,8 @@ } :root[data-theme='dark'] .prose .astro-code { + --code-gutter-background: var(--color-code-gutter); + --shortcode-code-gutter: var(--color-code-gutter); background-color: var(--color-code); color: var(--shiki-dark); } @@ -196,6 +232,8 @@ @media (prefers-color-scheme: dark) { :root[data-theme='auto'] .prose .astro-code { + --code-gutter-background: var(--color-code-gutter); + --shortcode-code-gutter: var(--color-code-gutter); background-color: var(--color-code); color: var(--shiki-dark); } @@ -205,9 +243,47 @@ } } -.prose .astro-code { +.shortcode-code { + --shortcode-code-gutter: color-mix(in srgb, var(--color-code) 65%, var(--color-border)); padding: 0; - background: var(--color-code); + overflow: hidden; + position: relative; +} + +.shortcode-code code { + display: block; + position: relative; + overflow-x: auto; + overflow-y: hidden; + overscroll-behavior-x: none; + width: 100%; + min-width: 0; + padding-block: 1rem; + font-size: .875rem; + line-height: 1.45; + scrollbar-color: color-mix(in srgb, var(--color-muted) 60%, transparent) transparent; + scrollbar-width: thin; + white-space: normal; +} + +.shortcode-code code::-webkit-scrollbar { height: 8px; } +.shortcode-code code::-webkit-scrollbar-thumb { border-radius: 4px; background: color-mix(in srgb, var(--color-muted) 60%, transparent); } +.shortcode-code code::-webkit-scrollbar-thumb:hover { background: color-mix(in srgb, var(--color-muted) 78%, transparent); } + +.shortcode-code .line { + display: block; + width: max-content; + min-width: 100%; + min-height: 1.45em; + padding-left: 4rem; + box-sizing: border-box; + font-size: .875rem; + line-height: 1.45; + white-space: pre; +} + +.shortcode-code .shortcode-line-number { + display: none; } .prose .katex-display { max-width: 100%; overflow-x: auto; overflow-y: hidden; padding-block: .35em; } @@ -215,9 +291,25 @@ .prose .mermaid { max-width: 100%; overflow-x: auto; overflow-y: hidden; padding: 1rem 0; } .prose .mermaid svg { max-width: none; height: auto; } +.prose .table-scroll { + width: 100%; + max-width: 100%; + margin: 1.25em 0; + overflow-x: auto; + overscroll-behavior-inline: contain; + scrollbar-color: color-mix(in srgb, var(--color-muted) 60%, transparent) transparent; + scrollbar-width: thin; +} + +.prose .table-scroll::-webkit-scrollbar { height: 8px; } +.prose .table-scroll::-webkit-scrollbar-thumb { border-radius: 4px; background: color-mix(in srgb, var(--color-muted) 60%, transparent); } + .prose table { width: 100%; + max-width: 100%; + margin: 0; border-collapse: collapse; + table-layout: auto; } .prose thead { @@ -239,6 +331,15 @@ .prose :where(th, td) { padding: 0.55em 0.8em; border: 1px solid var(--color-border); + overflow-wrap: anywhere; + word-break: break-word; + white-space: normal; +} + +@media (max-width: 48rem) { + .prose .responsive-table { + width: max(100%, calc(var(--table-columns, 1) * 8rem)); + } } .prose :where(th, td)[align='left'] { text-align: left; } diff --git a/src/styles/shortcodes.css b/src/styles/shortcodes.css index 4b84235..896edf4 100644 --- a/src/styles/shortcodes.css +++ b/src/styles/shortcodes.css @@ -6,7 +6,7 @@ border-radius: var(--radius-sm); background: var(--color-accent); color: var(--color-bg); - font-family: var(--mirages-font-ui); + font-family: var(--reading-font-family); font-size: 0.9em; } .prose .shortcode-button:hover { @@ -14,9 +14,54 @@ color: var(--color-bg); } .prose .shortcode-file { + display: inline-flex; + max-width: 100%; + min-height: 3.5rem; + align-items: center; + gap: 1rem; + margin: 0.25rem 0 0.25rem 0.5rem; + overflow: hidden; + padding: 0 1.5rem; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); background: transparent; + color: var(--color-text); + font-family: var(--reading-font-family); + text-decoration: none; + white-space: nowrap; + transition: border-color 0.6s ease; +} +.prose .shortcode-file:first-child { + margin-left: 0; +} +.prose .content-file-icon { + width: 1.5rem; + height: 1.5rem; + flex: 0 0 1.5rem; + color: var(--color-text); + transition: color 0.3s ease; +} +.prose .content-file-filename { + min-width: 0; + overflow: hidden; + color: var(--color-text); + font-weight: 700; + text-overflow: ellipsis; + transition: color 0.3s ease; +} +.prose .shortcode-file:hover { + border-color: var(--color-accent); color: var(--color-accent); } +.prose .shortcode-file:hover .content-file-icon, +.prose .shortcode-file:hover .content-file-filename { + color: var(--color-accent); +} +@media (prefers-reduced-motion: reduce) { + .prose .shortcode-file, + .prose .content-file-icon, + .prose .content-file-filename { transition: none; } +} .shortcode-tag { display: inline-block; margin: 0.15em 0.3em 0.15em 0; @@ -24,7 +69,7 @@ border-radius: 999px; background: var(--color-accent); color: var(--color-bg); - font: 0.82em var(--mirages-font-ui); + font: 0.82em var(--reading-font-family); } .shortcode-tag-outline { background: transparent; @@ -44,29 +89,60 @@ background: #35769b; } .shortcode-hint { + --hint-color: var(--color-accent); + position: relative; margin: 1.2em 0; - padding: 0.7em 1em; - border-left: 4px solid var(--color-accent); - background: var(--color-surface); + padding: 1.5rem 1.25rem 1.5rem 3.5rem; + border: 1px solid color-mix(in srgb, var(--hint-color) 28%, var(--color-border)); + border-left: 0.25rem solid var(--hint-color); + border-radius: var(--radius-sm); + background: color-mix(in srgb, var(--hint-color) 7%, var(--color-surface)); } -.shortcode-hint strong { +.content-hint-icon { + position: absolute; + top: 50%; + left: 1rem; + width: 1.5rem; + height: 1.5rem; + transform: translateY(-50%); + color: var(--hint-color); +} +.content-hint-heading { + display: grid; + min-height: 1.5rem; + grid-template-columns: 1.5rem minmax(0, 1fr); + align-items: center; + gap: 1rem; + margin: 0 0 0.5rem -2.5rem; +} +.content-hint-heading .content-hint-icon { + position: static; + transform: none; +} +.prose .content-hint-title { display: block; - margin-bottom: 0.2em; - font-family: var(--mirages-font-ui); + margin: 0; + color: var(--hint-color); + font-size: 1.2em; + line-height: 1.5rem; + font-weight: 600; } -.shortcode-hint p { +.shortcode-hint p:not(.content-hint-title) { margin: 0.25em 0; } .shortcode-hint-warning, -.shortcode-hint-warn { - border-color: #a36b16; +.shortcode-hint-warn, +.hint-warning { + --hint-color: #d99b00; } .shortcode-hint-error, -.shortcode-hint-danger { - border-color: #ad3d48; +.shortcode-hint-danger, +.hint-danger { + --hint-color: #d34b50; } -.shortcode-hint-success { - border-color: #287d5b; +.shortcode-hint-success, +.hint-success { + --hint-color: #209b61; } .shortcode-collapse { margin: 1.2em 0; @@ -132,10 +208,6 @@ @media (prefers-reduced-motion: reduce) { .shortcode-body { transition: none; } } -.shortcode-body-inner table { - width: 100%; - border-collapse: collapse; -} .shortcode-body .shortcode-collapse { margin: 1rem 0 0.25rem; } @@ -156,6 +228,7 @@ } .shortcode-tabs { margin: 1.2em 0; + overflow: hidden; border: 1px solid var(--color-border); border-radius: var(--radius-sm); background: var(--color-surface); @@ -170,15 +243,19 @@ } .shortcode-tab-list button { cursor: pointer; - padding: 0.4rem 0.7rem; + padding: 0.4rem 0.75rem; border: 0; + border-radius: var(--radius-xs); background: transparent; color: var(--color-muted); + font-family: var(--reading-font-family); + transition: background-color 150ms ease, color 150ms ease; } .shortcode-tab-list button[aria-selected="true"] { - background: var(--color-bg); + background: color-mix(in srgb, var(--color-accent) 12%, var(--color-surface)); color: var(--color-accent); font-weight: 600; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); } .shortcode-tabs [role="tabpanel"] { padding: 0.8rem 1rem; @@ -266,7 +343,7 @@ line-height: 1.25; } .shortcode-tool-description { - color: var(--color-muted); + color: color-mix(in srgb, var(--color-text) 82%, var(--color-muted)); font-size: 0.78rem; line-height: 1.3; } @@ -308,7 +385,7 @@ min-width: 0; margin: 0; overflow-wrap: anywhere; - font: 600 1.08rem/1.35 var(--mirages-font-ui); + font: 600 1.08rem/1.35 var(--reading-font-family); } .shortcode-github-icon { display: inline-block; @@ -336,7 +413,7 @@ justify-content: flex-end; gap: 0.8rem; color: var(--color-muted); - font: 0.75rem var(--mirages-font-ui); + font: 0.75rem var(--reading-font-family); text-align: right; } .shortcode-github-stat { @@ -365,7 +442,7 @@ border: 1px solid var(--color-border); border-radius: var(--radius-sm); color: var(--color-accent); - font: 0.78rem var(--mirages-font-ui); + font: 0.78rem var(--reading-font-family); text-decoration: none; } .shortcode-github-read-more:hover, @@ -383,7 +460,7 @@ padding: 0.6rem 1rem; border-top: 1px solid var(--color-border); color: var(--color-muted); - font: 0.75rem var(--mirages-font-ui); + font: 0.75rem var(--reading-font-family); } .shortcode-github-commit { min-width: 0; @@ -420,9 +497,17 @@ font-style: italic; } .shortcode-notice { - display: inline-block; - padding: 0.1em 0.45em; - border-left: 2px solid var(--color-accent); - background: color-mix(in srgb, var(--color-accent) 8%, transparent); - color: var(--color-accent); + display: inline-flex; + width: 1em; + height: 1em; + align-items: center; + justify-content: center; + color: inherit; + line-height: 1; + vertical-align: -0.125em; +} +.shortcode-notice svg { + display: block; + width: 1em; + height: 1em; }