@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,443 @@
1
+ import {
2
+ addDays,
3
+ differenceInCalendarDays,
4
+ eachDayOfInterval,
5
+ eachMonthOfInterval,
6
+ eachWeekOfInterval,
7
+ format,
8
+ isValid,
9
+ parseISO,
10
+ } from "date-fns";
11
+ import { createContext, useContext, useMemo } from "react";
12
+ import type { ReactNode } from "react";
13
+ import * as v from "valibot";
14
+
15
+ import { defineComponent, flattenChildren } from "../define.js";
16
+ import { DocContext } from "../doc-context.js";
17
+ import { nonEmpty } from "../guards.js";
18
+ import { attrFalse, NUMISH, numOf } from "./attrs.js";
19
+ import { CaptionBar, Panel, RowLabel } from "./bits.js";
20
+ import { isEl, propOf } from "./children.js";
21
+ import { Icon } from "./icon.js";
22
+ import { STATUS_PROP } from "./status-badge.js";
23
+ import type { Status } from "./status-badge.js";
24
+ import {
25
+ CAPTION_TITLE_CLS,
26
+ MONO_NUM_CLS,
27
+ RAIL_BG_CLS,
28
+ TEXT,
29
+ TEXT_MICRO,
30
+ TRACK_CLS,
31
+ } from "./tones.js";
32
+
33
+ /**
34
+ * Date-based Gantt chart — task bars and milestone diamonds on a day scale.
35
+ * `start`/`end` accept ISO dates (`YYYY-MM-DD`); the range defaults to the
36
+ * earliest task start through the latest task end. A "today" marker is drawn
37
+ * at `DocContext.now` when it falls inside the range.
38
+ */
39
+
40
+ const ROW_GRID =
41
+ "grid grid-cols-[minmax(7rem,10rem)_minmax(0,1fr)_5rem] items-center gap-x-3";
42
+
43
+ const BAR_TRACK_CLS = `relative h-4.5 overflow-hidden rounded ${TRACK_CLS}`;
44
+
45
+ /** Bar fill per status — `soft` backs the progress overlay, `solid` is the bar. */
46
+ const BAR: Record<Status, { soft: string; solid: string }> = {
47
+ blocked: { soft: "bg-red-500/30", solid: "bg-red-500" },
48
+ doing: { soft: "bg-sky-500/30", solid: "bg-sky-500" },
49
+ done: { soft: "bg-emerald-500/30", solid: "bg-emerald-500" },
50
+ todo: { soft: "bg-neutral-400/40", solid: "bg-neutral-400" },
51
+ };
52
+ const DEFAULT_BAR = { soft: "bg-sky-500/30", solid: "bg-sky-500" };
53
+ const DEFAULT_MILESTONE = "bg-amber-500";
54
+
55
+ const toDate = (x: unknown): Date | undefined => {
56
+ if (typeof x !== "string" || x === "") {
57
+ return undefined;
58
+ }
59
+ const d = parseISO(x);
60
+ return isValid(d) ? d : undefined;
61
+ };
62
+
63
+ interface GanttScale {
64
+ /** Range day 0 — day offsets are calendar days from here. */
65
+ origin: Date;
66
+ /** Inclusive range end day (for the caption label). */
67
+ last: Date;
68
+ /** Inclusive day span of the range (>= 1). */
69
+ spanDays: number;
70
+ /** Header/gridline ticks as percents of the range. */
71
+ ticks: { label: string; pct: number }[];
72
+ /** Percent position of the "today" marker, when shown. */
73
+ todayPct?: number;
74
+ }
75
+
76
+ const ScaleCtx = createContext<GanttScale | undefined>(undefined);
77
+
78
+ const pctOf = (d: Date, scale: GanttScale): number =>
79
+ (differenceInCalendarDays(d, scale.origin) / scale.spanDays) * 100;
80
+
81
+ const clampPct = (pct: number): number => Math.min(100, Math.max(0, pct));
82
+
83
+ /** `Sep 1–10` / `Sep 1–Oct 5` / `Sep 1` — the compact range in the right column. */
84
+ const fmtRange = (s: Date, e: Date): string => {
85
+ if (differenceInCalendarDays(e, s) === 0) {
86
+ return format(s, "MMM d");
87
+ }
88
+ if (s.getMonth() === e.getMonth() && s.getFullYear() === e.getFullYear()) {
89
+ return `${format(s, "MMM d")}–${format(e, "d")}`;
90
+ }
91
+ return `${format(s, "MMM d")}–${format(e, "MMM d")}`;
92
+ };
93
+
94
+ const MAX_TICKS = 8;
95
+
96
+ const tickLabel = (d: Date, monthly: boolean, sparse: boolean): string => {
97
+ if (!monthly) {
98
+ return format(d, "MMM d");
99
+ }
100
+ return sparse || d.getMonth() === 0
101
+ ? format(d, "MMM ''yy")
102
+ : format(d, "MMM");
103
+ };
104
+
105
+ /** Axis ticks: days on short ranges, Mondays mid-range, month starts after. */
106
+ const ticksOf = (
107
+ origin: Date,
108
+ last: Date,
109
+ spanDays: number
110
+ ): { label: string; pct: number }[] => {
111
+ const monthly = spanDays > 70;
112
+ const bounds = { end: last, start: origin };
113
+ let days: Date[];
114
+ if (monthly) {
115
+ days = eachMonthOfInterval(bounds);
116
+ } else if (spanDays > 16) {
117
+ days = eachWeekOfInterval(bounds, { weekStartsOn: 1 });
118
+ } else {
119
+ days = eachDayOfInterval(bounds);
120
+ }
121
+ const ticks = days
122
+ .map((d) => ({
123
+ d,
124
+ pct: (differenceInCalendarDays(d, origin) / spanDays) * 100,
125
+ }))
126
+ // The track edges already mark 0%/100% — drop labels on the boundary.
127
+ .filter((t) => t.pct > 2 && t.pct < 98);
128
+ const stride = Math.max(1, Math.ceil(ticks.length / MAX_TICKS));
129
+ return ticks
130
+ .filter((_, i) => i % stride === 0)
131
+ .map((t) => ({
132
+ label: tickLabel(t.d, monthly, stride > 1),
133
+ pct: t.pct,
134
+ }));
135
+ };
136
+
137
+ /** Tick lines + today marker shared by every row's track (aligned by width). */
138
+ const Guides = (): ReactNode => {
139
+ const scale = useContext(ScaleCtx);
140
+ if (scale === undefined) {
141
+ return null;
142
+ }
143
+ return (
144
+ <>
145
+ {scale.ticks.map((t) => (
146
+ <div
147
+ aria-hidden
148
+ className={`absolute inset-y-0 w-px ${RAIL_BG_CLS}`}
149
+ key={t.pct}
150
+ style={{ left: `${t.pct}%` }}
151
+ />
152
+ ))}
153
+ {scale.todayPct === undefined ? null : (
154
+ <div
155
+ aria-hidden
156
+ className="absolute inset-y-0 w-px bg-red-500/70"
157
+ style={{ left: `${scale.todayPct}%` }}
158
+ />
159
+ )}
160
+ </>
161
+ );
162
+ };
163
+
164
+ /** One chart row: name + optional `owner · note` | track | date label. */
165
+ const Row = (props: {
166
+ children?: ReactNode;
167
+ dates?: string;
168
+ name: string;
169
+ note?: string;
170
+ owner?: string;
171
+ }): ReactNode => {
172
+ const sub = [props.owner, props.note].filter(nonEmpty).join(" · ");
173
+ return (
174
+ <div className={`${ROW_GRID} py-1`}>
175
+ <RowLabel name={props.name} sub={sub} />
176
+ <div className={BAR_TRACK_CLS}>
177
+ <Guides />
178
+ {props.children}
179
+ </div>
180
+ <div
181
+ className={`text-right font-mono ${TEXT_MICRO} tabular-nums ${TEXT.muted}`}
182
+ >
183
+ {props.dates}
184
+ </div>
185
+ </div>
186
+ );
187
+ };
188
+
189
+ export const Task = defineComponent(
190
+ {
191
+ description:
192
+ "ガントの1タスク行。name/start は必須 (ISO 日付)、end 省略時は start と同日。status でバーの色 (todo|doing|done|blocked)、progress は 0–100 の進捗塗り、owner/note は補足行",
193
+ schema: v.looseObject({
194
+ end: v.optional(v.string()),
195
+ name: v.string(),
196
+ note: v.optional(v.string()),
197
+ owner: v.optional(v.string()),
198
+ progress: v.optional(NUMISH),
199
+ start: v.string(),
200
+ status: STATUS_PROP,
201
+ }),
202
+ },
203
+ ({ name, start, end, status, progress, owner, note }) => {
204
+ const scale = useContext(ScaleCtx);
205
+ const s = toDate(start);
206
+ const e0 = toDate(end);
207
+ const e = e0 === undefined || (s !== undefined && e0 < s) ? s : e0;
208
+ const p = numOf(progress);
209
+ const colors = status === undefined ? DEFAULT_BAR : BAR[status];
210
+
211
+ let bar: { left: number; width: number } | undefined;
212
+ if (scale !== undefined && s !== undefined && e !== undefined) {
213
+ const left = clampPct(pctOf(s, scale));
214
+ const right = clampPct(pctOf(addDays(e, 1), scale));
215
+ bar = { left, width: Math.max(right - left, 0.8) };
216
+ }
217
+ const dates = s === undefined ? start : fmtRange(s, e ?? s);
218
+ const tip = `${name} · ${dates}${status === undefined ? "" : ` · ${status}`}`;
219
+
220
+ return (
221
+ <Row dates={dates} name={name} note={note} owner={owner}>
222
+ {bar === undefined ? null : (
223
+ <div
224
+ className={`absolute inset-y-0.5 overflow-hidden rounded-sm ${p === undefined ? colors.solid : colors.soft}`}
225
+ style={{ left: `${bar.left}%`, width: `${bar.width}%` }}
226
+ title={tip}
227
+ >
228
+ {p === undefined ? null : (
229
+ <div
230
+ className={`absolute inset-y-0 left-0 ${colors.solid}`}
231
+ style={{ width: `${Math.min(100, Math.max(0, p))}%` }}
232
+ />
233
+ )}
234
+ </div>
235
+ )}
236
+ </Row>
237
+ );
238
+ }
239
+ );
240
+
241
+ export const Milestone = defineComponent(
242
+ {
243
+ description:
244
+ "ガントのマイルストーン。name/date は必須 (ISO 日付)。status で菱形マーカーの色、owner/note は補足行",
245
+ schema: v.looseObject({
246
+ date: v.string(),
247
+ name: v.string(),
248
+ note: v.optional(v.string()),
249
+ owner: v.optional(v.string()),
250
+ status: STATUS_PROP,
251
+ }),
252
+ },
253
+ ({ name, date, status, owner, note }) => {
254
+ const scale = useContext(ScaleCtx);
255
+ const d = toDate(date);
256
+ const cls = status === undefined ? DEFAULT_MILESTONE : BAR[status].solid;
257
+ const pct =
258
+ scale === undefined || d === undefined
259
+ ? undefined
260
+ : clampPct(pctOf(d, scale));
261
+ return (
262
+ <Row
263
+ dates={d === undefined ? date : format(d, "MMM d")}
264
+ name={name}
265
+ note={note}
266
+ owner={owner}
267
+ >
268
+ {pct === undefined ? null : (
269
+ <div
270
+ className={`absolute top-1/2 h-2.5 w-2.5 -translate-x-1/2 -translate-y-1/2 rotate-45 rounded-[2px] ${cls}`}
271
+ style={{ left: `${pct}%` }}
272
+ title={`${name} · ${date}`}
273
+ />
274
+ )}
275
+ </Row>
276
+ );
277
+ }
278
+ );
279
+
280
+ /** Earliest/latest day across <Task>/<Milestone> children props. */
281
+ const collectDates = (children: ReactNode): Date[] => {
282
+ const dates: Date[] = [];
283
+ for (const n of flattenChildren(children)) {
284
+ if (isEl(n, Task)) {
285
+ const s = toDate(propOf(n, "start"));
286
+ if (s !== undefined) {
287
+ const e = toDate(propOf(n, "end"));
288
+ // Same clamp Task applies to its own bar — an inverted end<start
289
+ // renders as a one-day bar, so it must not stretch the axis.
290
+ dates.push(s, e === undefined || e < s ? s : e);
291
+ }
292
+ } else if (isEl(n, Milestone)) {
293
+ const d = toDate(propOf(n, "date"));
294
+ if (d !== undefined) {
295
+ dates.push(d);
296
+ }
297
+ }
298
+ }
299
+ return dates;
300
+ };
301
+
302
+ /** Min/max across collected child dates. */
303
+ const dateBounds = (
304
+ children: ReactNode
305
+ ): { max: Date | undefined; min: Date | undefined } => {
306
+ let min: Date | undefined;
307
+ let max: Date | undefined;
308
+ for (const d of collectDates(children)) {
309
+ if (min === undefined || d < min) {
310
+ min = d;
311
+ }
312
+ if (max === undefined || d > max) {
313
+ max = d;
314
+ }
315
+ }
316
+ return { max, min };
317
+ };
318
+
319
+ /**
320
+ * The chart range: `start`/`end` props win, else the earliest child start
321
+ * through the latest child end. Empty charts default to a 30-day window
322
+ * from `fallback`.
323
+ */
324
+ const resolveRange = (
325
+ children: ReactNode,
326
+ start: unknown,
327
+ end: unknown,
328
+ fallback: Date
329
+ ): { last: Date; origin: Date; spanDays: number } => {
330
+ const boundStart = toDate(start);
331
+ const boundEnd = toDate(end);
332
+ let origin = boundStart;
333
+ let last = boundEnd;
334
+ if (origin === undefined || last === undefined) {
335
+ // An explicit bound is a hard edge — only the unset side derives from
336
+ // child dates. Extending a set side would silently un-zoom the chart.
337
+ const { min, max } = dateBounds(children);
338
+ origin ??= min;
339
+ last ??= max;
340
+ }
341
+ let o = origin ?? fallback;
342
+ let l = last ?? addDays(o, 29);
343
+ if (l < o) {
344
+ // Inverted range — needs an explicit side (derived min <= max by
345
+ // construction). A lone explicit end wins; otherwise keep the origin.
346
+ if (boundEnd !== undefined && boundStart === undefined) {
347
+ o = l;
348
+ } else {
349
+ l = o;
350
+ }
351
+ }
352
+ return {
353
+ last: l,
354
+ origin: o,
355
+ spanDays: Math.max(1, differenceInCalendarDays(l, o) + 1),
356
+ };
357
+ };
358
+
359
+ /** Day offset of the "today" marker — `false` disables, a date pins, else `now`. */
360
+ const todayOffsetOf = (
361
+ today: unknown,
362
+ origin: Date,
363
+ fallback: Date
364
+ ): number => {
365
+ let d: Date | undefined;
366
+ if (attrFalse(today)) {
367
+ d = undefined;
368
+ } else if (nonEmpty(today) && today !== "true") {
369
+ d = toDate(today);
370
+ } else {
371
+ d = fallback;
372
+ }
373
+ return d === undefined ? -1 : differenceInCalendarDays(d, origin);
374
+ };
375
+
376
+ export const Gantt = defineComponent(
377
+ {
378
+ description:
379
+ 'ガントチャートコンテナ (日付ベースのスケジュール図)。<Task> と <Milestone> を並べる。start/end で表示範囲を上書き、today="false" で今日マーカー非表示、日付指定で固定',
380
+ schema: v.looseObject({
381
+ end: v.optional(v.string()),
382
+ start: v.optional(v.string()),
383
+ title: v.optional(v.string()),
384
+ today: v.optional(v.string()),
385
+ }),
386
+ },
387
+ ({ title, start, end, today, children }) => {
388
+ const { now } = useContext(DocContext);
389
+ const scale = useMemo<GanttScale>(() => {
390
+ const fallback = now ?? new Date();
391
+ const { origin, last, spanDays } = resolveRange(
392
+ children,
393
+ start,
394
+ end,
395
+ fallback
396
+ );
397
+ const todayOffset = todayOffsetOf(today, origin, fallback);
398
+ return {
399
+ last,
400
+ origin,
401
+ spanDays,
402
+ ticks: ticksOf(origin, last, spanDays),
403
+ todayPct:
404
+ todayOffset < 0 || todayOffset >= spanDays
405
+ ? undefined
406
+ : (todayOffset / spanDays) * 100,
407
+ };
408
+ }, [children, start, end, today, now]);
409
+
410
+ return (
411
+ <Panel>
412
+ {nonEmpty(title) ? (
413
+ <CaptionBar className={CAPTION_TITLE_CLS}>
414
+ <Icon className="h-3.5 w-3.5" name="lucide:chart-gantt" />
415
+ <span className="min-w-0 flex-1 truncate">{title}</span>
416
+ <span className={MONO_NUM_CLS}>
417
+ {format(scale.origin, "MMM d")} –{" "}
418
+ {format(scale.last, "MMM d, yyyy")}
419
+ </span>
420
+ </CaptionBar>
421
+ ) : null}
422
+ <div className="px-4 py-3">
423
+ <div className={`${ROW_GRID} pb-1`}>
424
+ <span />
425
+ <span className="relative h-3.5">
426
+ {scale.ticks.map((t) => (
427
+ <span
428
+ className={`absolute -translate-x-1/2 font-mono ${TEXT_MICRO} whitespace-nowrap tabular-nums ${TEXT.faint}`}
429
+ key={t.pct}
430
+ style={{ left: `${t.pct}%` }}
431
+ >
432
+ {t.label}
433
+ </span>
434
+ ))}
435
+ </span>
436
+ <span />
437
+ </div>
438
+ <ScaleCtx.Provider value={scale}>{children}</ScaleCtx.Provider>
439
+ </div>
440
+ </Panel>
441
+ );
442
+ }
443
+ );
@@ -0,0 +1,135 @@
1
+ import * as v from "valibot";
2
+
3
+ import { defineComponent } from "../define.js";
4
+ import { nonEmpty } from "../guards.js";
5
+ import { LINK_LINES_PROPS, numOf, NUMISH } from "./attrs.js";
6
+ import { CaptionBar, ListPanel, MaybeLink } from "./bits.js";
7
+ import { useFileLink } from "./file-link.js";
8
+ import { Icon } from "./icon.js";
9
+ import { CAPTION_TITLE_CLS, TEXT, TEXT_SUB, TRACK_CLS } from "./tones.js";
10
+
11
+ export const GAUGE_TONES = [
12
+ "emerald",
13
+ "amber",
14
+ "red",
15
+ "sky",
16
+ "violet",
17
+ "neutral",
18
+ ] as const;
19
+ export type GaugeTone = (typeof GAUGE_TONES)[number];
20
+
21
+ const FILL_CLS: Record<GaugeTone, string> = {
22
+ amber: "bg-amber-500",
23
+ emerald: "bg-emerald-500",
24
+ neutral: "bg-neutral-400 dark:bg-neutral-500",
25
+ red: "bg-red-500",
26
+ sky: "bg-sky-500",
27
+ violet: "bg-violet-500",
28
+ };
29
+
30
+ /** `pct`/`target` → pass/fail tone; without a target, graded 80/50 bands. */
31
+ const autoTone = (pct: number, target: number | undefined): GaugeTone => {
32
+ if (target !== undefined) {
33
+ return pct >= target ? "emerald" : "red";
34
+ }
35
+ if (pct >= 80) {
36
+ return "emerald";
37
+ }
38
+ if (pct >= 50) {
39
+ return "amber";
40
+ }
41
+ return "red";
42
+ };
43
+
44
+ export const Gauge = defineComponent(
45
+ {
46
+ description:
47
+ 'ラベル付きパーセントバー1行。value (0-100 または value/max) は必須。target で閾値判定 (未達は赤)、tone で色指定可。label/path で行ラベル、path は実ファイルへのエディタリンク、detail に補足 ("412/500 lines" など)。カバレッジ・スコア・進捗の定量表示向け',
48
+ schema: v.looseObject({
49
+ ...LINK_LINES_PROPS,
50
+ detail: v.optional(v.string()),
51
+ label: v.optional(v.string()),
52
+ max: v.optional(NUMISH),
53
+ path: v.optional(v.string()),
54
+ target: v.optional(NUMISH),
55
+ tone: v.optional(v.picklist(GAUGE_TONES)),
56
+ value: NUMISH,
57
+ }),
58
+ },
59
+ ({ value, max, target, tone, label, path, lines, href, detail }) => {
60
+ const vNum = numOf(value) ?? 0;
61
+ const mNum = numOf(max);
62
+ const tNum = numOf(target);
63
+ const pct =
64
+ mNum !== undefined && mNum > 0
65
+ ? Math.min(100, Math.max(0, (vNum / mNum) * 100))
66
+ : Math.min(100, Math.max(0, vNum));
67
+ const t = tone ?? autoTone(pct, tNum);
68
+ const shown =
69
+ mNum === undefined ? `${Math.round(pct * 10) / 10}%` : `${vNum}/${mNum}`;
70
+ const link = useFileLink(path, lines, href);
71
+ const name = nonEmpty(label) ? label : path;
72
+ return (
73
+ <div className="px-4 py-2.5">
74
+ <div className="flex items-center gap-3">
75
+ {name === undefined ? null : (
76
+ <MaybeLink
77
+ className={`w-44 shrink-0 truncate font-mono text-xs ${TEXT.code} no-underline hover:underline`}
78
+ href={link}
79
+ >
80
+ {name}
81
+ </MaybeLink>
82
+ )}
83
+ <div
84
+ className={`h-2 flex-1 overflow-hidden rounded-full ${TRACK_CLS}`}
85
+ >
86
+ <div
87
+ className={`h-full rounded-full ${FILL_CLS[t]}`}
88
+ style={{ width: `${pct}%` }}
89
+ />
90
+ </div>
91
+ <span
92
+ className={`shrink-0 font-mono text-xs tabular-nums ${TEXT.muted}`}
93
+ >
94
+ {shown}
95
+ {tNum === undefined ? null : (
96
+ <span className={TEXT.faint}> / {String(target)}</span>
97
+ )}
98
+ </span>
99
+ </div>
100
+ {nonEmpty(detail) ? (
101
+ <div className={`mt-1 ${TEXT_SUB} ${TEXT.faint}`}>{detail}</div>
102
+ ) : null}
103
+ </div>
104
+ );
105
+ }
106
+ );
107
+
108
+ export const Gauges = defineComponent(
109
+ {
110
+ description:
111
+ "パーセントバー一覧のコンテナ。<Gauge> を並べる。title はキャプション。カバレッジレポート・スコア一覧・進捗内訳向け",
112
+ schema: v.looseObject({
113
+ title: v.optional(v.string()),
114
+ unit: v.optional(v.string()),
115
+ }),
116
+ },
117
+ ({ title, unit, children }) => (
118
+ <ListPanel>
119
+ {nonEmpty(title) || nonEmpty(unit) ? (
120
+ <CaptionBar className={CAPTION_TITLE_CLS}>
121
+ <Icon className="h-3.5 w-3.5" name="lucide:gauge" />
122
+ {nonEmpty(title) ? title : "Gauges"}
123
+ {nonEmpty(unit) ? (
124
+ <span
125
+ className={`ml-auto font-mono ${TEXT_SUB} font-normal ${TEXT.faint}`}
126
+ >
127
+ {unit}
128
+ </span>
129
+ ) : null}
130
+ </CaptionBar>
131
+ ) : null}
132
+ {children}
133
+ </ListPanel>
134
+ )
135
+ );
@@ -0,0 +1,29 @@
1
+ import * as v from "valibot";
2
+
3
+ import { defineComponent } from "../define.js";
4
+ import { ListPanel } from "./bits.js";
5
+ import { TEXT, TRIM_CLS } from "./tones.js";
6
+
7
+ export const Glossary = defineComponent(
8
+ {
9
+ description: "用語集コンテナ。<Term> を並べる",
10
+ },
11
+ ({ children }) => <ListPanel as="dl">{children}</ListPanel>
12
+ );
13
+
14
+ export const Term = defineComponent(
15
+ {
16
+ description: "用語1項目。name が用語、children が定義文",
17
+ schema: v.looseObject({
18
+ name: v.string(),
19
+ }),
20
+ },
21
+ ({ name, children }) => (
22
+ <div className="px-4 py-2.5">
23
+ <dt className={`font-mono text-[0.85em] font-semibold ${TEXT.strong}`}>
24
+ {name}
25
+ </dt>
26
+ <dd className={`mt-0.5 text-sm ${TEXT.body} ${TRIM_CLS}`}>{children}</dd>
27
+ </div>
28
+ )
29
+ );