feat(components): add md-border custom element

This commit is contained in:
hyper 2026-06-26 11:10:06 +08:00
parent 110451ae04
commit e861b80f57
3 changed files with 131 additions and 0 deletions

View File

@ -1 +1,12 @@
:md-font[Pacifico]{source=google} hello?
:md-dice[d6]{.bg-amber-100}
:md-border[crimson]{.b}
:::div{.flex.justify-stretch.gap-1}
foo
bar
:::

View File

@ -4,6 +4,7 @@ import "./md-table";
import "./md-link";
import "./md-pins";
import "./md-bg";
import "./md-border";
import "./md-font";
import "./md-embed";
import "./md-deck";
@ -26,6 +27,7 @@ export type { DiceProps } from "./md-dice";
export type { TableProps } from "./md-table";
export type { BgProps } from "./md-bg";
export type { FontProps } from "./md-font";
export type { BorderProps } from "./md-border";
export type { EmbedProps } from "./md-embed";
export type { YarnSpinnerProps } from "./md-yarn-spinner";
export type { TokenProps } from "./md-token";

View File

@ -0,0 +1,118 @@
import { customElement, noShadowDOM } from "solid-element";
import { onCleanup } from "solid-js";
import { resolvePath } from "./utils/path";
import { registerStyle } from "./utils/article-style-manager";
const SIDE_CLASSES = new Set(["t", "b", "l", "r", "all"]);
const STYLE_CLASSES = new Set([
"solid",
"dashed",
"dotted",
"double",
"groove",
"ridge",
"inset",
"outset",
"none",
]);
const REPEAT_CLASSES = new Set(["stretch", "repeat", "round", "space"]);
const SIDE_CSS: Record<string, string> = {
t: "Top",
b: "Bottom",
l: "Left",
r: "Right",
};
function parseSides(classes: string[]): string[] {
const sides = classes.filter((c) => SIDE_CLASSES.has(c));
if (sides.length === 0) return ["Top", "Bottom", "Left", "Right"];
if (sides.includes("all")) return ["Top", "Bottom", "Left", "Right"];
return sides.map((s) => SIDE_CSS[s]).filter(Boolean);
}
function parseStyle(classes: string[]): string {
const s = classes.find((c) => STYLE_CLASSES.has(c));
return s || "solid";
}
function parseRepeat(classes: string[]): string {
const r = classes.find((c) => REPEAT_CLASSES.has(c));
return r || "stretch";
}
function isImagePath(value: string): boolean {
return /^(\.{0,2}\/|[a-zA-Z]:\\|https?:\/\/)|\.(png|jpg|jpeg|gif|svg|webp|bmp)(\?|$)/i.test(
value,
);
}
export interface BorderProps {
width?: string;
slice?: string;
}
customElement(
"md-border",
{ width: undefined, slice: "10" },
(props, { element }) => {
noShadowDOM();
const rawValue = element?.textContent?.trim() || "";
if (element) element.textContent = "";
const articleEl = element?.closest("article") as HTMLElement;
const articlePath =
element?.closest("article[data-src]")?.getAttribute("data-src") || "";
// Parse classes from the element's className (marked-directive passes
// attrs.class as the element's className)
const classList = element?.className ? element.className.split(/\s+/) : [];
const sides = parseSides(classList);
const image = isImagePath(rawValue);
const resolvedSrc = image ? resolvePath(articlePath, rawValue) : "";
const styles: Partial<CSSStyleDeclaration> = {};
if (image) {
const repeat = parseRepeat(classList);
const borderWidth = props.width || "2mm";
styles.borderImageSource = `url(${resolvedSrc})`;
styles.borderImageSlice = props.slice || "10";
styles.borderImageWidth = borderWidth;
styles.borderImageRepeat = repeat;
styles.borderImageOutset = "0";
(styles as any).borderStyle = "solid";
const allSides = ["Top", "Bottom", "Left", "Right"];
for (const side of allSides) {
if (sides.includes(side)) {
(styles as any)[`border${side}Width`] = borderWidth;
} else {
(styles as any)[`border${side}Width`] = "0";
}
}
} else {
const color = rawValue || "currentColor";
const style = parseStyle(classList);
const borderWidth = props.width || ".2mm";
for (const side of sides) {
(styles as any)[`border${side}Width`] = borderWidth;
(styles as any)[`border${side}Style`] = style;
(styles as any)[`border${side}Color`] = color;
}
}
const handle = registerStyle({
key: "border",
article: articleEl,
styles,
});
onCleanup(() => handle.dispose());
return null;
},
);