diff --git a/astro.config.mjs b/astro.config.mjs
index 4fb3d4e..9bb0b54 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -6,6 +6,8 @@ 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 { copyFileSync, readFileSync } from 'node:fs';
import { resolve } from 'node:path';
@@ -46,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/package-lock.json b/package-lock.json
index f8edfd1..e823e4d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,10 +1,10 @@
{
- "name": "mirages-astro",
+ "name": "eidolon",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "mirages-astro",
+ "name": "eidolon",
"dependencies": {
"@astrojs/markdown-remark": "^7.2.2",
"@astrojs/rss": "^4.0.19",
diff --git a/package.json b/package.json
index ff78ca7..82ee1e2 100644
--- a/package.json
+++ b/package.json
@@ -1,5 +1,5 @@
{
- "name": "mirages-astro",
+ "name": "eidolon",
"private": true,
"type": "module",
"scripts": {
diff --git a/src/components/Footer.astro b/src/components/Footer.astro
index 0d24905..db99a6b 100644
--- a/src/components/Footer.astro
+++ b/src/components/Footer.astro
@@ -19,6 +19,7 @@ const blogsClubBadge = `https://www.blogsclub.org/badge/${blogDomain}/theme`;
{siteConfig.footer.links.length > 0 && }
+
Powered By Astro · Theme Eidolon
{siteConfig.footer.copyright && {siteConfig.footer.copyright}
}
{siteConfig.footer.note && }
{siteConfig.footer.theme && {siteConfig.footer.theme.label}
}
diff --git a/src/content/pages/about.md b/src/content/pages/about.md
index decd31a..bf2908a 100644
--- a/src/content/pages/about.md
+++ b/src/content/pages/about.md
@@ -131,11 +131,15 @@ GitHub:[virelyx258](https://github.com/virelyx258)
### 主题
-使用自己制作的 Astro 主题 Mirages。
+使用自己制作的 Astro 主题 Eidolon。
它极大程度上复刻了 Typecho 主题 Mirages 的风格和功能,又在此基础上使用了更先进的 TailWind CSS,使得站点响应速度更加优秀。
-因为 Mirages 是一款付费主题,如果贸然开源会影响到原主题的销售,且本站主题仍处于开发状态,所以本主题暂不开源。
+~~因为 Mirages 是一款付费主题,如果贸然开源会影响到原主题的销售,且本站主题仍处于开发状态,所以本主题暂不开源。~~
+
+仔细想想,发现我们其实不是同一个赛道。我做的是 Astro 主题,受众是 Astro 用户,不耽误 Typecho 主题的销售。况且,并不是所有 Typecho 用户都能无感切换到静态博客。
+
+所以,本主题准备开源。
[collapse title="在此之前"]
diff --git a/src/content/posts/2026/08/eidolon-short-code-preview.md b/src/content/posts/2026/08/eidolon-short-code-preview.md
new file mode 100644
index 0000000..bc865ef
--- /dev/null
+++ b/src/content/posts/2026/08/eidolon-short-code-preview.md
@@ -0,0 +1,335 @@
+---
+title: "Eidolon 主题短代码演示"
+pubDate: "2026-08-12T01:10:32.630Z"
+categories: ['科技']
+tags: ['主题','Astro']
+draft: false
+comments: true
+---
+
+Eidolon 主题支持短代码。除兼容 [Mirages](https://get233.com/archives/mirages-intro.html) 的全部短代码外,额外增加了“Tools”组件。
+
+善于运用短代码可以使文章内容更丰富。
+
+---
+
+短代码的统一书写形式:
+
+```
+[shortcode param="value"]内容[/shortcode]
+[shortcode param="value"/]
+```
+
+
+
+直接在 MarkDown 源代码里书写即可,博客构建时会自动将其转换为对应样式。
+
+---
+
+## 链接按钮
+
+短代码名称:`button`
+
+[tabs]
+[tab name="说明"]
+生成一个按钮样式的超链接。
+[/tab]
+[tab name="示例代码"]
+
+```
+[button href="https://src.luming.cool/riseforever2026/Eidolon"]Eidolon 主题[/button]
+```
+
+[/tab]
+[tab name="效果" selected]
+[button href="https://src.luming.cool/riseforever2026/Eidolon"]Eidolon 主题[/button]
+[/tab]
+[tab name="参数"]
+
+| 参数名 | 含义 |
+| -----: | :------------- |
+| href | 欲跳转到的链接 |
+| 内容 | 按钮标题 |
+
+[/tab]
+[/tabs]
+
+## 选项卡
+
+短代码名称:`tabs`
+
+[tabs]
+[tab name="说明"]
+生成一个多标签页的卡片。
+[/tab]
+[tab name="示例代码"]
+
+```
+[tabs]
+[tab name="标签页1标题"]
+我是标签页1的内容
+[/tab]
+[tab name="标签页2标题"]
+我是标签页2的内容
+[/tab]
+[tab name="标签页3标题" selected]
+我是标签页3的内容
+[/tab]
+[tab name="标签页4标题"]
+我是标签页4的内容
+[/tab]
+[/tabs]
+```
+
+[/tab]
+[tab name="效果" selected]
+如你所见。
+[/tab]
+[tab name="参数"]
+
+| 参数名 | 含义 |
+| -------: | :----------------- |
+| name | 标签页的名称 |
+| selected | 默认选中的标签页 |
+| 内容 | 标签页内显示的内容 |
+
+[/tab]
+[/tabs]
+
+## 提示框、警告框
+
+短代码名称:`hint`,`tip`
+
+[tabs]
+[tab name="说明"]
+生成一个提示或警告卡片。
+[/tab]
+[tab name="示例代码"]
+
+```
+[hint]提示内容[/hint]
+
+[hint warn]警告内容[/hint]
+
+[hint type="danger" title="提示标题"]
+提示内容
+[/hint]
+```
+
+[/tab]
+[tab name="效果" selected]
+[hint]提示内容[/hint]
+
+[hint warn]警告内容[/hint]
+
+[hint type="danger" title="提示标题"]
+提示内容
+[/hint]
+[/tab]
+[tab name="参数"]
+
+| 参数名 | 含义 |
+| -----: | :----------------------------------------------------------- |
+| type | 提示类型,默认为 `info`,在没有警告标题的情况下,可以直接将提示类型的值写到参数处。可选的提示类型为 `info`,`warning`(同 `warn`), `danger`(同 `error`),`success` |
+| title | 提示标题 |
+| 内容 | 提示内容 |
+
+[/tab]
+[/tabs]
+
+## 下载文件
+
+短代码名称:`file`
+
+[tabs]
+[tab name="说明"]
+生成一个下载文件的卡片。
+[/tab]
+[tab name="示例代码"]
+
+```
+[file url="https://example.com/download.zip"]示例文件名[/file]
+```
+
+[/tab]
+[tab name="效果" selected]
+[file url="https://example.com/download.zip"]示例文件名[/file]
+[/tab]
+[tab name="参数"]
+
+| 参数名 | 含义 |
+| -----: | :------------- |
+| href | 下载文件的链接 |
+| 内容 | 文件名 |
+
+[/tab]
+[/tabs]
+
+## 标签
+
+短代码名称:`tag`,`label`
+
+[tabs]
+[tab name="说明"]
+生成一个标签。
+[/tab]
+[tab name="示例代码"]
+
+```
+[tag]默认 Tag[/tag]
+[tag type="primary"]文字的颜色[/tag]
+[tag type="info"]文字的颜色[/tag]
+[tag type="warn"]文字的颜色[/tag]
+[tag type="danger"]文字的颜色[/tag]
+[tag type="success"]文字的颜色[/tag]
+
+[tag outline]文字的颜色[/tag]
+[tag type="primary" outline]文字的颜色[/tag]
+[tag type="info" outline]文字的颜色[/tag]
+[tag type="warn" outline]文字的颜色[/tag]
+[tag type="danger" outline]文字的颜色[/tag]
+[tag type="success" outline="1"]文字的颜色[/tag]
+```
+
+[/tab]
+[tab name="效果" selected]
+[tag]默认 Tag[/tag]
+[tag type="primary"]文字的颜色[/tag]
+[tag type="info"]文字的颜色[/tag]
+[tag type="warn"]文字的颜色[/tag]
+[tag type="danger"]文字的颜色[/tag]
+[tag type="success"]文字的颜色[/tag]
+
+[tag outline]文字的颜色[/tag]
+[tag type="primary" outline]文字的颜色[/tag]
+[tag type="info" outline]文字的颜色[/tag]
+[tag type="warn" outline]文字的颜色[/tag]
+[tag type="danger" outline]文字的颜色[/tag]
+[tag type="success" outline="1"]文字的颜色[/tag]
+[/tab]
+[tab name="参数"]
+
+| 参数名 | 含义 |
+| ------: | :----------------------------------------------------------- |
+| type | 默认为 `default`,可选值为 `info`,`warning`(同 `warn`), `danger`(同 `error`),`success`,`primary`, `default` |
+| outline | 是否使用 outline 样式的标签 |
+| 内容 | 标签展示的文字 |
+
+[/tab]
+[/tabs]
+
+## 快速输入警告符: [!/]
+
+短代码名称:`!`
+
+[tabs]
+[tab name="说明"]
+快速输入: [!/]
+[/tab]
+[tab name="示例代码"]
+
+```
+[!/]
+```
+
+[/tab]
+[tab name="效果" selected]
+[!/]
+[/tab]
+[/tabs]
+
+## 折叠框
+
+短代码名称:`collapse`
+
+[tabs]
+[tab name="说明"]
+可以点击标题展开 / 折叠的内容。
+[/tab]
+[tab name="示例代码"]
+
+```
+[collapse title="标题"]
+折叠内容
+[/collapse]
+```
+
+[/tab]
+[tab name="效果" selected]
+[collapse title="标题"]
+折叠内容
+[/collapse]
+[/tab]
+[tab name="参数"]
+
+| 参数名 | 含义 |
+| -----: | :--------- |
+| title | 折叠框标题 |
+| 内容 | 折叠框内容 |
+
+[/tab]
+[/tabs]
+
+## GitHub
+
+短代码名称:`github`
+
+[tabs]
+[tab name="说明"]
+放置一个 Github 仓库控件。
+[/tab]
+[tab name="示例代码"]
+
+```
+[github repo="virelyx258/Astro-Theme-Eidolon" /]
+```
+
+[/tab]
+[tab name="效果" selected]
+[github repo="virelyx258/Astro-Theme-Eidolon" /]
+[/tab]
+[tab name="参数"]
+
+| 参数名 | 含义 |
+| -----: | :----------------------------------------------- |
+| repo | Github 仓库所属用户及仓库名。格式:用户名/仓库名 |
+
+[/tab]
+[/tabs]
+
+## 工具列表
+
+短代码名称:`tools`
+
+[tabs]
+[tab name="说明"]
+放置一个工具列表卡片。
+[/tab]
+[tab name="示例代码"]
+
+```
+[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]
+```
+
+[/tab]
+[tab name="效果" selected]
+[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]
+[/tab]
+[tab name="参数"]
+
+| 参数名 | 含义 |
+| -----: | :--------- |
+| title | 工具集名称 |
+
+[/tab]
+[/tabs]
\ No newline at end of file
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..fed2e27 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,37 @@ function normalizeMarkdownHeadings(source: string): string {
}).join('\n');
}
-function markdown(source: string, preserveHtml = false): string {
+function markdown(source: string, preserveHtml = false, lineNumbers = false): 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 || code.children?.some((child: any) => child.type !== 'text')) return;
+ const value = code.children.map((child: any) => child.value).join('');
+ const lines = value.endsWith('\n') ? value.slice(0, -1).split('\n') : value.split('\n');
+ const classes = Array.isArray(node.properties?.className) ? node.properties.className : [];
+ node.properties = { ...node.properties, className: [...classes, 'shortcode-code'] };
+ code.children = lines.flatMap((line: string, index: number) => [
+ {
+ type: 'element',
+ tagName: 'span',
+ properties: { className: ['line'] },
+ children: [
+ { type: 'element', tagName: 'span', properties: { className: ['shortcode-line-number'], ariaHidden: 'true' }, children: [{ type: 'text', value: String(index + 1) }] },
+ ...(line ? [{ type: 'text', value: line }] : []),
+ ],
+ },
+ ...(index < lines.length - 1 ? [{ type: 'text', value: '\n' }] : []),
+ ]);
+ });
+ }
+ applyNoticeShortcodes(hast);
+ applyResponsiveTables(hast);
if (siteUrl) applyExternalLinks(hast, siteUrl);
return toHtml(hast, { allowDangerousHtml: true });
}
@@ -77,24 +110,32 @@ 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 || '展开内容')}
`;
+ return `${inline(label || '展开内容')}
${markdown(body, true, true)}
`;
}
if (name === 'tabs') {
const parsed = parseBlocks(body);
@@ -151,8 +192,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 +281,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 +323,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/styles/content.css b/src/styles/content.css
index 7ba1a59..4186d7f 100644
--- a/src/styles/content.css
+++ b/src/styles/content.css
@@ -210,14 +210,66 @@
background: var(--color-code);
}
+.shortcode-code {
+ --shortcode-code-gutter: color-mix(in srgb, var(--color-code) 78%, var(--color-accent));
+ padding: 0;
+}
+
+.shortcode-code code {
+ padding-block: 1rem;
+ font-size: 0;
+}
+
+.shortcode-code .line {
+ display: flex;
+ min-height: 1.45em;
+ font-size: .875rem;
+ line-height: 1.45;
+}
+
+.shortcode-code .shortcode-line-number {
+ display: inline-block;
+ box-sizing: border-box;
+ min-width: 3rem;
+ flex: 0 0 3rem;
+ margin-right: 1rem;
+ padding-inline: 1rem;
+ background: var(--shortcode-code-gutter);
+ color: var(--color-muted);
+ font-size: .875rem;
+ font-variant-numeric: tabular-nums;
+ opacity: 0.72;
+ text-align: right;
+ user-select: none;
+}
+
+.shortcode-code .line:first-of-type .shortcode-line-number { box-shadow: 0 -1rem var(--shortcode-code-gutter); }
+.shortcode-code .line:last-of-type .shortcode-line-number { box-shadow: 0 1rem var(--shortcode-code-gutter); }
+.shortcode-code .line:only-of-type .shortcode-line-number { box-shadow: 0 -1rem var(--shortcode-code-gutter), 0 1rem var(--shortcode-code-gutter); }
+
.prose .katex-display { max-width: 100%; overflow-x: auto; overflow-y: hidden; padding-block: .35em; }
.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%;
+ margin: 0;
border-collapse: collapse;
+ table-layout: fixed;
}
.prose thead {
@@ -239,6 +291,14 @@
.prose :where(th, td) {
padding: 0.55em 0.8em;
border: 1px solid var(--color-border);
+ overflow-wrap: anywhere;
+ word-break: break-word;
+}
+
+@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..2ad5012 100644
--- a/src/styles/shortcodes.css
+++ b/src/styles/shortcodes.css
@@ -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(--mirages-font-ui);
+ 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;
@@ -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;
}
@@ -420,9 +492,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;
}