@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.
Files changed (236) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +56 -0
  3. package/dist/cli.d.mts +1 -0
  4. package/dist/cli.mjs +2902 -0
  5. package/dist/components.d.mts +791 -0
  6. package/dist/components.mjs +2 -0
  7. package/dist/config-SI9IyFiC.mjs +184 -0
  8. package/dist/doc-context-CEqzMYKv.d.mts +568 -0
  9. package/dist/index.d.mts +20 -0
  10. package/dist/index.mjs +4 -0
  11. package/dist/ui-GKFD0mx5.mjs +15986 -0
  12. package/package.json +131 -0
  13. package/skill/SKILL.md +49 -0
  14. package/skill/references/components/charts.md +172 -0
  15. package/skill/references/components/document.md +98 -0
  16. package/skill/references/components/forms.md +35 -0
  17. package/skill/references/components/investigation.md +198 -0
  18. package/skill/references/components/layout.md +68 -0
  19. package/skill/references/components/output.md +158 -0
  20. package/skill/references/components/planning.md +155 -0
  21. package/skill/references/components/reports.md +252 -0
  22. package/skill/references/components/shadcn.md +19 -0
  23. package/skill/references/components.md +186 -0
  24. package/skill/references/extending.md +48 -0
  25. package/src/ask-sheet.ts +40 -0
  26. package/src/assets/css.ts +392 -0
  27. package/src/assets/scripts.ts +78 -0
  28. package/src/catalog.ts +287 -0
  29. package/src/cli.ts +178 -0
  30. package/src/client/doc-events.ts +1108 -0
  31. package/src/client/entry.ts +27 -0
  32. package/src/client-js.ts +47 -0
  33. package/src/component-map.ts +47 -0
  34. package/src/components/ui/accordion.tsx +77 -0
  35. package/src/components/ui/alert-dialog.tsx +185 -0
  36. package/src/components/ui/alert.tsx +76 -0
  37. package/src/components/ui/aspect-ratio.tsx +22 -0
  38. package/src/components/ui/attachment.tsx +208 -0
  39. package/src/components/ui/avatar.tsx +106 -0
  40. package/src/components/ui/badge.tsx +52 -0
  41. package/src/components/ui/breadcrumb.tsx +121 -0
  42. package/src/components/ui/bubble.tsx +128 -0
  43. package/src/components/ui/button-group.tsx +88 -0
  44. package/src/components/ui/button.tsx +58 -0
  45. package/src/components/ui/calendar.tsx +226 -0
  46. package/src/components/ui/card.tsx +102 -0
  47. package/src/components/ui/carousel.tsx +246 -0
  48. package/src/components/ui/chart.tsx +379 -0
  49. package/src/components/ui/checkbox.tsx +27 -0
  50. package/src/components/ui/collapsible.tsx +19 -0
  51. package/src/components/ui/combobox.tsx +298 -0
  52. package/src/components/ui/command.tsx +193 -0
  53. package/src/components/ui/context-menu.tsx +271 -0
  54. package/src/components/ui/dialog.tsx +159 -0
  55. package/src/components/ui/direction.tsx +4 -0
  56. package/src/components/ui/drawer.tsx +227 -0
  57. package/src/components/ui/dropdown-menu.tsx +269 -0
  58. package/src/components/ui/empty.tsx +104 -0
  59. package/src/components/ui/field.tsx +237 -0
  60. package/src/components/ui/fieldset.tsx +32 -0
  61. package/src/components/ui/frame.tsx +87 -0
  62. package/src/components/ui/hover-card.tsx +50 -0
  63. package/src/components/ui/input-group.tsx +159 -0
  64. package/src/components/ui/input-otp.tsx +83 -0
  65. package/src/components/ui/input.tsx +19 -0
  66. package/src/components/ui/item.tsx +202 -0
  67. package/src/components/ui/kbd.tsx +26 -0
  68. package/src/components/ui/label.tsx +19 -0
  69. package/src/components/ui/marker.tsx +71 -0
  70. package/src/components/ui/menubar.tsx +284 -0
  71. package/src/components/ui/message-scroller.tsx +128 -0
  72. package/src/components/ui/message.tsx +91 -0
  73. package/src/components/ui/meter.tsx +80 -0
  74. package/src/components/ui/native-select.tsx +64 -0
  75. package/src/components/ui/navigation-menu.tsx +170 -0
  76. package/src/components/ui/pagination.tsx +133 -0
  77. package/src/components/ui/popover.tsx +87 -0
  78. package/src/components/ui/progress.tsx +82 -0
  79. package/src/components/ui/questionnaire.tsx +328 -0
  80. package/src/components/ui/radio-group.tsx +35 -0
  81. package/src/components/ui/resizable.tsx +49 -0
  82. package/src/components/ui/scroll-area.tsx +50 -0
  83. package/src/components/ui/select.tsx +201 -0
  84. package/src/components/ui/separator.tsx +22 -0
  85. package/src/components/ui/sheet.tsx +135 -0
  86. package/src/components/ui/sidebar.tsx +730 -0
  87. package/src/components/ui/skeleton.tsx +13 -0
  88. package/src/components/ui/slider.tsx +51 -0
  89. package/src/components/ui/spinner.tsx +16 -0
  90. package/src/components/ui/switch.tsx +31 -0
  91. package/src/components/ui/table.tsx +113 -0
  92. package/src/components/ui/tabs.tsx +82 -0
  93. package/src/components/ui/textarea.tsx +17 -0
  94. package/src/components/ui/toast.tsx +229 -0
  95. package/src/components/ui/toggle-group.tsx +87 -0
  96. package/src/components/ui/toggle.tsx +43 -0
  97. package/src/components/ui/tooltip.tsx +65 -0
  98. package/src/components.ts +78 -0
  99. package/src/config.ts +56 -0
  100. package/src/define.ts +122 -0
  101. package/src/doc-context.ts +20 -0
  102. package/src/editor.ts +94 -0
  103. package/src/format-error.ts +78 -0
  104. package/src/guards.ts +84 -0
  105. package/src/hooks/use-mobile.ts +21 -0
  106. package/src/html.ts +91 -0
  107. package/src/hydrate/export-index.ts +232 -0
  108. package/src/hydrate/import-scan.ts +169 -0
  109. package/src/hydrate/plugins.ts +118 -0
  110. package/src/hydrate/runtime-module.ts +145 -0
  111. package/src/hydrate-runtime.ts +67 -0
  112. package/src/hydrate.ts +147 -0
  113. package/src/index.ts +16 -0
  114. package/src/init.ts +57 -0
  115. package/src/langs.ts +148 -0
  116. package/src/lines.ts +53 -0
  117. package/src/load-user-module.ts +191 -0
  118. package/src/mdx.ts +248 -0
  119. package/src/paths.ts +36 -0
  120. package/src/rehype/shiki.ts +533 -0
  121. package/src/remark/alerts.ts +53 -0
  122. package/src/remark/ast.ts +96 -0
  123. package/src/remark/callouts.ts +25 -0
  124. package/src/remark/code-file.ts +85 -0
  125. package/src/remark/code-meta.ts +21 -0
  126. package/src/remark/directives.ts +170 -0
  127. package/src/remark/file-paths.ts +64 -0
  128. package/src/remark/headings.ts +131 -0
  129. package/src/remark/no-js.ts +40 -0
  130. package/src/render.ts +337 -0
  131. package/src/serve.ts +322 -0
  132. package/src/styles/globals.css +134 -0
  133. package/src/styles/shadcn.css +641 -0
  134. package/src/tailwind.ts +119 -0
  135. package/src/ui/approvals.tsx +76 -0
  136. package/src/ui/ask-question.tsx +386 -0
  137. package/src/ui/ask.tsx +206 -0
  138. package/src/ui/attrs.ts +51 -0
  139. package/src/ui/audit.tsx +139 -0
  140. package/src/ui/bar-chart.tsx +334 -0
  141. package/src/ui/benchmarks.tsx +143 -0
  142. package/src/ui/bits.tsx +537 -0
  143. package/src/ui/board.tsx +173 -0
  144. package/src/ui/bridge.tsx +207 -0
  145. package/src/ui/bumps.tsx +178 -0
  146. package/src/ui/callout.tsx +106 -0
  147. package/src/ui/changes.tsx +89 -0
  148. package/src/ui/chart-bits.tsx +52 -0
  149. package/src/ui/chart.ts +577 -0
  150. package/src/ui/checks.tsx +203 -0
  151. package/src/ui/child-index.tsx +44 -0
  152. package/src/ui/children.ts +43 -0
  153. package/src/ui/chips.tsx +49 -0
  154. package/src/ui/cmd.tsx +27 -0
  155. package/src/ui/columns.tsx +31 -0
  156. package/src/ui/comments.tsx +544 -0
  157. package/src/ui/compare.tsx +67 -0
  158. package/src/ui/decision.tsx +79 -0
  159. package/src/ui/deps.tsx +78 -0
  160. package/src/ui/details.tsx +43 -0
  161. package/src/ui/diff-parse.ts +307 -0
  162. package/src/ui/diff.tsx +458 -0
  163. package/src/ui/diffstat.tsx +58 -0
  164. package/src/ui/due.tsx +65 -0
  165. package/src/ui/effort.tsx +29 -0
  166. package/src/ui/endpoints.tsx +118 -0
  167. package/src/ui/envvars.tsx +96 -0
  168. package/src/ui/figure.tsx +37 -0
  169. package/src/ui/file-icon.ts +1020 -0
  170. package/src/ui/file-link.ts +32 -0
  171. package/src/ui/file-ref.tsx +39 -0
  172. package/src/ui/files.tsx +112 -0
  173. package/src/ui/findings.tsx +85 -0
  174. package/src/ui/flow.tsx +73 -0
  175. package/src/ui/funnel.tsx +121 -0
  176. package/src/ui/gantt.tsx +443 -0
  177. package/src/ui/gauges.tsx +135 -0
  178. package/src/ui/glossary.tsx +29 -0
  179. package/src/ui/graph-layout.ts +149 -0
  180. package/src/ui/graph-specs.tsx +102 -0
  181. package/src/ui/graph.tsx +278 -0
  182. package/src/ui/grid.tsx +119 -0
  183. package/src/ui/hypothesis.tsx +94 -0
  184. package/src/ui/icon.tsx +80 -0
  185. package/src/ui/incident.tsx +130 -0
  186. package/src/ui/index.ts +342 -0
  187. package/src/ui/ins-del.tsx +41 -0
  188. package/src/ui/json.tsx +190 -0
  189. package/src/ui/layout.ts +9 -0
  190. package/src/ui/line-chart.tsx +251 -0
  191. package/src/ui/matrix.tsx +208 -0
  192. package/src/ui/meta.tsx +73 -0
  193. package/src/ui/option.tsx +64 -0
  194. package/src/ui/owner.tsx +40 -0
  195. package/src/ui/packages.tsx +104 -0
  196. package/src/ui/pathway.tsx +96 -0
  197. package/src/ui/phase.tsx +41 -0
  198. package/src/ui/pie-chart.tsx +170 -0
  199. package/src/ui/plan.tsx +77 -0
  200. package/src/ui/pre.tsx +175 -0
  201. package/src/ui/priority.tsx +51 -0
  202. package/src/ui/props.tsx +77 -0
  203. package/src/ui/quadrant.tsx +172 -0
  204. package/src/ui/radar.tsx +200 -0
  205. package/src/ui/ref.tsx +160 -0
  206. package/src/ui/release.tsx +178 -0
  207. package/src/ui/req.tsx +42 -0
  208. package/src/ui/review.tsx +133 -0
  209. package/src/ui/risk.tsx +67 -0
  210. package/src/ui/sankey.tsx +287 -0
  211. package/src/ui/scatter.tsx +237 -0
  212. package/src/ui/schema.tsx +113 -0
  213. package/src/ui/score.tsx +105 -0
  214. package/src/ui/search.tsx +120 -0
  215. package/src/ui/series.tsx +38 -0
  216. package/src/ui/severity.tsx +69 -0
  217. package/src/ui/shadcn.tsx +216 -0
  218. package/src/ui/spark.tsx +86 -0
  219. package/src/ui/stack.tsx +57 -0
  220. package/src/ui/stats.tsx +63 -0
  221. package/src/ui/status-badge.tsx +66 -0
  222. package/src/ui/statuspage.tsx +238 -0
  223. package/src/ui/steps.tsx +74 -0
  224. package/src/ui/summary.tsx +41 -0
  225. package/src/ui/symbol-ref.tsx +73 -0
  226. package/src/ui/terminal.tsx +117 -0
  227. package/src/ui/tests.tsx +218 -0
  228. package/src/ui/timeline.tsx +63 -0
  229. package/src/ui/toc.tsx +56 -0
  230. package/src/ui/tones.ts +187 -0
  231. package/src/ui/trace.tsx +69 -0
  232. package/src/ui/tree.tsx +281 -0
  233. package/src/ui/treemap.tsx +128 -0
  234. package/src/ui/venn.tsx +258 -0
  235. package/src/ui/verdict.tsx +78 -0
  236. 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
+ );
@@ -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
+ );
@@ -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
+ );