site: admonitions custom title with icons

This commit is contained in:
Johannes Kirschbauer
2025-10-06 21:04:10 +02:00
parent 5cac9e7704
commit a40c6884d9
9 changed files with 837 additions and 342 deletions

View File

@@ -105,7 +105,7 @@ def render_option(
read_only = option.get("readOnly")
res = f"""
{"#" * level} {sanitize(name) if short_head is None else sanitize(short_head)} {"{: #" + sanitize_anchor(name) + "}" if level > 1 else ""}
{"#" * level} {sanitize(name) if short_head is None else sanitize(short_head)}
"""

5
site/.gitignore vendored
View File

@@ -21,4 +21,7 @@ vite.config.ts.timestamp-*
# Generated docs
src/routes/docs/reference/options
src/routes/docs/reference/clan.core
src/routes/docs/reference/clan.core
# Icons and other assets
static/icons

View File

@@ -21,5 +21,9 @@ buildNpmPackage {
cp -r ${module-docs}/reference/* src/routes/docs/reference
chmod +w -R src/routes/docs/reference
mkdir -p static/icons
cp -af ${../pkgs/clan-app/ui/icons}/* ./static/icons
chmod +w -R static/icons
'';
}

883
site/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -20,6 +20,7 @@
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.43.2",
"@sveltejs/vite-plugin-svelte": "^6.2.0",
"hastscript": "^9.0.1",
"mdast": "^2.3.2",
"mdast-util-toc": "^7.1.0",
"mdsvex": "^0.12.6",
@@ -29,7 +30,9 @@
"rehype-slug": "^6.0.0",
"rehype-stringify": "^10.0.1",
"remark": "^15.0.1",
"remark-directive": "^4.0.0",
"remark-frontmatter": "^5.0.0",
"remark-gfm": "^4.0.1",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.2",
"remark-stringify": "^11.0.0",
@@ -39,6 +42,7 @@
"to-vfile": "^8.0.0",
"typescript": "^5.9.2",
"unified": "^11.0.5",
"unist-util-visit": "^5.0.0",
"vfile": "^6.0.3",
"vfile-matter": "^5.0.1",
"vite": "^7.1.7"

View File

@@ -46,4 +46,90 @@ inventory.instances = {
## Step Foo
Miscellaneous Symbols
☀ ☁ ☂ ☃ ☄ ★ ☆ ☇ ☈ ☉ ☊ ☋ ☌ ☍ ☎ ☏ ☐ ☑ ☒ ☓ ☚ ☛ ☜ ☝ ☞ ☟ ☠ ☡ ☢ ☣ ☤ ☥ ☦ ☧ ☨ ☩ ☪ ☫ ☬ ☭ ☮ ☯ ☰ ☱ ☲ ☳ ☴ ☵ ☶ ☷ ☸ ☹ ☺ ☻ ☼ ☽ ☾ ☿ ♀ ♁ ♂ ♃ ♄ ♅ ♆ ♇ ♈ ♉ ♊ ♋ ♌ ♍ ♎ ♏ ♐ ♑ ♒ ♓ ♔ ♕ ♖ ♗ ♘ ♙ ♚ ♛ ♜ ♝ ♞ ♟ ♠ ♡ ♢ ♣ ♤ ♥ ♦ ♧ ♨ ♩ ♪ ♫ ♬ ♭ ♮ ♯
## Step Bar
Duplicate heading, should still be linked
This is a divider
---
---
:::note[Note about nature]
Respect the nature of things
:::
::::important[Its important]
Follow this and your life will be happy
:::note
nested note probably a bad idea
but technically valid
:::
::::
:::danger[Attention Footgun]
Please don't erase your disk
```nix
erase = false;
```
:::
:::tip[Outsmart]
Lets be really clever
- List
- Inside
:::
This is a table
| A/B | A | ¬A |
| - | - | - |
| B | AB | B |
| ¬B | A | 0 |
## GFM
### Autolink literals
www.example.com, https://example.com, and contact@example.com.
### Footnote
A note[^1]
[^1]: Big note.
### Strikethrough
~one~ or ~~two~~ tildes.
### Table
| a | b | c | d |
| - | :- | -: | :-: |
| 1 | 2 | 3 | 4 |
### Tasklist
* [ ] to do
* [x] done
- item
- normal

View File

@@ -3,3 +3,105 @@
body {
font-family: "Geist Variable";
}
.admonition {
border-left: 4px solid;
padding: 1rem;
margin: 1rem 0;
.admonition-title {
text-transform: capitalize;
font-weight: 600;
display: flex;
align-items: center;
justify-content: start;
gap: 0.5rem;
}
.admonition-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.25rem;
height: 1.25rem;
flex-shrink: 0;
}
.admonition-icon::before {
content: "";
display: block;
width: 1rem;
height: 1rem;
background-color: currentColor;
}
}
/* TODO: Adjust styling */
.admonition.note {
border-left-color:#3b82f6;
background-color: #eff6ff;
.admonition-title {
color: #1e40af;
}
.admonition-icon::before {
mask: url("/icons/info.svg") no-repeat center;
mask-size: contain;
}
}
.admonition.important {
border-left-color: #facc15;
background-color: #fffbeb;
.admonition-title {
color: #b45309;
}
.admonition-icon::before {
mask: url("/icons/attention.svg") no-repeat center;
mask-size: contain;
}
}
.admonition.danger {
border-left-color: #ef4444;
background-color: #fef2f2;
.admonition-title {
color: #b91c1c;
}
.admonition-icon::before {
mask: url("/icons/warning-filled.svg") no-repeat center;
mask-size: contain;
}
}
.admonition.tip {
border-left-color: #10b981;
background-color: #ecfdf5;
.admonition-title {
color: #065f46;
}
.admonition-icon::before {
mask: url("/icons/heart.svg") no-repeat center;
mask-size: contain;
}
}
ul {
list-style: none;
padding-left: 1rem;
}
ul li {
display: flex;
align-items: flex-start;
margin-top: 0.35em;
gap: 0.5rem;
}
/* TODO: Checklist */
ul li::before {
content: "-";
display: block;
width: 1rem;
height: 1rem;
flex-shrink: 0;
}

View File

@@ -12,6 +12,10 @@ const config = {
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
adapter: adapter(),
prerender: {
handleHttpError: "warn",
handleMissingId: "warn",
},
},
extensions: [".svelte"],
};

View File

@@ -8,6 +8,8 @@ import remarkRehype from "remark-rehype";
import rehypeStringify from "rehype-stringify";
import rehypeShiki from "@shikijs/rehype";
import rehypeSlug from "rehype-slug";
import remarkGfm from "remark-gfm";
import remarkDirective from "remark-directive";
import rehypeAutolinkHeadings from "rehype-autolink-headings";
import { toc } from "mdast-util-toc";
import type { Nodes } from "mdast";
@@ -17,6 +19,90 @@ import {
transformerRenderIndentGuides,
transformerMetaHighlight,
} from "@shikijs/transformers";
import { visit } from "unist-util-visit";
import { h } from "hastscript";
// Needed according to:
// https://github.com/remarkjs/remark-directive
export function styleDirectives() {
/**
* @param {Root} tree
* Tree.
* @returns {undefined}
* Nothing.
*/
return (tree: Nodes) => {
visit(tree, (node) => {
if (
node.type === "textDirective" ||
node.type === "leafDirective" ||
node.type === "containerDirective"
) {
const data = (node.data ||= {});
const hast = h(node.name, node.attributes);
// Detect whether first child is a label paragraph
const hasCustomTitle =
node.children?.[0]?.data?.directiveLabel === true;
// For custom title: use the existing paragraph node (will be converted to HAST)
// For fallback: create a new paragraph with text
const titleNode = hasCustomTitle
? node.children[0]
: {
type: "paragraph" as const,
children: [
{
type: "text" as const,
value: node.name,
},
],
};
// Remove label paragraph from children if it exists
const contentChildren = hasCustomTitle
? node.children.slice(1)
: node.children;
data.hName = "div";
data.hProperties = {
className: `admonition ${hast.tagName}`,
};
// Synthetic icon node
const iconNode = {
type: "text" as const,
value: "",
data: {
hName: "span",
hProperties: {
className: ["admonition-icon"],
"data-icon": hast.tagName,
},
},
};
// Create new children array with title wrapped in div
// The remark-rehype plugin will convert these MDAST nodes to HAST
node.children = [
// Title node
{
type: "paragraph" as const,
data: {
hName: "div",
hProperties: { className: ["admonition-title"] },
},
children:
titleNode.type === "paragraph"
? [iconNode, ...titleNode.children]
: [iconNode, titleNode],
},
...contentChildren,
];
}
});
};
}
export default defineConfig({
plugins: [
@@ -31,6 +117,9 @@ export default defineConfig({
matter(file, { strip: true });
const html = await unified()
.use(remarkParse)
.use(remarkGfm)
.use(remarkDirective)
.use(styleDirectives)
.use(remarkRehype)
.use(rehypeShiki, {
themes: {