@haruhimemoe/ui 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -1
- package/README.md +171 -3
- package/dist/components/basics/Prose.d.ts +9 -6
- package/dist/components/basics/Prose.js +4 -4
- package/dist/components/mdx/Callout.d.ts +24 -0
- package/dist/components/mdx/Callout.js +22 -0
- package/dist/components/mdx/CodeBlock.d.ts +26 -0
- package/dist/components/mdx/CodeBlock.js +48 -0
- package/dist/components/mdx/CodeCopyButton.d.ts +21 -0
- package/dist/components/mdx/CodeCopyButton.js +34 -0
- package/dist/components/mdx/MdxBlockquote.d.ts +23 -0
- package/dist/components/mdx/MdxBlockquote.js +17 -0
- package/dist/components/mdx/MdxHeading.d.ts +26 -0
- package/dist/components/mdx/MdxHeading.js +23 -0
- package/dist/components/mdx/MdxLink.d.ts +29 -0
- package/dist/components/mdx/MdxLink.js +26 -0
- package/dist/components/mdx/MdxPre.d.ts +25 -0
- package/dist/components/mdx/MdxPre.js +39 -0
- package/dist/components/mdx/MdxTable.d.ts +20 -0
- package/dist/components/mdx/MdxTable.js +23 -0
- package/dist/components/mdx/highlighter.d.ts +37 -0
- package/dist/components/mdx/highlighter.js +66 -0
- package/dist/components/mdx/mdxComponents.d.ts +21 -0
- package/dist/components/mdx/mdxComponents.js +22 -0
- package/dist/components/mdx/parseCodeMeta.d.ts +21 -0
- package/dist/components/mdx/parseCodeMeta.js +40 -0
- package/dist/components/mdx/textOf.d.ts +17 -0
- package/dist/components/mdx/textOf.js +28 -0
- package/dist/components/osu/PlayerCard.d.ts +59 -0
- package/dist/components/osu/PlayerCard.js +40 -0
- package/dist/components/osu/playerLinks.d.ts +47 -0
- package/dist/components/osu/playerLinks.js +65 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mdx.d.ts +17 -0
- package/dist/mdx.js +17 -0
- package/dist/remark/callouts.d.ts +20 -0
- package/dist/remark/callouts.js +58 -0
- package/dist/remark/codeMeta.d.ts +17 -0
- package/dist/remark/codeMeta.js +22 -0
- package/dist/remark/headingIds.d.ts +16 -0
- package/dist/remark/headingIds.js +28 -0
- package/dist/remark/index.d.ts +29 -0
- package/dist/remark/index.js +33 -0
- package/dist/remark/mdast.d.ts +42 -0
- package/dist/remark/mdast.js +37 -0
- package/dist/remark/slugify.d.ts +23 -0
- package/dist/remark/slugify.js +40 -0
- package/dist/shiki.d.ts +13 -0
- package/dist/shiki.js +38 -0
- package/dist/theme.css +18 -0
- package/package.json +28 -3
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/mdx/MdxPre.tsx
|
|
3
|
+
* @desc react-markdown/MDX's `pre` override: a fenced code block (a `<pre>` wrapping a single
|
|
4
|
+
* child with a `language-*` class, a `data-meta` prop, or the literal `code` element type)
|
|
5
|
+
* is handed to CodeBlock with its parsed meta; detecting the fence by the child's props
|
|
6
|
+
* rather than only `child.type === "code"` means an app that overrides the `code` component
|
|
7
|
+
* (through `mdxComponents`' own map or MDX's `useMDXComponents`) still gets highlighting.
|
|
8
|
+
* Anything else renders as a plain, focusable `<pre>`. Drops the `node` prop react-markdown
|
|
9
|
+
* passes to every component.
|
|
10
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
11
|
+
* @created Sat Oct 3, 2026
|
|
12
|
+
* @modified Sat Oct 3, 2026
|
|
13
|
+
*/
|
|
14
|
+
import { type ComponentProps } from "react";
|
|
15
|
+
/** Every native `<pre>` prop, plus the `node` react-markdown passes (dropped). */
|
|
16
|
+
export type MdxPreProps = ComponentProps<"pre"> & {
|
|
17
|
+
node?: unknown;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* @function MdxPre
|
|
21
|
+
* @param props {MdxPreProps} native pre props; a single `code` child carries the language class
|
|
22
|
+
* and the fence's meta string in `data-meta`
|
|
23
|
+
* @returns {JSX.Element} a CodeBlock for a fenced `code` child, otherwise a plain, focusable pre
|
|
24
|
+
*/
|
|
25
|
+
export declare function MdxPre({ node: _node, children, ...props }: MdxPreProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* @file src/components/mdx/MdxPre.tsx
|
|
4
|
+
* @desc react-markdown/MDX's `pre` override: a fenced code block (a `<pre>` wrapping a single
|
|
5
|
+
* child with a `language-*` class, a `data-meta` prop, or the literal `code` element type)
|
|
6
|
+
* is handed to CodeBlock with its parsed meta; detecting the fence by the child's props
|
|
7
|
+
* rather than only `child.type === "code"` means an app that overrides the `code` component
|
|
8
|
+
* (through `mdxComponents`' own map or MDX's `useMDXComponents`) still gets highlighting.
|
|
9
|
+
* Anything else renders as a plain, focusable `<pre>`. Drops the `node` prop react-markdown
|
|
10
|
+
* passes to every component.
|
|
11
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
12
|
+
* @created Sat Oct 3, 2026
|
|
13
|
+
* @modified Sat Oct 3, 2026
|
|
14
|
+
*/
|
|
15
|
+
import { Children, isValidElement } from "react";
|
|
16
|
+
import { CodeBlock } from "./CodeBlock.js";
|
|
17
|
+
import { parseCodeMeta } from "./parseCodeMeta.js";
|
|
18
|
+
import { textOf } from "./textOf.js";
|
|
19
|
+
/**
|
|
20
|
+
* @function MdxPre
|
|
21
|
+
* @param props {MdxPreProps} native pre props; a single `code` child carries the language class
|
|
22
|
+
* and the fence's meta string in `data-meta`
|
|
23
|
+
* @returns {JSX.Element} a CodeBlock for a fenced `code` child, otherwise a plain, focusable pre
|
|
24
|
+
*/
|
|
25
|
+
export function MdxPre({ node: _node, children, ...props }) {
|
|
26
|
+
const [child] = Children.toArray(children);
|
|
27
|
+
const isFence = isValidElement(child) &&
|
|
28
|
+
(child.type === "code" ||
|
|
29
|
+
/(?:^|\s)language-/.test(child.props.className ?? "") ||
|
|
30
|
+
child.props["data-meta"] !== undefined);
|
|
31
|
+
if (!isFence || !isValidElement(child)) {
|
|
32
|
+
return (_jsx("pre", {
|
|
33
|
+
// biome-ignore lint/a11y/noNoninteractiveTabindex: a scrollable region needs keyboard focus
|
|
34
|
+
tabIndex: 0, ...props, children: children }));
|
|
35
|
+
}
|
|
36
|
+
const lang = /(?:^|\s)language-(\S+)/.exec(child.props.className ?? "")?.[1];
|
|
37
|
+
const { title, highlight } = parseCodeMeta(child.props["data-meta"]);
|
|
38
|
+
return (_jsx(CodeBlock, { code: textOf(child.props.children), lang: lang, title: title, highlight: highlight }));
|
|
39
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/mdx/MdxTable.tsx
|
|
3
|
+
* @desc react-markdown/MDX's `table` override: wraps the table in a named, focusable scroll
|
|
4
|
+
* region (a table can overflow its column on narrow screens), named by its caption when
|
|
5
|
+
* one is given. Drops the `node` prop react-markdown passes to every component.
|
|
6
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
7
|
+
* @created Sat Oct 3, 2026
|
|
8
|
+
* @modified Sat Oct 3, 2026
|
|
9
|
+
*/
|
|
10
|
+
import { type ComponentProps } from "react";
|
|
11
|
+
/** Every native `<table>` prop, plus the `node` react-markdown passes (dropped). */
|
|
12
|
+
export type MdxTableProps = ComponentProps<"table"> & {
|
|
13
|
+
node?: unknown;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* @function MdxTable
|
|
17
|
+
* @param props {MdxTableProps} native table props; a `caption` child names the scroll region
|
|
18
|
+
* @returns {JSX.Element} a horizontally scrollable, focusable, labelled region wrapping the table
|
|
19
|
+
*/
|
|
20
|
+
export declare function MdxTable({ node: _node, children, ...props }: MdxTableProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* @file src/components/mdx/MdxTable.tsx
|
|
4
|
+
* @desc react-markdown/MDX's `table` override: wraps the table in a named, focusable scroll
|
|
5
|
+
* region (a table can overflow its column on narrow screens), named by its caption when
|
|
6
|
+
* one is given. Drops the `node` prop react-markdown passes to every component.
|
|
7
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
8
|
+
* @created Sat Oct 3, 2026
|
|
9
|
+
* @modified Sat Oct 3, 2026
|
|
10
|
+
*/
|
|
11
|
+
import { Children, isValidElement } from "react";
|
|
12
|
+
import { textOf } from "./textOf.js";
|
|
13
|
+
/**
|
|
14
|
+
* @function MdxTable
|
|
15
|
+
* @param props {MdxTableProps} native table props; a `caption` child names the scroll region
|
|
16
|
+
* @returns {JSX.Element} a horizontally scrollable, focusable, labelled region wrapping the table
|
|
17
|
+
*/
|
|
18
|
+
export function MdxTable({ node: _node, children, ...props }) {
|
|
19
|
+
const caption = Children.toArray(children).find((child) => isValidElement(child) && child.type === "caption");
|
|
20
|
+
return (_jsx("div", { role: "group", "aria-label": textOf(caption) || "Table",
|
|
21
|
+
// biome-ignore lint/a11y/noNoninteractiveTabindex: a scrollable region needs keyboard focus
|
|
22
|
+
tabIndex: 0, className: "overflow-x-auto", children: _jsx("table", { ...props, children: children }) }));
|
|
23
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/mdx/highlighter.ts
|
|
3
|
+
* @desc The highlighter CodeBlock asks for. Nothing here names Shiki outside `import type`, so
|
|
4
|
+
* an app without Shiki builds: `@haruhimemoe/ui/shiki` (src/shiki.ts) registers the real
|
|
5
|
+
* loader when an app imports it. The loader lives in a `globalThis` slot keyed by
|
|
6
|
+
* `Symbol.for`, so it survives duplicate copies of this module. The first highlighter is
|
|
7
|
+
* cached; with no loader registered, or a loader that rejects, code renders plain (warned
|
|
8
|
+
* once, outside production) rather than throwing.
|
|
9
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
10
|
+
* @created Sat Oct 3, 2026
|
|
11
|
+
* @modified Sat Oct 3, 2026
|
|
12
|
+
*/
|
|
13
|
+
import type { HighlighterCore } from "shiki/core";
|
|
14
|
+
/** The Shiki CSS-variables theme name CodeBlock renders with. */
|
|
15
|
+
export declare const THEME = "haruhime";
|
|
16
|
+
/** Loads a Shiki highlighter: what `@haruhimemoe/ui/shiki` registers. */
|
|
17
|
+
export type HighlighterLoader = () => Promise<HighlighterCore>;
|
|
18
|
+
/**
|
|
19
|
+
* @function setHighlighterLoader
|
|
20
|
+
* @param loader {HighlighterLoader} loads the Shiki highlighter; replaces any earlier one
|
|
21
|
+
* @returns {void} registers the loader in the global slot and drops the cached highlighter
|
|
22
|
+
*/
|
|
23
|
+
export declare const setHighlighterLoader: (loader: HighlighterLoader) => void;
|
|
24
|
+
/**
|
|
25
|
+
* @function getHighlighter
|
|
26
|
+
* @param loader {HighlighterLoader | undefined} loads the highlighter; defaults to the registered
|
|
27
|
+
* one, overridable in tests
|
|
28
|
+
* @returns {Promise<HighlighterCore | null>} the cached highlighter, or null when no loader is
|
|
29
|
+
* registered or it failed to load
|
|
30
|
+
*/
|
|
31
|
+
export declare const getHighlighter: (loader?: HighlighterLoader | undefined) => Promise<HighlighterCore | null>;
|
|
32
|
+
/**
|
|
33
|
+
* @function resetHighlighter
|
|
34
|
+
* @returns {void} clears the registered loader, the cached highlighter and the warned flag
|
|
35
|
+
* (tests only)
|
|
36
|
+
*/
|
|
37
|
+
export declare const resetHighlighter: () => void;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/mdx/highlighter.ts
|
|
3
|
+
* @desc The highlighter CodeBlock asks for. Nothing here names Shiki outside `import type`, so
|
|
4
|
+
* an app without Shiki builds: `@haruhimemoe/ui/shiki` (src/shiki.ts) registers the real
|
|
5
|
+
* loader when an app imports it. The loader lives in a `globalThis` slot keyed by
|
|
6
|
+
* `Symbol.for`, so it survives duplicate copies of this module. The first highlighter is
|
|
7
|
+
* cached; with no loader registered, or a loader that rejects, code renders plain (warned
|
|
8
|
+
* once, outside production) rather than throwing.
|
|
9
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
10
|
+
* @created Sat Oct 3, 2026
|
|
11
|
+
* @modified Sat Oct 3, 2026
|
|
12
|
+
*/
|
|
13
|
+
/** The Shiki CSS-variables theme name CodeBlock renders with. */
|
|
14
|
+
export const THEME = "haruhime";
|
|
15
|
+
const SLOT = Symbol.for("@haruhimemoe/ui/highlighter");
|
|
16
|
+
// No node types in the build (tsconfig.build.json "types": []), so read process through globalThis.
|
|
17
|
+
const isProduction = () => globalThis.process?.env
|
|
18
|
+
?.NODE_ENV === "production";
|
|
19
|
+
let cached;
|
|
20
|
+
let warned = false;
|
|
21
|
+
const warnOnce = (message, error) => {
|
|
22
|
+
if (warned || isProduction())
|
|
23
|
+
return;
|
|
24
|
+
warned = true;
|
|
25
|
+
if (error === undefined)
|
|
26
|
+
console.warn(message);
|
|
27
|
+
else
|
|
28
|
+
console.warn(message, error);
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* @function setHighlighterLoader
|
|
32
|
+
* @param loader {HighlighterLoader} loads the Shiki highlighter; replaces any earlier one
|
|
33
|
+
* @returns {void} registers the loader in the global slot and drops the cached highlighter
|
|
34
|
+
*/
|
|
35
|
+
export const setHighlighterLoader = (loader) => {
|
|
36
|
+
globalThis[SLOT] = loader;
|
|
37
|
+
cached = undefined;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* @function getHighlighter
|
|
41
|
+
* @param loader {HighlighterLoader | undefined} loads the highlighter; defaults to the registered
|
|
42
|
+
* one, overridable in tests
|
|
43
|
+
* @returns {Promise<HighlighterCore | null>} the cached highlighter, or null when no loader is
|
|
44
|
+
* registered or it failed to load
|
|
45
|
+
*/
|
|
46
|
+
export const getHighlighter = (loader = globalThis[SLOT]) => {
|
|
47
|
+
if (!loader) {
|
|
48
|
+
warnOnce('@haruhimemoe/ui: code blocks aren\'t highlighted. Install shiki and add `import "@haruhimemoe/ui/shiki";` once, e.g. in mdx-components.tsx.');
|
|
49
|
+
return Promise.resolve(null);
|
|
50
|
+
}
|
|
51
|
+
cached ??= loader().catch((error) => {
|
|
52
|
+
warnOnce("@haruhimemoe/ui: Shiki didn't load, so code blocks aren't highlighted.", error);
|
|
53
|
+
return null;
|
|
54
|
+
});
|
|
55
|
+
return cached;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* @function resetHighlighter
|
|
59
|
+
* @returns {void} clears the registered loader, the cached highlighter and the warned flag
|
|
60
|
+
* (tests only)
|
|
61
|
+
*/
|
|
62
|
+
export const resetHighlighter = () => {
|
|
63
|
+
globalThis[SLOT] = undefined;
|
|
64
|
+
cached = undefined;
|
|
65
|
+
warned = false;
|
|
66
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/mdx/mdxComponents.ts
|
|
3
|
+
* @desc The element override map for @next/mdx's `useMDXComponents` and react-markdown's
|
|
4
|
+
* `components` prop: links, headings (h2/h3), fenced code, tables and callout blockquotes.
|
|
5
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
6
|
+
* @created Sat Oct 3, 2026
|
|
7
|
+
* @modified Sat Oct 3, 2026
|
|
8
|
+
*/
|
|
9
|
+
import { MdxBlockquote } from "./MdxBlockquote.js";
|
|
10
|
+
import { MdxLink } from "./MdxLink.js";
|
|
11
|
+
import { MdxPre } from "./MdxPre.js";
|
|
12
|
+
import { MdxTable } from "./MdxTable.js";
|
|
13
|
+
/** The element overrides for @next/mdx's useMDXComponents and react-markdown's `components`. */
|
|
14
|
+
export declare const mdxComponents: {
|
|
15
|
+
a: typeof MdxLink;
|
|
16
|
+
blockquote: typeof MdxBlockquote;
|
|
17
|
+
h2: ({ node: _node, id, children, ...props }: import("./MdxHeading.js").MdxHeadingProps) => import("react").JSX.Element;
|
|
18
|
+
h3: ({ node: _node, id, children, ...props }: import("./MdxHeading.js").MdxHeadingProps) => import("react").JSX.Element;
|
|
19
|
+
pre: typeof MdxPre;
|
|
20
|
+
table: typeof MdxTable;
|
|
21
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/mdx/mdxComponents.ts
|
|
3
|
+
* @desc The element override map for @next/mdx's `useMDXComponents` and react-markdown's
|
|
4
|
+
* `components` prop: links, headings (h2/h3), fenced code, tables and callout blockquotes.
|
|
5
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
6
|
+
* @created Sat Oct 3, 2026
|
|
7
|
+
* @modified Sat Oct 3, 2026
|
|
8
|
+
*/
|
|
9
|
+
import { MdxBlockquote } from "./MdxBlockquote.js";
|
|
10
|
+
import { MdxH2, MdxH3 } from "./MdxHeading.js";
|
|
11
|
+
import { MdxLink } from "./MdxLink.js";
|
|
12
|
+
import { MdxPre } from "./MdxPre.js";
|
|
13
|
+
import { MdxTable } from "./MdxTable.js";
|
|
14
|
+
/** The element overrides for @next/mdx's useMDXComponents and react-markdown's `components`. */
|
|
15
|
+
export const mdxComponents = {
|
|
16
|
+
a: MdxLink,
|
|
17
|
+
blockquote: MdxBlockquote,
|
|
18
|
+
h2: MdxH2,
|
|
19
|
+
h3: MdxH3,
|
|
20
|
+
pre: MdxPre,
|
|
21
|
+
table: MdxTable,
|
|
22
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/mdx/parseCodeMeta.ts
|
|
3
|
+
* @desc Parses a fenced code block's meta string (the text after the language on ```ts fences)
|
|
4
|
+
* into a title (`title="..."` or `title='...'`) and a sorted, deduped list of highlighted
|
|
5
|
+
* line numbers (`{1,3-5}`), capped at 10000 lines so a huge or malicious range can't hang
|
|
6
|
+
* the render.
|
|
7
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
8
|
+
* @created Sat Oct 3, 2026
|
|
9
|
+
* @modified Sat Oct 3, 2026
|
|
10
|
+
*/
|
|
11
|
+
/** A parsed code fence meta string. */
|
|
12
|
+
export type CodeMeta = {
|
|
13
|
+
title?: string;
|
|
14
|
+
highlight: number[];
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* @function parseCodeMeta
|
|
18
|
+
* @param meta {string | null | undefined} the code fence's meta string, if any
|
|
19
|
+
* @returns {CodeMeta} the title (when present) and the sorted, deduped, capped highlight lines
|
|
20
|
+
*/
|
|
21
|
+
export declare const parseCodeMeta: (meta?: string | null) => CodeMeta;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/mdx/parseCodeMeta.ts
|
|
3
|
+
* @desc Parses a fenced code block's meta string (the text after the language on ```ts fences)
|
|
4
|
+
* into a title (`title="..."` or `title='...'`) and a sorted, deduped list of highlighted
|
|
5
|
+
* line numbers (`{1,3-5}`), capped at 10000 lines so a huge or malicious range can't hang
|
|
6
|
+
* the render.
|
|
7
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
8
|
+
* @created Sat Oct 3, 2026
|
|
9
|
+
* @modified Sat Oct 3, 2026
|
|
10
|
+
*/
|
|
11
|
+
const TITLE = /\btitle=(?:"([^"]*)"|'([^']*)')/;
|
|
12
|
+
const RANGES = /\{([^}]*)\}/;
|
|
13
|
+
const MAX_LINES = 10000;
|
|
14
|
+
/**
|
|
15
|
+
* @function parseCodeMeta
|
|
16
|
+
* @param meta {string | null | undefined} the code fence's meta string, if any
|
|
17
|
+
* @returns {CodeMeta} the title (when present) and the sorted, deduped, capped highlight lines
|
|
18
|
+
*/
|
|
19
|
+
export const parseCodeMeta = (meta) => {
|
|
20
|
+
const text = meta ?? "";
|
|
21
|
+
const lines = new Set();
|
|
22
|
+
for (const part of RANGES.exec(text)?.[1]?.split(",") ?? []) {
|
|
23
|
+
const match = /^\s*(\d+)\s*(?:-\s*(\d+)\s*)?$/.exec(part);
|
|
24
|
+
if (!match)
|
|
25
|
+
continue;
|
|
26
|
+
let from = Number(match[1]);
|
|
27
|
+
let to = match[2] === undefined ? from : Number(match[2]);
|
|
28
|
+
if (from > to)
|
|
29
|
+
[from, to] = [to, from];
|
|
30
|
+
from = Math.max(from, 1);
|
|
31
|
+
for (let line = from; line <= to && lines.size < MAX_LINES; line++)
|
|
32
|
+
lines.add(line);
|
|
33
|
+
}
|
|
34
|
+
const title = TITLE.exec(text);
|
|
35
|
+
const result = { highlight: [...lines].sort((a, b) => a - b) };
|
|
36
|
+
const name = title?.[1] ?? title?.[2];
|
|
37
|
+
if (name)
|
|
38
|
+
result.title = name;
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/mdx/textOf.ts
|
|
3
|
+
* @desc Flattens a ReactNode tree to plain text: strings and numbers pass through, arrays and
|
|
4
|
+
* element children are recursively joined, and null/undefined/boolean contribute nothing.
|
|
5
|
+
* Used to get a code block's plain text for Shiki and the copy button, since react-markdown
|
|
6
|
+
* hands components React children, not a string.
|
|
7
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
8
|
+
* @created Sat Oct 3, 2026
|
|
9
|
+
* @modified Sat Oct 3, 2026
|
|
10
|
+
*/
|
|
11
|
+
import { type ReactNode } from "react";
|
|
12
|
+
/**
|
|
13
|
+
* @function textOf
|
|
14
|
+
* @param node {ReactNode} the node (or tree of nodes) to flatten
|
|
15
|
+
* @returns {string} the concatenated text content
|
|
16
|
+
*/
|
|
17
|
+
export declare const textOf: (node: ReactNode) => string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/mdx/textOf.ts
|
|
3
|
+
* @desc Flattens a ReactNode tree to plain text: strings and numbers pass through, arrays and
|
|
4
|
+
* element children are recursively joined, and null/undefined/boolean contribute nothing.
|
|
5
|
+
* Used to get a code block's plain text for Shiki and the copy button, since react-markdown
|
|
6
|
+
* hands components React children, not a string.
|
|
7
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
8
|
+
* @created Sat Oct 3, 2026
|
|
9
|
+
* @modified Sat Oct 3, 2026
|
|
10
|
+
*/
|
|
11
|
+
import { isValidElement } from "react";
|
|
12
|
+
/**
|
|
13
|
+
* @function textOf
|
|
14
|
+
* @param node {ReactNode} the node (or tree of nodes) to flatten
|
|
15
|
+
* @returns {string} the concatenated text content
|
|
16
|
+
*/
|
|
17
|
+
export const textOf = (node) => {
|
|
18
|
+
if (node === null || node === undefined || typeof node === "boolean")
|
|
19
|
+
return "";
|
|
20
|
+
if (typeof node === "string" || typeof node === "number" || typeof node === "bigint") {
|
|
21
|
+
return String(node);
|
|
22
|
+
}
|
|
23
|
+
if (Array.isArray(node))
|
|
24
|
+
return node.map(textOf).join("");
|
|
25
|
+
if (isValidElement(node))
|
|
26
|
+
return textOf(node.props.children);
|
|
27
|
+
return "";
|
|
28
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/osu/PlayerCard.tsx
|
|
3
|
+
* @desc osu!-web's user card (the 120px card from the friends list and user tooltips) from plain
|
|
4
|
+
* props: cover under a b5 overlay, 60px avatar, country flag, team flag and supporter heart,
|
|
5
|
+
* the username (the whole card links to the profile), and an optional status row. It never
|
|
6
|
+
* fetches: the app passes a snapshot, so nothing here claims live data unless told to. The
|
|
7
|
+
* online ring's lime is osu!'s own green-light, hue-independent like StarRating's spectrum.
|
|
8
|
+
* Keyboard focus outlines the whole card in h1 as well as the username's own ring.
|
|
9
|
+
* Plain `<img>`s, not next/image, so apps need no `images.remotePatterns` for osu!'s hosts.
|
|
10
|
+
* Server-safe.
|
|
11
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
12
|
+
* @created Sun Oct 4, 2026
|
|
13
|
+
* @modified Sun Oct 4, 2026
|
|
14
|
+
*/
|
|
15
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
16
|
+
/** An osu! team: its name (the flag's alt text and tooltip) and its flag image. */
|
|
17
|
+
export type PlayerTeam = {
|
|
18
|
+
/** The team's name. */
|
|
19
|
+
name: string;
|
|
20
|
+
/** The team flag's URL (osu! serves them from assets.ppy.sh). */
|
|
21
|
+
flagUrl: string;
|
|
22
|
+
};
|
|
23
|
+
/** Every native `<div>` prop except children, plus the player's snapshot. */
|
|
24
|
+
export type PlayerCardProps = Omit<ComponentProps<"div">, "children"> & {
|
|
25
|
+
/** The name shown on the card, as-is. */
|
|
26
|
+
username: string;
|
|
27
|
+
/** The osu! user id: the avatar comes from a.ppy.sh and the card links to the profile. */
|
|
28
|
+
userId?: number | undefined;
|
|
29
|
+
/** Replaces the profile link; `null` draws no link even with a `userId`. */
|
|
30
|
+
href?: string | null | undefined;
|
|
31
|
+
/** Replaces the a.ppy.sh avatar. Without it and a `userId`, a letter stands in. */
|
|
32
|
+
avatarUrl?: string | undefined;
|
|
33
|
+
/** The profile cover, drawn behind everything. None leaves the card plain b4. */
|
|
34
|
+
coverUrl?: string | undefined;
|
|
35
|
+
/** ISO 3166-1 alpha-2 code; draws osu!'s flag. Anything else draws none. */
|
|
36
|
+
countryCode?: string | undefined;
|
|
37
|
+
/** The flag's alt text (default: the English name from Intl, else the code). */
|
|
38
|
+
countryName?: string | undefined;
|
|
39
|
+
/** The player's osu! team, drawn as its flag beside the country's. */
|
|
40
|
+
team?: PlayerTeam | undefined;
|
|
41
|
+
/** Draws the supporter heart. */
|
|
42
|
+
supporter?: boolean | undefined;
|
|
43
|
+
/** What screen readers hear for the heart (default "osu! supporter"). */
|
|
44
|
+
supporterLabel?: string | undefined;
|
|
45
|
+
/** Draws the status ring. Leave it out for static data: the card never guesses a status. */
|
|
46
|
+
status?: "online" | "offline" | undefined;
|
|
47
|
+
/** The bottom row's main line (default "Online" or "Offline" when `status` is set). */
|
|
48
|
+
statusText?: ReactNode;
|
|
49
|
+
/** A small line above it ("Last seen 29 days ago", "formerly RMarc"). */
|
|
50
|
+
statusNote?: ReactNode;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* @function PlayerCard
|
|
54
|
+
* @param props {PlayerCardProps} the player's snapshot (name, id, cover, flags, supporter,
|
|
55
|
+
* status) and native div props
|
|
56
|
+
* @returns {JSX.Element} a 120px osu!-web user card; the whole card links to the profile when it
|
|
57
|
+
* has a link
|
|
58
|
+
*/
|
|
59
|
+
export declare function PlayerCard({ username, userId, href, avatarUrl, coverUrl, countryCode, countryName, team, supporter, supporterLabel, status, statusText, statusNote, className, ...props }: PlayerCardProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "../../utils/cx.js";
|
|
3
|
+
import { AutoLink } from "../basics/AutoLink.js";
|
|
4
|
+
import { avatarUrl as defaultAvatarUrl, countryName as defaultCountryName, flagUrl, isAnimatedImage, normalizeCountryCode, profileUrl, } from "./playerLinks.js";
|
|
5
|
+
/** The first letter or digit of a name, for the stand-in avatar. */
|
|
6
|
+
function initialOf(name) {
|
|
7
|
+
return name.match(/[\p{L}\p{N}]/u)?.[0]?.toUpperCase() ?? "?";
|
|
8
|
+
}
|
|
9
|
+
/** Whether a status line has something to show: not null, undefined, a boolean or "". */
|
|
10
|
+
function shown(node) {
|
|
11
|
+
return node != null && typeof node !== "boolean" && node !== "";
|
|
12
|
+
}
|
|
13
|
+
/** The supporter heart: osu!'s pink-circle badge with a white heart. */
|
|
14
|
+
function SupporterHeart({ label }) {
|
|
15
|
+
return (_jsx("span", { role: "img", "aria-label": label, className: "inline-flex size-[26px] shrink-0 items-center justify-center rounded-full bg-h2 text-c1", children: _jsx("svg", { "aria-hidden": "true", viewBox: "0 0 24 24", className: "size-3.5", fill: "currentColor", children: _jsx("path", { d: "M12 21s-7.5-4.6-10-9.3C.3 8.4 2.2 4.5 6 4.5c2.2 0 3.6 1.2 4.5 2.5l1.5 2 1.5-2c.9-1.3 2.3-2.5 4.5-2.5 3.8 0 5.7 3.9 4 7.2C19.5 16.4 12 21 12 21z" }) }) }));
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* @function PlayerCard
|
|
19
|
+
* @param props {PlayerCardProps} the player's snapshot (name, id, cover, flags, supporter,
|
|
20
|
+
* status) and native div props
|
|
21
|
+
* @returns {JSX.Element} a 120px osu!-web user card; the whole card links to the profile when it
|
|
22
|
+
* has a link
|
|
23
|
+
*/
|
|
24
|
+
export function PlayerCard({ username, userId, href, avatarUrl, coverUrl, countryCode, countryName, team, supporter = false, supporterLabel = "osu! supporter", status, statusText, statusNote, className, ...props }) {
|
|
25
|
+
const link = href === null ? null : (href ?? (userId === undefined ? null : profileUrl(userId)));
|
|
26
|
+
const avatar = avatarUrl ?? (userId === undefined ? null : defaultAvatarUrl(userId));
|
|
27
|
+
const code = normalizeCountryCode(countryCode);
|
|
28
|
+
const flag = flagUrl(countryCode);
|
|
29
|
+
const mainLine = shown(statusText)
|
|
30
|
+
? statusText
|
|
31
|
+
: status === "online"
|
|
32
|
+
? "Online"
|
|
33
|
+
: status
|
|
34
|
+
? "Offline"
|
|
35
|
+
: null;
|
|
36
|
+
const note = shown(statusNote) ? statusNote : null;
|
|
37
|
+
const hasStatusRow = status !== undefined || mainLine !== null || note !== null;
|
|
38
|
+
return (_jsxs("div", { className: cx("relative isolate flex h-[120px] flex-col justify-between overflow-hidden rounded-[10px] bg-b4 text-c1", link &&
|
|
39
|
+
"hover:outline-2 hover:outline-c3 has-[a:focus-visible]:outline-2 has-[a:focus-visible]:outline-h1", className), ...props, children: [coverUrl ? (_jsx("img", { src: coverUrl, alt: "", loading: "lazy", decoding: "async", className: cx("absolute inset-0 -z-10 size-full object-cover", isAnimatedImage(coverUrl) && "motion-reduce:hidden") })) : null, coverUrl ? _jsx("div", { "aria-hidden": "true", className: "absolute inset-0 -z-10 bg-b5/80" }) : null, _jsxs("div", { className: "flex gap-2.5 p-2.5", children: [avatar ? (_jsx("img", { src: avatar, alt: "", width: 60, height: 60, loading: "lazy", decoding: "async", className: "size-[60px] shrink-0 rounded-md bg-b4 object-cover" })) : (_jsx("span", { "aria-hidden": "true", className: "flex size-[60px] shrink-0 items-center justify-center rounded-md bg-b3 font-bold text-2xl text-c3", children: initialOf(username) })), _jsxs("div", { className: "grid min-w-0 flex-1 grid-rows-[26px_1fr]", children: [_jsxs("div", { className: "flex h-[26px] items-center gap-1.5", children: [flag && code ? (_jsx("img", { src: flag, alt: countryName ?? defaultCountryName(code), width: 36, height: 26, loading: "lazy", decoding: "async", className: "h-[26px] w-auto shrink-0" })) : null, team ? (_jsx("img", { src: team.flagUrl, alt: team.name, width: 52, height: 26, loading: "lazy", decoding: "async", className: "h-[26px] w-[52px] shrink-0 rounded-sm object-cover" })) : null, supporter ? _jsx(SupporterHeart, { label: supporterLabel }) : null] }), _jsx("div", { className: "flex min-w-0 items-center", children: link ? (_jsx(AutoLink, { href: link, className: "truncate font-semibold text-base text-c1 after:absolute after:inset-0", children: username })) : (_jsx("span", { className: "truncate font-semibold text-base", children: username })) })] })] }), hasStatusRow ? (_jsxs("div", { className: "flex items-center gap-2.5 px-2.5 pb-2.5", children: [_jsx("div", { className: "flex w-[60px] shrink-0 justify-center", children: status ? (_jsx("span", { "aria-hidden": "true", className: cx("size-[25px] rounded-full border-4", status === "online" ? "border-lime-400" : "border-b6") })) : null }), _jsxs("div", { className: "flex min-w-0 flex-col", children: [note !== null ? _jsx("span", { className: "truncate text-c2 text-xs", children: note }) : null, mainLine !== null ? _jsx("span", { className: "truncate text-sm", children: mainLine }) : null] })] })) : null] }));
|
|
40
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/osu/playerLinks.ts
|
|
3
|
+
* @desc The osu! URLs and names PlayerCard builds from plain props: profile and avatar from a
|
|
4
|
+
* user id, osu-web's own country flag SVG from a two-letter code, the country's English
|
|
5
|
+
* name for the flag's alt text, and whether a cover is an animated GIF. Pure, server-safe.
|
|
6
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
7
|
+
* @created Sun Oct 4, 2026
|
|
8
|
+
* @modified Sun Oct 4, 2026
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* @function profileUrl
|
|
12
|
+
* @param id {number} the osu! user id
|
|
13
|
+
* @returns {string} the user's osu! profile page
|
|
14
|
+
*/
|
|
15
|
+
export declare const profileUrl: (id: number) => string;
|
|
16
|
+
/**
|
|
17
|
+
* @function avatarUrl
|
|
18
|
+
* @param id {number} the osu! user id
|
|
19
|
+
* @returns {string} the user's current avatar on a.ppy.sh (osu! serves the default for none)
|
|
20
|
+
*/
|
|
21
|
+
export declare const avatarUrl: (id: number) => string;
|
|
22
|
+
/**
|
|
23
|
+
* @function normalizeCountryCode
|
|
24
|
+
* @param code {string | undefined} an ISO 3166-1 alpha-2 code, any case, maybe padded
|
|
25
|
+
* @returns {string | null} the code in capitals, or null when it isn't two ASCII letters
|
|
26
|
+
*/
|
|
27
|
+
export declare function normalizeCountryCode(code: string | undefined): string | null;
|
|
28
|
+
/**
|
|
29
|
+
* @function flagUrl
|
|
30
|
+
* @param code {string | undefined} an ISO 3166-1 alpha-2 code
|
|
31
|
+
* @returns {string | null} osu-web's flag SVG (named by the flag emoji's regional-indicator code
|
|
32
|
+
* points, lowercase hex joined by "-"), or null for anything but two ASCII letters
|
|
33
|
+
*/
|
|
34
|
+
export declare function flagUrl(code: string | undefined): string | null;
|
|
35
|
+
/**
|
|
36
|
+
* @function countryName
|
|
37
|
+
* @param code {string} an ISO 3166-1 alpha-2 code
|
|
38
|
+
* @returns {string} the region's English name from Intl, or the code in capitals when Intl
|
|
39
|
+
* doesn't know it
|
|
40
|
+
*/
|
|
41
|
+
export declare function countryName(code: string): string;
|
|
42
|
+
/**
|
|
43
|
+
* @function isAnimatedImage
|
|
44
|
+
* @param url {string} an image URL
|
|
45
|
+
* @returns {boolean} true when the path ends in .gif (before any query or hash)
|
|
46
|
+
*/
|
|
47
|
+
export declare const isAnimatedImage: (url: string) => boolean;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/osu/playerLinks.ts
|
|
3
|
+
* @desc The osu! URLs and names PlayerCard builds from plain props: profile and avatar from a
|
|
4
|
+
* user id, osu-web's own country flag SVG from a two-letter code, the country's English
|
|
5
|
+
* name for the flag's alt text, and whether a cover is an animated GIF. Pure, server-safe.
|
|
6
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
7
|
+
* @created Sun Oct 4, 2026
|
|
8
|
+
* @modified Sun Oct 4, 2026
|
|
9
|
+
*/
|
|
10
|
+
const REGIONAL_INDICATOR_A = 0x1f1e6;
|
|
11
|
+
/**
|
|
12
|
+
* @function profileUrl
|
|
13
|
+
* @param id {number} the osu! user id
|
|
14
|
+
* @returns {string} the user's osu! profile page
|
|
15
|
+
*/
|
|
16
|
+
export const profileUrl = (id) => `https://osu.ppy.sh/users/${id}`;
|
|
17
|
+
/**
|
|
18
|
+
* @function avatarUrl
|
|
19
|
+
* @param id {number} the osu! user id
|
|
20
|
+
* @returns {string} the user's current avatar on a.ppy.sh (osu! serves the default for none)
|
|
21
|
+
*/
|
|
22
|
+
export const avatarUrl = (id) => `https://a.ppy.sh/${id}`;
|
|
23
|
+
/**
|
|
24
|
+
* @function normalizeCountryCode
|
|
25
|
+
* @param code {string | undefined} an ISO 3166-1 alpha-2 code, any case, maybe padded
|
|
26
|
+
* @returns {string | null} the code in capitals, or null when it isn't two ASCII letters
|
|
27
|
+
*/
|
|
28
|
+
export function normalizeCountryCode(code) {
|
|
29
|
+
const upper = code?.trim().toUpperCase() ?? "";
|
|
30
|
+
return /^[A-Z]{2}$/.test(upper) ? upper : null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* @function flagUrl
|
|
34
|
+
* @param code {string | undefined} an ISO 3166-1 alpha-2 code
|
|
35
|
+
* @returns {string | null} osu-web's flag SVG (named by the flag emoji's regional-indicator code
|
|
36
|
+
* points, lowercase hex joined by "-"), or null for anything but two ASCII letters
|
|
37
|
+
*/
|
|
38
|
+
export function flagUrl(code) {
|
|
39
|
+
const upper = normalizeCountryCode(code);
|
|
40
|
+
if (!upper)
|
|
41
|
+
return null;
|
|
42
|
+
const points = [...upper].map((letter) => (REGIONAL_INDICATOR_A + letter.charCodeAt(0) - 65).toString(16));
|
|
43
|
+
return `https://osu.ppy.sh/assets/images/flags/${points.join("-")}.svg`;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* @function countryName
|
|
47
|
+
* @param code {string} an ISO 3166-1 alpha-2 code
|
|
48
|
+
* @returns {string} the region's English name from Intl, or the code in capitals when Intl
|
|
49
|
+
* doesn't know it
|
|
50
|
+
*/
|
|
51
|
+
export function countryName(code) {
|
|
52
|
+
const upper = code.trim().toUpperCase();
|
|
53
|
+
try {
|
|
54
|
+
return new Intl.DisplayNames(["en"], { type: "region" }).of(upper) ?? upper;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return upper;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* @function isAnimatedImage
|
|
62
|
+
* @param url {string} an image URL
|
|
63
|
+
* @returns {boolean} true when the path ends in .gif (before any query or hash)
|
|
64
|
+
*/
|
|
65
|
+
export const isAnimatedImage = (url) => /\.gif(?:[?#]|$)/i.test(url);
|
package/dist/index.d.ts
CHANGED
|
@@ -49,6 +49,7 @@ export { HaruhimeWordmarkLink, type HaruhimeWordmarkLinkProps, } from "./compone
|
|
|
49
49
|
export { JsonLd, type JsonLdProps } from "./components/meta/JsonLd.js";
|
|
50
50
|
export { type BeatmapStatKey, BeatmapStats, type BeatmapStatsProps, } from "./components/osu/BeatmapStats.js";
|
|
51
51
|
export { ModBadge, type ModBadgeProps } from "./components/osu/ModBadge.js";
|
|
52
|
+
export { PlayerCard, type PlayerCardProps, type PlayerTeam, } from "./components/osu/PlayerCard.js";
|
|
52
53
|
export { StarRating, type StarRatingProps } from "./components/osu/StarRating.js";
|
|
53
54
|
export { CommandPalette } from "./components/palette/CommandPalette.js";
|
|
54
55
|
export { CommandPaletteButton, type CommandPaletteButtonProps, } from "./components/palette/CommandPaletteButton.js";
|
package/dist/index.js
CHANGED
|
@@ -54,6 +54,7 @@ export { JsonLd } from "./components/meta/JsonLd.js";
|
|
|
54
54
|
// osu!
|
|
55
55
|
export { BeatmapStats, } from "./components/osu/BeatmapStats.js";
|
|
56
56
|
export { ModBadge } from "./components/osu/ModBadge.js";
|
|
57
|
+
export { PlayerCard, } from "./components/osu/PlayerCard.js";
|
|
57
58
|
export { StarRating } from "./components/osu/StarRating.js";
|
|
58
59
|
// Palette
|
|
59
60
|
export { CommandPalette } from "./components/palette/CommandPalette.js";
|
package/dist/mdx.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/mdx.ts
|
|
3
|
+
* @desc The `@haruhimemoe/ui/mdx` subpath: MDX element overrides and the pieces apps wire them
|
|
4
|
+
* up with. Every export here is a server component or a plain server-safe function; the
|
|
5
|
+
* only client code (CodeCopyButton) is rendered internally by CodeBlock, never exported.
|
|
6
|
+
* `CodeBlock` colorizes code only when the app installs `shiki` (an optional peer
|
|
7
|
+
* dependency) and imports `@haruhimemoe/ui/shiki` once; otherwise code renders as plain,
|
|
8
|
+
* unstyled text.
|
|
9
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
10
|
+
* @created Sat Oct 3, 2026
|
|
11
|
+
* @modified Sat Oct 3, 2026
|
|
12
|
+
*/
|
|
13
|
+
export { Callout, type CalloutProps, type CalloutType } from "./components/mdx/Callout.js";
|
|
14
|
+
export { CodeBlock, type CodeBlockProps } from "./components/mdx/CodeBlock.js";
|
|
15
|
+
export { mdxComponents } from "./components/mdx/mdxComponents.js";
|
|
16
|
+
export { type CodeMeta, parseCodeMeta } from "./components/mdx/parseCodeMeta.js";
|
|
17
|
+
export { slugify } from "./remark/slugify.js";
|
package/dist/mdx.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/mdx.ts
|
|
3
|
+
* @desc The `@haruhimemoe/ui/mdx` subpath: MDX element overrides and the pieces apps wire them
|
|
4
|
+
* up with. Every export here is a server component or a plain server-safe function; the
|
|
5
|
+
* only client code (CodeCopyButton) is rendered internally by CodeBlock, never exported.
|
|
6
|
+
* `CodeBlock` colorizes code only when the app installs `shiki` (an optional peer
|
|
7
|
+
* dependency) and imports `@haruhimemoe/ui/shiki` once; otherwise code renders as plain,
|
|
8
|
+
* unstyled text.
|
|
9
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
10
|
+
* @created Sat Oct 3, 2026
|
|
11
|
+
* @modified Sat Oct 3, 2026
|
|
12
|
+
*/
|
|
13
|
+
export { Callout } from "./components/mdx/Callout.js";
|
|
14
|
+
export { CodeBlock } from "./components/mdx/CodeBlock.js";
|
|
15
|
+
export { mdxComponents } from "./components/mdx/mdxComponents.js";
|
|
16
|
+
export { parseCodeMeta } from "./components/mdx/parseCodeMeta.js";
|
|
17
|
+
export { slugify } from "./remark/slugify.js";
|