@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/gantt.tsx
ADDED
|
@@ -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
|
+
);
|