@suzumiyaaoba/mdxr 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +56 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +2902 -0
- package/dist/components.d.mts +791 -0
- package/dist/components.mjs +2 -0
- package/dist/config-SI9IyFiC.mjs +184 -0
- package/dist/doc-context-CEqzMYKv.d.mts +568 -0
- package/dist/index.d.mts +20 -0
- package/dist/index.mjs +4 -0
- package/dist/ui-GKFD0mx5.mjs +15986 -0
- package/package.json +131 -0
- package/skill/SKILL.md +49 -0
- package/skill/references/components/charts.md +172 -0
- package/skill/references/components/document.md +98 -0
- package/skill/references/components/forms.md +35 -0
- package/skill/references/components/investigation.md +198 -0
- package/skill/references/components/layout.md +68 -0
- package/skill/references/components/output.md +158 -0
- package/skill/references/components/planning.md +155 -0
- package/skill/references/components/reports.md +252 -0
- package/skill/references/components/shadcn.md +19 -0
- package/skill/references/components.md +186 -0
- package/skill/references/extending.md +48 -0
- package/src/ask-sheet.ts +40 -0
- package/src/assets/css.ts +392 -0
- package/src/assets/scripts.ts +78 -0
- package/src/catalog.ts +287 -0
- package/src/cli.ts +178 -0
- package/src/client/doc-events.ts +1108 -0
- package/src/client/entry.ts +27 -0
- package/src/client-js.ts +47 -0
- package/src/component-map.ts +47 -0
- package/src/components/ui/accordion.tsx +77 -0
- package/src/components/ui/alert-dialog.tsx +185 -0
- package/src/components/ui/alert.tsx +76 -0
- package/src/components/ui/aspect-ratio.tsx +22 -0
- package/src/components/ui/attachment.tsx +208 -0
- package/src/components/ui/avatar.tsx +106 -0
- package/src/components/ui/badge.tsx +52 -0
- package/src/components/ui/breadcrumb.tsx +121 -0
- package/src/components/ui/bubble.tsx +128 -0
- package/src/components/ui/button-group.tsx +88 -0
- package/src/components/ui/button.tsx +58 -0
- package/src/components/ui/calendar.tsx +226 -0
- package/src/components/ui/card.tsx +102 -0
- package/src/components/ui/carousel.tsx +246 -0
- package/src/components/ui/chart.tsx +379 -0
- package/src/components/ui/checkbox.tsx +27 -0
- package/src/components/ui/collapsible.tsx +19 -0
- package/src/components/ui/combobox.tsx +298 -0
- package/src/components/ui/command.tsx +193 -0
- package/src/components/ui/context-menu.tsx +271 -0
- package/src/components/ui/dialog.tsx +159 -0
- package/src/components/ui/direction.tsx +4 -0
- package/src/components/ui/drawer.tsx +227 -0
- package/src/components/ui/dropdown-menu.tsx +269 -0
- package/src/components/ui/empty.tsx +104 -0
- package/src/components/ui/field.tsx +237 -0
- package/src/components/ui/fieldset.tsx +32 -0
- package/src/components/ui/frame.tsx +87 -0
- package/src/components/ui/hover-card.tsx +50 -0
- package/src/components/ui/input-group.tsx +159 -0
- package/src/components/ui/input-otp.tsx +83 -0
- package/src/components/ui/input.tsx +19 -0
- package/src/components/ui/item.tsx +202 -0
- package/src/components/ui/kbd.tsx +26 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/marker.tsx +71 -0
- package/src/components/ui/menubar.tsx +284 -0
- package/src/components/ui/message-scroller.tsx +128 -0
- package/src/components/ui/message.tsx +91 -0
- package/src/components/ui/meter.tsx +80 -0
- package/src/components/ui/native-select.tsx +64 -0
- package/src/components/ui/navigation-menu.tsx +170 -0
- package/src/components/ui/pagination.tsx +133 -0
- package/src/components/ui/popover.tsx +87 -0
- package/src/components/ui/progress.tsx +82 -0
- package/src/components/ui/questionnaire.tsx +328 -0
- package/src/components/ui/radio-group.tsx +35 -0
- package/src/components/ui/resizable.tsx +49 -0
- package/src/components/ui/scroll-area.tsx +50 -0
- package/src/components/ui/select.tsx +201 -0
- package/src/components/ui/separator.tsx +22 -0
- package/src/components/ui/sheet.tsx +135 -0
- package/src/components/ui/sidebar.tsx +730 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +51 -0
- package/src/components/ui/spinner.tsx +16 -0
- package/src/components/ui/switch.tsx +31 -0
- package/src/components/ui/table.tsx +113 -0
- package/src/components/ui/tabs.tsx +82 -0
- package/src/components/ui/textarea.tsx +17 -0
- package/src/components/ui/toast.tsx +229 -0
- package/src/components/ui/toggle-group.tsx +87 -0
- package/src/components/ui/toggle.tsx +43 -0
- package/src/components/ui/tooltip.tsx +65 -0
- package/src/components.ts +78 -0
- package/src/config.ts +56 -0
- package/src/define.ts +122 -0
- package/src/doc-context.ts +20 -0
- package/src/editor.ts +94 -0
- package/src/format-error.ts +78 -0
- package/src/guards.ts +84 -0
- package/src/hooks/use-mobile.ts +21 -0
- package/src/html.ts +91 -0
- package/src/hydrate/export-index.ts +232 -0
- package/src/hydrate/import-scan.ts +169 -0
- package/src/hydrate/plugins.ts +118 -0
- package/src/hydrate/runtime-module.ts +145 -0
- package/src/hydrate-runtime.ts +67 -0
- package/src/hydrate.ts +147 -0
- package/src/index.ts +16 -0
- package/src/init.ts +57 -0
- package/src/langs.ts +148 -0
- package/src/lines.ts +53 -0
- package/src/load-user-module.ts +191 -0
- package/src/mdx.ts +248 -0
- package/src/paths.ts +36 -0
- package/src/rehype/shiki.ts +533 -0
- package/src/remark/alerts.ts +53 -0
- package/src/remark/ast.ts +96 -0
- package/src/remark/callouts.ts +25 -0
- package/src/remark/code-file.ts +85 -0
- package/src/remark/code-meta.ts +21 -0
- package/src/remark/directives.ts +170 -0
- package/src/remark/file-paths.ts +64 -0
- package/src/remark/headings.ts +131 -0
- package/src/remark/no-js.ts +40 -0
- package/src/render.ts +337 -0
- package/src/serve.ts +322 -0
- package/src/styles/globals.css +134 -0
- package/src/styles/shadcn.css +641 -0
- package/src/tailwind.ts +119 -0
- package/src/ui/approvals.tsx +76 -0
- package/src/ui/ask-question.tsx +386 -0
- package/src/ui/ask.tsx +206 -0
- package/src/ui/attrs.ts +51 -0
- package/src/ui/audit.tsx +139 -0
- package/src/ui/bar-chart.tsx +334 -0
- package/src/ui/benchmarks.tsx +143 -0
- package/src/ui/bits.tsx +537 -0
- package/src/ui/board.tsx +173 -0
- package/src/ui/bridge.tsx +207 -0
- package/src/ui/bumps.tsx +178 -0
- package/src/ui/callout.tsx +106 -0
- package/src/ui/changes.tsx +89 -0
- package/src/ui/chart-bits.tsx +52 -0
- package/src/ui/chart.ts +577 -0
- package/src/ui/checks.tsx +203 -0
- package/src/ui/child-index.tsx +44 -0
- package/src/ui/children.ts +43 -0
- package/src/ui/chips.tsx +49 -0
- package/src/ui/cmd.tsx +27 -0
- package/src/ui/columns.tsx +31 -0
- package/src/ui/comments.tsx +544 -0
- package/src/ui/compare.tsx +67 -0
- package/src/ui/decision.tsx +79 -0
- package/src/ui/deps.tsx +78 -0
- package/src/ui/details.tsx +43 -0
- package/src/ui/diff-parse.ts +307 -0
- package/src/ui/diff.tsx +458 -0
- package/src/ui/diffstat.tsx +58 -0
- package/src/ui/due.tsx +65 -0
- package/src/ui/effort.tsx +29 -0
- package/src/ui/endpoints.tsx +118 -0
- package/src/ui/envvars.tsx +96 -0
- package/src/ui/figure.tsx +37 -0
- package/src/ui/file-icon.ts +1020 -0
- package/src/ui/file-link.ts +32 -0
- package/src/ui/file-ref.tsx +39 -0
- package/src/ui/files.tsx +112 -0
- package/src/ui/findings.tsx +85 -0
- package/src/ui/flow.tsx +73 -0
- package/src/ui/funnel.tsx +121 -0
- package/src/ui/gantt.tsx +443 -0
- package/src/ui/gauges.tsx +135 -0
- package/src/ui/glossary.tsx +29 -0
- package/src/ui/graph-layout.ts +149 -0
- package/src/ui/graph-specs.tsx +102 -0
- package/src/ui/graph.tsx +278 -0
- package/src/ui/grid.tsx +119 -0
- package/src/ui/hypothesis.tsx +94 -0
- package/src/ui/icon.tsx +80 -0
- package/src/ui/incident.tsx +130 -0
- package/src/ui/index.ts +342 -0
- package/src/ui/ins-del.tsx +41 -0
- package/src/ui/json.tsx +190 -0
- package/src/ui/layout.ts +9 -0
- package/src/ui/line-chart.tsx +251 -0
- package/src/ui/matrix.tsx +208 -0
- package/src/ui/meta.tsx +73 -0
- package/src/ui/option.tsx +64 -0
- package/src/ui/owner.tsx +40 -0
- package/src/ui/packages.tsx +104 -0
- package/src/ui/pathway.tsx +96 -0
- package/src/ui/phase.tsx +41 -0
- package/src/ui/pie-chart.tsx +170 -0
- package/src/ui/plan.tsx +77 -0
- package/src/ui/pre.tsx +175 -0
- package/src/ui/priority.tsx +51 -0
- package/src/ui/props.tsx +77 -0
- package/src/ui/quadrant.tsx +172 -0
- package/src/ui/radar.tsx +200 -0
- package/src/ui/ref.tsx +160 -0
- package/src/ui/release.tsx +178 -0
- package/src/ui/req.tsx +42 -0
- package/src/ui/review.tsx +133 -0
- package/src/ui/risk.tsx +67 -0
- package/src/ui/sankey.tsx +287 -0
- package/src/ui/scatter.tsx +237 -0
- package/src/ui/schema.tsx +113 -0
- package/src/ui/score.tsx +105 -0
- package/src/ui/search.tsx +120 -0
- package/src/ui/series.tsx +38 -0
- package/src/ui/severity.tsx +69 -0
- package/src/ui/shadcn.tsx +216 -0
- package/src/ui/spark.tsx +86 -0
- package/src/ui/stack.tsx +57 -0
- package/src/ui/stats.tsx +63 -0
- package/src/ui/status-badge.tsx +66 -0
- package/src/ui/statuspage.tsx +238 -0
- package/src/ui/steps.tsx +74 -0
- package/src/ui/summary.tsx +41 -0
- package/src/ui/symbol-ref.tsx +73 -0
- package/src/ui/terminal.tsx +117 -0
- package/src/ui/tests.tsx +218 -0
- package/src/ui/timeline.tsx +63 -0
- package/src/ui/toc.tsx +56 -0
- package/src/ui/tones.ts +187 -0
- package/src/ui/trace.tsx +69 -0
- package/src/ui/tree.tsx +281 -0
- package/src/ui/treemap.tsx +128 -0
- package/src/ui/venn.tsx +258 -0
- package/src/ui/verdict.tsx +78 -0
- package/src/ui/waterfall.tsx +142 -0
|
@@ -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
|
+
);
|