@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/tree.tsx
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { Fragment } from "react";
|
|
2
|
+
import type { ReactElement, ReactNode } from "react";
|
|
3
|
+
import * as v from "valibot";
|
|
4
|
+
|
|
5
|
+
import { defineComponent, flattenChildren, textOf } from "../define.js";
|
|
6
|
+
import { nonEmpty } from "../guards.js";
|
|
7
|
+
import { attrFalse, BOOLISH_PROP } from "./attrs.js";
|
|
8
|
+
import { FoldChev } from "./bits.js";
|
|
9
|
+
import { isEl } from "./children.js";
|
|
10
|
+
import { fileIcon, folderIcon } from "./file-icon.js";
|
|
11
|
+
import { Icon } from "./icon.js";
|
|
12
|
+
import {
|
|
13
|
+
BORDER_CLS,
|
|
14
|
+
DISCLOSURE_ROW_CLS,
|
|
15
|
+
RAIL_BG_CLS,
|
|
16
|
+
SUNKEN_CLS,
|
|
17
|
+
TEXT,
|
|
18
|
+
TONE_TEXT,
|
|
19
|
+
TREE_ROW_CLS,
|
|
20
|
+
} from "./tones.js";
|
|
21
|
+
|
|
22
|
+
type El = ReactElement<{ children?: ReactNode }>;
|
|
23
|
+
|
|
24
|
+
/** `name — note` or `name # note` inside a list item attaches a muted note. */
|
|
25
|
+
const NOTE_RE = /\s+(?:—|#)\s+/u;
|
|
26
|
+
|
|
27
|
+
/** `...` or `…` as the whole name marks intentionally omitted entries. */
|
|
28
|
+
const PLACEHOLDER_RE = /^(?:\.{3}|…)$/u;
|
|
29
|
+
|
|
30
|
+
const closed = attrFalse;
|
|
31
|
+
|
|
32
|
+
const NodeIcon = ({
|
|
33
|
+
dir,
|
|
34
|
+
name,
|
|
35
|
+
}: {
|
|
36
|
+
dir: boolean;
|
|
37
|
+
name: string;
|
|
38
|
+
}): ReactElement => (
|
|
39
|
+
<Icon
|
|
40
|
+
className={`h-3.5 w-3.5 shrink-0 self-center ${dir ? TONE_TEXT.amber : TEXT.faint}`}
|
|
41
|
+
name={dir ? folderIcon(name) : fileIcon(name)}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
/** Horizontal tick joining a nested row to its parent's vertical rail. */
|
|
46
|
+
const Tick = (): ReactElement => (
|
|
47
|
+
<span
|
|
48
|
+
aria-hidden
|
|
49
|
+
className={`absolute top-[0.72em] -left-3 h-px w-2.5 ${RAIL_BG_CLS}`}
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Vertical rail joining a nested row to its siblings. `-top-0.5` bridges the
|
|
55
|
+
* `space-y-0.5` gap between rows so the line stays continuous; on the last row
|
|
56
|
+
* it stops at the tick, turning `├─` into `└─`.
|
|
57
|
+
*/
|
|
58
|
+
const Rail = ({ last }: { last: boolean }): ReactElement => (
|
|
59
|
+
<span
|
|
60
|
+
aria-hidden
|
|
61
|
+
className={`absolute -top-0.5 -left-3 w-px ${RAIL_BG_CLS} ${
|
|
62
|
+
last ? "h-[calc(0.72em_+_3px)]" : "bottom-0"
|
|
63
|
+
}`}
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Column spacer matching the disclosure chevron's width so file rows align
|
|
69
|
+
* with directory rows (which lead with a chevron, like Starlight's FileTree).
|
|
70
|
+
*/
|
|
71
|
+
const Spacer = (): ReactElement => (
|
|
72
|
+
<span aria-hidden className="w-3 shrink-0" />
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
/** A tree row: guide tick plus its flex children. */
|
|
76
|
+
const Row = ({
|
|
77
|
+
depth,
|
|
78
|
+
children,
|
|
79
|
+
}: {
|
|
80
|
+
children?: ReactNode;
|
|
81
|
+
depth: number;
|
|
82
|
+
}): ReactElement => (
|
|
83
|
+
<div className={TREE_ROW_CLS}>
|
|
84
|
+
{depth > 0 ? <Tick /> : null}
|
|
85
|
+
{children}
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
/** Muted `…` row marking omitted entries (Starlight-style placeholder). */
|
|
90
|
+
const PlaceholderRow = ({ depth }: { depth: number }): ReactElement => (
|
|
91
|
+
<Row depth={depth}>
|
|
92
|
+
<Spacer />
|
|
93
|
+
<span className={TEXT.faint}>…</span>
|
|
94
|
+
</Row>
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
/** Icon + name; wrapped in a highlight pill when the name was bold. */
|
|
98
|
+
const Entry = ({
|
|
99
|
+
dir,
|
|
100
|
+
highlighted,
|
|
101
|
+
label,
|
|
102
|
+
children,
|
|
103
|
+
}: {
|
|
104
|
+
children?: ReactNode;
|
|
105
|
+
dir: boolean;
|
|
106
|
+
highlighted: boolean;
|
|
107
|
+
label: string;
|
|
108
|
+
}): ReactElement => {
|
|
109
|
+
const nameEl = (
|
|
110
|
+
<span
|
|
111
|
+
className={`group-hover:text-sky-600 dark:group-hover:text-sky-400${dir ? " font-medium" : ""}`}
|
|
112
|
+
>
|
|
113
|
+
{children}
|
|
114
|
+
</span>
|
|
115
|
+
);
|
|
116
|
+
return highlighted ? (
|
|
117
|
+
<span className="-mx-1 inline-flex items-baseline gap-1.5 rounded bg-amber-500/15 px-1 outline-1 outline-amber-500/35">
|
|
118
|
+
<NodeIcon dir={dir} name={label} />
|
|
119
|
+
{nameEl}
|
|
120
|
+
</span>
|
|
121
|
+
) : (
|
|
122
|
+
<>
|
|
123
|
+
<NodeIcon dir={dir} name={label} />
|
|
124
|
+
{nameEl}
|
|
125
|
+
</>
|
|
126
|
+
);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const nestedListCls = "mt-0.5 ml-[7px] list-none space-y-0.5 p-0 pl-3";
|
|
130
|
+
|
|
131
|
+
/** A list item's pieces: label/note text, nested <ul>s, inline row content. */
|
|
132
|
+
const rowParts = (
|
|
133
|
+
child: El
|
|
134
|
+
): {
|
|
135
|
+
label: string;
|
|
136
|
+
name: string | null;
|
|
137
|
+
nested: El[];
|
|
138
|
+
note: string | null;
|
|
139
|
+
row: ReactNode[];
|
|
140
|
+
} => {
|
|
141
|
+
const kids = flattenChildren(child.props.children);
|
|
142
|
+
const nested = kids.filter((k): k is El => isEl(k, "ul"));
|
|
143
|
+
const nestedSet = new Set<ReactNode>(nested);
|
|
144
|
+
const row = kids
|
|
145
|
+
.filter((k) => !nestedSet.has(k))
|
|
146
|
+
.flatMap((k): ReactNode[] =>
|
|
147
|
+
isEl(k, "p") ? flattenChildren(k.props.children) : [k]
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const full = textOf(row);
|
|
151
|
+
const m = NOTE_RE.exec(full);
|
|
152
|
+
const name = m === null ? null : full.slice(0, m.index).trimEnd();
|
|
153
|
+
const note = m === null ? null : full.slice(m.index + m[0].length).trim();
|
|
154
|
+
return { label: (name ?? full).trim(), name, nested, note, row };
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/** Render a `<ul>` element (and its nested lists) as file-tree rows. */
|
|
158
|
+
const renderList = (
|
|
159
|
+
node: El,
|
|
160
|
+
depth: number,
|
|
161
|
+
expanded: boolean
|
|
162
|
+
): ReactElement => {
|
|
163
|
+
const renderItem = (child: El, last: boolean): ReactElement => {
|
|
164
|
+
const { label, name, nested, note, row } = rowParts(child);
|
|
165
|
+
|
|
166
|
+
// A `...`/`…` entry marks omitted files — no icon, not collapsible.
|
|
167
|
+
if (PLACEHOLDER_RE.test(label)) {
|
|
168
|
+
return (
|
|
169
|
+
<li className="relative">
|
|
170
|
+
{depth > 0 ? <Rail last={last} /> : null}
|
|
171
|
+
<PlaceholderRow depth={depth} />
|
|
172
|
+
</li>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const isDir = nested.length > 0 || label.endsWith("/");
|
|
177
|
+
const entry = (
|
|
178
|
+
<Entry dir={isDir} highlighted={isEl(row[0], "strong")} label={label}>
|
|
179
|
+
{name ?? row}
|
|
180
|
+
</Entry>
|
|
181
|
+
);
|
|
182
|
+
const noteEl =
|
|
183
|
+
note === null ? null : (
|
|
184
|
+
<span className={`text-[0.85em] ${TEXT.faint}`}>{note}</span>
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
if (!isDir) {
|
|
188
|
+
return (
|
|
189
|
+
<li className="relative">
|
|
190
|
+
{depth > 0 ? <Rail last={last} /> : null}
|
|
191
|
+
<Row depth={depth}>
|
|
192
|
+
<Spacer />
|
|
193
|
+
{entry}
|
|
194
|
+
{noteEl}
|
|
195
|
+
</Row>
|
|
196
|
+
</li>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Directory: collapsible via native <details>. A bare `dir/` (no children)
|
|
201
|
+
// starts closed and reveals a `…` placeholder — same as Starlight.
|
|
202
|
+
return (
|
|
203
|
+
<li className="relative">
|
|
204
|
+
{depth > 0 ? <Rail last={last} /> : null}
|
|
205
|
+
<details open={expanded && nested.length > 0}>
|
|
206
|
+
<summary className={`group ${DISCLOSURE_ROW_CLS}`}>
|
|
207
|
+
{depth > 0 ? <Tick /> : null}
|
|
208
|
+
<FoldChev className="group-hover:text-neutral-600 dark:group-hover:text-neutral-300" />
|
|
209
|
+
{entry}
|
|
210
|
+
{noteEl}
|
|
211
|
+
</summary>
|
|
212
|
+
{nested.length > 0 ? (
|
|
213
|
+
nested.map((ul, j) => (
|
|
214
|
+
<Fragment key={ul.key ?? j}>
|
|
215
|
+
{renderList(ul, depth + 1, expanded)}
|
|
216
|
+
</Fragment>
|
|
217
|
+
))
|
|
218
|
+
) : (
|
|
219
|
+
<ul className={nestedListCls}>
|
|
220
|
+
<li className="relative">
|
|
221
|
+
<Rail last />
|
|
222
|
+
<PlaceholderRow depth={depth + 1} />
|
|
223
|
+
</li>
|
|
224
|
+
</ul>
|
|
225
|
+
)}
|
|
226
|
+
</details>
|
|
227
|
+
</li>
|
|
228
|
+
);
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const items = flattenChildren(node.props.children);
|
|
232
|
+
const lastLi = items.findLastIndex((c) => isEl(c, "li"));
|
|
233
|
+
return (
|
|
234
|
+
<ul
|
|
235
|
+
className={depth === 0 ? "m-0 list-none space-y-0.5 p-0" : nestedListCls}
|
|
236
|
+
>
|
|
237
|
+
{items.map((child, i) => {
|
|
238
|
+
if (!isEl(child, "li")) {
|
|
239
|
+
return <Fragment key={i}>{child}</Fragment>;
|
|
240
|
+
}
|
|
241
|
+
return (
|
|
242
|
+
<Fragment key={child.key ?? i}>
|
|
243
|
+
{renderItem(child, i === lastLi)}
|
|
244
|
+
</Fragment>
|
|
245
|
+
);
|
|
246
|
+
})}
|
|
247
|
+
</ul>
|
|
248
|
+
);
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
export const Tree = defineComponent(
|
|
252
|
+
{
|
|
253
|
+
description:
|
|
254
|
+
'ファイルツリー。子のネストしたリストを描画する。末尾 `/` または子を持つ項目はフォルダでクリックで折り畳める (JS 不要)。open="false" で全フォルダを初期折り畳み。`名前 — 注記` で注釈、`...` または `…` で省略プレースホルダ、`**太字**` でハイライト',
|
|
255
|
+
schema: v.looseObject({
|
|
256
|
+
open: BOOLISH_PROP,
|
|
257
|
+
root: v.optional(v.string()),
|
|
258
|
+
}),
|
|
259
|
+
},
|
|
260
|
+
({ root, open, children }) => (
|
|
261
|
+
<div
|
|
262
|
+
className={`mdxr-tree not-prose my-6 overflow-x-auto rounded-lg border ${SUNKEN_CLS} px-4 py-3 font-mono text-sm ${BORDER_CLS}`}
|
|
263
|
+
>
|
|
264
|
+
{nonEmpty(root) ? (
|
|
265
|
+
<div className="mb-1.5 flex items-center gap-1.5 font-semibold">
|
|
266
|
+
<NodeIcon dir name={root} />
|
|
267
|
+
{root}
|
|
268
|
+
</div>
|
|
269
|
+
) : null}
|
|
270
|
+
{flattenChildren(children).map((n, i) =>
|
|
271
|
+
isEl(n, "ul") ? (
|
|
272
|
+
<Fragment key={n.key ?? i}>
|
|
273
|
+
{renderList(n, 0, !closed(open))}
|
|
274
|
+
</Fragment>
|
|
275
|
+
) : (
|
|
276
|
+
<Fragment key={i}>{n}</Fragment>
|
|
277
|
+
)
|
|
278
|
+
)}
|
|
279
|
+
</div>
|
|
280
|
+
)
|
|
281
|
+
);
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
import * as v from "valibot";
|
|
3
|
+
|
|
4
|
+
import { defineComponent, flattenChildren, parseProps } from "../define.js";
|
|
5
|
+
import { nonEmpty } from "../guards.js";
|
|
6
|
+
import { NUMISH, numOf } from "./attrs.js";
|
|
7
|
+
import { CODE_CHIP_CLS } from "./bits.js";
|
|
8
|
+
import { ChartPanel } from "./chart-bits.js";
|
|
9
|
+
import { CHART_TONES, fmtNum, squarify, toneOf } from "./chart.js";
|
|
10
|
+
import { isEl } from "./children.js";
|
|
11
|
+
import { TEXT_MICRO } from "./tones.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Treemap — part-of-whole areas packed by the squarified algorithm.
|
|
15
|
+
* <Tile> size ∝ `value`; labels carry name and share percent.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const TILE_SCHEMA = v.looseObject({
|
|
19
|
+
name: v.string(),
|
|
20
|
+
note: v.optional(v.string()),
|
|
21
|
+
tone: v.optional(v.picklist(CHART_TONES)),
|
|
22
|
+
value: NUMISH,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
type TileSpec = v.InferOutput<typeof TILE_SCHEMA>;
|
|
26
|
+
|
|
27
|
+
export const Tile = defineComponent(
|
|
28
|
+
{
|
|
29
|
+
description:
|
|
30
|
+
"ツリーマップの1領域。<Treemap> の子。name/value は必須、note で補足、tone で色指定",
|
|
31
|
+
schema: TILE_SCHEMA,
|
|
32
|
+
},
|
|
33
|
+
// Standalone use: a `name value` chip so misplaced tiles still render.
|
|
34
|
+
({ name, value, note }): ReactElement => (
|
|
35
|
+
<span className={`font-mono ${CODE_CHIP_CLS}`}>
|
|
36
|
+
{name}
|
|
37
|
+
<span className="opacity-70">{value}</span>
|
|
38
|
+
{nonEmpty(note) ? <span className="opacity-70">{note}</span> : null}
|
|
39
|
+
</span>
|
|
40
|
+
)
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const collectTiles = (
|
|
44
|
+
children: ReactNode
|
|
45
|
+
): { rest: ReactNode[]; tiles: { n: number; spec: TileSpec }[] } => {
|
|
46
|
+
const tiles: { n: number; spec: TileSpec }[] = [];
|
|
47
|
+
const rest: ReactNode[] = [];
|
|
48
|
+
for (const child of flattenChildren(children)) {
|
|
49
|
+
if (isEl(child, Tile)) {
|
|
50
|
+
const spec = parseProps(TILE_SCHEMA, child.props, "Tile");
|
|
51
|
+
tiles.push({ n: Math.max(0, numOf(spec.value) ?? 0), spec });
|
|
52
|
+
} else {
|
|
53
|
+
rest.push(child);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { rest, tiles };
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** Squarify space — a 16:9 canvas in percentage units. */
|
|
60
|
+
const CANVAS = { h: 56.25, w: 100, x: 0, y: 0 };
|
|
61
|
+
|
|
62
|
+
export const Treemap = defineComponent(
|
|
63
|
+
{
|
|
64
|
+
description:
|
|
65
|
+
"ツリーマップコンテナ (階層的な構成比)。<Tile name value> を並べる。面積は value に比例、squarified レイアウト。unit は値の単位",
|
|
66
|
+
schema: v.looseObject({
|
|
67
|
+
title: v.optional(v.string()),
|
|
68
|
+
unit: v.optional(v.string()),
|
|
69
|
+
}),
|
|
70
|
+
},
|
|
71
|
+
({ title, unit, children }) => {
|
|
72
|
+
const { tiles, rest } = collectTiles(children);
|
|
73
|
+
const total = tiles.reduce((a, t) => a + t.n, 0);
|
|
74
|
+
const rects = squarify(
|
|
75
|
+
tiles.map((t) => t.n),
|
|
76
|
+
CANVAS
|
|
77
|
+
);
|
|
78
|
+
return (
|
|
79
|
+
<ChartPanel
|
|
80
|
+
figure={nonEmpty(unit) ? `${fmtNum(total)}${unit}` : undefined}
|
|
81
|
+
icon="lucide:layout-grid"
|
|
82
|
+
title={title}
|
|
83
|
+
>
|
|
84
|
+
<div className="px-4 py-3">
|
|
85
|
+
<div className="relative aspect-video w-full">
|
|
86
|
+
{tiles.map((t, i) => {
|
|
87
|
+
const r = rects[i];
|
|
88
|
+
if (r === undefined || r.w <= 0 || r.h <= 0) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const share = total === 0 ? 0 : (t.n / total) * 100;
|
|
92
|
+
const small = r.w < 16 || r.h < 12;
|
|
93
|
+
return (
|
|
94
|
+
<div
|
|
95
|
+
className={`absolute overflow-hidden rounded border border-white dark:border-neutral-950 ${toneOf(t.spec.tone, i).tile}`}
|
|
96
|
+
key={t.spec.name}
|
|
97
|
+
style={{
|
|
98
|
+
height: `${r.h}%`,
|
|
99
|
+
left: `${r.x}%`,
|
|
100
|
+
top: `${r.y}%`,
|
|
101
|
+
width: `${r.w}%`,
|
|
102
|
+
}}
|
|
103
|
+
title={`${t.spec.name} · ${fmtNum(t.n)}${unit ?? ""} (${Math.round(share)}%)`}
|
|
104
|
+
>
|
|
105
|
+
<div className="flex h-full flex-col p-1.5">
|
|
106
|
+
<span className="truncate text-xs font-medium">
|
|
107
|
+
{t.spec.name}
|
|
108
|
+
</span>
|
|
109
|
+
{small ? null : (
|
|
110
|
+
<span
|
|
111
|
+
className={`font-mono ${TEXT_MICRO} tabular-nums opacity-80`}
|
|
112
|
+
>
|
|
113
|
+
{fmtNum(t.n)}
|
|
114
|
+
{unit}
|
|
115
|
+
{` · ${Math.round(share)}%`}
|
|
116
|
+
</span>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
})}
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
{rest.length > 0 ? <div className="px-4 pb-3">{rest}</div> : null}
|
|
125
|
+
</ChartPanel>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
);
|
package/src/ui/venn.tsx
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
import * as v from "valibot";
|
|
3
|
+
|
|
4
|
+
import { defineComponent, flattenChildren, parseProps } from "../define.js";
|
|
5
|
+
import { NUMISH, numOf } from "./attrs.js";
|
|
6
|
+
import { CODE_CHIP_CLS } from "./bits.js";
|
|
7
|
+
import { ChartPanel } from "./chart-bits.js";
|
|
8
|
+
import { CHART_TONES, fmtNum, textList, toneOf } from "./chart.js";
|
|
9
|
+
import { isEl } from "./children.js";
|
|
10
|
+
import { TEXT, TEXT_MICRO } from "./tones.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Venn diagram — 2 or 3 <Set> circles plus <Overlap> labels at the
|
|
14
|
+
* intersections. Circles are uniform (a Venn is not to scale); values
|
|
15
|
+
* print inside each set's own region.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const SET_SCHEMA = v.looseObject({
|
|
19
|
+
name: v.string(),
|
|
20
|
+
tone: v.optional(v.picklist(CHART_TONES)),
|
|
21
|
+
value: NUMISH,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const OVERLAP_SCHEMA = v.looseObject({
|
|
25
|
+
sets: v.string(),
|
|
26
|
+
value: NUMISH,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
type SetSpec = v.InferOutput<typeof SET_SCHEMA>;
|
|
30
|
+
type OverlapSpec = v.InferOutput<typeof OVERLAP_SCHEMA>;
|
|
31
|
+
|
|
32
|
+
export const Set = defineComponent(
|
|
33
|
+
{
|
|
34
|
+
description:
|
|
35
|
+
"ベン図の1集合。<Venn> の子 (2–3個)。name/value は必須、tone で色",
|
|
36
|
+
schema: SET_SCHEMA,
|
|
37
|
+
},
|
|
38
|
+
// Standalone use: a `name value` chip so misplaced sets still render.
|
|
39
|
+
({ name, value }): ReactElement => (
|
|
40
|
+
<span className={`font-mono ${CODE_CHIP_CLS}`}>
|
|
41
|
+
{name}
|
|
42
|
+
<span className="opacity-70">{value}</span>
|
|
43
|
+
</span>
|
|
44
|
+
)
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
export const Overlap = defineComponent(
|
|
48
|
+
{
|
|
49
|
+
description:
|
|
50
|
+
'ベン図の重なり領域。<Venn> の子。sets="a,b" で対象の Set 名 (2個なら両方)、value は必須',
|
|
51
|
+
schema: OVERLAP_SCHEMA,
|
|
52
|
+
},
|
|
53
|
+
// Standalone use: a `a∩b value` chip so misplaced overlaps still render.
|
|
54
|
+
({ sets, value }): ReactElement => (
|
|
55
|
+
<span className={`font-mono ${CODE_CHIP_CLS}`}>
|
|
56
|
+
{sets.replaceAll(",", "∩")}
|
|
57
|
+
<span className="opacity-70">{value}</span>
|
|
58
|
+
</span>
|
|
59
|
+
)
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
interface Circle {
|
|
63
|
+
cx: number;
|
|
64
|
+
cy: number;
|
|
65
|
+
r: number;
|
|
66
|
+
spec: SetSpec;
|
|
67
|
+
value: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const collectSets = (
|
|
71
|
+
children: ReactNode
|
|
72
|
+
): {
|
|
73
|
+
overlaps: { names: string[]; spec: OverlapSpec; value: number }[];
|
|
74
|
+
rest: ReactNode[];
|
|
75
|
+
sets: { spec: SetSpec; value: number }[];
|
|
76
|
+
} => {
|
|
77
|
+
const sets: { spec: SetSpec; value: number }[] = [];
|
|
78
|
+
const overlaps: { names: string[]; spec: OverlapSpec; value: number }[] = [];
|
|
79
|
+
const rest: ReactNode[] = [];
|
|
80
|
+
for (const child of flattenChildren(children)) {
|
|
81
|
+
if (isEl(child, Set)) {
|
|
82
|
+
const spec = parseProps(SET_SCHEMA, child.props, "Set");
|
|
83
|
+
sets.push({ spec, value: numOf(spec.value) ?? 0 });
|
|
84
|
+
} else if (isEl(child, Overlap)) {
|
|
85
|
+
const spec = parseProps(OVERLAP_SCHEMA, child.props, "Overlap");
|
|
86
|
+
overlaps.push({
|
|
87
|
+
names: textList(spec.sets),
|
|
88
|
+
spec,
|
|
89
|
+
value: numOf(spec.value) ?? 0,
|
|
90
|
+
});
|
|
91
|
+
} else {
|
|
92
|
+
rest.push(child);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { overlaps, rest, sets };
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const VB_W = 400;
|
|
99
|
+
const VB_H = 300;
|
|
100
|
+
|
|
101
|
+
/** Fixed circle geometry — index-aligned with the first 2–3 sets. */
|
|
102
|
+
const GEOMETRY: { cx: number; cy: number; r: number }[][] = [
|
|
103
|
+
[],
|
|
104
|
+
[],
|
|
105
|
+
[
|
|
106
|
+
{ cx: 150, cy: 150, r: 92 },
|
|
107
|
+
{ cx: 250, cy: 150, r: 92 },
|
|
108
|
+
],
|
|
109
|
+
[
|
|
110
|
+
{ cx: 200, cy: 116, r: 78 },
|
|
111
|
+
{ cx: 148, cy: 206, r: 78 },
|
|
112
|
+
{ cx: 252, cy: 206, r: 78 },
|
|
113
|
+
],
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
const NAME_TEXT = "fill-neutral-600 dark:fill-neutral-300";
|
|
117
|
+
const VALUE_TEXT = "fill-neutral-500 dark:fill-neutral-400";
|
|
118
|
+
|
|
119
|
+
export const Venn = defineComponent(
|
|
120
|
+
{
|
|
121
|
+
description:
|
|
122
|
+
'ベン図コンテナ (集合の重なり)。<Set name value> を 2–3 個と <Overlap sets="a,b" value> で交差部の値。unit は値の単位',
|
|
123
|
+
schema: v.looseObject({
|
|
124
|
+
title: v.optional(v.string()),
|
|
125
|
+
unit: v.optional(v.string()),
|
|
126
|
+
}),
|
|
127
|
+
},
|
|
128
|
+
({ title, unit, children }) => {
|
|
129
|
+
const { sets, overlaps, rest } = collectSets(children);
|
|
130
|
+
const geom = GEOMETRY[sets.length] ?? GEOMETRY[3];
|
|
131
|
+
const circles: Circle[] = sets.map((s, i) => ({
|
|
132
|
+
...(geom[i] ?? geom.at(-1)),
|
|
133
|
+
spec: s.spec,
|
|
134
|
+
value: s.value,
|
|
135
|
+
}));
|
|
136
|
+
// A set's own label sits at its centroid pushed away from the other
|
|
137
|
+
// circles' centers, so it lands in the non-overlapping region.
|
|
138
|
+
const ownLabel = (c: Circle): { x: number; y: number } => {
|
|
139
|
+
let dx = 0;
|
|
140
|
+
let dy = 0;
|
|
141
|
+
for (const o of circles) {
|
|
142
|
+
if (o !== c) {
|
|
143
|
+
dx += c.cx - o.cx;
|
|
144
|
+
dy += c.cy - o.cy;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
148
|
+
return {
|
|
149
|
+
x: c.cx + (dx / len) * c.r * 0.42,
|
|
150
|
+
y: c.cy + (dy / len) * c.r * 0.42,
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
// An overlap label sits at the mean of its member centers, pushed away
|
|
154
|
+
// from the non-members' centers (into the member-only lens).
|
|
155
|
+
const overlapLabel = (names: string[]): { x: number; y: number } => {
|
|
156
|
+
const isMember = (c: Circle): boolean =>
|
|
157
|
+
names.some(
|
|
158
|
+
(n) =>
|
|
159
|
+
c.spec.name === n || c.spec.name.toLowerCase() === n.toLowerCase()
|
|
160
|
+
);
|
|
161
|
+
const members = circles.filter(isMember);
|
|
162
|
+
const others = circles.filter((c) => !isMember(c));
|
|
163
|
+
const mx =
|
|
164
|
+
members.reduce((a, c) => a + c.cx, 0) / Math.max(1, members.length);
|
|
165
|
+
const my =
|
|
166
|
+
members.reduce((a, c) => a + c.cy, 0) / Math.max(1, members.length);
|
|
167
|
+
if (others.length === 0) {
|
|
168
|
+
return { x: mx, y: my };
|
|
169
|
+
}
|
|
170
|
+
const ox =
|
|
171
|
+
others.reduce((a, c) => a + c.cx, 0) / Math.max(1, others.length);
|
|
172
|
+
const oy =
|
|
173
|
+
others.reduce((a, c) => a + c.cy, 0) / Math.max(1, others.length);
|
|
174
|
+
const dx = mx - ox;
|
|
175
|
+
const dy = my - oy;
|
|
176
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
177
|
+
return { x: mx + (dx / len) * 18, y: my + (dy / len) * 18 };
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<ChartPanel icon="lucide:blend" title={title}>
|
|
182
|
+
<div className="px-4 py-3">
|
|
183
|
+
{circles.length < 2 ? (
|
|
184
|
+
<p className={`text-sm ${TEXT.muted}`}>
|
|
185
|
+
Venn needs at least 2 sets.
|
|
186
|
+
</p>
|
|
187
|
+
) : (
|
|
188
|
+
<svg
|
|
189
|
+
className="mx-auto h-auto w-full max-w-md"
|
|
190
|
+
viewBox={`0 0 ${VB_W} ${VB_H}`}
|
|
191
|
+
>
|
|
192
|
+
<title>{title ?? "venn diagram"}</title>
|
|
193
|
+
{circles.map((c, i) => {
|
|
194
|
+
const style = toneOf(c.spec.tone, i);
|
|
195
|
+
return (
|
|
196
|
+
<circle
|
|
197
|
+
className={`${style.soft} ${style.stroke}`}
|
|
198
|
+
cx={c.cx}
|
|
199
|
+
cy={c.cy}
|
|
200
|
+
key={c.spec.name}
|
|
201
|
+
r={c.r}
|
|
202
|
+
strokeWidth={1.5}
|
|
203
|
+
/>
|
|
204
|
+
);
|
|
205
|
+
})}
|
|
206
|
+
{circles.map((c) => {
|
|
207
|
+
const p = ownLabel(c);
|
|
208
|
+
return (
|
|
209
|
+
<g key={c.spec.name}>
|
|
210
|
+
<text
|
|
211
|
+
className={NAME_TEXT}
|
|
212
|
+
fontSize={11}
|
|
213
|
+
fontWeight={600}
|
|
214
|
+
textAnchor="middle"
|
|
215
|
+
x={p.x}
|
|
216
|
+
y={p.y}
|
|
217
|
+
>
|
|
218
|
+
{c.spec.name}
|
|
219
|
+
</text>
|
|
220
|
+
<text
|
|
221
|
+
className={VALUE_TEXT}
|
|
222
|
+
fontSize={9}
|
|
223
|
+
textAnchor="middle"
|
|
224
|
+
x={p.x}
|
|
225
|
+
y={p.y + 12}
|
|
226
|
+
>
|
|
227
|
+
{fmtNum(c.value)}
|
|
228
|
+
{unit}
|
|
229
|
+
</text>
|
|
230
|
+
</g>
|
|
231
|
+
);
|
|
232
|
+
})}
|
|
233
|
+
{overlaps.map((o) => {
|
|
234
|
+
const p = overlapLabel(o.names);
|
|
235
|
+
return (
|
|
236
|
+
<text
|
|
237
|
+
className={VALUE_TEXT}
|
|
238
|
+
fontSize={10}
|
|
239
|
+
fontWeight={600}
|
|
240
|
+
key={o.spec.sets}
|
|
241
|
+
textAnchor="middle"
|
|
242
|
+
x={p.x}
|
|
243
|
+
y={p.y + 3}
|
|
244
|
+
>
|
|
245
|
+
{fmtNum(o.value)}
|
|
246
|
+
</text>
|
|
247
|
+
);
|
|
248
|
+
})}
|
|
249
|
+
</svg>
|
|
250
|
+
)}
|
|
251
|
+
{rest.length > 0 ? (
|
|
252
|
+
<div className={`${TEXT_MICRO} ${TEXT.muted}`}>{rest}</div>
|
|
253
|
+
) : null}
|
|
254
|
+
</div>
|
|
255
|
+
</ChartPanel>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
);
|