@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,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
+ );
@@ -0,0 +1,9 @@
1
+ /** `gap` picklist → class, shared by Grid/Stack/Row so spacing stays uniform. */
2
+ export const GAPS: Record<string, string> = {
3
+ lg: "gap-6",
4
+ md: "gap-4",
5
+ none: "gap-0",
6
+ sm: "gap-2",
7
+ xl: "gap-8",
8
+ xs: "gap-1",
9
+ };
@@ -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
+ );