@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
package/src/ui/json.tsx
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import * as v from "valibot";
|
|
3
|
+
|
|
4
|
+
import { defineComponent, textOf } from "../define.js";
|
|
5
|
+
import { isRecord, nonEmpty } from "../guards.js";
|
|
6
|
+
import { attrFalse, BOOLISH_PROP } from "./attrs.js";
|
|
7
|
+
import { CaptionBar, CopyButton, FoldChev, Panel } from "./bits.js";
|
|
8
|
+
import { Icon } from "./icon.js";
|
|
9
|
+
import {
|
|
10
|
+
CAPTION_TITLE_CLS,
|
|
11
|
+
COUNT_CHIP_CLS,
|
|
12
|
+
DISCLOSURE_ROW_CLS,
|
|
13
|
+
RAIL_CLS,
|
|
14
|
+
SUNKEN_CLS,
|
|
15
|
+
TEXT,
|
|
16
|
+
TREE_ROW_CLS,
|
|
17
|
+
} from "./tones.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Collapsible JSON tree — objects/arrays fold via nested native <details>
|
|
21
|
+
* (no client JS). The document supplies JSON as a `value` attribute or as a
|
|
22
|
+
* fenced ```json block child; the fence's raw text is what gets parsed.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const MAX_DEPTH = 32;
|
|
26
|
+
const MAX_STRING = 160;
|
|
27
|
+
|
|
28
|
+
const closed = attrFalse;
|
|
29
|
+
|
|
30
|
+
const Leaf = ({ value }: { value: unknown }): ReactElement => {
|
|
31
|
+
if (value === null) {
|
|
32
|
+
return (
|
|
33
|
+
<span className="text-neutral-400 italic dark:text-neutral-500">
|
|
34
|
+
null
|
|
35
|
+
</span>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
if (typeof value === "string") {
|
|
39
|
+
const s =
|
|
40
|
+
value.length > MAX_STRING ? `${value.slice(0, MAX_STRING)}…` : value;
|
|
41
|
+
return (
|
|
42
|
+
<span className="break-all text-emerald-700 dark:text-emerald-400">
|
|
43
|
+
{JSON.stringify(s)}
|
|
44
|
+
</span>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
if (typeof value === "number") {
|
|
48
|
+
return (
|
|
49
|
+
<span className="text-sky-700 tabular-nums dark:text-sky-400">
|
|
50
|
+
{String(value)}
|
|
51
|
+
</span>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
if (typeof value === "boolean") {
|
|
55
|
+
return (
|
|
56
|
+
<span className="text-violet-700 dark:text-violet-400">
|
|
57
|
+
{String(value)}
|
|
58
|
+
</span>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return <span>{JSON.stringify(value)}</span>;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const Key = ({ name }: { name: number | string }): ReactElement => (
|
|
65
|
+
<>
|
|
66
|
+
<span className="text-sky-700 dark:text-sky-300">
|
|
67
|
+
{typeof name === "string" ? JSON.stringify(name) : name}
|
|
68
|
+
</span>
|
|
69
|
+
<span className={TEXT.faint}>:</span>
|
|
70
|
+
</>
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const NodeView = ({
|
|
74
|
+
depth,
|
|
75
|
+
name,
|
|
76
|
+
openAll,
|
|
77
|
+
value,
|
|
78
|
+
}: {
|
|
79
|
+
depth: number;
|
|
80
|
+
name?: number | string;
|
|
81
|
+
openAll: boolean;
|
|
82
|
+
value: unknown;
|
|
83
|
+
}): ReactElement => {
|
|
84
|
+
const keyEl =
|
|
85
|
+
name === undefined ? null : (
|
|
86
|
+
<span className="inline-flex shrink-0 items-baseline gap-1">
|
|
87
|
+
<Key name={name} />
|
|
88
|
+
</span>
|
|
89
|
+
);
|
|
90
|
+
if (depth > MAX_DEPTH) {
|
|
91
|
+
return (
|
|
92
|
+
<div className={TREE_ROW_CLS}>
|
|
93
|
+
{keyEl}
|
|
94
|
+
<span className="text-neutral-400">…</span>
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
const isArr = Array.isArray(value);
|
|
99
|
+
if (!isArr && !isRecord(value)) {
|
|
100
|
+
return (
|
|
101
|
+
<div className={TREE_ROW_CLS}>
|
|
102
|
+
{keyEl}
|
|
103
|
+
<Leaf value={value} />
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
const entries: [number | string, unknown][] = isArr
|
|
108
|
+
? value.map((item, i) => [i, item])
|
|
109
|
+
: Object.entries(value);
|
|
110
|
+
const openB = isArr ? "[" : "{";
|
|
111
|
+
const closeB = isArr ? "]" : "}";
|
|
112
|
+
if (entries.length === 0) {
|
|
113
|
+
return (
|
|
114
|
+
<div className={TREE_ROW_CLS}>
|
|
115
|
+
{keyEl}
|
|
116
|
+
<span className={TEXT.faint}>
|
|
117
|
+
{openB}
|
|
118
|
+
{closeB}
|
|
119
|
+
</span>
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
return (
|
|
124
|
+
<details className="group" open={openAll}>
|
|
125
|
+
<summary className={`list-none ${DISCLOSURE_ROW_CLS}`}>
|
|
126
|
+
<FoldChev />
|
|
127
|
+
{keyEl}
|
|
128
|
+
<span className={TEXT.faint}>{openB}</span>
|
|
129
|
+
<span className={`mdxr-count ${COUNT_CHIP_CLS}`}>
|
|
130
|
+
{entries.length} {isArr ? "items" : "keys"}
|
|
131
|
+
</span>
|
|
132
|
+
</summary>
|
|
133
|
+
<div className={`ml-[1.05rem] border-l pl-2.5 ${RAIL_CLS}`}>
|
|
134
|
+
{entries.map(([k, item]) => (
|
|
135
|
+
<NodeView
|
|
136
|
+
depth={depth + 1}
|
|
137
|
+
key={k}
|
|
138
|
+
name={k}
|
|
139
|
+
openAll={openAll}
|
|
140
|
+
value={item}
|
|
141
|
+
/>
|
|
142
|
+
))}
|
|
143
|
+
</div>
|
|
144
|
+
<div className={`ml-[1.05rem] ${TEXT.faint}`}>{closeB}</div>
|
|
145
|
+
</details>
|
|
146
|
+
);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export const Json = defineComponent(
|
|
150
|
+
{
|
|
151
|
+
description:
|
|
152
|
+
'折りたたみ可能な JSON ツリー (ネストした <details>、JS 不要)。value 属性に JSON 文字列、または子に ```json フェンス。open="false" で全階層を折り畳み。title でキャプション+コピー',
|
|
153
|
+
schema: v.looseObject({
|
|
154
|
+
open: BOOLISH_PROP,
|
|
155
|
+
title: v.optional(v.string()),
|
|
156
|
+
value: v.optional(v.string()),
|
|
157
|
+
}),
|
|
158
|
+
},
|
|
159
|
+
({ title, open, value, children }) => {
|
|
160
|
+
const text = nonEmpty(value) ? value : textOf(children).trim();
|
|
161
|
+
if (!nonEmpty(text)) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
"<Json> requires JSON text via the `value` attribute or a fenced block child"
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
let data: unknown;
|
|
167
|
+
try {
|
|
168
|
+
data = JSON.parse(text);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
171
|
+
throw new Error(`<Json> invalid JSON: ${msg}`, { cause: error });
|
|
172
|
+
}
|
|
173
|
+
return (
|
|
174
|
+
<Panel className="mdxr-json">
|
|
175
|
+
{nonEmpty(title) ? (
|
|
176
|
+
<CaptionBar className={CAPTION_TITLE_CLS}>
|
|
177
|
+
<Icon className="h-3.5 w-3.5" name="lucide:braces" />
|
|
178
|
+
<span className="min-w-0 flex-1 truncate">{title}</span>
|
|
179
|
+
<CopyButton copy={text} title="Copy JSON" />
|
|
180
|
+
</CaptionBar>
|
|
181
|
+
) : null}
|
|
182
|
+
<div
|
|
183
|
+
className={`overflow-x-auto ${SUNKEN_CLS} px-4 py-3 font-mono text-[0.8125rem] leading-relaxed`}
|
|
184
|
+
>
|
|
185
|
+
<NodeView depth={0} openAll={!closed(open)} value={data} />
|
|
186
|
+
</div>
|
|
187
|
+
</Panel>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
);
|
package/src/ui/layout.ts
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { useId } from "react";
|
|
2
|
+
import type { ReactElement, ReactNode } from "react";
|
|
3
|
+
import * as v from "valibot";
|
|
4
|
+
|
|
5
|
+
import { defineComponent, flattenChildren, parseProps } from "../define.js";
|
|
6
|
+
import { nonEmpty } from "../guards.js";
|
|
7
|
+
import { attrTrue, BOOLISH_PROP, NUMISH, numOf } from "./attrs.js";
|
|
8
|
+
import { ChartLegend, ChartPanel } from "./chart-bits.js";
|
|
9
|
+
import {
|
|
10
|
+
areaPath,
|
|
11
|
+
fmtNum,
|
|
12
|
+
linePath,
|
|
13
|
+
niceBounds,
|
|
14
|
+
numList,
|
|
15
|
+
textList,
|
|
16
|
+
toneOf,
|
|
17
|
+
} from "./chart.js";
|
|
18
|
+
import { isEl } from "./children.js";
|
|
19
|
+
import { SERIES_SCHEMA, Series } from "./series.js";
|
|
20
|
+
import type { SeriesSpec } from "./series.js";
|
|
21
|
+
import { TEXT, TEXT_MICRO } from "./tones.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Line chart — one or more <Series> trend lines over evenly-spaced
|
|
25
|
+
* categories (`labels`). The y domain snaps to round bounds (`min`/`max`
|
|
26
|
+
* override); `area` adds a soft fill under each line.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const VB_W = 640;
|
|
30
|
+
const VB_H = 280;
|
|
31
|
+
const PLOT = { bottom: 244, left: 44, right: 628, top: 12 };
|
|
32
|
+
|
|
33
|
+
/** Gridline stroke — the SVG counterpart of RAIL_BG_CLS. */
|
|
34
|
+
const GRID_STROKE = "stroke-neutral-200 dark:stroke-neutral-700";
|
|
35
|
+
const BASE_STROKE = "stroke-neutral-300 dark:stroke-neutral-600";
|
|
36
|
+
/** Tick/label text — the SVG counterpart of TEXT.faint. */
|
|
37
|
+
const TICK_TEXT = "fill-neutral-400 dark:fill-neutral-500";
|
|
38
|
+
|
|
39
|
+
interface SeriesData {
|
|
40
|
+
dash: boolean;
|
|
41
|
+
name: string;
|
|
42
|
+
nums: number[];
|
|
43
|
+
spec: SeriesSpec;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const collectSeries = (
|
|
47
|
+
children: ReactNode
|
|
48
|
+
): { rest: ReactNode[]; series: SeriesData[] } => {
|
|
49
|
+
const series: SeriesData[] = [];
|
|
50
|
+
const rest: ReactNode[] = [];
|
|
51
|
+
for (const child of flattenChildren(children)) {
|
|
52
|
+
if (isEl(child, Series)) {
|
|
53
|
+
const spec = parseProps(SERIES_SCHEMA, child.props, "Series");
|
|
54
|
+
series.push({
|
|
55
|
+
dash: attrTrue(spec.dash),
|
|
56
|
+
name: spec.name,
|
|
57
|
+
nums: numList(spec.values),
|
|
58
|
+
spec,
|
|
59
|
+
});
|
|
60
|
+
} else {
|
|
61
|
+
rest.push(child);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { rest, series };
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** One plotted series — optional area fill under the line plus point markers. */
|
|
68
|
+
const SeriesG = (props: {
|
|
69
|
+
cats: string[];
|
|
70
|
+
filled: boolean;
|
|
71
|
+
s: SeriesData;
|
|
72
|
+
si: number;
|
|
73
|
+
unit: string | undefined;
|
|
74
|
+
xOf: (i: number) => number;
|
|
75
|
+
yOf: (v: number) => number;
|
|
76
|
+
}): ReactElement => {
|
|
77
|
+
const { cats, filled, s, si, unit, xOf, yOf } = props;
|
|
78
|
+
const style = toneOf(s.spec.tone, si);
|
|
79
|
+
const pts = s.nums.map((val, i) => ({ x: xOf(i), y: yOf(val) }));
|
|
80
|
+
return (
|
|
81
|
+
<g>
|
|
82
|
+
{filled && pts.length > 1 ? (
|
|
83
|
+
<path className={style.soft} d={areaPath(pts, PLOT.bottom)} />
|
|
84
|
+
) : null}
|
|
85
|
+
<path
|
|
86
|
+
className={style.stroke}
|
|
87
|
+
d={linePath(pts)}
|
|
88
|
+
fill="none"
|
|
89
|
+
strokeDasharray={s.dash ? "4 3" : undefined}
|
|
90
|
+
strokeLinecap="round"
|
|
91
|
+
strokeLinejoin="round"
|
|
92
|
+
strokeWidth={1.8}
|
|
93
|
+
/>
|
|
94
|
+
{pts.map((p, i) => (
|
|
95
|
+
<circle className={style.fill} cx={p.x} cy={p.y} key={i} r={2.2}>
|
|
96
|
+
<title>
|
|
97
|
+
{`${s.name}${cats[i] === undefined ? "" : ` · ${cats[i]}`} · ${fmtNum(s.nums[i] ?? 0)}${unit ?? ""}`}
|
|
98
|
+
</title>
|
|
99
|
+
</circle>
|
|
100
|
+
))}
|
|
101
|
+
</g>
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/** Gridlines, y tick labels, x category labels, and the baseline. */
|
|
106
|
+
const Axes = (props: {
|
|
107
|
+
cats: string[];
|
|
108
|
+
ticks: number[];
|
|
109
|
+
xOf: (i: number) => number;
|
|
110
|
+
yOf: (v: number) => number;
|
|
111
|
+
}): ReactElement => {
|
|
112
|
+
const { cats, ticks, xOf, yOf } = props;
|
|
113
|
+
const [firstTick] = ticks;
|
|
114
|
+
return (
|
|
115
|
+
<>
|
|
116
|
+
{ticks.map((t) => (
|
|
117
|
+
<g key={t}>
|
|
118
|
+
<line
|
|
119
|
+
className={GRID_STROKE}
|
|
120
|
+
strokeDasharray={t === firstTick ? undefined : "2 3"}
|
|
121
|
+
strokeWidth={1}
|
|
122
|
+
x1={PLOT.left}
|
|
123
|
+
x2={PLOT.right}
|
|
124
|
+
y1={yOf(t)}
|
|
125
|
+
y2={yOf(t)}
|
|
126
|
+
/>
|
|
127
|
+
<text
|
|
128
|
+
className={TICK_TEXT}
|
|
129
|
+
fontSize={9}
|
|
130
|
+
textAnchor="end"
|
|
131
|
+
x={PLOT.left - 6}
|
|
132
|
+
y={yOf(t) + 3}
|
|
133
|
+
>
|
|
134
|
+
{fmtNum(t)}
|
|
135
|
+
</text>
|
|
136
|
+
</g>
|
|
137
|
+
))}
|
|
138
|
+
<line
|
|
139
|
+
className={BASE_STROKE}
|
|
140
|
+
strokeWidth={1}
|
|
141
|
+
x1={PLOT.left}
|
|
142
|
+
x2={PLOT.right}
|
|
143
|
+
y1={PLOT.bottom}
|
|
144
|
+
y2={PLOT.bottom}
|
|
145
|
+
/>
|
|
146
|
+
{cats.map((label, i) => (
|
|
147
|
+
<text
|
|
148
|
+
className={TICK_TEXT}
|
|
149
|
+
fontSize={9}
|
|
150
|
+
key={label}
|
|
151
|
+
textAnchor="middle"
|
|
152
|
+
x={xOf(i)}
|
|
153
|
+
y={PLOT.bottom + 16}
|
|
154
|
+
>
|
|
155
|
+
{label}
|
|
156
|
+
</text>
|
|
157
|
+
))}
|
|
158
|
+
</>
|
|
159
|
+
);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export const LineChart = defineComponent(
|
|
163
|
+
{
|
|
164
|
+
description:
|
|
165
|
+
'折れ線グラフコンテナ (推移)。<Series name values="1,2,3"> を並べる。labels="Mon,Tue" で x 軸ラベル、area で塗りつぶし、min/max/unit でスケールと単位',
|
|
166
|
+
schema: v.looseObject({
|
|
167
|
+
area: BOOLISH_PROP,
|
|
168
|
+
labels: v.optional(v.string()),
|
|
169
|
+
max: v.optional(NUMISH),
|
|
170
|
+
min: v.optional(NUMISH),
|
|
171
|
+
title: v.optional(v.string()),
|
|
172
|
+
unit: v.optional(v.string()),
|
|
173
|
+
}),
|
|
174
|
+
},
|
|
175
|
+
({ title, labels, area, min, max, unit, children }) => {
|
|
176
|
+
const clipId = useId().replaceAll(":", "");
|
|
177
|
+
const { series, rest } = collectSeries(children);
|
|
178
|
+
const cats = textList(labels);
|
|
179
|
+
const n = Math.max(cats.length, ...series.map((s) => s.nums.length));
|
|
180
|
+
const all = series.flatMap((s) => s.nums);
|
|
181
|
+
const lo = numOf(min);
|
|
182
|
+
const hi = numOf(max);
|
|
183
|
+
const extent = niceBounds(
|
|
184
|
+
lo ?? Math.min(0, ...all),
|
|
185
|
+
hi ?? Math.max(1, ...all)
|
|
186
|
+
);
|
|
187
|
+
const yMin = lo ?? extent.min;
|
|
188
|
+
const yMax = hi ?? extent.max;
|
|
189
|
+
const span = yMax - yMin || 1;
|
|
190
|
+
|
|
191
|
+
const xOf = (i: number): number =>
|
|
192
|
+
n <= 1
|
|
193
|
+
? (PLOT.left + PLOT.right) / 2
|
|
194
|
+
: PLOT.left + (i / (n - 1)) * (PLOT.right - PLOT.left);
|
|
195
|
+
const yOf = (val: number): number =>
|
|
196
|
+
PLOT.bottom - ((val - yMin) / span) * (PLOT.bottom - PLOT.top);
|
|
197
|
+
|
|
198
|
+
const ticks = extent.ticks.filter((t) => t >= yMin && t <= yMax);
|
|
199
|
+
const filled = attrTrue(area);
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<ChartPanel
|
|
203
|
+
figure={
|
|
204
|
+
nonEmpty(unit) ? `${fmtNum(yMin)}–${fmtNum(yMax)}${unit}` : undefined
|
|
205
|
+
}
|
|
206
|
+
icon="lucide:chart-line"
|
|
207
|
+
title={title}
|
|
208
|
+
>
|
|
209
|
+
<div className="px-4 py-3">
|
|
210
|
+
<svg className="h-auto w-full" viewBox={`0 0 ${VB_W} ${VB_H}`}>
|
|
211
|
+
<title>{title ?? "line chart"}</title>
|
|
212
|
+
<defs>
|
|
213
|
+
<clipPath id={clipId}>
|
|
214
|
+
<rect
|
|
215
|
+
height={PLOT.bottom - PLOT.top}
|
|
216
|
+
width={PLOT.right - PLOT.left}
|
|
217
|
+
x={PLOT.left}
|
|
218
|
+
y={PLOT.top}
|
|
219
|
+
/>
|
|
220
|
+
</clipPath>
|
|
221
|
+
</defs>
|
|
222
|
+
<Axes cats={cats} ticks={ticks} xOf={xOf} yOf={yOf} />
|
|
223
|
+
<g clipPath={`url(#${clipId})`}>
|
|
224
|
+
{series.map((s, si) => (
|
|
225
|
+
<SeriesG
|
|
226
|
+
cats={cats}
|
|
227
|
+
filled={filled}
|
|
228
|
+
key={s.name}
|
|
229
|
+
s={s}
|
|
230
|
+
si={si}
|
|
231
|
+
unit={unit}
|
|
232
|
+
xOf={xOf}
|
|
233
|
+
yOf={yOf}
|
|
234
|
+
/>
|
|
235
|
+
))}
|
|
236
|
+
</g>
|
|
237
|
+
</svg>
|
|
238
|
+
{rest.length > 0 ? (
|
|
239
|
+
<div className={`${TEXT_MICRO} ${TEXT.muted}`}>{rest}</div>
|
|
240
|
+
) : null}
|
|
241
|
+
</div>
|
|
242
|
+
<ChartLegend
|
|
243
|
+
items={series.map((s, i) => ({
|
|
244
|
+
name: s.name,
|
|
245
|
+
style: toneOf(s.spec.tone, i),
|
|
246
|
+
}))}
|
|
247
|
+
/>
|
|
248
|
+
</ChartPanel>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
);
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
import * as v from "valibot";
|
|
3
|
+
|
|
4
|
+
import { defineComponent, flattenChildren, textOf } from "../define.js";
|
|
5
|
+
import { nonEmpty, own } from "../guards.js";
|
|
6
|
+
import { CaptionBar, Panel } from "./bits.js";
|
|
7
|
+
import { isEl } from "./children.js";
|
|
8
|
+
import { Icon } from "./icon.js";
|
|
9
|
+
import {
|
|
10
|
+
BORDER_CLS,
|
|
11
|
+
DIVIDE_CLS,
|
|
12
|
+
SURFACE_CLS,
|
|
13
|
+
TEXT,
|
|
14
|
+
TONE_TEXT,
|
|
15
|
+
} from "./tones.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Comparison matrix — rows come from a nested Markdown list, cells are
|
|
19
|
+
* `|`-separated: `- Feature | yes | no | partial`. Cell values map to icons
|
|
20
|
+
* (yes/✓ → check, no/✗ → cross, partial/~ → minus); anything else renders
|
|
21
|
+
* as text. Column headers come from the `cols` prop.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
type CellKind = "empty" | "no" | "partial" | "text" | "yes";
|
|
25
|
+
|
|
26
|
+
const CELL_KINDS: Record<string, CellKind> = {
|
|
27
|
+
"-": "empty",
|
|
28
|
+
"?": "empty",
|
|
29
|
+
false: "no",
|
|
30
|
+
maybe: "partial",
|
|
31
|
+
n: "no",
|
|
32
|
+
ng: "no",
|
|
33
|
+
no: "no",
|
|
34
|
+
o: "yes",
|
|
35
|
+
ok: "yes",
|
|
36
|
+
partial: "partial",
|
|
37
|
+
true: "yes",
|
|
38
|
+
warn: "partial",
|
|
39
|
+
x: "no",
|
|
40
|
+
y: "yes",
|
|
41
|
+
yes: "yes",
|
|
42
|
+
"~": "partial",
|
|
43
|
+
"×": "no",
|
|
44
|
+
"–": "empty",
|
|
45
|
+
"—": "empty",
|
|
46
|
+
"△": "partial",
|
|
47
|
+
"○": "yes",
|
|
48
|
+
"✅": "yes",
|
|
49
|
+
"✓": "yes",
|
|
50
|
+
"✔": "yes",
|
|
51
|
+
"✕": "no",
|
|
52
|
+
"✗": "no",
|
|
53
|
+
"✘": "no",
|
|
54
|
+
"❌": "no",
|
|
55
|
+
"?": "empty",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const cellKind = (cell: string): CellKind => {
|
|
59
|
+
const key = cell.trim().toLowerCase();
|
|
60
|
+
return own(CELL_KINDS, key) ?? (key === "" ? "empty" : "text");
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const Cell = ({ cell }: { cell: string }): ReactElement => {
|
|
64
|
+
const kind = cellKind(cell);
|
|
65
|
+
if (kind === "yes") {
|
|
66
|
+
return (
|
|
67
|
+
<Icon
|
|
68
|
+
className={`h-4 w-4 ${TONE_TEXT.emerald}`}
|
|
69
|
+
label="yes"
|
|
70
|
+
name="lucide:check"
|
|
71
|
+
/>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
if (kind === "no") {
|
|
75
|
+
return (
|
|
76
|
+
<Icon className={`h-4 w-4 ${TONE_TEXT.red}`} label="no" name="lucide:x" />
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
if (kind === "partial") {
|
|
80
|
+
return (
|
|
81
|
+
<Icon
|
|
82
|
+
className={`h-4 w-4 ${TONE_TEXT.amber}`}
|
|
83
|
+
label="partial"
|
|
84
|
+
name="lucide:minus"
|
|
85
|
+
/>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
if (kind === "empty") {
|
|
89
|
+
return <span className={TEXT.ghost}>—</span>;
|
|
90
|
+
}
|
|
91
|
+
return <span className={`text-xs ${TEXT.body}`}>{cell.trim()}</span>;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
type El = ReactElement<{ children?: ReactNode }>;
|
|
95
|
+
|
|
96
|
+
/** A list item → row cells: first `|`-segment is the label, rest are cells. */
|
|
97
|
+
const rowCells = (li: El): string[] => {
|
|
98
|
+
const kids = flattenChildren(li.props.children);
|
|
99
|
+
const text = kids
|
|
100
|
+
.flatMap((k): ReactNode[] =>
|
|
101
|
+
isEl(k, "p") ? flattenChildren(k.props.children) : [k]
|
|
102
|
+
)
|
|
103
|
+
.map((k) => textOf(k))
|
|
104
|
+
.join("");
|
|
105
|
+
return text.split("|").map((c) => c.trim());
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/** Nested <ul>/<li> children → one cells array per list item. */
|
|
109
|
+
const collectRows = (children: ReactNode): string[][] => {
|
|
110
|
+
const rows: string[][] = [];
|
|
111
|
+
for (const child of flattenChildren(children)) {
|
|
112
|
+
if (!isEl(child, "ul")) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
for (const li of flattenChildren(child.props.children)) {
|
|
116
|
+
if (isEl(li, "li")) {
|
|
117
|
+
rows.push(rowCells(li));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return rows;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
interface GridStyle {
|
|
125
|
+
gridTemplateColumns: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const MatrixHeader = ({
|
|
129
|
+
colCount,
|
|
130
|
+
grid,
|
|
131
|
+
headers,
|
|
132
|
+
}: {
|
|
133
|
+
colCount: number;
|
|
134
|
+
grid: GridStyle;
|
|
135
|
+
headers: string[];
|
|
136
|
+
}): ReactElement => (
|
|
137
|
+
<div className={`grid border-b ${SURFACE_CLS} ${BORDER_CLS}`} style={grid}>
|
|
138
|
+
<div className="px-4 py-2" />
|
|
139
|
+
{Array.from({ length: colCount }, (_, i) => (
|
|
140
|
+
<div
|
|
141
|
+
className={`px-2 py-2 text-center text-xs font-semibold ${TEXT.body}`}
|
|
142
|
+
key={i}
|
|
143
|
+
>
|
|
144
|
+
{headers[i] ?? ""}
|
|
145
|
+
</div>
|
|
146
|
+
))}
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const MatrixRow = ({
|
|
151
|
+
cells,
|
|
152
|
+
colCount,
|
|
153
|
+
grid,
|
|
154
|
+
}: {
|
|
155
|
+
cells: string[];
|
|
156
|
+
colCount: number;
|
|
157
|
+
grid: GridStyle;
|
|
158
|
+
}): ReactElement => (
|
|
159
|
+
<div className="grid items-center" style={grid}>
|
|
160
|
+
<div className="px-4 py-2 text-sm font-medium">{cells[0]}</div>
|
|
161
|
+
{Array.from({ length: colCount }, (_, j) => (
|
|
162
|
+
<div className="px-2 py-2 text-center" key={j}>
|
|
163
|
+
<Cell cell={cells[j + 1] ?? ""} />
|
|
164
|
+
</div>
|
|
165
|
+
))}
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
export const Matrix = defineComponent(
|
|
170
|
+
{
|
|
171
|
+
description:
|
|
172
|
+
'比較マトリクス。cols はカンマ区切りの列名 ("Option A, Option B")。子のネストしたリスト1行が1行になり、"- 項目 | yes | no" のように | でセルを区切る。セル値 yes|no|partial|✓|✗|△|– はアイコン化、他は文字列のまま',
|
|
173
|
+
schema: v.looseObject({
|
|
174
|
+
cols: v.optional(v.string()),
|
|
175
|
+
title: v.optional(v.string()),
|
|
176
|
+
}),
|
|
177
|
+
},
|
|
178
|
+
({ title, cols, children }) => {
|
|
179
|
+
const headers = (cols ?? "")
|
|
180
|
+
.split(",")
|
|
181
|
+
.map((c) => c.trim())
|
|
182
|
+
.filter((c) => c !== "");
|
|
183
|
+
const rows = collectRows(children);
|
|
184
|
+
const colCount = Math.max(
|
|
185
|
+
headers.length,
|
|
186
|
+
...rows.map((r) => Math.max(0, r.length - 1)),
|
|
187
|
+
1
|
|
188
|
+
);
|
|
189
|
+
const grid = {
|
|
190
|
+
gridTemplateColumns: `minmax(8rem,1.4fr) repeat(${colCount}, minmax(4.5rem,1fr))`,
|
|
191
|
+
};
|
|
192
|
+
return (
|
|
193
|
+
<Panel>
|
|
194
|
+
{nonEmpty(title) ? (
|
|
195
|
+
<CaptionBar className="font-medium">{title}</CaptionBar>
|
|
196
|
+
) : null}
|
|
197
|
+
{headers.length > 0 ? (
|
|
198
|
+
<MatrixHeader colCount={colCount} grid={grid} headers={headers} />
|
|
199
|
+
) : null}
|
|
200
|
+
<div className={DIVIDE_CLS}>
|
|
201
|
+
{rows.map((cells, i) => (
|
|
202
|
+
<MatrixRow cells={cells} colCount={colCount} grid={grid} key={i} />
|
|
203
|
+
))}
|
|
204
|
+
</div>
|
|
205
|
+
</Panel>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
);
|