@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,287 @@
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 { ChartPanel } from "./chart-bits.js";
8
+ import {
9
+ CHART_TONES,
10
+ fmtNum,
11
+ layoutSankey,
12
+ textList,
13
+ toneOf,
14
+ } from "./chart.js";
15
+ import { isEl, propOf } from "./children.js";
16
+ import { NODE_SCHEMA, Node } from "./graph-specs.js";
17
+ import { Icon } from "./icon.js";
18
+ import { TEXT } from "./tones.js";
19
+
20
+ /**
21
+ * Sankey diagram — a quantity splitting/merging across stage columns;
22
+ * ribbon thickness ∝ `<Link>` `value`. Nodes are implicit from link
23
+ * endpoints; `<Node id label stage>` children rename/reorder them.
24
+ */
25
+
26
+ const LINK_SCHEMA = v.looseObject({
27
+ from: v.string(),
28
+ label: v.optional(v.string()),
29
+ to: v.string(),
30
+ value: NUMISH,
31
+ });
32
+
33
+ export const Link = defineComponent(
34
+ {
35
+ description:
36
+ "サンキー図のフロー。<Sankey> の子。from/to はノード id、value は流量 (リボンの太さ)。label で補足",
37
+ schema: LINK_SCHEMA,
38
+ },
39
+ // Standalone use: a `from → to value` line so misplaced links still render.
40
+ ({ from, to, value, label }): ReactElement => (
41
+ <div
42
+ className={`not-prose flex items-center gap-2 py-0.5 font-mono text-xs ${TEXT.muted}`}
43
+ >
44
+ <span>{from}</span>
45
+ <Icon className="h-3 w-3" name="lucide:arrow-right" />
46
+ <span>{to}</span>
47
+ <span className="opacity-70">{value}</span>
48
+ {nonEmpty(label) ? <span className="opacity-70">{label}</span> : null}
49
+ </div>
50
+ )
51
+ );
52
+
53
+ interface SankeyData {
54
+ /** Display label per node id (defaults to id). */
55
+ labels: Map<string, string>;
56
+ links: { from: string; to: string; value: number }[];
57
+ nodes: { id: string; stage?: number; tone?: string }[];
58
+ rest: ReactNode[];
59
+ }
60
+
61
+ /** Raw `stage` attr → number; numbers pass through, strings parse, rest is undefined. */
62
+ const stageOf = (raw: unknown): number | undefined => {
63
+ if (typeof raw === "number") {
64
+ return raw;
65
+ }
66
+ if (typeof raw === "string" && raw !== "") {
67
+ return numOf(raw);
68
+ }
69
+ return undefined;
70
+ };
71
+
72
+ /** Raw `tone` attr → palette tone name, or undefined for unknown values. */
73
+ const toneOfProp = (raw: unknown): string | undefined =>
74
+ typeof raw === "string" && (CHART_TONES as readonly string[]).includes(raw)
75
+ ? raw
76
+ : undefined;
77
+
78
+ const collectNode = (child: ReactElement, data: SankeyData): void => {
79
+ const spec = parseProps(NODE_SCHEMA, child.props, "Node");
80
+ data.nodes.push({
81
+ id: spec.id,
82
+ stage: stageOf(propOf(child, "stage")),
83
+ tone: toneOfProp(propOf(child, "tone")),
84
+ });
85
+ if (nonEmpty(spec.label)) {
86
+ data.labels.set(spec.id, spec.label);
87
+ }
88
+ };
89
+
90
+ const collectFlow = (children: ReactNode): SankeyData => {
91
+ const data: SankeyData = {
92
+ labels: new Map(),
93
+ links: [],
94
+ nodes: [],
95
+ rest: [],
96
+ };
97
+ for (const child of flattenChildren(children)) {
98
+ if (isEl(child, Link)) {
99
+ const spec = parseProps(LINK_SCHEMA, child.props, "Link");
100
+ data.links.push({
101
+ from: spec.from,
102
+ to: spec.to,
103
+ value: numOf(spec.value) ?? 0,
104
+ });
105
+ } else if (isEl(child, Node)) {
106
+ collectNode(child, data);
107
+ } else {
108
+ data.rest.push(child);
109
+ }
110
+ }
111
+ return data;
112
+ };
113
+
114
+ const VB_W = 640;
115
+ const VB_H = 340;
116
+ const TOP = 26;
117
+ const BOTTOM = 12;
118
+ const H = VB_H - TOP - BOTTOM;
119
+ const BAR_W = 10;
120
+ const GAP = 14;
121
+ const L_MARGIN = 96;
122
+ const R_MARGIN = 96;
123
+
124
+ const AXIS_TEXT = "fill-neutral-500 dark:fill-neutral-400";
125
+
126
+ /** Node label position — left of stage-0 bars, right of last-stage bars, centered above inner bars. */
127
+ const labelPos = (
128
+ stage: number,
129
+ last: number,
130
+ barX: number
131
+ ): { anchor: "end" | "middle" | "start"; x: number } => {
132
+ if (stage === 0) {
133
+ return { anchor: "end", x: barX - 8 };
134
+ }
135
+ if (stage === last) {
136
+ return { anchor: "start", x: barX + BAR_W + 8 };
137
+ }
138
+ return { anchor: "middle", x: barX + BAR_W / 2 };
139
+ };
140
+
141
+ export const Sankey = defineComponent(
142
+ {
143
+ description:
144
+ 'サンキー図コンテナ (量の分配・合流)。<Link from to value> がリボン、<Node id label stage> でラベル/列指定。stages="A,B,C" で列ヘッダ、unit は値の単位',
145
+ schema: v.looseObject({
146
+ stages: v.optional(v.string()),
147
+ title: v.optional(v.string()),
148
+ unit: v.optional(v.string()),
149
+ }),
150
+ },
151
+ ({ title, stages, unit, children }) => {
152
+ const { links, nodes, labels, rest } = collectFlow(children);
153
+ const layout = layoutSankey(
154
+ { links, nodes: nodes.map((n) => ({ id: n.id, stage: n.stage })) },
155
+ H,
156
+ GAP
157
+ );
158
+ const colX = (stage: number): number =>
159
+ layout.stages <= 1
160
+ ? VB_W / 2
161
+ : L_MARGIN +
162
+ (stage / (layout.stages - 1)) * (VB_W - L_MARGIN - R_MARGIN - BAR_W);
163
+ const headers = textList(stages);
164
+ const nodeById = new Map(layout.nodes.map((n) => [n.id, n]));
165
+ const toneIdx = new Map(layout.nodes.map((n, i) => [n.id, i] as const));
166
+ const styleOfNode = (id: string): ReturnType<typeof toneOf> => {
167
+ const specTone = nodes.find((n) => n.id === id)?.tone;
168
+ return toneOf(specTone, toneIdx.get(id) ?? 0);
169
+ };
170
+ const last = layout.stages - 1;
171
+
172
+ return (
173
+ <ChartPanel icon="lucide:waypoints" title={title}>
174
+ <div className="overflow-x-auto px-4 py-3">
175
+ <svg
176
+ className="h-auto w-full min-w-[28rem]"
177
+ viewBox={`0 0 ${VB_W} ${VB_H}`}
178
+ >
179
+ <title>{title ?? "sankey diagram"}</title>
180
+ {headers.map((h, i) =>
181
+ i >= layout.stages ? null : (
182
+ <text
183
+ className={AXIS_TEXT}
184
+ fontSize={8}
185
+ key={h}
186
+ letterSpacing="0.12em"
187
+ textAnchor="middle"
188
+ x={colX(i) + BAR_W / 2}
189
+ y={12}
190
+ >
191
+ {h.toUpperCase()}
192
+ </text>
193
+ )
194
+ )}
195
+ {layout.links.map((l, i) => {
196
+ const s = nodeById.get(l.from);
197
+ const t = nodeById.get(l.to);
198
+ if (s === undefined || t === undefined) {
199
+ return null;
200
+ }
201
+ const x1 = colX(s.stage) + BAR_W;
202
+ const x2 = colX(t.stage);
203
+ const y0 = TOP + s.y + l.sy;
204
+ const y1 = TOP + t.y + l.ty;
205
+ const th = Math.max(1, l.value * layout.k);
206
+ const midX = (x1 + x2) / 2;
207
+ const d = `M${x1} ${y0}C${midX} ${y0} ${midX} ${y1} ${x2} ${y1}L${x2} ${y1 + th}C${midX} ${y1 + th} ${midX} ${y0 + th} ${x1} ${y0 + th}Z`;
208
+ return (
209
+ <path className={styleOfNode(l.from).soft} d={d} key={`l${i}`}>
210
+ <title>
211
+ {`${labels.get(l.from) ?? l.from} → ${labels.get(l.to) ?? l.to} · ${fmtNum(l.value)}${unit ?? ""}`}
212
+ </title>
213
+ </path>
214
+ );
215
+ })}
216
+ {layout.nodes.map((n) => {
217
+ const x = colX(n.stage);
218
+ const label = labels.get(n.id) ?? n.id;
219
+ const edge = n.stage === 0 || n.stage === last;
220
+ const { anchor, x: textX } = labelPos(n.stage, last, x);
221
+ return (
222
+ <g key={n.id}>
223
+ <rect
224
+ className={styleOfNode(n.id).fill}
225
+ height={n.h}
226
+ rx={1.5}
227
+ width={BAR_W}
228
+ x={x}
229
+ y={TOP + n.y}
230
+ >
231
+ <title>{`${label} · ${fmtNum(n.value)}${unit ?? ""}`}</title>
232
+ </rect>
233
+ {edge ? (
234
+ <>
235
+ <text
236
+ className={AXIS_TEXT}
237
+ fontSize={10}
238
+ textAnchor={anchor}
239
+ x={textX}
240
+ y={TOP + n.y + n.h / 2 - 1}
241
+ >
242
+ {label}
243
+ </text>
244
+ <text
245
+ className="fill-neutral-400 dark:fill-neutral-500"
246
+ fontSize={8}
247
+ textAnchor={anchor}
248
+ x={textX}
249
+ y={TOP + n.y + n.h / 2 + 10}
250
+ >
251
+ {fmtNum(n.value)}
252
+ {unit}
253
+ </text>
254
+ </>
255
+ ) : (
256
+ <>
257
+ <text
258
+ className={AXIS_TEXT}
259
+ fontSize={10}
260
+ textAnchor={anchor}
261
+ x={textX}
262
+ y={TOP + n.y - 12}
263
+ >
264
+ {label}
265
+ </text>
266
+ <text
267
+ className="fill-neutral-400 dark:fill-neutral-500"
268
+ fontSize={8}
269
+ textAnchor={anchor}
270
+ x={textX}
271
+ y={TOP + n.y - 3}
272
+ >
273
+ {fmtNum(n.value)}
274
+ {unit}
275
+ </text>
276
+ </>
277
+ )}
278
+ </g>
279
+ );
280
+ })}
281
+ </svg>
282
+ </div>
283
+ {rest.length > 0 ? <div className="px-4 pb-3">{rest}</div> : null}
284
+ </ChartPanel>
285
+ );
286
+ }
287
+ );
@@ -0,0 +1,237 @@
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, niceBounds, toneOf } from "./chart.js";
10
+ import { isEl } from "./children.js";
11
+ import { TEXT, TEXT_MICRO } from "./tones.js";
12
+
13
+ /**
14
+ * Scatter plot — <Point> dots on two auto-scaled numeric axes (x/y props
15
+ * name them). `size` turns a point into a bubble; `name` labels it.
16
+ */
17
+
18
+ const POINT_SCHEMA = v.looseObject({
19
+ name: v.optional(v.string()),
20
+ size: v.optional(NUMISH),
21
+ tone: v.optional(v.picklist(CHART_TONES)),
22
+ x: NUMISH,
23
+ y: NUMISH,
24
+ });
25
+
26
+ type PointSpec = v.InferOutput<typeof POINT_SCHEMA>;
27
+
28
+ export const Point = defineComponent(
29
+ {
30
+ description:
31
+ "散布図の1点。<Scatter> の子。x/y は必須、name でラベル、size でバブル化、tone で色",
32
+ schema: POINT_SCHEMA,
33
+ },
34
+ // Standalone use: an `(x, y)` chip so misplaced points still render.
35
+ ({ x, y, name }): ReactElement => (
36
+ <span className={`font-mono ${CODE_CHIP_CLS}`}>
37
+ {nonEmpty(name) ? name : "point"}
38
+ <span className="opacity-70">
39
+ ({x}, {y})
40
+ </span>
41
+ </span>
42
+ )
43
+ );
44
+
45
+ const collectPoints = (
46
+ children: ReactNode
47
+ ): { pts: { spec: PointSpec; x: number; y: number }[]; rest: ReactNode[] } => {
48
+ const pts: { spec: PointSpec; x: number; y: number }[] = [];
49
+ const rest: ReactNode[] = [];
50
+ for (const child of flattenChildren(children)) {
51
+ if (isEl(child, Point)) {
52
+ const spec = parseProps(POINT_SCHEMA, child.props, "Point");
53
+ const x = numOf(spec.x);
54
+ const y = numOf(spec.y);
55
+ if (x !== undefined && y !== undefined) {
56
+ pts.push({ spec, x, y });
57
+ }
58
+ } else {
59
+ rest.push(child);
60
+ }
61
+ }
62
+ return { pts, rest };
63
+ };
64
+
65
+ const VB_W = 640;
66
+ const VB_H = 360;
67
+ const PLOT = { bottom: 316, left: 48, right: 628, top: 12 };
68
+
69
+ const GRID_STROKE = "stroke-neutral-200 dark:stroke-neutral-700";
70
+ const BASE_STROKE = "stroke-neutral-300 dark:stroke-neutral-600";
71
+ const TICK_TEXT = "fill-neutral-400 dark:fill-neutral-500";
72
+
73
+ export const Scatter = defineComponent(
74
+ {
75
+ description:
76
+ "散布図コンテナ (2変数の相関)。<Point x y> を並べる。x/y で軸名、size 指定でバブル。軸はデータから自動スケール",
77
+ schema: v.looseObject({
78
+ title: v.optional(v.string()),
79
+ x: v.optional(v.string()),
80
+ y: v.optional(v.string()),
81
+ }),
82
+ },
83
+ ({ title, x, y, children }) => {
84
+ const { pts, rest } = collectPoints(children);
85
+ const xs = pts.map((p) => p.x);
86
+ const ys = pts.map((p) => p.y);
87
+ const xExt = niceBounds(Math.min(0, ...xs), Math.max(1, ...xs));
88
+ const yExt = niceBounds(Math.min(0, ...ys), Math.max(1, ...ys));
89
+ const xSpan = xExt.max - xExt.min || 1;
90
+ const ySpan = yExt.max - yExt.min || 1;
91
+ const xOf = (val: number): number =>
92
+ PLOT.left + ((val - xExt.min) / xSpan) * (PLOT.right - PLOT.left);
93
+ const yOf = (val: number): number =>
94
+ PLOT.bottom - ((val - yExt.min) / ySpan) * (PLOT.bottom - PLOT.top);
95
+ const maxSize = Math.max(0, ...pts.map((p) => numOf(p.spec.size) ?? 0));
96
+
97
+ return (
98
+ <ChartPanel icon="lucide:chart-scatter" title={title}>
99
+ <div className="px-4 py-3">
100
+ <svg className="h-auto w-full" viewBox={`0 0 ${VB_W} ${VB_H}`}>
101
+ <title>{title ?? "scatter plot"}</title>
102
+ {xExt.ticks.map((t) => (
103
+ <g key={`x${t}`}>
104
+ <line
105
+ className={GRID_STROKE}
106
+ strokeDasharray="2 3"
107
+ strokeWidth={1}
108
+ x1={xOf(t)}
109
+ x2={xOf(t)}
110
+ y1={PLOT.top}
111
+ y2={PLOT.bottom}
112
+ />
113
+ <text
114
+ className={TICK_TEXT}
115
+ fontSize={9}
116
+ textAnchor="middle"
117
+ x={xOf(t)}
118
+ y={PLOT.bottom + 14}
119
+ >
120
+ {fmtNum(t)}
121
+ </text>
122
+ </g>
123
+ ))}
124
+ {yExt.ticks.map((t) => (
125
+ <g key={`y${t}`}>
126
+ <line
127
+ className={GRID_STROKE}
128
+ strokeDasharray="2 3"
129
+ strokeWidth={1}
130
+ x1={PLOT.left}
131
+ x2={PLOT.right}
132
+ y1={yOf(t)}
133
+ y2={yOf(t)}
134
+ />
135
+ <text
136
+ className={TICK_TEXT}
137
+ fontSize={9}
138
+ textAnchor="end"
139
+ x={PLOT.left - 6}
140
+ y={yOf(t) + 3}
141
+ >
142
+ {fmtNum(t)}
143
+ </text>
144
+ </g>
145
+ ))}
146
+ <line
147
+ className={BASE_STROKE}
148
+ strokeWidth={1}
149
+ x1={PLOT.left}
150
+ x2={PLOT.right}
151
+ y1={PLOT.bottom}
152
+ y2={PLOT.bottom}
153
+ />
154
+ <line
155
+ className={BASE_STROKE}
156
+ strokeWidth={1}
157
+ x1={PLOT.left}
158
+ x2={PLOT.left}
159
+ y1={PLOT.top}
160
+ y2={PLOT.bottom}
161
+ />
162
+ {nonEmpty(x) ? (
163
+ <text
164
+ className={TICK_TEXT}
165
+ fontSize={9}
166
+ letterSpacing="0.12em"
167
+ textAnchor="middle"
168
+ x={(PLOT.left + PLOT.right) / 2}
169
+ y={VB_H - 10}
170
+ >
171
+ {x.toUpperCase()}
172
+ </text>
173
+ ) : null}
174
+ {nonEmpty(y) ? (
175
+ <text
176
+ className={TICK_TEXT}
177
+ fontSize={9}
178
+ letterSpacing="0.12em"
179
+ textAnchor="middle"
180
+ transform={`rotate(-90 14 ${(PLOT.top + PLOT.bottom) / 2})`}
181
+ x={14}
182
+ y={(PLOT.top + PLOT.bottom) / 2}
183
+ >
184
+ {y.toUpperCase()}
185
+ </text>
186
+ ) : null}
187
+ {pts.map((p, i) => {
188
+ const style = toneOf(p.spec.tone, i);
189
+ const size = numOf(p.spec.size);
190
+ const r =
191
+ size === undefined || maxSize === 0
192
+ ? 4
193
+ : 4 + Math.sqrt(size / maxSize) * 10;
194
+ return (
195
+ <g key={i}>
196
+ <circle
197
+ className={size === undefined ? style.fill : style.soft}
198
+ cx={xOf(p.x)}
199
+ cy={yOf(p.y)}
200
+ r={r}
201
+ >
202
+ <title>
203
+ {`${p.spec.name ?? "point"} · ${fmtNum(p.x)}, ${fmtNum(p.y)}${size === undefined ? "" : ` · ${fmtNum(size)}`}`}
204
+ </title>
205
+ </circle>
206
+ {size === undefined ? null : (
207
+ <circle
208
+ className={style.stroke}
209
+ cx={xOf(p.x)}
210
+ cy={yOf(p.y)}
211
+ fill="none"
212
+ r={r}
213
+ strokeWidth={1}
214
+ />
215
+ )}
216
+ {nonEmpty(p.spec.name) ? (
217
+ <text
218
+ className="fill-neutral-600 dark:fill-neutral-300"
219
+ fontSize={9}
220
+ x={xOf(p.x) + r + 3}
221
+ y={yOf(p.y) + 3}
222
+ >
223
+ {p.spec.name}
224
+ </text>
225
+ ) : null}
226
+ </g>
227
+ );
228
+ })}
229
+ </svg>
230
+ {rest.length > 0 ? (
231
+ <div className={`${TEXT_MICRO} ${TEXT.muted}`}>{rest}</div>
232
+ ) : null}
233
+ </div>
234
+ </ChartPanel>
235
+ );
236
+ }
237
+ );
@@ -0,0 +1,113 @@
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 { RowNote, Section } from "./bits.js";
7
+ import { Icon } from "./icon.js";
8
+ import {
9
+ BORDER_CLS,
10
+ LOC_CLS,
11
+ MINI_CHIP_CLS,
12
+ SURFACE_CLS,
13
+ TEXT,
14
+ TEXT_SUB,
15
+ TONE,
16
+ TRIM_CLS,
17
+ } from "./tones.js";
18
+
19
+ export const DbField = defineComponent(
20
+ {
21
+ description:
22
+ 'テーブルカラム1行。name/type は必須。pk / fk="table.col" / unique / null で制約チップ、default で既定値。children は説明文',
23
+ schema: v.looseObject({
24
+ default: v.optional(v.string()),
25
+ fk: v.optional(v.string()),
26
+ name: v.string(),
27
+ null: BOOLISH_PROP,
28
+ pk: BOOLISH_PROP,
29
+ type: v.string(),
30
+ unique: BOOLISH_PROP,
31
+ }),
32
+ },
33
+ ({ name, type, pk, fk, unique, null: nullable, default: def, children }) => (
34
+ <div className="flex flex-wrap items-baseline gap-x-2.5 gap-y-1 px-4 py-2">
35
+ <span className={`w-40 shrink-0 font-mono text-[0.85em] ${TEXT.code}`}>
36
+ {name}
37
+ </span>
38
+ <span className={`${MINI_CHIP_CLS} ${TONE.neutral} font-mono`}>
39
+ {type}
40
+ </span>
41
+ {attrTrue(pk) ? (
42
+ <span className={`${MINI_CHIP_CLS} ${TONE.amber}`}>
43
+ <Icon className="h-2.5 w-2.5" name="lucide:key-round" />
44
+ PK
45
+ </span>
46
+ ) : null}
47
+ {nonEmpty(fk) ? (
48
+ <span className={`${MINI_CHIP_CLS} ${TONE.sky}`}>
49
+ <Icon className="h-2.5 w-2.5" name="lucide:link" />
50
+ FK → {fk}
51
+ </span>
52
+ ) : null}
53
+ {attrTrue(unique) ? (
54
+ <span className={`${MINI_CHIP_CLS} ${TONE.violet}`}>unique</span>
55
+ ) : null}
56
+ {attrTrue(nullable) ? (
57
+ <span className={`${MINI_CHIP_CLS} ${TONE.neutral}`}>null</span>
58
+ ) : null}
59
+ {nonEmpty(def) ? (
60
+ <span className={`font-mono text-xs ${TEXT.faint}`}>= {def}</span>
61
+ ) : null}
62
+ <RowNote className={TRIM_CLS}>{children}</RowNote>
63
+ </div>
64
+ )
65
+ );
66
+
67
+ export const DbTable = defineComponent(
68
+ {
69
+ description:
70
+ "テーブル定義ブロック。name は必須、note で補足 (エンジン・行数など)。<DbField> をカラム行として並べる",
71
+ schema: v.looseObject({
72
+ name: v.string(),
73
+ note: v.optional(v.string()),
74
+ }),
75
+ },
76
+ ({ name, note, children }) => (
77
+ <div
78
+ className={`not-prose mb-4 overflow-hidden rounded-lg border last:mb-0 ${BORDER_CLS}`}
79
+ >
80
+ <div
81
+ className={`flex flex-wrap items-center gap-x-2 gap-y-1 border-b px-4 py-2 text-xs ${BORDER_CLS} ${SURFACE_CLS}`}
82
+ >
83
+ <Icon className={`h-3.5 w-3.5 ${TEXT.faint}`} name="lucide:table-2" />
84
+ <code className={`font-mono font-semibold ${TEXT.code}`}>{name}</code>
85
+ {nonEmpty(note) ? (
86
+ <span className={`ml-auto ${TEXT_SUB} ${TEXT.faint}`}>{note}</span>
87
+ ) : null}
88
+ </div>
89
+ <div className="divide-y divide-neutral-100 dark:divide-neutral-800/60">
90
+ {children}
91
+ </div>
92
+ </div>
93
+ )
94
+ );
95
+
96
+ export const Schema = defineComponent(
97
+ {
98
+ description:
99
+ "DB スキーマ定義のコンテナ。<DbTable> を並べる。title は見出し。マイグレーション計画・データモデル説明向け",
100
+ schema: v.looseObject({
101
+ engine: v.optional(v.string()),
102
+ title: v.optional(v.string()),
103
+ }),
104
+ },
105
+ ({ title, engine, children }) => (
106
+ <Section title={title}>
107
+ {nonEmpty(engine) ? (
108
+ <div className={`mb-2 ${LOC_CLS}`}>{engine}</div>
109
+ ) : null}
110
+ {children}
111
+ </Section>
112
+ )
113
+ );