@haruhimemoe/ui 0.7.0 → 0.9.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.
Files changed (89) hide show
  1. package/CHANGELOG.md +31 -1
  2. package/README.md +246 -11
  3. package/dist/components/basics/Disclosure.d.ts +1 -1
  4. package/dist/components/basics/Disclosure.js +2 -2
  5. package/dist/components/basics/Prose.d.ts +9 -6
  6. package/dist/components/basics/Prose.js +4 -4
  7. package/dist/components/forms/Checkbox.d.ts +1 -1
  8. package/dist/components/forms/Checkbox.js +3 -1
  9. package/dist/components/forms/RadioGroup.js +3 -1
  10. package/dist/components/mdx/Callout.d.ts +24 -0
  11. package/dist/components/mdx/Callout.js +22 -0
  12. package/dist/components/mdx/CodeBlock.d.ts +26 -0
  13. package/dist/components/mdx/CodeBlock.js +48 -0
  14. package/dist/components/mdx/CodeCopyButton.d.ts +21 -0
  15. package/dist/components/mdx/CodeCopyButton.js +34 -0
  16. package/dist/components/mdx/MdxBlockquote.d.ts +23 -0
  17. package/dist/components/mdx/MdxBlockquote.js +17 -0
  18. package/dist/components/mdx/MdxHeading.d.ts +26 -0
  19. package/dist/components/mdx/MdxHeading.js +23 -0
  20. package/dist/components/mdx/MdxLink.d.ts +29 -0
  21. package/dist/components/mdx/MdxLink.js +26 -0
  22. package/dist/components/mdx/MdxPre.d.ts +25 -0
  23. package/dist/components/mdx/MdxPre.js +39 -0
  24. package/dist/components/mdx/MdxTable.d.ts +20 -0
  25. package/dist/components/mdx/MdxTable.js +23 -0
  26. package/dist/components/mdx/highlighter.d.ts +37 -0
  27. package/dist/components/mdx/highlighter.js +66 -0
  28. package/dist/components/mdx/mdxComponents.d.ts +21 -0
  29. package/dist/components/mdx/mdxComponents.js +22 -0
  30. package/dist/components/mdx/parseCodeMeta.d.ts +21 -0
  31. package/dist/components/mdx/parseCodeMeta.js +40 -0
  32. package/dist/components/mdx/textOf.d.ts +17 -0
  33. package/dist/components/mdx/textOf.js +28 -0
  34. package/dist/components/palette/CommandPalette.d.ts +21 -0
  35. package/dist/components/palette/CommandPalette.js +315 -0
  36. package/dist/components/palette/CommandPaletteButton.d.ts +21 -0
  37. package/dist/components/palette/CommandPaletteButton.js +26 -0
  38. package/dist/components/palette/PaletteFooter.d.ts +21 -0
  39. package/dist/components/palette/PaletteFooter.js +10 -0
  40. package/dist/components/palette/PaletteInput.d.ts +30 -0
  41. package/dist/components/palette/PaletteInput.js +23 -0
  42. package/dist/components/palette/PaletteList.d.ts +29 -0
  43. package/dist/components/palette/PaletteList.js +44 -0
  44. package/dist/components/palette/PaletteRow.d.ts +30 -0
  45. package/dist/components/palette/PaletteRow.js +33 -0
  46. package/dist/components/palette/calc.d.ts +28 -0
  47. package/dist/components/palette/calc.js +226 -0
  48. package/dist/components/palette/fuzzy.d.ts +51 -0
  49. package/dist/components/palette/fuzzy.js +138 -0
  50. package/dist/components/palette/hotkeys.d.ts +57 -0
  51. package/dist/components/palette/hotkeys.js +111 -0
  52. package/dist/components/palette/paletteEvents.d.ts +22 -0
  53. package/dist/components/palette/paletteEvents.js +22 -0
  54. package/dist/components/palette/platform.d.ts +13 -0
  55. package/dist/components/palette/platform.js +21 -0
  56. package/dist/components/palette/recents.d.ts +48 -0
  57. package/dist/components/palette/recents.js +85 -0
  58. package/dist/components/palette/rows.d.ts +63 -0
  59. package/dist/components/palette/rows.js +136 -0
  60. package/dist/components/palette/siteCommands.d.ts +38 -0
  61. package/dist/components/palette/siteCommands.js +152 -0
  62. package/dist/components/palette/store.d.ts +94 -0
  63. package/dist/components/palette/store.js +135 -0
  64. package/dist/components/palette/types.d.ts +102 -0
  65. package/dist/components/palette/types.js +11 -0
  66. package/dist/components/palette/useProviderSearch.d.ts +28 -0
  67. package/dist/components/palette/useProviderSearch.js +68 -0
  68. package/dist/components/shell/HeaderMenu.d.ts +1 -1
  69. package/dist/components/shell/HeaderMenu.js +2 -2
  70. package/dist/index.d.ts +8 -1
  71. package/dist/index.js +8 -1
  72. package/dist/mdx.d.ts +17 -0
  73. package/dist/mdx.js +17 -0
  74. package/dist/remark/callouts.d.ts +20 -0
  75. package/dist/remark/callouts.js +58 -0
  76. package/dist/remark/codeMeta.d.ts +17 -0
  77. package/dist/remark/codeMeta.js +22 -0
  78. package/dist/remark/headingIds.d.ts +16 -0
  79. package/dist/remark/headingIds.js +28 -0
  80. package/dist/remark/index.d.ts +29 -0
  81. package/dist/remark/index.js +33 -0
  82. package/dist/remark/mdast.d.ts +42 -0
  83. package/dist/remark/mdast.js +37 -0
  84. package/dist/remark/slugify.d.ts +23 -0
  85. package/dist/remark/slugify.js +40 -0
  86. package/dist/shiki.d.ts +13 -0
  87. package/dist/shiki.js +38 -0
  88. package/dist/theme.css +18 -0
  89. package/package.json +34 -4
