@suzumiyaaoba/mdxr 0.1.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/LICENSE +21 -0
- package/README.md +56 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +2902 -0
- package/dist/components.d.mts +791 -0
- package/dist/components.mjs +2 -0
- package/dist/config-SI9IyFiC.mjs +184 -0
- package/dist/doc-context-CEqzMYKv.d.mts +568 -0
- package/dist/index.d.mts +20 -0
- package/dist/index.mjs +4 -0
- package/dist/ui-GKFD0mx5.mjs +15986 -0
- package/package.json +131 -0
- package/skill/SKILL.md +49 -0
- package/skill/references/components/charts.md +172 -0
- package/skill/references/components/document.md +98 -0
- package/skill/references/components/forms.md +35 -0
- package/skill/references/components/investigation.md +198 -0
- package/skill/references/components/layout.md +68 -0
- package/skill/references/components/output.md +158 -0
- package/skill/references/components/planning.md +155 -0
- package/skill/references/components/reports.md +252 -0
- package/skill/references/components/shadcn.md +19 -0
- package/skill/references/components.md +186 -0
- package/skill/references/extending.md +48 -0
- package/src/ask-sheet.ts +40 -0
- package/src/assets/css.ts +392 -0
- package/src/assets/scripts.ts +78 -0
- package/src/catalog.ts +287 -0
- package/src/cli.ts +178 -0
- package/src/client/doc-events.ts +1108 -0
- package/src/client/entry.ts +27 -0
- package/src/client-js.ts +47 -0
- package/src/component-map.ts +47 -0
- package/src/components/ui/accordion.tsx +77 -0
- package/src/components/ui/alert-dialog.tsx +185 -0
- package/src/components/ui/alert.tsx +76 -0
- package/src/components/ui/aspect-ratio.tsx +22 -0
- package/src/components/ui/attachment.tsx +208 -0
- package/src/components/ui/avatar.tsx +106 -0
- package/src/components/ui/badge.tsx +52 -0
- package/src/components/ui/breadcrumb.tsx +121 -0
- package/src/components/ui/bubble.tsx +128 -0
- package/src/components/ui/button-group.tsx +88 -0
- package/src/components/ui/button.tsx +58 -0
- package/src/components/ui/calendar.tsx +226 -0
- package/src/components/ui/card.tsx +102 -0
- package/src/components/ui/carousel.tsx +246 -0
- package/src/components/ui/chart.tsx +379 -0
- package/src/components/ui/checkbox.tsx +27 -0
- package/src/components/ui/collapsible.tsx +19 -0
- package/src/components/ui/combobox.tsx +298 -0
- package/src/components/ui/command.tsx +193 -0
- package/src/components/ui/context-menu.tsx +271 -0
- package/src/components/ui/dialog.tsx +159 -0
- package/src/components/ui/direction.tsx +4 -0
- package/src/components/ui/drawer.tsx +227 -0
- package/src/components/ui/dropdown-menu.tsx +269 -0
- package/src/components/ui/empty.tsx +104 -0
- package/src/components/ui/field.tsx +237 -0
- package/src/components/ui/fieldset.tsx +32 -0
- package/src/components/ui/frame.tsx +87 -0
- package/src/components/ui/hover-card.tsx +50 -0
- package/src/components/ui/input-group.tsx +159 -0
- package/src/components/ui/input-otp.tsx +83 -0
- package/src/components/ui/input.tsx +19 -0
- package/src/components/ui/item.tsx +202 -0
- package/src/components/ui/kbd.tsx +26 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/marker.tsx +71 -0
- package/src/components/ui/menubar.tsx +284 -0
- package/src/components/ui/message-scroller.tsx +128 -0
- package/src/components/ui/message.tsx +91 -0
- package/src/components/ui/meter.tsx +80 -0
- package/src/components/ui/native-select.tsx +64 -0
- package/src/components/ui/navigation-menu.tsx +170 -0
- package/src/components/ui/pagination.tsx +133 -0
- package/src/components/ui/popover.tsx +87 -0
- package/src/components/ui/progress.tsx +82 -0
- package/src/components/ui/questionnaire.tsx +328 -0
- package/src/components/ui/radio-group.tsx +35 -0
- package/src/components/ui/resizable.tsx +49 -0
- package/src/components/ui/scroll-area.tsx +50 -0
- package/src/components/ui/select.tsx +201 -0
- package/src/components/ui/separator.tsx +22 -0
- package/src/components/ui/sheet.tsx +135 -0
- package/src/components/ui/sidebar.tsx +730 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +51 -0
- package/src/components/ui/spinner.tsx +16 -0
- package/src/components/ui/switch.tsx +31 -0
- package/src/components/ui/table.tsx +113 -0
- package/src/components/ui/tabs.tsx +82 -0
- package/src/components/ui/textarea.tsx +17 -0
- package/src/components/ui/toast.tsx +229 -0
- package/src/components/ui/toggle-group.tsx +87 -0
- package/src/components/ui/toggle.tsx +43 -0
- package/src/components/ui/tooltip.tsx +65 -0
- package/src/components.ts +78 -0
- package/src/config.ts +56 -0
- package/src/define.ts +122 -0
- package/src/doc-context.ts +20 -0
- package/src/editor.ts +94 -0
- package/src/format-error.ts +78 -0
- package/src/guards.ts +84 -0
- package/src/hooks/use-mobile.ts +21 -0
- package/src/html.ts +91 -0
- package/src/hydrate/export-index.ts +232 -0
- package/src/hydrate/import-scan.ts +169 -0
- package/src/hydrate/plugins.ts +118 -0
- package/src/hydrate/runtime-module.ts +145 -0
- package/src/hydrate-runtime.ts +67 -0
- package/src/hydrate.ts +147 -0
- package/src/index.ts +16 -0
- package/src/init.ts +57 -0
- package/src/langs.ts +148 -0
- package/src/lines.ts +53 -0
- package/src/load-user-module.ts +191 -0
- package/src/mdx.ts +248 -0
- package/src/paths.ts +36 -0
- package/src/rehype/shiki.ts +533 -0
- package/src/remark/alerts.ts +53 -0
- package/src/remark/ast.ts +96 -0
- package/src/remark/callouts.ts +25 -0
- package/src/remark/code-file.ts +85 -0
- package/src/remark/code-meta.ts +21 -0
- package/src/remark/directives.ts +170 -0
- package/src/remark/file-paths.ts +64 -0
- package/src/remark/headings.ts +131 -0
- package/src/remark/no-js.ts +40 -0
- package/src/render.ts +337 -0
- package/src/serve.ts +322 -0
- package/src/styles/globals.css +134 -0
- package/src/styles/shadcn.css +641 -0
- package/src/tailwind.ts +119 -0
- package/src/ui/approvals.tsx +76 -0
- package/src/ui/ask-question.tsx +386 -0
- package/src/ui/ask.tsx +206 -0
- package/src/ui/attrs.ts +51 -0
- package/src/ui/audit.tsx +139 -0
- package/src/ui/bar-chart.tsx +334 -0
- package/src/ui/benchmarks.tsx +143 -0
- package/src/ui/bits.tsx +537 -0
- package/src/ui/board.tsx +173 -0
- package/src/ui/bridge.tsx +207 -0
- package/src/ui/bumps.tsx +178 -0
- package/src/ui/callout.tsx +106 -0
- package/src/ui/changes.tsx +89 -0
- package/src/ui/chart-bits.tsx +52 -0
- package/src/ui/chart.ts +577 -0
- package/src/ui/checks.tsx +203 -0
- package/src/ui/child-index.tsx +44 -0
- package/src/ui/children.ts +43 -0
- package/src/ui/chips.tsx +49 -0
- package/src/ui/cmd.tsx +27 -0
- package/src/ui/columns.tsx +31 -0
- package/src/ui/comments.tsx +544 -0
- package/src/ui/compare.tsx +67 -0
- package/src/ui/decision.tsx +79 -0
- package/src/ui/deps.tsx +78 -0
- package/src/ui/details.tsx +43 -0
- package/src/ui/diff-parse.ts +307 -0
- package/src/ui/diff.tsx +458 -0
- package/src/ui/diffstat.tsx +58 -0
- package/src/ui/due.tsx +65 -0
- package/src/ui/effort.tsx +29 -0
- package/src/ui/endpoints.tsx +118 -0
- package/src/ui/envvars.tsx +96 -0
- package/src/ui/figure.tsx +37 -0
- package/src/ui/file-icon.ts +1020 -0
- package/src/ui/file-link.ts +32 -0
- package/src/ui/file-ref.tsx +39 -0
- package/src/ui/files.tsx +112 -0
- package/src/ui/findings.tsx +85 -0
- package/src/ui/flow.tsx +73 -0
- package/src/ui/funnel.tsx +121 -0
- package/src/ui/gantt.tsx +443 -0
- package/src/ui/gauges.tsx +135 -0
- package/src/ui/glossary.tsx +29 -0
- package/src/ui/graph-layout.ts +149 -0
- package/src/ui/graph-specs.tsx +102 -0
- package/src/ui/graph.tsx +278 -0
- package/src/ui/grid.tsx +119 -0
- package/src/ui/hypothesis.tsx +94 -0
- package/src/ui/icon.tsx +80 -0
- package/src/ui/incident.tsx +130 -0
- package/src/ui/index.ts +342 -0
- package/src/ui/ins-del.tsx +41 -0
- package/src/ui/json.tsx +190 -0
- package/src/ui/layout.ts +9 -0
- package/src/ui/line-chart.tsx +251 -0
- package/src/ui/matrix.tsx +208 -0
- package/src/ui/meta.tsx +73 -0
- package/src/ui/option.tsx +64 -0
- package/src/ui/owner.tsx +40 -0
- package/src/ui/packages.tsx +104 -0
- package/src/ui/pathway.tsx +96 -0
- package/src/ui/phase.tsx +41 -0
- package/src/ui/pie-chart.tsx +170 -0
- package/src/ui/plan.tsx +77 -0
- package/src/ui/pre.tsx +175 -0
- package/src/ui/priority.tsx +51 -0
- package/src/ui/props.tsx +77 -0
- package/src/ui/quadrant.tsx +172 -0
- package/src/ui/radar.tsx +200 -0
- package/src/ui/ref.tsx +160 -0
- package/src/ui/release.tsx +178 -0
- package/src/ui/req.tsx +42 -0
- package/src/ui/review.tsx +133 -0
- package/src/ui/risk.tsx +67 -0
- package/src/ui/sankey.tsx +287 -0
- package/src/ui/scatter.tsx +237 -0
- package/src/ui/schema.tsx +113 -0
- package/src/ui/score.tsx +105 -0
- package/src/ui/search.tsx +120 -0
- package/src/ui/series.tsx +38 -0
- package/src/ui/severity.tsx +69 -0
- package/src/ui/shadcn.tsx +216 -0
- package/src/ui/spark.tsx +86 -0
- package/src/ui/stack.tsx +57 -0
- package/src/ui/stats.tsx +63 -0
- package/src/ui/status-badge.tsx +66 -0
- package/src/ui/statuspage.tsx +238 -0
- package/src/ui/steps.tsx +74 -0
- package/src/ui/summary.tsx +41 -0
- package/src/ui/symbol-ref.tsx +73 -0
- package/src/ui/terminal.tsx +117 -0
- package/src/ui/tests.tsx +218 -0
- package/src/ui/timeline.tsx +63 -0
- package/src/ui/toc.tsx +56 -0
- package/src/ui/tones.ts +187 -0
- package/src/ui/trace.tsx +69 -0
- package/src/ui/tree.tsx +281 -0
- package/src/ui/treemap.tsx +128 -0
- package/src/ui/venn.tsx +258 -0
- package/src/ui/verdict.tsx +78 -0
- package/src/ui/waterfall.tsx +142 -0
package/src/ui/pre.tsx
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { isValidElement, useContext } from "react";
|
|
2
|
+
import type { ReactElement, ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import type { DocProps } from "../define.js";
|
|
5
|
+
import { textOf } from "../define.js";
|
|
6
|
+
import { DocContext } from "../doc-context.js";
|
|
7
|
+
import { asString, isRecord, nonEmpty } from "../guards.js";
|
|
8
|
+
import { fenceFilename, firstLine, splitPathLines } from "../lines.js";
|
|
9
|
+
import { CaptionBar, CopyButton, MaybeLink, Panel } from "./bits.js";
|
|
10
|
+
import type { DiffHl } from "./diff-parse.js";
|
|
11
|
+
import { parseDiffHl } from "./diff-parse.js";
|
|
12
|
+
import { DiffView } from "./diff.js";
|
|
13
|
+
import { fileIcon } from "./file-icon.js";
|
|
14
|
+
import { Icon } from "./icon.js";
|
|
15
|
+
import { TERMINAL_LANGS, Transcript } from "./terminal.js";
|
|
16
|
+
import { BORDER_CLS, LINK_CLS, SURFACE_CLS } from "./tones.js";
|
|
17
|
+
|
|
18
|
+
/** "src/x.ts:40-52" → { path: "src/x.ts", line: "40" }; labels stay untouched. */
|
|
19
|
+
const splitFileLine = (filename: string): { line?: string; path: string } => {
|
|
20
|
+
const { lines, path } = splitPathLines(filename);
|
|
21
|
+
return { line: firstLine(lines), path };
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Editor link for a code-header filename, when it resolves to a real file. */
|
|
25
|
+
const useFilenameLink = (filename: string | undefined): string | undefined => {
|
|
26
|
+
const { fileLink } = useContext(DocContext);
|
|
27
|
+
if (filename === undefined || fileLink === undefined) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
const target = splitFileLine(filename);
|
|
31
|
+
return nonEmpty(target.path) ? fileLink(target.path, target.line) : undefined;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Filename/language bar for a fenced block; links to the file when it exists. */
|
|
35
|
+
export const CodeHeader = (props: {
|
|
36
|
+
filename?: string;
|
|
37
|
+
lang?: string;
|
|
38
|
+
text: string;
|
|
39
|
+
}): ReactElement => {
|
|
40
|
+
const link = useFilenameLink(props.filename);
|
|
41
|
+
const label = (
|
|
42
|
+
<>
|
|
43
|
+
<Icon
|
|
44
|
+
className="h-3.5 w-3.5"
|
|
45
|
+
name={fileIcon(props.filename ?? props.lang ?? "code")}
|
|
46
|
+
/>
|
|
47
|
+
{props.filename ?? props.lang ?? "code"}
|
|
48
|
+
</>
|
|
49
|
+
);
|
|
50
|
+
return (
|
|
51
|
+
<CaptionBar className="flex items-center justify-between">
|
|
52
|
+
<MaybeLink
|
|
53
|
+
className={`inline-flex items-center gap-1.5 font-mono ${LINK_CLS}`}
|
|
54
|
+
href={link}
|
|
55
|
+
>
|
|
56
|
+
{label}
|
|
57
|
+
</MaybeLink>
|
|
58
|
+
<CopyButton copy={props.text} title="Copy code" />
|
|
59
|
+
</CaptionBar>
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* ```console / ```terminal / ```shellsession fences render as a terminal
|
|
65
|
+
* transcript; `exit="N"` in the fence meta adds an exit-code badge and
|
|
66
|
+
* `title="…"` becomes the window title.
|
|
67
|
+
*/
|
|
68
|
+
const terminalView = (
|
|
69
|
+
lang: string | undefined,
|
|
70
|
+
meta: string,
|
|
71
|
+
text: string,
|
|
72
|
+
filename: string | undefined
|
|
73
|
+
): ReactElement | undefined => {
|
|
74
|
+
if (lang === undefined || !TERMINAL_LANGS.has(lang)) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
const exit =
|
|
78
|
+
/(?:^|\s)exit=(?:"(?<dq>[^"]*)"|'(?<sq>[^']*)'|(?<bare>\S+))/u.exec(
|
|
79
|
+
meta
|
|
80
|
+
)?.groups;
|
|
81
|
+
return (
|
|
82
|
+
<Transcript
|
|
83
|
+
exit={exit?.dq ?? exit?.sq ?? exit?.bare}
|
|
84
|
+
text={text}
|
|
85
|
+
title={filename}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Fences that render as something other than a code block: ```mermaid becomes
|
|
92
|
+
* a diagram (mermaid loads from CDN only when present), console/terminal
|
|
93
|
+
* sessions become transcripts, ```diff/```patch become structured per-file
|
|
94
|
+
* diff cards. Returns undefined for ordinary code fences.
|
|
95
|
+
*/
|
|
96
|
+
const specialView = (
|
|
97
|
+
lang: string | undefined,
|
|
98
|
+
meta: string,
|
|
99
|
+
text: string,
|
|
100
|
+
filename: string | undefined,
|
|
101
|
+
hl: DiffHl | undefined
|
|
102
|
+
): ReactElement | undefined => {
|
|
103
|
+
if (lang === "mermaid") {
|
|
104
|
+
return (
|
|
105
|
+
<pre
|
|
106
|
+
className={`mermaid my-6 flex justify-center rounded-lg border p-4 ${SURFACE_CLS} ${BORDER_CLS}`}
|
|
107
|
+
>
|
|
108
|
+
{text}
|
|
109
|
+
</pre>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
if (lang === "diff" || lang === "patch") {
|
|
113
|
+
// An empty diff fence renders as nothing — fall back to a code block.
|
|
114
|
+
if (text.trim() === "") {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
return <DiffView filename={filename} hl={hl} text={text} />;
|
|
118
|
+
}
|
|
119
|
+
return terminalView(lang, meta, text, filename);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Overrides the `pre` element: fenced code blocks become framed blocks with a
|
|
124
|
+
* filename/language header and a copy button. ```mermaid fences render as
|
|
125
|
+
* diagrams (mermaid is loaded from CDN only when present).
|
|
126
|
+
*/
|
|
127
|
+
export const Pre = (props: DocProps): ReactElement => {
|
|
128
|
+
const code = props.children;
|
|
129
|
+
const codeProps: Record<string, unknown> =
|
|
130
|
+
isRecord(code) && isRecord(code.props) ? code.props : {};
|
|
131
|
+
const lang = /language-(?<lang>[\w-]+)/u.exec(
|
|
132
|
+
asString(codeProps.className) ?? ""
|
|
133
|
+
)?.groups?.lang;
|
|
134
|
+
const meta =
|
|
135
|
+
asString(props.meta) ??
|
|
136
|
+
asString(codeProps.meta) ??
|
|
137
|
+
asString(codeProps.metastring) ??
|
|
138
|
+
"";
|
|
139
|
+
// Boundary-anchored: `data-title="x"` must not yield a filename header.
|
|
140
|
+
const filename = fenceFilename(meta);
|
|
141
|
+
const text = textOf(codeProps.children);
|
|
142
|
+
|
|
143
|
+
const special = specialView(
|
|
144
|
+
lang,
|
|
145
|
+
meta,
|
|
146
|
+
text,
|
|
147
|
+
filename,
|
|
148
|
+
parseDiffHl(asString(codeProps["data-diffhl"]))
|
|
149
|
+
);
|
|
150
|
+
if (special !== undefined) {
|
|
151
|
+
return special;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// `meta`/`metastring` are plumbing for the filename header, not markup —
|
|
155
|
+
// rebuild <code> with just class+children so they don't leak as attributes.
|
|
156
|
+
const cleanCode = isValidElement<{
|
|
157
|
+
children?: ReactNode;
|
|
158
|
+
className?: unknown;
|
|
159
|
+
}>(code) ? (
|
|
160
|
+
<code className={asString(code.props.className)}>
|
|
161
|
+
{code.props.children}
|
|
162
|
+
</code>
|
|
163
|
+
) : (
|
|
164
|
+
code
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<Panel>
|
|
169
|
+
<CodeHeader filename={filename} lang={lang} text={text} />
|
|
170
|
+
<pre className="m-0 overflow-x-auto bg-white p-4 text-sm dark:bg-neutral-950">
|
|
171
|
+
{cleanCode}
|
|
172
|
+
</pre>
|
|
173
|
+
</Panel>
|
|
174
|
+
);
|
|
175
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as v from "valibot";
|
|
2
|
+
|
|
3
|
+
import { defineComponent } from "../define.js";
|
|
4
|
+
import { Icon } from "./icon.js";
|
|
5
|
+
import { TONE } from "./tones.js";
|
|
6
|
+
|
|
7
|
+
export const PRIORITY_LEVELS = ["p0", "p1", "p2", "p3"] as const;
|
|
8
|
+
export type PriorityLevel = (typeof PRIORITY_LEVELS)[number];
|
|
9
|
+
|
|
10
|
+
const STYLES: Record<PriorityLevel, { cls: string; label: string }> = {
|
|
11
|
+
p0: {
|
|
12
|
+
cls: TONE.red,
|
|
13
|
+
label: "P0",
|
|
14
|
+
},
|
|
15
|
+
p1: {
|
|
16
|
+
cls: TONE.orange,
|
|
17
|
+
label: "P1",
|
|
18
|
+
},
|
|
19
|
+
p2: {
|
|
20
|
+
cls: TONE.amber,
|
|
21
|
+
label: "P2",
|
|
22
|
+
},
|
|
23
|
+
p3: {
|
|
24
|
+
cls: TONE.neutral,
|
|
25
|
+
label: "P3",
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const Priority = defineComponent(
|
|
30
|
+
{
|
|
31
|
+
description:
|
|
32
|
+
"優先度ピル。level は p0|p1|p2|p3。children を付けると補足表示になる",
|
|
33
|
+
schema: v.looseObject({
|
|
34
|
+
level: v.optional(v.picklist(PRIORITY_LEVELS), "p2"),
|
|
35
|
+
}),
|
|
36
|
+
},
|
|
37
|
+
({ level, children }) => {
|
|
38
|
+
const s = STYLES[level];
|
|
39
|
+
return (
|
|
40
|
+
<span
|
|
41
|
+
className={`not-prose inline-flex items-baseline gap-1 rounded-full px-2 py-0.5 text-xs font-semibold ${s.cls}`}
|
|
42
|
+
>
|
|
43
|
+
<Icon className="h-3 w-3 self-center" name="lucide:flag" />
|
|
44
|
+
{s.label}
|
|
45
|
+
{children === undefined || children === "" ? null : (
|
|
46
|
+
<span className="font-normal">{children}</span>
|
|
47
|
+
)}
|
|
48
|
+
</span>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
);
|
package/src/ui/props.tsx
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import * as v from "valibot";
|
|
2
|
+
|
|
3
|
+
import { defineComponent } from "../define.js";
|
|
4
|
+
import { nonEmpty } from "../guards.js";
|
|
5
|
+
import { attrTrue, BOOLISH_PROP } from "./attrs.js";
|
|
6
|
+
import { CaptionBar, Panel } from "./bits.js";
|
|
7
|
+
import { BORDER_CLS, DIVIDE_CLS, TEXT, TONE_TEXT, TRIM_CLS } from "./tones.js";
|
|
8
|
+
|
|
9
|
+
export const Props = defineComponent(
|
|
10
|
+
{
|
|
11
|
+
description:
|
|
12
|
+
"コンポーネント/関数の API 表コンテナ。of に対象名。<Prop> を子に取る",
|
|
13
|
+
schema: v.looseObject({
|
|
14
|
+
of: v.optional(v.string()),
|
|
15
|
+
}),
|
|
16
|
+
},
|
|
17
|
+
({ of, children }) => (
|
|
18
|
+
<Panel>
|
|
19
|
+
{nonEmpty(of) ? (
|
|
20
|
+
<CaptionBar className="font-mono">{of}</CaptionBar>
|
|
21
|
+
) : null}
|
|
22
|
+
<table className="m-0 w-full text-sm">
|
|
23
|
+
<thead>
|
|
24
|
+
<tr
|
|
25
|
+
className={`border-b text-left text-xs ${BORDER_CLS} ${TEXT.muted}`}
|
|
26
|
+
>
|
|
27
|
+
<th className="py-2 pr-3 pl-4 font-medium">Name</th>
|
|
28
|
+
<th className="px-3 py-2 font-medium">Type</th>
|
|
29
|
+
<th className="px-3 py-2 font-medium">Default</th>
|
|
30
|
+
<th className="py-2 pr-4 pl-3 font-medium">Description</th>
|
|
31
|
+
</tr>
|
|
32
|
+
</thead>
|
|
33
|
+
<tbody className={DIVIDE_CLS}>{children}</tbody>
|
|
34
|
+
</table>
|
|
35
|
+
</Panel>
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const isTruthy = (x: unknown): boolean => attrTrue(x) || x === "required";
|
|
40
|
+
|
|
41
|
+
export const Prop = defineComponent(
|
|
42
|
+
{
|
|
43
|
+
description:
|
|
44
|
+
"API 表の1行。name は必須。type/default は文字列。required を付けると必須マーク。children は説明文",
|
|
45
|
+
schema: v.looseObject({
|
|
46
|
+
default: v.optional(v.string()),
|
|
47
|
+
name: v.string(),
|
|
48
|
+
required: BOOLISH_PROP,
|
|
49
|
+
type: v.optional(v.string()),
|
|
50
|
+
}),
|
|
51
|
+
},
|
|
52
|
+
({ name, type, required, default: def, children }) => (
|
|
53
|
+
<tr>
|
|
54
|
+
<td className="py-2 pr-3 pl-4 align-top font-mono text-[0.85em] whitespace-nowrap">
|
|
55
|
+
{name}
|
|
56
|
+
{isTruthy(required) ? (
|
|
57
|
+
<span className={`ml-0.5 ${TONE_TEXT.red}`} title="required">
|
|
58
|
+
*
|
|
59
|
+
</span>
|
|
60
|
+
) : null}
|
|
61
|
+
</td>
|
|
62
|
+
<td
|
|
63
|
+
className={`px-3 py-2 align-top font-mono text-[0.85em] ${TEXT.muted}`}
|
|
64
|
+
>
|
|
65
|
+
{type ?? ""}
|
|
66
|
+
</td>
|
|
67
|
+
<td
|
|
68
|
+
className={`px-3 py-2 align-top font-mono text-[0.85em] ${TEXT.muted}`}
|
|
69
|
+
>
|
|
70
|
+
{nonEmpty(def) ? def : "—"}
|
|
71
|
+
</td>
|
|
72
|
+
<td className={`py-2 pr-4 pl-3 align-top ${TEXT.body} ${TRIM_CLS}`}>
|
|
73
|
+
{children}
|
|
74
|
+
</td>
|
|
75
|
+
</tr>
|
|
76
|
+
)
|
|
77
|
+
);
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
import * as v from "valibot";
|
|
3
|
+
|
|
4
|
+
import { defineComponent, flattenChildren, parseProps } from "../define.js";
|
|
5
|
+
import { nonEmpty } from "../guards.js";
|
|
6
|
+
import { NUMISH, numOf } from "./attrs.js";
|
|
7
|
+
import { CODE_CHIP_CLS } from "./bits.js";
|
|
8
|
+
import { ChartPanel } from "./chart-bits.js";
|
|
9
|
+
import { CHART_TONES, textList, toneOf } from "./chart.js";
|
|
10
|
+
import { isEl } from "./children.js";
|
|
11
|
+
import { BORDER_CLS, RAIL_BG_CLS, TEXT, TEXT_MICRO } from "./tones.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Quadrant map — <Pin> items positioned on two 0–100 axes (`x`/`y` name
|
|
15
|
+
* them). Midlines split the square into four regions; `quadrants` labels
|
|
16
|
+
* them top-left → top-right → bottom-left → bottom-right.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const PIN_SCHEMA = v.looseObject({
|
|
20
|
+
name: v.string(),
|
|
21
|
+
note: v.optional(v.string()),
|
|
22
|
+
tone: v.optional(v.picklist(CHART_TONES)),
|
|
23
|
+
x: NUMISH,
|
|
24
|
+
y: NUMISH,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
type PinSpec = v.InferOutput<typeof PIN_SCHEMA>;
|
|
28
|
+
|
|
29
|
+
export const Pin = defineComponent(
|
|
30
|
+
{
|
|
31
|
+
description:
|
|
32
|
+
"クアドラントの1項目。<Quadrant> の子。name/x/y は必須 (x/y は 0–100)、note で補足、tone で色",
|
|
33
|
+
schema: PIN_SCHEMA,
|
|
34
|
+
},
|
|
35
|
+
// Standalone use: a `name (x, y)` chip so misplaced pins still render.
|
|
36
|
+
({ name, x, y }): ReactElement => (
|
|
37
|
+
<span className={`font-mono ${CODE_CHIP_CLS}`}>
|
|
38
|
+
{name}
|
|
39
|
+
<span className="opacity-70">
|
|
40
|
+
({x}, {y})
|
|
41
|
+
</span>
|
|
42
|
+
</span>
|
|
43
|
+
)
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const collectPins = (
|
|
47
|
+
children: ReactNode
|
|
48
|
+
): { pins: { spec: PinSpec; x: number; y: number }[]; rest: ReactNode[] } => {
|
|
49
|
+
const pins: { spec: PinSpec; x: number; y: number }[] = [];
|
|
50
|
+
const rest: ReactNode[] = [];
|
|
51
|
+
for (const child of flattenChildren(children)) {
|
|
52
|
+
if (isEl(child, Pin)) {
|
|
53
|
+
const spec = parseProps(PIN_SCHEMA, child.props, "Pin");
|
|
54
|
+
const x = numOf(spec.x);
|
|
55
|
+
const y = numOf(spec.y);
|
|
56
|
+
if (x !== undefined && y !== undefined) {
|
|
57
|
+
pins.push({ spec, x, y });
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
rest.push(child);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { pins, rest };
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const CORNER_CLS = `absolute font-mono ${TEXT_MICRO} uppercase tracking-wider ${TEXT.faint}`;
|
|
67
|
+
|
|
68
|
+
export const Quadrant = defineComponent(
|
|
69
|
+
{
|
|
70
|
+
description:
|
|
71
|
+
'クアドラントマップコンテナ (2軸ポジショニング)。<Pin name x y> を 0–100 空間に置く。x/y で軸名、quadrants="TL,TR,BL,BR" で各象限ラベル',
|
|
72
|
+
schema: v.looseObject({
|
|
73
|
+
quadrants: v.optional(v.string()),
|
|
74
|
+
title: v.optional(v.string()),
|
|
75
|
+
x: v.optional(v.string()),
|
|
76
|
+
y: v.optional(v.string()),
|
|
77
|
+
}),
|
|
78
|
+
},
|
|
79
|
+
({ title, x, y, quadrants, children }) => {
|
|
80
|
+
const { pins, rest } = collectPins(children);
|
|
81
|
+
const corners = textList(quadrants);
|
|
82
|
+
return (
|
|
83
|
+
<ChartPanel icon="lucide:grid-2x2" title={title}>
|
|
84
|
+
<div className="px-4 py-4">
|
|
85
|
+
<div
|
|
86
|
+
className={`relative mx-auto aspect-square w-full max-w-md rounded-lg border ${BORDER_CLS}`}
|
|
87
|
+
>
|
|
88
|
+
{/* midlines */}
|
|
89
|
+
<div
|
|
90
|
+
aria-hidden
|
|
91
|
+
className={`absolute inset-y-0 left-1/2 w-px ${RAIL_BG_CLS}`}
|
|
92
|
+
/>
|
|
93
|
+
<div
|
|
94
|
+
aria-hidden
|
|
95
|
+
className={`absolute inset-x-0 top-1/2 h-px ${RAIL_BG_CLS}`}
|
|
96
|
+
/>
|
|
97
|
+
{/* quadrant labels */}
|
|
98
|
+
{corners[0] === undefined ? null : (
|
|
99
|
+
<span className={`${CORNER_CLS} top-2 left-2.5`}>
|
|
100
|
+
{corners[0]}
|
|
101
|
+
</span>
|
|
102
|
+
)}
|
|
103
|
+
{corners[1] === undefined ? null : (
|
|
104
|
+
<span className={`${CORNER_CLS} top-2 right-2.5`}>
|
|
105
|
+
{corners[1]}
|
|
106
|
+
</span>
|
|
107
|
+
)}
|
|
108
|
+
{corners[2] === undefined ? null : (
|
|
109
|
+
<span className={`${CORNER_CLS} bottom-2 left-2.5`}>
|
|
110
|
+
{corners[2]}
|
|
111
|
+
</span>
|
|
112
|
+
)}
|
|
113
|
+
{corners[3] === undefined ? null : (
|
|
114
|
+
<span className={`${CORNER_CLS} right-2.5 bottom-2`}>
|
|
115
|
+
{corners[3]}
|
|
116
|
+
</span>
|
|
117
|
+
)}
|
|
118
|
+
{pins.map((p, i) => {
|
|
119
|
+
const style = toneOf(p.spec.tone, i);
|
|
120
|
+
return (
|
|
121
|
+
<div
|
|
122
|
+
className="absolute -translate-x-1/2 translate-y-1/2"
|
|
123
|
+
key={p.spec.name}
|
|
124
|
+
style={{
|
|
125
|
+
bottom: `${Math.min(100, Math.max(0, p.y))}%`,
|
|
126
|
+
left: `${Math.min(100, Math.max(0, p.x))}%`,
|
|
127
|
+
}}
|
|
128
|
+
title={`${p.spec.name} · ${p.x}, ${p.y}`}
|
|
129
|
+
>
|
|
130
|
+
<div className="flex flex-col items-center">
|
|
131
|
+
<span
|
|
132
|
+
className={`mb-0.5 rounded px-1 text-[0.65rem] font-medium whitespace-nowrap ${TEXT.chip} bg-white/80 dark:bg-neutral-900/80`}
|
|
133
|
+
>
|
|
134
|
+
{p.spec.name}
|
|
135
|
+
</span>
|
|
136
|
+
<span
|
|
137
|
+
className={`h-2.5 w-2.5 rounded-full ring-2 ring-white dark:ring-neutral-950 ${style.bg}`}
|
|
138
|
+
/>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
})}
|
|
143
|
+
</div>
|
|
144
|
+
{/* axis titles */}
|
|
145
|
+
<div className="mx-auto mt-1.5 flex max-w-md items-center justify-between">
|
|
146
|
+
{nonEmpty(y) ? (
|
|
147
|
+
<span
|
|
148
|
+
className={`font-mono ${TEXT_MICRO} uppercase ${TEXT.faint}`}
|
|
149
|
+
>
|
|
150
|
+
↑ {y}
|
|
151
|
+
</span>
|
|
152
|
+
) : (
|
|
153
|
+
<span />
|
|
154
|
+
)}
|
|
155
|
+
{nonEmpty(x) ? (
|
|
156
|
+
<span
|
|
157
|
+
className={`font-mono ${TEXT_MICRO} uppercase ${TEXT.faint}`}
|
|
158
|
+
>
|
|
159
|
+
{x} →
|
|
160
|
+
</span>
|
|
161
|
+
) : (
|
|
162
|
+
<span />
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
{rest.length > 0 ? (
|
|
166
|
+
<div className={`${TEXT_MICRO} ${TEXT.muted}`}>{rest}</div>
|
|
167
|
+
) : null}
|
|
168
|
+
</div>
|
|
169
|
+
</ChartPanel>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
);
|
package/src/ui/radar.tsx
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import * as v from "valibot";
|
|
3
|
+
|
|
4
|
+
import { defineComponent, flattenChildren, parseProps } from "../define.js";
|
|
5
|
+
import { nonEmpty } from "../guards.js";
|
|
6
|
+
import { NUMISH, numOf } from "./attrs.js";
|
|
7
|
+
import { ChartLegend, ChartPanel } from "./chart-bits.js";
|
|
8
|
+
import {
|
|
9
|
+
fmtNum,
|
|
10
|
+
linePath,
|
|
11
|
+
niceScale,
|
|
12
|
+
numList,
|
|
13
|
+
polar,
|
|
14
|
+
textList,
|
|
15
|
+
toneOf,
|
|
16
|
+
} from "./chart.js";
|
|
17
|
+
import { isEl } from "./children.js";
|
|
18
|
+
import { SERIES_SCHEMA, Series } from "./series.js";
|
|
19
|
+
import type { SeriesSpec } from "./series.js";
|
|
20
|
+
import { TEXT, TEXT_MICRO } from "./tones.js";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Radar/spider chart — each <Series> becomes a polygon across `axes`
|
|
24
|
+
* (3+ labels). Ring gridlines at quarter steps of `max` (default: the
|
|
25
|
+
* largest value, snapped to a nice bound).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const VB = 340;
|
|
29
|
+
const CX = VB / 2;
|
|
30
|
+
const CY = VB / 2 + 4;
|
|
31
|
+
const R = 118;
|
|
32
|
+
|
|
33
|
+
const GRID_STROKE = "stroke-neutral-200 dark:stroke-neutral-700";
|
|
34
|
+
const TICK_TEXT = "fill-neutral-400 dark:fill-neutral-500";
|
|
35
|
+
const AXIS_TEXT = "fill-neutral-500 dark:fill-neutral-400";
|
|
36
|
+
|
|
37
|
+
/** Label text-anchor — centered near the vertical, outward otherwise. */
|
|
38
|
+
const anchorAt = (x: number): "end" | "middle" | "start" => {
|
|
39
|
+
if (Math.abs(x - CX) < 8) {
|
|
40
|
+
return "middle";
|
|
41
|
+
}
|
|
42
|
+
return x > CX ? "start" : "end";
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const collectSeries = (
|
|
46
|
+
children: ReactNode
|
|
47
|
+
): { rest: ReactNode[]; series: { nums: number[]; spec: SeriesSpec }[] } => {
|
|
48
|
+
const series: { nums: number[]; spec: SeriesSpec }[] = [];
|
|
49
|
+
const rest: ReactNode[] = [];
|
|
50
|
+
for (const child of flattenChildren(children)) {
|
|
51
|
+
if (isEl(child, Series)) {
|
|
52
|
+
const spec = parseProps(SERIES_SCHEMA, child.props, "Series");
|
|
53
|
+
series.push({ nums: numList(spec.values), spec });
|
|
54
|
+
} else {
|
|
55
|
+
rest.push(child);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { rest, series };
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const Radar = defineComponent(
|
|
62
|
+
{
|
|
63
|
+
description:
|
|
64
|
+
'レーダーチャートコンテナ (多変量比較)。<Series name values> を並べる。axes="Perf,DX,Tests" で軸ラベル (系列値と同数)、max/unit でスケールと単位',
|
|
65
|
+
schema: v.looseObject({
|
|
66
|
+
axes: v.optional(v.string()),
|
|
67
|
+
max: v.optional(NUMISH),
|
|
68
|
+
title: v.optional(v.string()),
|
|
69
|
+
unit: v.optional(v.string()),
|
|
70
|
+
}),
|
|
71
|
+
},
|
|
72
|
+
({ title, axes, max, unit, children }) => {
|
|
73
|
+
const { series, rest } = collectSeries(children);
|
|
74
|
+
const labels = textList(axes);
|
|
75
|
+
const n = Math.max(labels.length, ...series.map((s) => s.nums.length));
|
|
76
|
+
const all = series.flatMap((s) => s.nums);
|
|
77
|
+
const scale = niceScale(numOf(max) ?? Math.max(1, ...all));
|
|
78
|
+
|
|
79
|
+
const angleOf = (i: number): number => -Math.PI / 2 + (i / n) * Math.PI * 2;
|
|
80
|
+
const ringAt = (frac: number): string =>
|
|
81
|
+
`${linePath(
|
|
82
|
+
Array.from({ length: n }, (_, i) => polar(CX, CY, R * frac, angleOf(i)))
|
|
83
|
+
)}Z`;
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<ChartPanel
|
|
87
|
+
figure={nonEmpty(unit) ? `0–${fmtNum(scale.ceil)}${unit}` : undefined}
|
|
88
|
+
icon="lucide:target"
|
|
89
|
+
title={title}
|
|
90
|
+
>
|
|
91
|
+
<div className="px-4 py-3">
|
|
92
|
+
{n < 3 ? (
|
|
93
|
+
<p className={`text-sm ${TEXT.muted}`}>
|
|
94
|
+
Radar needs at least 3 axes.
|
|
95
|
+
</p>
|
|
96
|
+
) : (
|
|
97
|
+
<svg
|
|
98
|
+
className="mx-auto h-auto w-full max-w-sm"
|
|
99
|
+
viewBox={`0 0 ${VB} ${VB}`}
|
|
100
|
+
>
|
|
101
|
+
<title>{title ?? "radar chart"}</title>
|
|
102
|
+
{[0.25, 0.5, 0.75, 1].map((f) => (
|
|
103
|
+
<path
|
|
104
|
+
className={GRID_STROKE}
|
|
105
|
+
d={ringAt(f)}
|
|
106
|
+
fill="none"
|
|
107
|
+
key={f}
|
|
108
|
+
strokeDasharray={f === 1 ? undefined : "2 3"}
|
|
109
|
+
strokeWidth={1}
|
|
110
|
+
/>
|
|
111
|
+
))}
|
|
112
|
+
{Array.from({ length: n }, (_, i) => (
|
|
113
|
+
<line
|
|
114
|
+
className={GRID_STROKE}
|
|
115
|
+
key={i}
|
|
116
|
+
strokeWidth={1}
|
|
117
|
+
x1={CX}
|
|
118
|
+
x2={polar(CX, CY, R, angleOf(i)).x}
|
|
119
|
+
y1={CY}
|
|
120
|
+
y2={polar(CX, CY, R, angleOf(i)).y}
|
|
121
|
+
/>
|
|
122
|
+
))}
|
|
123
|
+
<text
|
|
124
|
+
className={TICK_TEXT}
|
|
125
|
+
fontSize={8}
|
|
126
|
+
textAnchor="middle"
|
|
127
|
+
x={CX}
|
|
128
|
+
y={CY - R - 6}
|
|
129
|
+
>
|
|
130
|
+
{fmtNum(scale.ceil)}
|
|
131
|
+
{unit}
|
|
132
|
+
</text>
|
|
133
|
+
{labels.map((label, i) => {
|
|
134
|
+
const p = polar(CX, CY, R + 18, angleOf(i));
|
|
135
|
+
const anchor = anchorAt(p.x);
|
|
136
|
+
return (
|
|
137
|
+
<text
|
|
138
|
+
className={AXIS_TEXT}
|
|
139
|
+
fontSize={10}
|
|
140
|
+
key={label}
|
|
141
|
+
textAnchor={anchor}
|
|
142
|
+
x={p.x}
|
|
143
|
+
y={p.y + 3}
|
|
144
|
+
>
|
|
145
|
+
{label}
|
|
146
|
+
</text>
|
|
147
|
+
);
|
|
148
|
+
})}
|
|
149
|
+
{series.map((s, si) => {
|
|
150
|
+
const style = toneOf(s.spec.tone, si);
|
|
151
|
+
const pts = s.nums.map((val, i) =>
|
|
152
|
+
polar(
|
|
153
|
+
CX,
|
|
154
|
+
CY,
|
|
155
|
+
R * Math.min(1, Math.max(0, val / scale.ceil)),
|
|
156
|
+
angleOf(i)
|
|
157
|
+
)
|
|
158
|
+
);
|
|
159
|
+
return (
|
|
160
|
+
<g key={s.spec.name}>
|
|
161
|
+
<path className={style.soft} d={`${linePath(pts)}Z`} />
|
|
162
|
+
<path
|
|
163
|
+
className={style.stroke}
|
|
164
|
+
d={`${linePath(pts)}Z`}
|
|
165
|
+
fill="none"
|
|
166
|
+
strokeLinejoin="round"
|
|
167
|
+
strokeWidth={1.6}
|
|
168
|
+
/>
|
|
169
|
+
{pts.map((p, i) => (
|
|
170
|
+
<circle
|
|
171
|
+
className={style.fill}
|
|
172
|
+
cx={p.x}
|
|
173
|
+
cy={p.y}
|
|
174
|
+
key={i}
|
|
175
|
+
r={2}
|
|
176
|
+
>
|
|
177
|
+
<title>
|
|
178
|
+
{`${s.spec.name}${labels[i] === undefined ? "" : ` · ${labels[i]}`} · ${fmtNum(s.nums[i] ?? 0)}${unit ?? ""}`}
|
|
179
|
+
</title>
|
|
180
|
+
</circle>
|
|
181
|
+
))}
|
|
182
|
+
</g>
|
|
183
|
+
);
|
|
184
|
+
})}
|
|
185
|
+
</svg>
|
|
186
|
+
)}
|
|
187
|
+
{rest.length > 0 ? (
|
|
188
|
+
<div className={`${TEXT_MICRO} ${TEXT.muted}`}>{rest}</div>
|
|
189
|
+
) : null}
|
|
190
|
+
</div>
|
|
191
|
+
<ChartLegend
|
|
192
|
+
items={series.map((s, i) => ({
|
|
193
|
+
name: s.spec.name,
|
|
194
|
+
style: toneOf(s.spec.tone, i),
|
|
195
|
+
}))}
|
|
196
|
+
/>
|
|
197
|
+
</ChartPanel>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
);
|