@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
@@ -0,0 +1,281 @@
1
+ import { Fragment } from "react";
2
+ import type { ReactElement, ReactNode } from "react";
3
+ import * as v from "valibot";
4
+
5
+ import { defineComponent, flattenChildren, textOf } from "../define.js";
6
+ import { nonEmpty } from "../guards.js";
7
+ import { attrFalse, BOOLISH_PROP } from "./attrs.js";
8
+ import { FoldChev } from "./bits.js";
9
+ import { isEl } from "./children.js";
10
+ import { fileIcon, folderIcon } from "./file-icon.js";
11
+ import { Icon } from "./icon.js";
12
+ import {
13
+ BORDER_CLS,
14
+ DISCLOSURE_ROW_CLS,
15
+ RAIL_BG_CLS,
16
+ SUNKEN_CLS,
17
+ TEXT,
18
+ TONE_TEXT,
19
+ TREE_ROW_CLS,
20
+ } from "./tones.js";
21
+
22
+ type El = ReactElement<{ children?: ReactNode }>;
23
+
24
+ /** `name — note` or `name # note` inside a list item attaches a muted note. */
25
+ const NOTE_RE = /\s+(?:—|#)\s+/u;
26
+
27
+ /** `...` or `…` as the whole name marks intentionally omitted entries. */
28
+ const PLACEHOLDER_RE = /^(?:\.{3}|…)$/u;
29
+
30
+ const closed = attrFalse;
31
+
32
+ const NodeIcon = ({
33
+ dir,
34
+ name,
35
+ }: {
36
+ dir: boolean;
37
+ name: string;
38
+ }): ReactElement => (
39
+ <Icon
40
+ className={`h-3.5 w-3.5 shrink-0 self-center ${dir ? TONE_TEXT.amber : TEXT.faint}`}
41
+ name={dir ? folderIcon(name) : fileIcon(name)}
42
+ />
43
+ );
44
+
45
+ /** Horizontal tick joining a nested row to its parent's vertical rail. */
46
+ const Tick = (): ReactElement => (
47
+ <span
48
+ aria-hidden
49
+ className={`absolute top-[0.72em] -left-3 h-px w-2.5 ${RAIL_BG_CLS}`}
50
+ />
51
+ );
52
+
53
+ /**
54
+ * Vertical rail joining a nested row to its siblings. `-top-0.5` bridges the
55
+ * `space-y-0.5` gap between rows so the line stays continuous; on the last row
56
+ * it stops at the tick, turning `├─` into `└─`.
57
+ */
58
+ const Rail = ({ last }: { last: boolean }): ReactElement => (
59
+ <span
60
+ aria-hidden
61
+ className={`absolute -top-0.5 -left-3 w-px ${RAIL_BG_CLS} ${
62
+ last ? "h-[calc(0.72em_+_3px)]" : "bottom-0"
63
+ }`}
64
+ />
65
+ );
66
+
67
+ /**
68
+ * Column spacer matching the disclosure chevron's width so file rows align
69
+ * with directory rows (which lead with a chevron, like Starlight's FileTree).
70
+ */
71
+ const Spacer = (): ReactElement => (
72
+ <span aria-hidden className="w-3 shrink-0" />
73
+ );
74
+
75
+ /** A tree row: guide tick plus its flex children. */
76
+ const Row = ({
77
+ depth,
78
+ children,
79
+ }: {
80
+ children?: ReactNode;
81
+ depth: number;
82
+ }): ReactElement => (
83
+ <div className={TREE_ROW_CLS}>
84
+ {depth > 0 ? <Tick /> : null}
85
+ {children}
86
+ </div>
87
+ );
88
+
89
+ /** Muted `…` row marking omitted entries (Starlight-style placeholder). */
90
+ const PlaceholderRow = ({ depth }: { depth: number }): ReactElement => (
91
+ <Row depth={depth}>
92
+ <Spacer />
93
+ <span className={TEXT.faint}>…</span>
94
+ </Row>
95
+ );
96
+
97
+ /** Icon + name; wrapped in a highlight pill when the name was bold. */
98
+ const Entry = ({
99
+ dir,
100
+ highlighted,
101
+ label,
102
+ children,
103
+ }: {
104
+ children?: ReactNode;
105
+ dir: boolean;
106
+ highlighted: boolean;
107
+ label: string;
108
+ }): ReactElement => {
109
+ const nameEl = (
110
+ <span
111
+ className={`group-hover:text-sky-600 dark:group-hover:text-sky-400${dir ? " font-medium" : ""}`}
112
+ >
113
+ {children}
114
+ </span>
115
+ );
116
+ return highlighted ? (
117
+ <span className="-mx-1 inline-flex items-baseline gap-1.5 rounded bg-amber-500/15 px-1 outline-1 outline-amber-500/35">
118
+ <NodeIcon dir={dir} name={label} />
119
+ {nameEl}
120
+ </span>
121
+ ) : (
122
+ <>
123
+ <NodeIcon dir={dir} name={label} />
124
+ {nameEl}
125
+ </>
126
+ );
127
+ };
128
+
129
+ const nestedListCls = "mt-0.5 ml-[7px] list-none space-y-0.5 p-0 pl-3";
130
+
131
+ /** A list item's pieces: label/note text, nested <ul>s, inline row content. */
132
+ const rowParts = (
133
+ child: El
134
+ ): {
135
+ label: string;
136
+ name: string | null;
137
+ nested: El[];
138
+ note: string | null;
139
+ row: ReactNode[];
140
+ } => {
141
+ const kids = flattenChildren(child.props.children);
142
+ const nested = kids.filter((k): k is El => isEl(k, "ul"));
143
+ const nestedSet = new Set<ReactNode>(nested);
144
+ const row = kids
145
+ .filter((k) => !nestedSet.has(k))
146
+ .flatMap((k): ReactNode[] =>
147
+ isEl(k, "p") ? flattenChildren(k.props.children) : [k]
148
+ );
149
+
150
+ const full = textOf(row);
151
+ const m = NOTE_RE.exec(full);
152
+ const name = m === null ? null : full.slice(0, m.index).trimEnd();
153
+ const note = m === null ? null : full.slice(m.index + m[0].length).trim();
154
+ return { label: (name ?? full).trim(), name, nested, note, row };
155
+ };
156
+
157
+ /** Render a `<ul>` element (and its nested lists) as file-tree rows. */
158
+ const renderList = (
159
+ node: El,
160
+ depth: number,
161
+ expanded: boolean
162
+ ): ReactElement => {
163
+ const renderItem = (child: El, last: boolean): ReactElement => {
164
+ const { label, name, nested, note, row } = rowParts(child);
165
+
166
+ // A `...`/`…` entry marks omitted files — no icon, not collapsible.
167
+ if (PLACEHOLDER_RE.test(label)) {
168
+ return (
169
+ <li className="relative">
170
+ {depth > 0 ? <Rail last={last} /> : null}
171
+ <PlaceholderRow depth={depth} />
172
+ </li>
173
+ );
174
+ }
175
+
176
+ const isDir = nested.length > 0 || label.endsWith("/");
177
+ const entry = (
178
+ <Entry dir={isDir} highlighted={isEl(row[0], "strong")} label={label}>
179
+ {name ?? row}
180
+ </Entry>
181
+ );
182
+ const noteEl =
183
+ note === null ? null : (
184
+ <span className={`text-[0.85em] ${TEXT.faint}`}>{note}</span>
185
+ );
186
+
187
+ if (!isDir) {
188
+ return (
189
+ <li className="relative">
190
+ {depth > 0 ? <Rail last={last} /> : null}
191
+ <Row depth={depth}>
192
+ <Spacer />
193
+ {entry}
194
+ {noteEl}
195
+ </Row>
196
+ </li>
197
+ );
198
+ }
199
+
200
+ // Directory: collapsible via native <details>. A bare `dir/` (no children)
201
+ // starts closed and reveals a `…` placeholder — same as Starlight.
202
+ return (
203
+ <li className="relative">
204
+ {depth > 0 ? <Rail last={last} /> : null}
205
+ <details open={expanded && nested.length > 0}>
206
+ <summary className={`group ${DISCLOSURE_ROW_CLS}`}>
207
+ {depth > 0 ? <Tick /> : null}
208
+ <FoldChev className="group-hover:text-neutral-600 dark:group-hover:text-neutral-300" />
209
+ {entry}
210
+ {noteEl}
211
+ </summary>
212
+ {nested.length > 0 ? (
213
+ nested.map((ul, j) => (
214
+ <Fragment key={ul.key ?? j}>
215
+ {renderList(ul, depth + 1, expanded)}
216
+ </Fragment>
217
+ ))
218
+ ) : (
219
+ <ul className={nestedListCls}>
220
+ <li className="relative">
221
+ <Rail last />
222
+ <PlaceholderRow depth={depth + 1} />
223
+ </li>
224
+ </ul>
225
+ )}
226
+ </details>
227
+ </li>
228
+ );
229
+ };
230
+
231
+ const items = flattenChildren(node.props.children);
232
+ const lastLi = items.findLastIndex((c) => isEl(c, "li"));
233
+ return (
234
+ <ul
235
+ className={depth === 0 ? "m-0 list-none space-y-0.5 p-0" : nestedListCls}
236
+ >
237
+ {items.map((child, i) => {
238
+ if (!isEl(child, "li")) {
239
+ return <Fragment key={i}>{child}</Fragment>;
240
+ }
241
+ return (
242
+ <Fragment key={child.key ?? i}>
243
+ {renderItem(child, i === lastLi)}
244
+ </Fragment>
245
+ );
246
+ })}
247
+ </ul>
248
+ );
249
+ };
250
+
251
+ export const Tree = defineComponent(
252
+ {
253
+ description:
254
+ 'ファイルツリー。子のネストしたリストを描画する。末尾 `/` または子を持つ項目はフォルダでクリックで折り畳める (JS 不要)。open="false" で全フォルダを初期折り畳み。`名前 — 注記` で注釈、`...` または `…` で省略プレースホルダ、`**太字**` でハイライト',
255
+ schema: v.looseObject({
256
+ open: BOOLISH_PROP,
257
+ root: v.optional(v.string()),
258
+ }),
259
+ },
260
+ ({ root, open, children }) => (
261
+ <div
262
+ className={`mdxr-tree not-prose my-6 overflow-x-auto rounded-lg border ${SUNKEN_CLS} px-4 py-3 font-mono text-sm ${BORDER_CLS}`}
263
+ >
264
+ {nonEmpty(root) ? (
265
+ <div className="mb-1.5 flex items-center gap-1.5 font-semibold">
266
+ <NodeIcon dir name={root} />
267
+ {root}
268
+ </div>
269
+ ) : null}
270
+ {flattenChildren(children).map((n, i) =>
271
+ isEl(n, "ul") ? (
272
+ <Fragment key={n.key ?? i}>
273
+ {renderList(n, 0, !closed(open))}
274
+ </Fragment>
275
+ ) : (
276
+ <Fragment key={i}>{n}</Fragment>
277
+ )
278
+ )}
279
+ </div>
280
+ )
281
+ );
@@ -0,0 +1,128 @@
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, fmtNum, squarify, toneOf } from "./chart.js";
10
+ import { isEl } from "./children.js";
11
+ import { TEXT_MICRO } from "./tones.js";
12
+
13
+ /**
14
+ * Treemap — part-of-whole areas packed by the squarified algorithm.
15
+ * <Tile> size ∝ `value`; labels carry name and share percent.
16
+ */
17
+
18
+ const TILE_SCHEMA = v.looseObject({
19
+ name: v.string(),
20
+ note: v.optional(v.string()),
21
+ tone: v.optional(v.picklist(CHART_TONES)),
22
+ value: NUMISH,
23
+ });
24
+
25
+ type TileSpec = v.InferOutput<typeof TILE_SCHEMA>;
26
+
27
+ export const Tile = defineComponent(
28
+ {
29
+ description:
30
+ "ツリーマップの1領域。<Treemap> の子。name/value は必須、note で補足、tone で色指定",
31
+ schema: TILE_SCHEMA,
32
+ },
33
+ // Standalone use: a `name value` chip so misplaced tiles still render.
34
+ ({ name, value, note }): ReactElement => (
35
+ <span className={`font-mono ${CODE_CHIP_CLS}`}>
36
+ {name}
37
+ <span className="opacity-70">{value}</span>
38
+ {nonEmpty(note) ? <span className="opacity-70">{note}</span> : null}
39
+ </span>
40
+ )
41
+ );
42
+
43
+ const collectTiles = (
44
+ children: ReactNode
45
+ ): { rest: ReactNode[]; tiles: { n: number; spec: TileSpec }[] } => {
46
+ const tiles: { n: number; spec: TileSpec }[] = [];
47
+ const rest: ReactNode[] = [];
48
+ for (const child of flattenChildren(children)) {
49
+ if (isEl(child, Tile)) {
50
+ const spec = parseProps(TILE_SCHEMA, child.props, "Tile");
51
+ tiles.push({ n: Math.max(0, numOf(spec.value) ?? 0), spec });
52
+ } else {
53
+ rest.push(child);
54
+ }
55
+ }
56
+ return { rest, tiles };
57
+ };
58
+
59
+ /** Squarify space — a 16:9 canvas in percentage units. */
60
+ const CANVAS = { h: 56.25, w: 100, x: 0, y: 0 };
61
+
62
+ export const Treemap = defineComponent(
63
+ {
64
+ description:
65
+ "ツリーマップコンテナ (階層的な構成比)。<Tile name value> を並べる。面積は value に比例、squarified レイアウト。unit は値の単位",
66
+ schema: v.looseObject({
67
+ title: v.optional(v.string()),
68
+ unit: v.optional(v.string()),
69
+ }),
70
+ },
71
+ ({ title, unit, children }) => {
72
+ const { tiles, rest } = collectTiles(children);
73
+ const total = tiles.reduce((a, t) => a + t.n, 0);
74
+ const rects = squarify(
75
+ tiles.map((t) => t.n),
76
+ CANVAS
77
+ );
78
+ return (
79
+ <ChartPanel
80
+ figure={nonEmpty(unit) ? `${fmtNum(total)}${unit}` : undefined}
81
+ icon="lucide:layout-grid"
82
+ title={title}
83
+ >
84
+ <div className="px-4 py-3">
85
+ <div className="relative aspect-video w-full">
86
+ {tiles.map((t, i) => {
87
+ const r = rects[i];
88
+ if (r === undefined || r.w <= 0 || r.h <= 0) {
89
+ return null;
90
+ }
91
+ const share = total === 0 ? 0 : (t.n / total) * 100;
92
+ const small = r.w < 16 || r.h < 12;
93
+ return (
94
+ <div
95
+ className={`absolute overflow-hidden rounded border border-white dark:border-neutral-950 ${toneOf(t.spec.tone, i).tile}`}
96
+ key={t.spec.name}
97
+ style={{
98
+ height: `${r.h}%`,
99
+ left: `${r.x}%`,
100
+ top: `${r.y}%`,
101
+ width: `${r.w}%`,
102
+ }}
103
+ title={`${t.spec.name} · ${fmtNum(t.n)}${unit ?? ""} (${Math.round(share)}%)`}
104
+ >
105
+ <div className="flex h-full flex-col p-1.5">
106
+ <span className="truncate text-xs font-medium">
107
+ {t.spec.name}
108
+ </span>
109
+ {small ? null : (
110
+ <span
111
+ className={`font-mono ${TEXT_MICRO} tabular-nums opacity-80`}
112
+ >
113
+ {fmtNum(t.n)}
114
+ {unit}
115
+ {` · ${Math.round(share)}%`}
116
+ </span>
117
+ )}
118
+ </div>
119
+ </div>
120
+ );
121
+ })}
122
+ </div>
123
+ </div>
124
+ {rest.length > 0 ? <div className="px-4 pb-3">{rest}</div> : null}
125
+ </ChartPanel>
126
+ );
127
+ }
128
+ );
@@ -0,0 +1,258 @@
1
+ import type { ReactElement, ReactNode } from "react";
2
+ import * as v from "valibot";
3
+
4
+ import { defineComponent, flattenChildren, parseProps } from "../define.js";
5
+ import { NUMISH, numOf } from "./attrs.js";
6
+ import { CODE_CHIP_CLS } from "./bits.js";
7
+ import { ChartPanel } from "./chart-bits.js";
8
+ import { CHART_TONES, fmtNum, textList, toneOf } from "./chart.js";
9
+ import { isEl } from "./children.js";
10
+ import { TEXT, TEXT_MICRO } from "./tones.js";
11
+
12
+ /**
13
+ * Venn diagram — 2 or 3 <Set> circles plus <Overlap> labels at the
14
+ * intersections. Circles are uniform (a Venn is not to scale); values
15
+ * print inside each set's own region.
16
+ */
17
+
18
+ const SET_SCHEMA = v.looseObject({
19
+ name: v.string(),
20
+ tone: v.optional(v.picklist(CHART_TONES)),
21
+ value: NUMISH,
22
+ });
23
+
24
+ const OVERLAP_SCHEMA = v.looseObject({
25
+ sets: v.string(),
26
+ value: NUMISH,
27
+ });
28
+
29
+ type SetSpec = v.InferOutput<typeof SET_SCHEMA>;
30
+ type OverlapSpec = v.InferOutput<typeof OVERLAP_SCHEMA>;
31
+
32
+ export const Set = defineComponent(
33
+ {
34
+ description:
35
+ "ベン図の1集合。<Venn> の子 (2–3個)。name/value は必須、tone で色",
36
+ schema: SET_SCHEMA,
37
+ },
38
+ // Standalone use: a `name value` chip so misplaced sets still render.
39
+ ({ name, value }): ReactElement => (
40
+ <span className={`font-mono ${CODE_CHIP_CLS}`}>
41
+ {name}
42
+ <span className="opacity-70">{value}</span>
43
+ </span>
44
+ )
45
+ );
46
+
47
+ export const Overlap = defineComponent(
48
+ {
49
+ description:
50
+ 'ベン図の重なり領域。<Venn> の子。sets="a,b" で対象の Set 名 (2個なら両方)、value は必須',
51
+ schema: OVERLAP_SCHEMA,
52
+ },
53
+ // Standalone use: a `a∩b value` chip so misplaced overlaps still render.
54
+ ({ sets, value }): ReactElement => (
55
+ <span className={`font-mono ${CODE_CHIP_CLS}`}>
56
+ {sets.replaceAll(",", "∩")}
57
+ <span className="opacity-70">{value}</span>
58
+ </span>
59
+ )
60
+ );
61
+
62
+ interface Circle {
63
+ cx: number;
64
+ cy: number;
65
+ r: number;
66
+ spec: SetSpec;
67
+ value: number;
68
+ }
69
+
70
+ const collectSets = (
71
+ children: ReactNode
72
+ ): {
73
+ overlaps: { names: string[]; spec: OverlapSpec; value: number }[];
74
+ rest: ReactNode[];
75
+ sets: { spec: SetSpec; value: number }[];
76
+ } => {
77
+ const sets: { spec: SetSpec; value: number }[] = [];
78
+ const overlaps: { names: string[]; spec: OverlapSpec; value: number }[] = [];
79
+ const rest: ReactNode[] = [];
80
+ for (const child of flattenChildren(children)) {
81
+ if (isEl(child, Set)) {
82
+ const spec = parseProps(SET_SCHEMA, child.props, "Set");
83
+ sets.push({ spec, value: numOf(spec.value) ?? 0 });
84
+ } else if (isEl(child, Overlap)) {
85
+ const spec = parseProps(OVERLAP_SCHEMA, child.props, "Overlap");
86
+ overlaps.push({
87
+ names: textList(spec.sets),
88
+ spec,
89
+ value: numOf(spec.value) ?? 0,
90
+ });
91
+ } else {
92
+ rest.push(child);
93
+ }
94
+ }
95
+ return { overlaps, rest, sets };
96
+ };
97
+
98
+ const VB_W = 400;
99
+ const VB_H = 300;
100
+
101
+ /** Fixed circle geometry — index-aligned with the first 2–3 sets. */
102
+ const GEOMETRY: { cx: number; cy: number; r: number }[][] = [
103
+ [],
104
+ [],
105
+ [
106
+ { cx: 150, cy: 150, r: 92 },
107
+ { cx: 250, cy: 150, r: 92 },
108
+ ],
109
+ [
110
+ { cx: 200, cy: 116, r: 78 },
111
+ { cx: 148, cy: 206, r: 78 },
112
+ { cx: 252, cy: 206, r: 78 },
113
+ ],
114
+ ];
115
+
116
+ const NAME_TEXT = "fill-neutral-600 dark:fill-neutral-300";
117
+ const VALUE_TEXT = "fill-neutral-500 dark:fill-neutral-400";
118
+
119
+ export const Venn = defineComponent(
120
+ {
121
+ description:
122
+ 'ベン図コンテナ (集合の重なり)。<Set name value> を 2–3 個と <Overlap sets="a,b" value> で交差部の値。unit は値の単位',
123
+ schema: v.looseObject({
124
+ title: v.optional(v.string()),
125
+ unit: v.optional(v.string()),
126
+ }),
127
+ },
128
+ ({ title, unit, children }) => {
129
+ const { sets, overlaps, rest } = collectSets(children);
130
+ const geom = GEOMETRY[sets.length] ?? GEOMETRY[3];
131
+ const circles: Circle[] = sets.map((s, i) => ({
132
+ ...(geom[i] ?? geom.at(-1)),
133
+ spec: s.spec,
134
+ value: s.value,
135
+ }));
136
+ // A set's own label sits at its centroid pushed away from the other
137
+ // circles' centers, so it lands in the non-overlapping region.
138
+ const ownLabel = (c: Circle): { x: number; y: number } => {
139
+ let dx = 0;
140
+ let dy = 0;
141
+ for (const o of circles) {
142
+ if (o !== c) {
143
+ dx += c.cx - o.cx;
144
+ dy += c.cy - o.cy;
145
+ }
146
+ }
147
+ const len = Math.hypot(dx, dy) || 1;
148
+ return {
149
+ x: c.cx + (dx / len) * c.r * 0.42,
150
+ y: c.cy + (dy / len) * c.r * 0.42,
151
+ };
152
+ };
153
+ // An overlap label sits at the mean of its member centers, pushed away
154
+ // from the non-members' centers (into the member-only lens).
155
+ const overlapLabel = (names: string[]): { x: number; y: number } => {
156
+ const isMember = (c: Circle): boolean =>
157
+ names.some(
158
+ (n) =>
159
+ c.spec.name === n || c.spec.name.toLowerCase() === n.toLowerCase()
160
+ );
161
+ const members = circles.filter(isMember);
162
+ const others = circles.filter((c) => !isMember(c));
163
+ const mx =
164
+ members.reduce((a, c) => a + c.cx, 0) / Math.max(1, members.length);
165
+ const my =
166
+ members.reduce((a, c) => a + c.cy, 0) / Math.max(1, members.length);
167
+ if (others.length === 0) {
168
+ return { x: mx, y: my };
169
+ }
170
+ const ox =
171
+ others.reduce((a, c) => a + c.cx, 0) / Math.max(1, others.length);
172
+ const oy =
173
+ others.reduce((a, c) => a + c.cy, 0) / Math.max(1, others.length);
174
+ const dx = mx - ox;
175
+ const dy = my - oy;
176
+ const len = Math.hypot(dx, dy) || 1;
177
+ return { x: mx + (dx / len) * 18, y: my + (dy / len) * 18 };
178
+ };
179
+
180
+ return (
181
+ <ChartPanel icon="lucide:blend" title={title}>
182
+ <div className="px-4 py-3">
183
+ {circles.length < 2 ? (
184
+ <p className={`text-sm ${TEXT.muted}`}>
185
+ Venn needs at least 2 sets.
186
+ </p>
187
+ ) : (
188
+ <svg
189
+ className="mx-auto h-auto w-full max-w-md"
190
+ viewBox={`0 0 ${VB_W} ${VB_H}`}
191
+ >
192
+ <title>{title ?? "venn diagram"}</title>
193
+ {circles.map((c, i) => {
194
+ const style = toneOf(c.spec.tone, i);
195
+ return (
196
+ <circle
197
+ className={`${style.soft} ${style.stroke}`}
198
+ cx={c.cx}
199
+ cy={c.cy}
200
+ key={c.spec.name}
201
+ r={c.r}
202
+ strokeWidth={1.5}
203
+ />
204
+ );
205
+ })}
206
+ {circles.map((c) => {
207
+ const p = ownLabel(c);
208
+ return (
209
+ <g key={c.spec.name}>
210
+ <text
211
+ className={NAME_TEXT}
212
+ fontSize={11}
213
+ fontWeight={600}
214
+ textAnchor="middle"
215
+ x={p.x}
216
+ y={p.y}
217
+ >
218
+ {c.spec.name}
219
+ </text>
220
+ <text
221
+ className={VALUE_TEXT}
222
+ fontSize={9}
223
+ textAnchor="middle"
224
+ x={p.x}
225
+ y={p.y + 12}
226
+ >
227
+ {fmtNum(c.value)}
228
+ {unit}
229
+ </text>
230
+ </g>
231
+ );
232
+ })}
233
+ {overlaps.map((o) => {
234
+ const p = overlapLabel(o.names);
235
+ return (
236
+ <text
237
+ className={VALUE_TEXT}
238
+ fontSize={10}
239
+ fontWeight={600}
240
+ key={o.spec.sets}
241
+ textAnchor="middle"
242
+ x={p.x}
243
+ y={p.y + 3}
244
+ >
245
+ {fmtNum(o.value)}
246
+ </text>
247
+ );
248
+ })}
249
+ </svg>
250
+ )}
251
+ {rest.length > 0 ? (
252
+ <div className={`${TEXT_MICRO} ${TEXT.muted}`}>{rest}</div>
253
+ ) : null}
254
+ </div>
255
+ </ChartPanel>
256
+ );
257
+ }
258
+ );