@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,32 @@
1
+ import { useContext } from "react";
2
+
3
+ import { DocContext } from "../doc-context.js";
4
+ import { nonEmpty, safeHref } from "../guards.js";
5
+ import { firstLine } from "../lines.js";
6
+
7
+ /**
8
+ * Resolve a component's `href` override or doc-relative `path`/`lines` to a
9
+ * link target. Explicit `href` always wins; without a render-time fileLink
10
+ * (e.g. Storybook) the component renders unlinked.
11
+ */
12
+ export const useFileLink = (
13
+ relPath: string | undefined,
14
+ lines?: string,
15
+ href?: string
16
+ ): string | undefined => {
17
+ const { fileLink } = useContext(DocContext);
18
+ const override = safeHref(href);
19
+ if (override !== undefined) {
20
+ return override;
21
+ }
22
+ if (!nonEmpty(relPath) || fileLink === undefined) {
23
+ return undefined;
24
+ }
25
+ return fileLink(relPath, firstLine(lines));
26
+ };
27
+
28
+ /** `target`/`rel` for web URLs; editor schemes stay in the same tab. */
29
+ export const linkTarget = (href: string): Record<string, string> =>
30
+ /^https?:/u.test(href)
31
+ ? { rel: "noopener noreferrer", target: "_blank" }
32
+ : {};
@@ -0,0 +1,39 @@
1
+ import * as v from "valibot";
2
+
3
+ import { defineComponent } from "../define.js";
4
+ import { FILE_LINK_PROPS } from "./attrs.js";
5
+ import { CodeChip, CopyButton, MaybeLink, PathLabel } from "./bits.js";
6
+ import { fileIcon } from "./file-icon.js";
7
+ import { useFileLink } from "./file-link.js";
8
+ import { Icon } from "./icon.js";
9
+ import { LINK_CLS } from "./tones.js";
10
+
11
+ export const FileRef = defineComponent(
12
+ {
13
+ description:
14
+ 'ソースファイルへの参照チップ。lines="10-20" で行範囲を示せる。実在ファイルはエディタリンク(既定 vscode://)になり、href で上書き可。アイコンは拡張子から自動選択',
15
+ schema: v.looseObject({
16
+ ...FILE_LINK_PROPS,
17
+ path: v.string(),
18
+ }),
19
+ },
20
+ ({ path, lines, href }) => {
21
+ const link = useFileLink(path, lines, href);
22
+ const label = (
23
+ <PathLabel lines={lines} linesClassName="opacity-60" path={path} />
24
+ );
25
+ return (
26
+ <CodeChip>
27
+ <Icon className="h-3.5 w-3.5 opacity-60" name={fileIcon(path)} />
28
+ <MaybeLink className={LINK_CLS} href={link}>
29
+ {label}
30
+ </MaybeLink>
31
+ <CopyButton
32
+ className="-mr-0.5 opacity-40"
33
+ copy={path}
34
+ title="Copy path"
35
+ />
36
+ </CodeChip>
37
+ );
38
+ }
39
+ );
@@ -0,0 +1,112 @@
1
+ import * as v from "valibot";
2
+
3
+ import { defineComponent } from "../define.js";
4
+ import { nonEmpty, own } from "../guards.js";
5
+ import { FILE_LINK_PROPS, TITLE_PROP } from "./attrs.js";
6
+ import {
7
+ ListPanel,
8
+ ListRow,
9
+ MaybeLink,
10
+ PathLabel,
11
+ RowIcon,
12
+ RowNote,
13
+ Tag,
14
+ } from "./bits.js";
15
+ import { fileIcon } from "./file-icon.js";
16
+ import { useFileLink } from "./file-link.js";
17
+ import { LINK_CLS, MONO_CLS, TEXT, TONE_TEXT } from "./tones.js";
18
+
19
+ export const FILE_KINDS = [
20
+ "config",
21
+ "core",
22
+ "docs",
23
+ "entry",
24
+ "generated",
25
+ "test",
26
+ "types",
27
+ ] as const;
28
+
29
+ const KINDS: Record<string, { cls: string; icon: string }> = {
30
+ config: {
31
+ cls: TONE_TEXT.neutral,
32
+ icon: "lucide:settings",
33
+ },
34
+ core: {
35
+ cls: TONE_TEXT.violet,
36
+ icon: "lucide:layers",
37
+ },
38
+ docs: {
39
+ cls: TONE_TEXT.neutral,
40
+ icon: "lucide:book-open",
41
+ },
42
+ entry: {
43
+ cls: TONE_TEXT.sky,
44
+ icon: "lucide:log-in",
45
+ },
46
+ generated: {
47
+ cls: TONE_TEXT.neutral,
48
+ icon: "lucide:bot",
49
+ },
50
+ test: {
51
+ cls: TONE_TEXT.amber,
52
+ icon: "lucide:flask-conical",
53
+ },
54
+ types: {
55
+ cls: TONE_TEXT.teal,
56
+ icon: "lucide:braces",
57
+ },
58
+ };
59
+
60
+ const FALLBACK_KIND = {
61
+ cls: TONE_TEXT.neutral,
62
+ icon: "lucide:tag",
63
+ };
64
+
65
+ export const Files = defineComponent(
66
+ {
67
+ description:
68
+ "関連ファイル一覧のコンテナ。<File> を並べる。title でキャプションバー",
69
+ schema: v.looseObject(TITLE_PROP),
70
+ },
71
+ ({ title, children }) => <ListPanel title={title}>{children}</ListPanel>
72
+ );
73
+
74
+ export const File = defineComponent(
75
+ {
76
+ description:
77
+ "関連ファイル1行。path は必須、lines で行範囲を併記。実在ファイルはエディタリンク(既定 vscode://)になり、href で上書き可。アイコンはファイル名/拡張子から自動選択。kind は entry|core|types|config|test|docs|generated など(既知の値はアイコン/色付き、それ以外も表示可)。children は役割の注記",
78
+ schema: v.looseObject({
79
+ ...FILE_LINK_PROPS,
80
+ kind: v.optional(v.string()),
81
+ path: v.string(),
82
+ }),
83
+ },
84
+ ({ path, lines, kind, href, children }) => {
85
+ // `own`: `kind` is a free-form string — "toString" would otherwise pull
86
+ // a function off the prototype instead of the fallback styling.
87
+ let r: { cls: string; icon: string } | undefined;
88
+ if (nonEmpty(kind)) {
89
+ r = own(KINDS, kind) ?? FALLBACK_KIND;
90
+ }
91
+ const link = useFileLink(path, lines, href);
92
+ const label = (
93
+ <code className={MONO_CLS}>
94
+ <PathLabel lines={lines} linesClassName={TEXT.faint} path={path} />
95
+ </code>
96
+ );
97
+ return (
98
+ <ListRow>
99
+ <RowIcon name={fileIcon(path)} />
100
+ <MaybeLink className={LINK_CLS} href={link}>
101
+ {label}
102
+ </MaybeLink>
103
+ {r === undefined ? null : (
104
+ <Tag className={r.cls} icon={r.icon}>
105
+ {kind}
106
+ </Tag>
107
+ )}
108
+ <RowNote>{children}</RowNote>
109
+ </ListRow>
110
+ );
111
+ }
112
+ );
@@ -0,0 +1,85 @@
1
+ import * as v from "valibot";
2
+
3
+ import { defineComponent } from "../define.js";
4
+ import { isOneOf } from "../guards.js";
5
+ import { TITLE_PROP } from "./attrs.js";
6
+ import { CountedList, IndexedCard, Pill } from "./bits.js";
7
+ import { useChildIndex } from "./child-index.js";
8
+ import { countByProp } from "./children.js";
9
+ import { TONE } from "./tones.js";
10
+
11
+ export const CONFIDENCES = ["confirmed", "inferred", "unverified"] as const;
12
+ export type Confidence = (typeof CONFIDENCES)[number];
13
+
14
+ export const isConfidence = isOneOf(CONFIDENCES);
15
+
16
+ const CONF: Record<Confidence, { cls: string; icon: string; label: string }> = {
17
+ confirmed: {
18
+ cls: TONE.emerald,
19
+ icon: "lucide:badge-check",
20
+ label: "Confirmed",
21
+ },
22
+ inferred: {
23
+ cls: TONE.amber,
24
+ icon: "lucide:lightbulb",
25
+ label: "Inferred",
26
+ },
27
+ unverified: {
28
+ cls: TONE.neutral,
29
+ icon: "lucide:circle-dashed",
30
+ label: "Unverified",
31
+ },
32
+ };
33
+
34
+ export const Finding = defineComponent(
35
+ {
36
+ description:
37
+ "調査の発見事項。confidence は confirmed|inferred|unverified(確証済み/推論/未検証)。title と children(根拠・証拠)",
38
+ schema: v.looseObject({
39
+ confidence: v.optional(v.picklist(CONFIDENCES), "confirmed"),
40
+ title: v.optional(v.string()),
41
+ }),
42
+ },
43
+ ({ confidence, title, children }) => {
44
+ const { n } = useChildIndex();
45
+ const c = CONF[confidence];
46
+ return (
47
+ <IndexedCard
48
+ n={n}
49
+ pill={
50
+ <Pill className={c.cls} icon={c.icon}>
51
+ {c.label}
52
+ </Pill>
53
+ }
54
+ title={title}
55
+ >
56
+ {children}
57
+ </IndexedCard>
58
+ );
59
+ }
60
+ );
61
+
62
+ export const Findings = defineComponent(
63
+ {
64
+ description:
65
+ "発見事項リストのコンテナ。<Finding> を並べ、confidence 別の件数サマリを上部に表示",
66
+ schema: v.looseObject(TITLE_PROP),
67
+ },
68
+ ({ title, children }) => (
69
+ <CountedList
70
+ counts={countByProp(
71
+ children,
72
+ Finding,
73
+ "confidence",
74
+ isConfidence,
75
+ "confirmed"
76
+ )}
77
+ labelOf={(k) => (isConfidence(k) ? CONF[k].label.toLowerCase() : k)}
78
+ noun={["finding", "findings"]}
79
+ order={CONFIDENCES}
80
+ title={title}
81
+ >
82
+ {children}
83
+ </CountedList>
84
+ )
85
+ );
@@ -0,0 +1,73 @@
1
+ import * as v from "valibot";
2
+
3
+ import { defineComponent } from "../define.js";
4
+ import { nonEmpty } from "../guards.js";
5
+ import { FILE_LINK_PROPS, TITLE_PROP } from "./attrs.js";
6
+ import { LocLink, Section, TrimBody } from "./bits.js";
7
+ import { indexChildren, useChildIndex } from "./child-index.js";
8
+ import { Icon } from "./icon.js";
9
+ import { CHIP_BORDER_CLS, RAIL_BG_CLS, TEXT } from "./tones.js";
10
+
11
+ export const Flow = defineComponent(
12
+ {
13
+ description:
14
+ "呼び出し・実行フローのコンテナ。<FlowStep> を番号付きで縦に連結する。title で見出し",
15
+ schema: v.looseObject(TITLE_PROP),
16
+ },
17
+ ({ title, children }) => (
18
+ <Section title={title}>
19
+ <ol className="not-prose m-0 list-none p-0">{indexChildren(children)}</ol>
20
+ </Section>
21
+ )
22
+ );
23
+
24
+ export const FlowStep = defineComponent(
25
+ {
26
+ description:
27
+ "フローの1ホップ。name は関数名や処理名、path/lines で発生箇所を併記(実在すればエディタリンク、href で上書き可)。children はその処理の説明",
28
+ schema: v.looseObject({
29
+ ...FILE_LINK_PROPS,
30
+ name: v.optional(v.string()),
31
+ }),
32
+ },
33
+ ({ name, path, lines, href, children }) => {
34
+ const { n, last } = useChildIndex();
35
+ return (
36
+ <li className="relative pt-1 pb-4 pl-10 last:pb-0">
37
+ {last ? null : (
38
+ <span
39
+ aria-hidden
40
+ className={`absolute top-9 bottom-0 left-[13px] w-px ${RAIL_BG_CLS}`}
41
+ />
42
+ )}
43
+ <span
44
+ aria-hidden
45
+ className={`bg-background absolute top-0 left-0 flex h-7 w-7 items-center justify-center rounded-full border ${CHIP_BORDER_CLS} font-mono text-xs font-medium ${TEXT.muted}`}
46
+ >
47
+ {n > 0 ? (
48
+ n
49
+ ) : (
50
+ <Icon className="h-3.5 w-3.5" name="lucide:circle-dot" />
51
+ )}
52
+ </span>
53
+ {nonEmpty(name) || nonEmpty(path) ? (
54
+ <div className="flex flex-wrap items-baseline gap-x-2 gap-y-0.5">
55
+ {nonEmpty(name) ? (
56
+ <code
57
+ className={`font-mono text-[0.85em] font-semibold ${TEXT.strong}`}
58
+ >
59
+ {name}
60
+ </code>
61
+ ) : null}
62
+ {nonEmpty(path) ? (
63
+ <LocLink href={href} lines={lines} path={path} />
64
+ ) : null}
65
+ </div>
66
+ ) : null}
67
+ <TrimBody className={`mt-1.5 text-sm ${TEXT.body}`}>
68
+ {children}
69
+ </TrimBody>
70
+ </li>
71
+ );
72
+ }
73
+ );
@@ -0,0 +1,121 @@
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, toneOf } from "./chart.js";
10
+ import { isEl } from "./children.js";
11
+ import { TEXT, TEXT_MICRO } from "./tones.js";
12
+
13
+ /**
14
+ * Funnel/pyramid — centered bars shrinking through <Stage> rows. Each row
15
+ * carries the stage value and its share of the first stage; conversion
16
+ * between adjacent stages shows on the left when it drops.
17
+ */
18
+
19
+ const STAGE_SCHEMA = v.looseObject({
20
+ name: v.string(),
21
+ note: v.optional(v.string()),
22
+ tone: v.optional(v.picklist(CHART_TONES)),
23
+ value: NUMISH,
24
+ });
25
+
26
+ type StageSpec = v.InferOutput<typeof STAGE_SCHEMA>;
27
+
28
+ export const Stage = defineComponent(
29
+ {
30
+ description:
31
+ "ファネルの1段。<Funnel> の子。name/value は必須、note で補足、tone で色指定",
32
+ schema: STAGE_SCHEMA,
33
+ },
34
+ // Standalone use: a `name value` chip so misplaced stages still render.
35
+ ({ name, value, note }): ReactElement => (
36
+ <span className={`font-mono ${CODE_CHIP_CLS}`}>
37
+ {name}
38
+ <span className="opacity-70">{value}</span>
39
+ {nonEmpty(note) ? <span className="opacity-70">{note}</span> : null}
40
+ </span>
41
+ )
42
+ );
43
+
44
+ const collectStages = (
45
+ children: ReactNode
46
+ ): { rest: ReactNode[]; stages: { n: number; spec: StageSpec }[] } => {
47
+ const stages: { n: number; spec: StageSpec }[] = [];
48
+ const rest: ReactNode[] = [];
49
+ for (const child of flattenChildren(children)) {
50
+ if (isEl(child, Stage)) {
51
+ const spec = parseProps(STAGE_SCHEMA, child.props, "Stage");
52
+ stages.push({ n: Math.max(0, numOf(spec.value) ?? 0), spec });
53
+ } else {
54
+ rest.push(child);
55
+ }
56
+ }
57
+ return { rest, stages };
58
+ };
59
+
60
+ export const Funnel = defineComponent(
61
+ {
62
+ description:
63
+ "ファネルコンテナ (段階ごとの絞り込み/ピラミッド)。<Stage name value> を上から順に並べる。unit は値の単位",
64
+ schema: v.looseObject({
65
+ title: v.optional(v.string()),
66
+ unit: v.optional(v.string()),
67
+ }),
68
+ },
69
+ ({ title, unit, children }) => {
70
+ const { stages, rest } = collectStages(children);
71
+ const first = stages[0]?.n ?? 0;
72
+ const max = Math.max(0, ...stages.map((s) => s.n));
73
+ return (
74
+ <ChartPanel
75
+ figure={
76
+ nonEmpty(unit) && first > 0 ? `${fmtNum(first)}${unit}` : undefined
77
+ }
78
+ icon="lucide:funnel"
79
+ title={title}
80
+ >
81
+ <div className="space-y-1.5 px-4 py-3">
82
+ {stages.map((s, i) => {
83
+ const prev = i === 0 ? undefined : stages[i - 1]?.n;
84
+ const conv =
85
+ prev !== undefined && prev > 0 ? (s.n / prev) * 100 : undefined;
86
+ const share = first === 0 ? 0 : (s.n / first) * 100;
87
+ const width = max === 0 ? 0 : (s.n / max) * 100;
88
+ return (
89
+ <div className="flex items-center gap-2" key={s.spec.name}>
90
+ <span
91
+ className={`w-12 shrink-0 text-right font-mono ${TEXT_MICRO} tabular-nums ${TEXT.faint}`}
92
+ >
93
+ {conv === undefined ? "" : `${Math.round(conv)}%`}
94
+ </span>
95
+ <div className="flex min-w-0 flex-1 justify-center">
96
+ <div
97
+ className={`flex h-7 min-w-10 items-center justify-center rounded ${toneOf(s.spec.tone, i).bg}`}
98
+ style={{ width: `${Math.max(8, width)}%` }}
99
+ title={`${s.spec.name} · ${fmtNum(s.n)}${unit ?? ""}`}
100
+ >
101
+ <span className="truncate px-2 text-xs font-medium text-white dark:text-neutral-900">
102
+ {s.spec.name}
103
+ </span>
104
+ </div>
105
+ </div>
106
+ <span
107
+ className={`w-24 shrink-0 font-mono ${TEXT_MICRO} tabular-nums ${TEXT.muted}`}
108
+ >
109
+ {fmtNum(s.n)}
110
+ {unit}
111
+ {i === 0 ? "" : ` · ${Math.round(share)}%`}
112
+ </span>
113
+ </div>
114
+ );
115
+ })}
116
+ </div>
117
+ {rest.length > 0 ? <div className="px-4 pb-3">{rest}</div> : null}
118
+ </ChartPanel>
119
+ );
120
+ }
121
+ );