@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,334 @@
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 { attrTrue, BOOLISH_PROP, NUMISH, numOf } from "./attrs.js";
7
+ import { CODE_CHIP_CLS } from "./bits.js";
8
+ import { ChartLegend, ChartPanel } from "./chart-bits.js";
9
+ import {
10
+ CHART_TONES,
11
+ fmtNum,
12
+ niceScale,
13
+ numList,
14
+ textList,
15
+ toneOf,
16
+ } from "./chart.js";
17
+ import { isEl } from "./children.js";
18
+ import { RAIL_BG_CLS, TEXT, TEXT_MICRO, TEXT_SUB, TRACK_CLS } from "./tones.js";
19
+
20
+ /**
21
+ * Bar/column chart — categorical comparison with optional multi-series
22
+ * bars (`values` on `<Bar>` + `series` names on `<BarChart>`). Vertical
23
+ * columns are the default; `direction="horizontal"` flips to labeled rows
24
+ * for long names or many categories.
25
+ */
26
+
27
+ const BAR_SCHEMA = v.looseObject({
28
+ name: v.string(),
29
+ note: v.optional(v.string()),
30
+ tone: v.optional(v.picklist(CHART_TONES)),
31
+ value: v.optional(NUMISH),
32
+ values: v.optional(v.string()),
33
+ });
34
+
35
+ type BarSpec = v.InferOutput<typeof BAR_SCHEMA> & { nums: number[] };
36
+
37
+ export const Bar = defineComponent(
38
+ {
39
+ description:
40
+ 'バーチャートの1カテゴリ。<BarChart> の子。name/value は必須。values="12,8" で複数系列 (series 名は <BarChart> の series で指定)、tone で色指定',
41
+ schema: BAR_SCHEMA,
42
+ },
43
+ // Standalone use: a `name value` chip so misplaced bars still render.
44
+ ({ name, value, values, note }): ReactElement => (
45
+ <span className={`font-mono ${CODE_CHIP_CLS}`}>
46
+ {name}
47
+ <span className="opacity-70">{nonEmpty(values) ? values : value}</span>
48
+ {nonEmpty(note) ? <span className="opacity-70">{note}</span> : null}
49
+ </span>
50
+ )
51
+ );
52
+
53
+ /** Splits children into <Bar> specs (parsed, with numeric lists) and rest. */
54
+ const collectBars = (
55
+ children: ReactNode
56
+ ): { bars: BarSpec[]; rest: ReactNode[] } => {
57
+ const bars: BarSpec[] = [];
58
+ const rest: ReactNode[] = [];
59
+ for (const child of flattenChildren(children)) {
60
+ if (isEl(child, Bar)) {
61
+ const spec = parseProps(BAR_SCHEMA, child.props, "Bar");
62
+ const nums = nonEmpty(spec.values)
63
+ ? numList(spec.values)
64
+ : (() => {
65
+ const n = numOf(spec.value);
66
+ return n === undefined ? [] : [n];
67
+ })();
68
+ bars.push({ ...spec, nums });
69
+ } else {
70
+ rest.push(child);
71
+ }
72
+ }
73
+ return { bars, rest };
74
+ };
75
+
76
+ /** Horizontal guide lines shared by every column's plot area. */
77
+ const Guides = (props: { ceil: number; ticks: number[] }): ReactElement => (
78
+ <>
79
+ <div
80
+ aria-hidden
81
+ className={`absolute bottom-0 h-px w-full ${RAIL_BG_CLS}`}
82
+ />
83
+ {props.ticks.map((t) => (
84
+ <div
85
+ aria-hidden
86
+ className={`absolute h-px w-full ${RAIL_BG_CLS} opacity-50`}
87
+ key={t}
88
+ style={{ bottom: `${(t / props.ceil) * 100}%` }}
89
+ />
90
+ ))}
91
+ </>
92
+ );
93
+
94
+ const BAR_MIN_PCT = 0.8;
95
+
96
+ /** One vertical column — value label, stacked/grouped bars, category name. */
97
+ const Column = (props: {
98
+ bar: BarSpec;
99
+ ceil: number;
100
+ stacked: boolean;
101
+ }): ReactElement => {
102
+ const { bar, ceil } = props;
103
+ const total = bar.nums.reduce((a, b) => a + b, 0);
104
+ const single = bar.nums.length <= 1;
105
+ return (
106
+ <div className="flex min-w-0 flex-1 flex-col">
107
+ <div className="flex h-36 flex-col items-center justify-end">
108
+ {single || props.stacked ? (
109
+ <span
110
+ className={`font-mono ${TEXT_MICRO} tabular-nums ${TEXT.muted}`}
111
+ >
112
+ {fmtNum(total)}
113
+ </span>
114
+ ) : null}
115
+ {props.stacked ? (
116
+ <div
117
+ className="flex w-full max-w-10 flex-col-reverse overflow-hidden rounded-t-sm"
118
+ style={{ height: `${Math.min(100, (total / ceil) * 100)}%` }}
119
+ title={`${bar.name} · ${fmtNum(total)}`}
120
+ >
121
+ {bar.nums.map((n, k) => (
122
+ <div
123
+ className={`w-full ${toneOf(undefined, k).bg}`}
124
+ key={k}
125
+ style={{
126
+ height: `${total === 0 ? 0 : (n / total) * 100}%`,
127
+ }}
128
+ />
129
+ ))}
130
+ </div>
131
+ ) : (
132
+ <div className="flex min-h-0 w-full max-w-16 flex-1 items-end justify-center gap-0.5">
133
+ {bar.nums.map((n, k) => (
134
+ <div
135
+ className={`min-w-1 flex-1 rounded-t-sm ${toneOf(single ? bar.tone : undefined, k).bg}`}
136
+ key={k}
137
+ style={{
138
+ height: `${Math.min(100, Math.max(n > 0 ? BAR_MIN_PCT : 0, (n / ceil) * 100))}%`,
139
+ }}
140
+ title={`${bar.name} · ${fmtNum(n)}`}
141
+ />
142
+ ))}
143
+ </div>
144
+ )}
145
+ </div>
146
+ <div className="mt-1 truncate text-center text-xs">{bar.name}</div>
147
+ {nonEmpty(bar.note) ? (
148
+ <div className={`truncate text-center ${TEXT_SUB} ${TEXT.faint}`}>
149
+ {bar.note}
150
+ </div>
151
+ ) : null}
152
+ </div>
153
+ );
154
+ };
155
+
156
+ /** The track cell of a horizontal row — stacked band, single bar, or thin grouped bars. */
157
+ const HTrack = (props: {
158
+ bar: BarSpec;
159
+ ceil: number;
160
+ stacked: boolean;
161
+ total: number;
162
+ }): ReactElement => {
163
+ const { bar, ceil, total } = props;
164
+ if (props.stacked) {
165
+ return (
166
+ <div className={`flex h-2.5 overflow-hidden rounded ${TRACK_CLS}`}>
167
+ {bar.nums.map((n, k) => (
168
+ <div
169
+ className={`h-full ${toneOf(undefined, k).bg}`}
170
+ key={k}
171
+ style={{ width: `${(n / ceil) * 100}%` }}
172
+ />
173
+ ))}
174
+ </div>
175
+ );
176
+ }
177
+ if (bar.nums.length <= 1) {
178
+ return (
179
+ <div className={`relative h-4.5 rounded ${TRACK_CLS}`}>
180
+ <div
181
+ className={`absolute inset-y-0 rounded ${toneOf(bar.tone, 0).bg}`}
182
+ style={{
183
+ width: `${Math.min(100, Math.max(total > 0 ? BAR_MIN_PCT : 0, (total / ceil) * 100))}%`,
184
+ }}
185
+ />
186
+ </div>
187
+ );
188
+ }
189
+ return (
190
+ <div
191
+ className={`flex h-5 flex-col justify-center gap-0.5 rounded ${TRACK_CLS} px-1`}
192
+ >
193
+ {bar.nums.map((n, k) => (
194
+ <div
195
+ className={`h-1 rounded-full ${toneOf(undefined, k).bg}`}
196
+ key={k}
197
+ style={{ width: `${Math.min(100, (n / ceil) * 100)}%` }}
198
+ />
199
+ ))}
200
+ </div>
201
+ );
202
+ };
203
+
204
+ /** One horizontal row — label, track with bars, value. */
205
+ const HRow = (props: {
206
+ bar: BarSpec;
207
+ ceil: number;
208
+ stacked: boolean;
209
+ }): ReactElement => {
210
+ const { bar, ceil } = props;
211
+ const total = bar.nums.reduce((a, b) => a + b, 0);
212
+ return (
213
+ <div className="grid grid-cols-[minmax(6rem,9rem)_minmax(0,1fr)_4.5rem] items-center gap-x-3 py-1">
214
+ <div className="min-w-0">
215
+ <div className="truncate text-sm">{bar.name}</div>
216
+ {nonEmpty(bar.note) ? (
217
+ <div className={`truncate ${TEXT_SUB} ${TEXT.faint}`}>{bar.note}</div>
218
+ ) : null}
219
+ </div>
220
+ <HTrack bar={bar} ceil={ceil} stacked={props.stacked} total={total} />
221
+ <div
222
+ className={`text-right font-mono ${TEXT_MICRO} tabular-nums ${TEXT.muted}`}
223
+ >
224
+ {fmtNum(total)}
225
+ </div>
226
+ </div>
227
+ );
228
+ };
229
+
230
+ export const BarChart = defineComponent(
231
+ {
232
+ description:
233
+ 'バーチャートコンテナ (カテゴリ比較)。<Bar name value> を並べる。direction="horizontal" で横棒、stacked で積み上げ、series="a,b" で系列名 (凡例)、max/unit でスケールと単位',
234
+ schema: v.looseObject({
235
+ direction: v.optional(v.picklist(["vertical", "horizontal"]), "vertical"),
236
+ max: v.optional(NUMISH),
237
+ series: v.optional(v.string()),
238
+ stacked: BOOLISH_PROP,
239
+ title: v.optional(v.string()),
240
+ unit: v.optional(v.string()),
241
+ }),
242
+ },
243
+ ({ title, direction, max, series, stacked, unit, children }) => {
244
+ const { bars, rest } = collectBars(children);
245
+ const isStacked = attrTrue(stacked);
246
+ const seriesNames = textList(series);
247
+ const maxTotal = Math.max(
248
+ 0,
249
+ ...bars.map((b) =>
250
+ isStacked ? b.nums.reduce((a, n) => a + n, 0) : Math.max(0, ...b.nums)
251
+ )
252
+ );
253
+ const scale = niceScale(numOf(max) ?? maxTotal);
254
+ const seriesCount = Math.max(1, ...bars.map((b) => b.nums.length));
255
+ let legend: { name: string; style: ReturnType<typeof toneOf> }[] = [];
256
+ if (seriesNames.length > 0) {
257
+ legend = seriesNames.map((name, i) => ({
258
+ name,
259
+ style: toneOf(undefined, i),
260
+ }));
261
+ } else if (seriesCount > 1) {
262
+ legend = Array.from({ length: seriesCount }, (_, i) => ({
263
+ name: `s${i + 1}`,
264
+ style: toneOf(undefined, i),
265
+ }));
266
+ }
267
+ const horizontal = direction === "horizontal";
268
+
269
+ return (
270
+ <ChartPanel
271
+ figure={nonEmpty(unit) ? `0–${fmtNum(scale.ceil)}${unit}` : undefined}
272
+ icon={horizontal ? "lucide:chart-bar" : "lucide:chart-column"}
273
+ title={title}
274
+ >
275
+ <div className="px-4 py-3">
276
+ {horizontal ? (
277
+ <>
278
+ <div className="grid grid-cols-[minmax(6rem,9rem)_minmax(0,1fr)_4.5rem] gap-x-3 pb-1">
279
+ <span />
280
+ <span
281
+ className={`flex justify-between font-mono ${TEXT_MICRO} ${TEXT.faint} tabular-nums`}
282
+ >
283
+ <span>0</span>
284
+ <span>
285
+ {fmtNum(scale.ceil)}
286
+ {unit}
287
+ </span>
288
+ </span>
289
+ <span />
290
+ </div>
291
+ {bars.map((bar) => (
292
+ <HRow
293
+ bar={bar}
294
+ ceil={scale.ceil}
295
+ key={bar.name}
296
+ stacked={isStacked}
297
+ />
298
+ ))}
299
+ </>
300
+ ) : (
301
+ <>
302
+ <div className="relative">
303
+ <div aria-hidden className="absolute inset-0 h-36">
304
+ <Guides ceil={scale.ceil} ticks={scale.ticks} />
305
+ </div>
306
+ <div className="relative flex items-end gap-x-3">
307
+ {bars.map((bar) => (
308
+ <Column
309
+ bar={bar}
310
+ ceil={scale.ceil}
311
+ key={bar.name}
312
+ stacked={isStacked}
313
+ />
314
+ ))}
315
+ </div>
316
+ </div>
317
+ <div
318
+ className={`mt-1 flex justify-between font-mono ${TEXT_MICRO} ${TEXT.faint} tabular-nums`}
319
+ >
320
+ <span>0</span>
321
+ <span>
322
+ {fmtNum(scale.ceil)}
323
+ {unit}
324
+ </span>
325
+ </div>
326
+ </>
327
+ )}
328
+ </div>
329
+ <ChartLegend items={legend} />
330
+ {rest.length > 0 ? <div className="px-4 pb-3">{rest}</div> : null}
331
+ </ChartPanel>
332
+ );
333
+ }
334
+ );
@@ -0,0 +1,143 @@
1
+ import { createContext, useContext } from "react";
2
+ import * as v from "valibot";
3
+
4
+ import { defineComponent } from "../define.js";
5
+ import { nonEmpty } from "../guards.js";
6
+ import { numOf, NUMISH } from "./attrs.js";
7
+ import { CaptionBar, ListPanel, ListRow, RowNote } from "./bits.js";
8
+ import { Icon } from "./icon.js";
9
+ import {
10
+ CAPTION_TITLE_CLS,
11
+ COUNT_CHIP_CLS,
12
+ MINI_CHIP_CLS,
13
+ TEXT,
14
+ TEXT_SUB,
15
+ TONE,
16
+ TRIM_CLS,
17
+ } from "./tones.js";
18
+
19
+ export const BENCH_BETTER = ["higher", "lower"] as const;
20
+ export type BenchBetter = (typeof BENCH_BETTER)[number];
21
+
22
+ /** `better` from <Benchmarks> — which direction is an improvement. */
23
+ const BetterCtx = createContext<BenchBetter>("higher");
24
+
25
+ /** Signed pct change, formatted `+21%` / `−8%` / `±0%`. */
26
+ const deltaOf = (before: number, after: number): string => {
27
+ if (before === 0) {
28
+ return after === 0 ? "±0%" : "+∞";
29
+ }
30
+ const d = ((after - before) / Math.abs(before)) * 100;
31
+ if (Math.abs(d) < 0.5) {
32
+ return "±0%";
33
+ }
34
+ const rounded = Math.abs(d) >= 100 ? Math.round(d) : Math.round(d * 10) / 10;
35
+ return `${d > 0 ? "+" : "−"}${Math.abs(rounded)}%`;
36
+ };
37
+
38
+ interface DeltaView {
39
+ cls: string;
40
+ icon: string;
41
+ text: string;
42
+ }
43
+
44
+ /** before/after の変化率チップ。undefined = 計算不能 (非数値入力)。 */
45
+ const deltaView = (
46
+ before: number | undefined,
47
+ after: number | undefined,
48
+ better: BenchBetter
49
+ ): DeltaView | undefined => {
50
+ if (before === undefined || after === undefined) {
51
+ return undefined;
52
+ }
53
+ const text = deltaOf(before, after);
54
+ if (text === "±0%") {
55
+ return { cls: TONE.neutral, icon: "lucide:minus", text };
56
+ }
57
+ const improved = better === "lower" ? after < before : after > before;
58
+ return improved
59
+ ? { cls: TONE.emerald, icon: "lucide:trending-up", text }
60
+ : { cls: TONE.red, icon: "lucide:trending-down", text };
61
+ };
62
+
63
+ export const Bench = defineComponent(
64
+ {
65
+ description:
66
+ 'ベンチマーク比較1行。name は必須、before → after を表示し変化率を自動計算。<Benchmarks better="lower"> の中では減少が改善 (緑) になる。unit で単位表示、children はメモ',
67
+ schema: v.looseObject({
68
+ after: NUMISH,
69
+ before: NUMISH,
70
+ name: v.string(),
71
+ note: v.optional(v.string()),
72
+ unit: v.optional(v.string()),
73
+ }),
74
+ },
75
+ ({ name, before, after, unit, note, children }) => {
76
+ const better = useContext(BetterCtx);
77
+ const delta = deltaView(numOf(before), numOf(after), better);
78
+ return (
79
+ <ListRow>
80
+ <Icon
81
+ className={`h-3.5 w-3.5 shrink-0 self-center ${TEXT.faint}`}
82
+ name="lucide:timer"
83
+ />
84
+ <span className="text-sm">{name}</span>
85
+ <span className="inline-flex items-baseline gap-1.5 font-mono text-xs tabular-nums">
86
+ <span className={TEXT.muted}>
87
+ {String(before)}
88
+ {nonEmpty(unit) ? unit : ""}
89
+ </span>
90
+ <Icon
91
+ className={`h-3 w-3 self-center ${TEXT.faint}`}
92
+ name="lucide:arrow-right"
93
+ />
94
+ <span className={TEXT.code}>
95
+ {String(after)}
96
+ {nonEmpty(unit) ? unit : ""}
97
+ </span>
98
+ </span>
99
+ {delta === undefined ? null : (
100
+ <span className={`${MINI_CHIP_CLS} ${delta.cls}`}>
101
+ <Icon className="h-2.5 w-2.5" name={delta.icon} />
102
+ {delta.text}
103
+ </span>
104
+ )}
105
+ {nonEmpty(note) ? (
106
+ <span className={`text-sm ${TEXT.muted}`}>{note}</span>
107
+ ) : null}
108
+ <RowNote className={TRIM_CLS}>{children}</RowNote>
109
+ </ListRow>
110
+ );
111
+ }
112
+ );
113
+
114
+ export const Benchmarks = defineComponent(
115
+ {
116
+ description:
117
+ "ベンチマーク比較一覧のコンテナ。<Bench> を並べる。better は higher|lower (高い方/低い方が良い) で Δ の色判定を反転。title/unit はキャプション",
118
+ schema: v.looseObject({
119
+ better: v.optional(v.picklist(BENCH_BETTER), "higher"),
120
+ title: v.optional(v.string()),
121
+ unit: v.optional(v.string()),
122
+ }),
123
+ },
124
+ ({ title, unit, better, children }) => (
125
+ <ListPanel>
126
+ {nonEmpty(title) || nonEmpty(unit) ? (
127
+ <CaptionBar className={CAPTION_TITLE_CLS}>
128
+ <Icon className="h-3.5 w-3.5" name="lucide:gauge" />
129
+ {nonEmpty(title) ? title : "Benchmarks"}
130
+ <span
131
+ className={`ml-auto flex items-center gap-x-2 font-normal ${TEXT_SUB}`}
132
+ >
133
+ <span className={COUNT_CHIP_CLS}>{better} is better</span>
134
+ {nonEmpty(unit) ? (
135
+ <span className={`font-mono ${TEXT.faint}`}>{unit}</span>
136
+ ) : null}
137
+ </span>
138
+ </CaptionBar>
139
+ ) : null}
140
+ <BetterCtx.Provider value={better}>{children}</BetterCtx.Provider>
141
+ </ListPanel>
142
+ )
143
+ );