@@ -0,0 +1,135 @@
1
+ /**
2
+ * @file src/components/palette/store.ts
3
+ * @desc The palette's state and reducer, pure: open or closed, a stack of page frames (each with
4
+ * its own query, active row and provider results, so popping back restores where you
5
+ * were), and the argument prompt in progress. Every key the palette handles is an action
6
+ * here, so the rules test without a DOM. CommandPalette owns it through useReducer.
7
+ * @author David @dvhsh (https://dvh.sh)
8
+ * @created Sat Oct 3, 2026
9
+ * @modified Sat Oct 3, 2026
10
+ */
11
+ const frame = (page) => ({ page, query: "", active: 0, providers: {} });
12
+ /**
13
+ * @function initialState
14
+ * @returns {PaletteState} closed, empty
15
+ */
16
+ export const initialState = () => ({ open: false, stack: [], arg: null });
17
+ /**
18
+ * @function topFrame
19
+ * @param state {PaletteState} the state
20
+ * @returns {Frame | undefined} the page on top, undefined while closed
21
+ */
22
+ export const topFrame = (state) => state.stack[state.stack.length - 1];
23
+ const clamp = (index, count) => count === 0 ? 0 : Math.min(Math.max(index, 0), count - 1);
24
+ const moved = (active, delta, count) => {
25
+ if (count === 0)
26
+ return 0;
27
+ if (delta === "home")
28
+ return 0;
29
+ if (delta === "end")
30
+ return count - 1;
31
+ return (active + delta + count) % count;
32
+ };
33
+ const withTop = (state, patch) => {
34
+ const top = topFrame(state);
35
+ if (!top)
36
+ return state;
37
+ return { ...state, stack: [...state.stack.slice(0, -1), { ...top, ...patch }] };
38
+ };
39
+ const withArg = (state, patch) => state.arg ? { ...state, arg: { ...state.arg, ...patch } } : state;
40
+ /**
41
+ * @function reduce
42
+ * @param state {PaletteState} the state before
43
+ * @param action {PaletteAction} what happened
44
+ * @returns {PaletteState} the state after; the same object when nothing changes
45
+ */
46
+ export function reduce(state, action) {
47
+ switch (action.type) {
48
+ case "open": {
49
+ const stack = [frame(action.root)];
50
+ if (action.page)
51
+ stack.push(frame(action.page));
52
+ return { open: true, stack, arg: null };
53
+ }
54
+ case "close":
55
+ return state.open ? { ...state, open: false } : state;
56
+ case "query":
57
+ return state.arg
58
+ ? withArg(state, { query: action.value, active: 0, error: undefined })
59
+ : withTop(state, { query: action.value, active: 0 });
60
+ case "active": {
61
+ const index = clamp(action.index, action.count);
62
+ const current = state.arg ? state.arg.active : topFrame(state)?.active;
63
+ if (index === current)
64
+ return state;
65
+ return state.arg ? withArg(state, { active: index }) : withTop(state, { active: index });
66
+ }
67
+ case "move": {
68
+ const current = state.arg ? state.arg.active : (topFrame(state)?.active ?? 0);
69
+ const active = moved(current, action.delta, action.count);
70
+ return state.arg ? withArg(state, { active }) : withTop(state, { active });
71
+ }
72
+ case "push":
73
+ return { ...state, stack: [...state.stack, frame(action.page)], arg: null };
74
+ case "pop": {
75
+ if (state.arg) {
76
+ if (state.arg.index === 0)
77
+ return { ...state, arg: null };
78
+ const spec = state.arg.command.args?.[state.arg.index - 1];
79
+ const values = { ...state.arg.values };
80
+ if (spec)
81
+ delete values[spec.name];
82
+ return withArg(state, {
83
+ index: state.arg.index - 1,
84
+ values,
85
+ query: "",
86
+ active: 0,
87
+ error: undefined,
88
+ });
89
+ }
90
+ if (state.stack.length <= 1)
91
+ return state;
92
+ return { ...state, stack: state.stack.slice(0, -1) };
93
+ }
94
+ case "beginArgs":
95
+ return {
96
+ ...state,
97
+ arg: { command: action.command, index: 0, values: {}, query: "", active: 0 },
98
+ };
99
+ case "argAccept": {
100
+ if (!state.arg)
101
+ return state;
102
+ const specs = state.arg.command.args ?? [];
103
+ const spec = specs[state.arg.index];
104
+ if (!spec)
105
+ return { ...state, arg: null };
106
+ const values = { ...state.arg.values, [spec.name]: action.value };
107
+ if (state.arg.index + 1 >= specs.length)
108
+ return { ...state, arg: null };
109
+ return withArg(state, {
110
+ index: state.arg.index + 1,
111
+ values,
112
+ query: "",
113
+ active: 0,
114
+ error: undefined,
115
+ });
116
+ }
117
+ case "argError":
118
+ return withArg(state, { error: action.message });
119
+ case "provided": {
120
+ const target = state.stack[action.depth];
121
+ if (!target)
122
+ return state;
123
+ // A search that starts keeps the previous rows on screen until the new ones land.
124
+ const previous = target.providers[action.providerId];
125
+ const result = action.result.state === "loading" && previous
126
+ ? { ...action.result, rows: previous.rows }
127
+ : action.result;
128
+ const updated = {
129
+ ...target,
130
+ providers: { ...target.providers, [action.providerId]: result },
131
+ };
132
+ return { ...state, stack: state.stack.map((f, i) => (i === action.depth ? updated : f)) };
133
+ }
134
+ }
135
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * @file src/components/palette/types.ts
3
+ * @desc The command palette's data: a Command (leaf with `run`, or nested with `page`, with
4
+ * optional `args` collected first), a Page (static commands and async providers), a
5
+ * Provider (search as you type), an ArgSpec (one prompt), the PaletteContext a command
6
+ * runs with, and CommandPalette's props. Plain types, server-safe.
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
+ /** One row of a "choice" argument. */
13
+ export type ArgChoice = {
14
+ value: string;
15
+ label: string;
16
+ subtitle?: string | undefined;
17
+ };
18
+ /** One prompt collected before a command runs. */
19
+ export type ArgSpec = {
20
+ name: string;
21
+ /** The input's placeholder while collecting it. */
22
+ label: string;
23
+ type: "text" | "number" | "choice";
24
+ /** For "choice": the rows to pick from, fuzzy-filtered by the input. */
25
+ choices?: readonly ArgChoice[] | undefined;
26
+ /** Returns a message to block Enter. */
27
+ validate?: ((value: string) => string | undefined) | undefined;
28
+ };
29
+ /** Rows searched as the query changes. `search` must honor the signal. */
30
+ export type Provider = {
31
+ id: string;
32
+ /** Group heading for its rows. Default: the page title. */
33
+ group?: string | undefined;
34
+ /** Characters before the first search. Default 2. */
35
+ minLength?: number | undefined;
36
+ /** Debounce in ms. Default 200. */
37
+ debounceMs?: number | undefined;
38
+ search: (query: string, signal: AbortSignal) => Promise<readonly Command[]>;
39
+ };
40
+ /** A list the palette can show: static rows, async rows, or both. */
41
+ export type Page = {
42
+ title: string;
43
+ /** Input placeholder. Default "Search…". */
44
+ placeholder?: string | undefined;
45
+ commands?: readonly Command[] | undefined;
46
+ providers?: readonly Provider[] | undefined;
47
+ };
48
+ /** What a command runs with. */
49
+ export type PaletteContext = {
50
+ /** `router.push`. */
51
+ navigate: (href: string) => void;
52
+ close: () => void;
53
+ /** Push a page; opens the palette first when it's closed. */
54
+ push: (page: Page) => void;
55
+ /** Writes to the clipboard and announces "Copied" (or the failure). */
56
+ copy: (text: string) => Promise<void>;
57
+ /** `usePathname()`, "" outside the router. */
58
+ pathname: string;
59
+ /** Every root command, after `when`. */
60
+ commands: readonly Command[];
61
+ };
62
+ /** One row. Exactly one of `run` and `page` is set. */
63
+ export type Command = {
64
+ /** Unique in the app. Recents are stored by it. */
65
+ id: string;
66
+ title: string;
67
+ subtitle?: string | undefined;
68
+ icon?: ReactNode;
69
+ /** Matched at a lower weight than the title. */
70
+ keywords?: readonly string[] | undefined;
71
+ /** Heading the row sits under. Default "Commands". */
72
+ group?: string | undefined;
73
+ /** "mod+shift+c", "?" or a chord "g p". Shown as kbd and active while the palette is mounted. */
74
+ shortcut?: string | undefined;
75
+ /** Hidden when false. Read on every render of the list. */
76
+ when?: ((ctx: PaletteContext) => boolean) | undefined;
77
+ run?: ((ctx: PaletteContext, args: Record<string, string>) => void | Promise<void>) | undefined;
78
+ page?: Page | undefined;
79
+ args?: readonly ArgSpec[] | undefined;
80
+ /** Close after `run`. Default true. */
81
+ closeOnRun?: boolean | undefined;
82
+ };
83
+ /** CommandPalette's props. */
84
+ export type CommandPaletteProps = {
85
+ commands: readonly Command[];
86
+ /** Searched at the root once the query reaches each one's minLength. */
87
+ providers?: readonly Provider[] | undefined;
88
+ /** Namespaces recents in localStorage. Default "default". */
89
+ storageKey?: string | undefined;
90
+ /** Default "mod+k". */
91
+ hotkey?: string | undefined;
92
+ /** Root placeholder. Default "Search commands…". */
93
+ placeholder?: string | undefined;
94
+ /** The dialog's accessible name. Default "Command palette". */
95
+ label?: string | undefined;
96
+ /** Default true. */
97
+ calculator?: boolean | undefined;
98
+ /** Default true. */
99
+ recents?: boolean | undefined;
100
+ /** Classes for the panel. */
101
+ className?: string | undefined;
102
+ };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @file src/components/palette/types.ts
3
+ * @desc The command palette's data: a Command (leaf with `run`, or nested with `page`, with
4
+ * optional `args` collected first), a Page (static commands and async providers), a
5
+ * Provider (search as you type), an ArgSpec (one prompt), the PaletteContext a command
6
+ * runs with, and CommandPalette's props. Plain types, server-safe.
7
+ * @author David @dvhsh (https://dvh.sh)
8
+ * @created Sat Oct 3, 2026
9
+ * @modified Sat Oct 3, 2026
10
+ */
11
+ export {};
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @file src/components/palette/useProviderSearch.ts
3
+ * @desc Runs a frame's providers as the query changes: each one waits for its minLength,
4
+ * debounces, and gets an AbortSignal that fires when a newer query starts, the frame goes
5
+ * away, or the palette closes. A superseded search's result is dropped even if it
6
+ * resolves. Results go to the store by frame depth and provider id.
7
+ * @author David @dvhsh (https://dvh.sh)
8
+ * @created Sat Oct 3, 2026
9
+ * @modified Sat Oct 3, 2026
10
+ */
11
+ import type { PaletteAction } from "./store.js";
12
+ import type { Provider } from "./types.js";
13
+ /** What the hook needs. */
14
+ export type ProviderSearchArgs = {
15
+ providers: readonly Provider[];
16
+ query: string;
17
+ /** The frame's index in the stack, so results land on it even after a push. */
18
+ depth: number;
19
+ dispatch: (action: PaletteAction) => void;
20
+ /** False while the palette is closed, or the frame isn't on top. */
21
+ enabled: boolean;
22
+ };
23
+ /**
24
+ * @function useProviderSearch
25
+ * @param args {ProviderSearchArgs} providers, the query, the frame depth, dispatch and enabled
26
+ * @returns {void}
27
+ */
28
+ export declare function useProviderSearch({ providers, query, depth, dispatch, enabled, }: ProviderSearchArgs): void;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * @file src/components/palette/useProviderSearch.ts
3
+ * @desc Runs a frame's providers as the query changes: each one waits for its minLength,
4
+ * debounces, and gets an AbortSignal that fires when a newer query starts, the frame goes
5
+ * away, or the palette closes. A superseded search's result is dropped even if it
6
+ * resolves. Results go to the store by frame depth and provider id.
7
+ * @author David @dvhsh (https://dvh.sh)
8
+ * @created Sat Oct 3, 2026
9
+ * @modified Sat Oct 3, 2026
10
+ */
11
+ "use client";
12
+ import { useEffect } from "react";
13
+ const DEFAULT_DEBOUNCE = 200;
14
+ const DEFAULT_MIN = 2;
15
+ /**
16
+ * @function useProviderSearch
17
+ * @param args {ProviderSearchArgs} providers, the query, the frame depth, dispatch and enabled
18
+ * @returns {void}
19
+ */
20
+ export function useProviderSearch({ providers, query, depth, dispatch, enabled, }) {
21
+ useEffect(() => {
22
+ if (!enabled)
23
+ return;
24
+ const trimmed = query.trim();
25
+ const controllers = [];
26
+ const timers = [];
27
+ for (const provider of providers) {
28
+ if (trimmed.length < (provider.minLength ?? DEFAULT_MIN))
29
+ continue;
30
+ const controller = new AbortController();
31
+ controllers.push(controller);
32
+ dispatch({
33
+ type: "provided",
34
+ depth,
35
+ providerId: provider.id,
36
+ result: { state: "loading", rows: [] },
37
+ });
38
+ timers.push(setTimeout(() => {
39
+ provider.search(trimmed, controller.signal).then((rows) => {
40
+ if (controller.signal.aborted)
41
+ return;
42
+ dispatch({
43
+ type: "provided",
44
+ depth,
45
+ providerId: provider.id,
46
+ result: { state: "done", rows },
47
+ });
48
+ }, (error) => {
49
+ if (controller.signal.aborted)
50
+ return;
51
+ console.error(`CommandPalette: provider "${provider.id}" failed`, error);
52
+ dispatch({
53
+ type: "provided",
54
+ depth,
55
+ providerId: provider.id,
56
+ result: { state: "error", rows: [] },
57
+ });
58
+ });
59
+ }, provider.debounceMs ?? DEFAULT_DEBOUNCE));
60
+ }
61
+ return () => {
62
+ for (const timer of timers)
63
+ clearTimeout(timer);
64
+ for (const controller of controllers)
65
+ controller.abort();
66
+ };
67
+ }, [providers, query, depth, dispatch, enabled]);
68
+ }
@@ -6,7 +6,7 @@
6
6
  * a click on a link, or focus leaving it closes it too.
7
7
  * @author David @dvhsh (https://dvh.sh)
8
8
  * @created Mon Sep 28, 2026
9
- * @modified Mon Sep 28, 2026
9
+ * @modified Sat Oct 3, 2026
10
10
  */
11
11
  import { type ComponentProps, type ReactNode } from "react";
12
12
  /** One link in the panel. */
@@ -6,7 +6,7 @@
6
6
  * a click on a link, or focus leaving it closes it too.
7
7
  * @author David @dvhsh (https://dvh.sh)
8
8
  * @created Mon Sep 28, 2026
9
- * @modified Mon Sep 28, 2026
9
+ * @modified Sat Oct 3, 2026
10
10
  */
11
11
  "use client";
12
12
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
@@ -54,5 +54,5 @@ export function HeaderMenu({ label, items = [], children, align = "end", buttonL
54
54
  box?.removeEventListener("focusout", onFocusOut);
55
55
  };
56
56
  }, [open]);
57
- return (_jsxs("div", { ref: wrapper, className: cx("relative", className), ...props, children: [_jsx("button", { ref: button, type: "button", "aria-expanded": open, "aria-controls": panelId, "aria-label": buttonLabel, onClick: () => setOpen(!open), className: cx("flex items-center gap-2 font-bold text-c1 text-sm transition-colors hover:text-h1", buttonClassName), children: label }), _jsxs("div", { id: panelId, hidden: !open, className: cx("absolute z-10 mt-2 w-44 flex-col gap-1 rounded-lg border border-b3 bg-b6 p-2", open && "flex", align === "end" ? "right-0" : "left-0"), children: [items.length > 0 ? (_jsx("ul", { className: "flex flex-col gap-1", children: items.map((item) => (_jsx("li", { children: _jsx(AutoLink, { href: item.href, className: ITEM, onClick: () => setOpen(false), children: item.label }) }, item.href))) })) : null, children] })] }));
57
+ return (_jsxs("div", { ref: wrapper, className: cx("relative", className), ...props, children: [_jsx("button", { ref: button, type: "button", "aria-expanded": open, "aria-controls": panelId, "aria-label": buttonLabel, onClick: () => setOpen(!open), className: cx("flex min-h-6 items-center gap-2 font-bold text-c1 text-sm transition-colors hover:text-h1", buttonClassName), children: label }), _jsxs("div", { id: panelId, hidden: !open, className: cx("absolute z-10 mt-2 w-44 flex-col gap-1 rounded-lg border border-b3 bg-b6 p-2", open && "flex", align === "end" ? "right-0" : "left-0"), children: [items.length > 0 ? (_jsx("ul", { className: "flex flex-col gap-1", children: items.map((item) => (_jsx("li", { children: _jsx(AutoLink, { href: item.href, className: ITEM, onClick: () => setOpen(false), children: item.label }) }, item.href))) })) : null, children] })] }));
58
58
  }
package/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * their own "use client" directive, so this barrel is safe to import from Server Components.
6
6
  * @author David @dvhsh (https://dvh.sh)
7
7
  * @created Wed Sep 23, 2026
8
- * @modified Mon Sep 28, 2026
8
+ * @modified Sat Oct 3, 2026
9
9
  */
10
10
  export { AsyncButton, type AsyncButtonProps } from "./components/actions/AsyncButton.js";
11
11
  export { CopyButton, type CopyButtonProps } from "./components/actions/CopyButton.js";
@@ -50,6 +50,13 @@ 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
52
  export { StarRating, type StarRatingProps } from "./components/osu/StarRating.js";
53
+ export { CommandPalette } from "./components/palette/CommandPalette.js";
54
+ export { CommandPaletteButton, type CommandPaletteButtonProps, } from "./components/palette/CommandPaletteButton.js";
55
+ export { evaluate, formatResult } from "./components/palette/calc.js";
56
+ export { type FuzzyMatch, fuzzyScore } from "./components/palette/fuzzy.js";
57
+ export { openCommandPalette } from "./components/palette/paletteEvents.js";
58
+ export { type SiteCommandsOptions, siteCommands } from "./components/palette/siteCommands.js";
59
+ export type { ArgChoice, ArgSpec, Command, CommandPaletteProps, Page, PaletteContext, Provider, } from "./components/palette/types.js";
53
60
  export { HeaderMenu, type HeaderMenuItem, type HeaderMenuProps, } from "./components/shell/HeaderMenu.js";
54
61
  export { HARUHIME_TOOLS, type HaruhimeTool, type HaruhimeToolId, type HaruhimeToolsOptions, haruhimeToolsColumn, } from "./components/shell/haruhimeTools.js";
55
62
  export { type LinkTabItem, LinkTabs, type LinkTabsProps } from "./components/shell/LinkTabs.js";
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * their own "use client" directive, so this barrel is safe to import from Server Components.
6
6
  * @author David @dvhsh (https://dvh.sh)
7
7
  * @created Wed Sep 23, 2026
8
- * @modified Mon Sep 28, 2026
8
+ * @modified Sat Oct 3, 2026
9
9
  */
10
10
  // Actions
11
11
  export { AsyncButton } from "./components/actions/AsyncButton.js";
@@ -55,6 +55,13 @@ export { JsonLd } from "./components/meta/JsonLd.js";
55
55
  export { BeatmapStats, } from "./components/osu/BeatmapStats.js";
56
56
  export { ModBadge } from "./components/osu/ModBadge.js";
57
57
  export { StarRating } from "./components/osu/StarRating.js";
58
+ // Palette
59
+ export { CommandPalette } from "./components/palette/CommandPalette.js";
60
+ export { CommandPaletteButton, } from "./components/palette/CommandPaletteButton.js";
61
+ export { evaluate, formatResult } from "./components/palette/calc.js";
62
+ export { fuzzyScore } from "./components/palette/fuzzy.js";
63
+ export { openCommandPalette } from "./components/palette/paletteEvents.js";
64
+ export { siteCommands } from "./components/palette/siteCommands.js";
58
65
  // Shell
59
66
  export { HeaderMenu, } from "./components/shell/HeaderMenu.js";
60
67
  export { HARUHIME_TOOLS, haruhimeToolsColumn, } from "./components/shell/haruhimeTools.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";
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @file src/remark/callouts.ts
3
+ * @desc A remark plugin for GitHub-style callouts: a blockquote whose first paragraph starts
4
+ * with `[!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]` or `[!CAUTION]` (case-insensitive)
5
+ * gets `data.hProperties.dataCallout` set to one of three rendered types, and the marker
6
+ * text is stripped from the paragraph (the paragraph itself is dropped if the marker was
7
+ * its only content).
8
+ * @author David @dvhsh (https://dvh.sh)
9
+ * @created Sat Oct 3, 2026
10
+ * @modified Sat Oct 3, 2026
11
+ */
12
+ import { type MdNode } from "./mdast.js";
13
+ /** The three callout types this plugin renders. */
14
+ export type CalloutType = "note" | "tip" | "warning";
15
+ /**
16
+ * @function remarkCallouts
17
+ * @returns {(tree: MdNode) => void} a remark transformer that types matching blockquotes and
18
+ * strips their marker text
19
+ */
20
+ export declare const remarkCallouts: () => (tree: MdNode) => void;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * @file src/remark/callouts.ts
3
+ * @desc A remark plugin for GitHub-style callouts: a blockquote whose first paragraph starts
4
+ * with `[!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]` or `[!CAUTION]` (case-insensitive)
5
+ * gets `data.hProperties.dataCallout` set to one of three rendered types, and the marker
6
+ * text is stripped from the paragraph (the paragraph itself is dropped if the marker was
7
+ * its only content).
8
+ * @author David @dvhsh (https://dvh.sh)
9
+ * @created Sat Oct 3, 2026
10
+ * @modified Sat Oct 3, 2026
11
+ */
12
+ import { setProperty, walk } from "./mdast.js";
13
+ const TYPES = {
14
+ note: "note",
15
+ tip: "tip",
16
+ important: "tip",
17
+ warning: "warning",
18
+ caution: "warning",
19
+ };
20
+ const MARKER = /^\[!(note|tip|important|warning|caution)\][ \t]*(?:\r?\n)?/i;
21
+ /**
22
+ * @function remarkCallouts
23
+ * @returns {(tree: MdNode) => void} a remark transformer that types matching blockquotes and
24
+ * strips their marker text
25
+ */
26
+ export const remarkCallouts = () => (tree) => {
27
+ walk(tree, (node) => {
28
+ if (node.type !== "blockquote")
29
+ return;
30
+ const paragraph = node.children?.[0];
31
+ if (paragraph?.type !== "paragraph" || !paragraph.children)
32
+ return;
33
+ // A parser may split "[!NOTE]" over several text nodes: join the leading run first.
34
+ let end = 0;
35
+ while (paragraph.children[end]?.type === "text")
36
+ end++;
37
+ if (end === 0)
38
+ return;
39
+ const text = paragraph.children
40
+ .slice(0, end)
41
+ .map((child) => child.value ?? "")
42
+ .join("");
43
+ const match = MARKER.exec(text);
44
+ if (!match)
45
+ return;
46
+ const key = match[1].toLowerCase();
47
+ const calloutType = TYPES[key];
48
+ if (!calloutType)
49
+ return;
50
+ setProperty(node, "dataCallout", calloutType);
51
+ const rest = text.slice(match[0].length);
52
+ paragraph.children.splice(0, end, ...(rest ? [{ type: "text", value: rest }] : []));
53
+ if (paragraph.children[0]?.type === "break")
54
+ paragraph.children.shift();
55
+ if (paragraph.children.length === 0)
56
+ node.children?.shift();
57
+ });
58
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @file src/remark/codeMeta.ts
3
+ * @desc A remark plugin that copies a fenced code block's meta string (the text after the
4
+ * language on the opening fence, e.g. `title="a.ts" {2}`) onto the node as
5
+ * `data.hProperties.dataMeta`, so mdast-util-to-hast renders it as `data-meta` and
6
+ * CodeBlock can read it client-side.
7
+ * @author David @dvhsh (https://dvh.sh)
8
+ * @created Sat Oct 3, 2026
9
+ * @modified Sat Oct 3, 2026
10
+ */
11
+ import { type MdNode } from "./mdast.js";
12
+ /**
13
+ * @function remarkCodeMeta
14
+ * @returns {(tree: MdNode) => void} a remark transformer that sets `dataMeta` on every code node
15
+ * that has a non-empty `meta` string
16
+ */
17
+ export declare const remarkCodeMeta: () => (tree: MdNode) => void;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @file src/remark/codeMeta.ts
3
+ * @desc A remark plugin that copies a fenced code block's meta string (the text after the
4
+ * language on the opening fence, e.g. `title="a.ts" {2}`) onto the node as
5
+ * `data.hProperties.dataMeta`, so mdast-util-to-hast renders it as `data-meta` and
6
+ * CodeBlock can read it client-side.
7
+ * @author David @dvhsh (https://dvh.sh)
8
+ * @created Sat Oct 3, 2026
9
+ * @modified Sat Oct 3, 2026
10
+ */
11
+ import { setProperty, walk } from "./mdast.js";
12
+ /**
13
+ * @function remarkCodeMeta
14
+ * @returns {(tree: MdNode) => void} a remark transformer that sets `dataMeta` on every code node
15
+ * that has a non-empty `meta` string
16
+ */
17
+ export const remarkCodeMeta = () => (tree) => {
18
+ walk(tree, (node) => {
19
+ if (node.type === "code" && node.meta)
20
+ setProperty(node, "dataMeta", node.meta);
21
+ });
22
+ };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @file src/remark/headingIds.ts
3
+ * @desc A remark plugin that gives every depth-2 and depth-3 heading a GitHub-style id slugged
4
+ * from its text, deduplicated across the document. A heading that already has an id
5
+ * (`data.hProperties.id`) keeps it, and a heading whose text slugs to an empty string gets
6
+ * no id at all.
7
+ * @author David @dvhsh (https://dvh.sh)
8
+ * @created Sat Oct 3, 2026
9
+ * @modified Sat Oct 3, 2026
10
+ */
11
+ import { type MdNode } from "./mdast.js";
12
+ /**
13
+ * @function remarkHeadingIds
14
+ * @returns {(tree: MdNode) => void} a remark transformer that ids depth-2/3 headings
15
+ */
16
+ export declare const remarkHeadingIds: () => (tree: MdNode) => void;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @file src/remark/headingIds.ts
3
+ * @desc A remark plugin that gives every depth-2 and depth-3 heading a GitHub-style id slugged
4
+ * from its text, deduplicated across the document. A heading that already has an id
5
+ * (`data.hProperties.id`) keeps it, and a heading whose text slugs to an empty string gets
6
+ * no id at all.
7
+ * @author David @dvhsh (https://dvh.sh)
8
+ * @created Sat Oct 3, 2026
9
+ * @modified Sat Oct 3, 2026
10
+ */
11
+ import { setProperty, textContent, walk } from "./mdast.js";
12
+ import { createSlugger } from "./slugify.js";
13
+ /**
14
+ * @function remarkHeadingIds
15
+ * @returns {(tree: MdNode) => void} a remark transformer that ids depth-2/3 headings
16
+ */
17
+ export const remarkHeadingIds = () => (tree) => {
18
+ const slug = createSlugger();
19
+ walk(tree, (node) => {
20
+ if (node.type !== "heading" || (node.depth !== 2 && node.depth !== 3))
21
+ return;
22
+ if (node.data?.hProperties?.id)
23
+ return;
24
+ const id = slug(textContent(node));
25
+ if (id)
26
+ setProperty(node, "id", id);
27
+ });
28
+ };
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @file src/remark/index.ts
3
+ * @desc The public entry point for `@haruhimemoe/ui/remark`: one remark plugin for MDX and
4
+ * react-markdown that combines code meta, callouts and heading ids, plus the individual
5
+ * plugins and the slug helpers for callers who want only one piece.
6
+ * @author David @dvhsh (https://dvh.sh)
7
+ * @created Sat Oct 3, 2026
8
+ * @modified Sat Oct 3, 2026
9
+ */
10
+ import type { MdNode } from "./mdast.js";
11
+ export type { CalloutType } from "./callouts.js";
12
+ export { remarkCallouts } from "./callouts.js";
13
+ export { remarkCodeMeta } from "./codeMeta.js";
14
+ export { remarkHeadingIds } from "./headingIds.js";
15
+ export { createSlugger, slugify } from "./slugify.js";
16
+ /** Which of the three plugins `remarkHaruhime` runs; each defaults to on. */
17
+ export type RemarkHaruhimeOptions = {
18
+ codeMeta?: boolean;
19
+ callouts?: boolean;
20
+ headingIds?: boolean;
21
+ };
22
+ /**
23
+ * @function remarkHaruhime
24
+ * @param options {RemarkHaruhimeOptions} per-plugin opt-outs; omit to run all three
25
+ * @returns {(tree: MdNode) => void} a remark transformer running code meta, callouts and heading
26
+ * ids over one tree. A default export, because Turbopack only takes MDX plugins by
27
+ * module name: `remarkPlugins: ["@haruhimemoe/ui/remark"]`.
28
+ */
29
+ export default function remarkHaruhime(options?: RemarkHaruhimeOptions): (tree: MdNode) => void;