@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
package/src/ui/ask.tsx ADDED
@@ -0,0 +1,206 @@
1
+ import { isValidElement } from "react";
2
+ import type { ReactElement, ReactNode } from "react";
3
+ import * as v from "valibot";
4
+
5
+ import { formatAnswerSheet } from "../ask-sheet.js";
6
+ import type { SheetEntry } from "../ask-sheet.js";
7
+ import type { DocProps } from "../define.js";
8
+ import { defineComponent, flattenChildren, textOf } from "../define.js";
9
+ import { nonEmpty } from "../guards.js";
10
+ import { Choice, Question, QUESTION_TYPES } from "./ask-question.js";
11
+ import type { QuestionType } from "./ask-question.js";
12
+ import { attrTrue } from "./attrs.js";
13
+ import {
14
+ ACTION_BUTTON_CLS,
15
+ CAPTION_CLS,
16
+ CopyFeedback,
17
+ PANEL_CLS,
18
+ } from "./bits.js";
19
+ import { isEl, propOf } from "./children.js";
20
+ import { Icon } from "./icon.js";
21
+ import { BORDER_CLS, DIVIDE_CLS, SURFACE_CLS, TEXT } from "./tones.js";
22
+
23
+ export { Choice, Question, QUESTION_TYPES } from "./ask-question.js";
24
+ export type { QuestionType } from "./ask-question.js";
25
+
26
+ type QuestionEl = ReactElement<DocProps>;
27
+
28
+ // The initial sheet must match what client/doc-events.ts would compute —
29
+ // syncAsk rewrites [data-ask-output] before hydration, so SSR renders the
30
+ // seeded sheet rather than a placeholder; otherwise hydrateRoot would patch
31
+ // the pane back and revert the reader's default answers.
32
+
33
+ /** choiceText: a <Choice>'s visible label text, falling back to `value`. */
34
+ const choiceLabel = (c: QuestionEl): string => {
35
+ const text = textOf(propOf(c, "children")).replaceAll(/\s+/gu, " ").trim();
36
+ if (text !== "") {
37
+ return text;
38
+ }
39
+ const value = propOf(c, "value");
40
+ return typeof value === "string" ? value : "";
41
+ };
42
+
43
+ const choicesOf = (q: QuestionEl): QuestionEl[] =>
44
+ flattenChildren(q.props.children).filter((c): c is QuestionEl =>
45
+ isEl(c, Choice)
46
+ );
47
+
48
+ const checkedChoicesOf = (q: QuestionEl): QuestionEl[] =>
49
+ choicesOf(q).filter((c) => attrTrue(propOf(c, "checked")));
50
+
51
+ // Same defaulting as Question's render: explicit `type`, else `choice` when
52
+ // <Choice> children exist, else `text`.
53
+ const questionType = (q: QuestionEl): QuestionType =>
54
+ QUESTION_TYPES.find((t) => t === propOf(q, "type")) ??
55
+ (choicesOf(q).length > 0 ? "choice" : "text");
56
+
57
+ const selectAnswer = (q: QuestionEl): string => {
58
+ const [checked] = checkedChoicesOf(q);
59
+ // <select defaultValue> only lands when the checked choice's `value` is
60
+ // non-empty; otherwise the browser stays on the first option — the
61
+ // placeholder (value "", i.e. unanswered) when present, else the first
62
+ // choice. The client reads s.value, so a picked option with value=""
63
+ // still reports "".
64
+ const sel =
65
+ checked !== undefined && nonEmpty(propOf(checked, "value"))
66
+ ? checked
67
+ : undefined;
68
+ const picked =
69
+ sel ?? (nonEmpty(propOf(q, "placeholder")) ? undefined : choicesOf(q)[0]);
70
+ if (picked === undefined || !nonEmpty(propOf(picked, "value"))) {
71
+ return "";
72
+ }
73
+ return choiceLabel(picked);
74
+ };
75
+
76
+ // answerOf for the untouched default state: checked choices read as their
77
+ // visible text, a select as its first selected option, a toggle as yes/no,
78
+ // free-form fields as their `value`.
79
+ const defaultAnswer = (q: QuestionEl): string => {
80
+ const t = questionType(q);
81
+ if (t === "multi") {
82
+ return checkedChoicesOf(q)
83
+ .map(choiceLabel)
84
+ .filter((s) => s !== "")
85
+ .join(", ");
86
+ }
87
+ if (t === "choice") {
88
+ const [first] = checkedChoicesOf(q);
89
+ return first === undefined ? "" : choiceLabel(first);
90
+ }
91
+ if (t === "select") {
92
+ return selectAnswer(q);
93
+ }
94
+ if (t === "toggle") {
95
+ return attrTrue(propOf(q, "checked")) ? "yes" : "no";
96
+ }
97
+ const value = propOf(q, "value");
98
+ return typeof value === "string" ? value.trim() : "";
99
+ };
100
+
101
+ // Every <Question> under <Ask>, matching the client's
102
+ // querySelectorAll("[data-mdxr-q]") reach — descend through element children
103
+ // (a Question's own children are choices, never questions).
104
+ const collectQuestions = (node: ReactNode, out: QuestionEl[]): void => {
105
+ for (const c of flattenChildren(node)) {
106
+ if (isEl<DocProps>(c, Question)) {
107
+ out.push(c);
108
+ } else if (isValidElement<DocProps>(c)) {
109
+ collectQuestions(c.props.children, out);
110
+ }
111
+ }
112
+ };
113
+
114
+ // askMarkdown for the initial DOM — the client's formatter (ask-sheet.ts)
115
+ // applied to the untouched default answers.
116
+ const answerSheet = (title: unknown, children: ReactNode): string => {
117
+ const questions: QuestionEl[] = [];
118
+ collectQuestions(children, questions);
119
+ const entries: SheetEntry[] = [];
120
+ for (const q of questions) {
121
+ const rawLabel = nonEmpty(propOf(q, "label"))
122
+ ? propOf(q, "label")
123
+ : propOf(q, "name");
124
+ entries.push({
125
+ answer: defaultAnswer(q),
126
+ label: typeof rawLabel === "string" ? rawLabel : "",
127
+ });
128
+ }
129
+ return formatAnswerSheet(
130
+ typeof title === "string" ? title : undefined,
131
+ entries
132
+ );
133
+ };
134
+
135
+ /**
136
+ * A block of questions asking the reader for input — e.g. open decisions in
137
+ * a plan. Built on native form controls so it is interactive in static HTML.
138
+ * Answers are rendered live as a Markdown sheet (`- **label**: answer` lines
139
+ * under `# title`) in the output pane; "Copy answers" copies it to the
140
+ * clipboard and "Save .md" downloads it (nothing is submitted anywhere).
141
+ * `Choice`/`Question` and the per-type field chrome live in
142
+ * `ask-question.tsx`.
143
+ */
144
+ export const Ask = defineComponent(
145
+ {
146
+ description:
147
+ "ユーザーへの質問フォーム。<Question> を並べる。ネイティブコントロールで JS なしに操作可能。回答はラベル付きの Markdown として表示され、「Copy answers」でコピー、「Save .md」で保存できる",
148
+ schema: v.looseObject({
149
+ description: v.optional(v.string()),
150
+ title: v.optional(v.string()),
151
+ }),
152
+ },
153
+ ({ title, description, children }) => (
154
+ <section
155
+ className={`mdxr-ask ${PANEL_CLS}`}
156
+ data-ask
157
+ data-ask-title={nonEmpty(title) ? title : undefined}
158
+ >
159
+ <div className={CAPTION_CLS}>
160
+ <div className="flex items-center gap-1.5 font-semibold">
161
+ <Icon className="h-3.5 w-3.5" name="lucide:list-checks" />
162
+ {nonEmpty(title) ? title : "Questions"}
163
+ </div>
164
+ {nonEmpty(description) ? <p className="mt-1">{description}</p> : null}
165
+ </div>
166
+ <div className={DIVIDE_CLS}>{children}</div>
167
+ <div className={`border-t ${BORDER_CLS}`}>
168
+ <div
169
+ className={`flex items-center gap-1.5 px-4 pt-2.5 text-xs font-medium ${TEXT.faint}`}
170
+ >
171
+ <Icon className="h-3 w-3" name="lucide:file-text" />
172
+ Markdown
173
+ </div>
174
+ <pre
175
+ className={`m-0 max-h-64 overflow-auto px-4 pt-1 pb-3 font-mono text-xs leading-relaxed break-words whitespace-pre-wrap ${TEXT.body}`}
176
+ data-ask-output
177
+ >
178
+ {answerSheet(title, children)}
179
+ </pre>
180
+ </div>
181
+ <div
182
+ className={`flex items-center justify-between gap-3 border-t ${SURFACE_CLS} px-4 py-2 ${BORDER_CLS}`}
183
+ >
184
+ <span className={`text-xs ${TEXT.faint}`}>
185
+ Updates live as you answer — copy or save it.
186
+ </span>
187
+ <div className="flex shrink-0 items-center gap-2">
188
+ <button className={ACTION_BUTTON_CLS} data-ask-copy type="button">
189
+ <CopyFeedback
190
+ done="Copied"
191
+ icon="lucide:clipboard-list"
192
+ label="Copy answers"
193
+ />
194
+ </button>
195
+ <button className={ACTION_BUTTON_CLS} data-ask-save type="button">
196
+ <CopyFeedback
197
+ done="Saved"
198
+ icon="lucide:download"
199
+ label="Save .md"
200
+ />
201
+ </button>
202
+ </div>
203
+ </div>
204
+ </section>
205
+ )
206
+ );
@@ -0,0 +1,51 @@
1
+ import * as v from "valibot";
2
+
3
+ /**
4
+ * MDX attributes arrive as strings: `open` → `""`, `open="false"` →
5
+ * `"false"`. `attrTrue` treats presence/`"true"` as on; `attrFalse` treats
6
+ * only an explicit `false`/`"false"` as off (absent stays open).
7
+ */
8
+ export const attrTrue = (x: unknown): boolean =>
9
+ x === true || x === "" || x === "true";
10
+
11
+ export const attrFalse = (x: unknown): boolean => x === false || x === "false";
12
+
13
+ /** Numeric part of `"40"`, `"-15"`, `"40%"`, `"120ms"`, `1.2` — suffix text is dropped. */
14
+ export const numOf = (x: unknown): number | undefined => {
15
+ if (typeof x === "number") {
16
+ return Number.isFinite(x) ? x : undefined;
17
+ }
18
+ if (typeof x !== "string") {
19
+ return undefined;
20
+ }
21
+ const m = /^\s*(?<n>[+-]?\d+(?:\.\d+)?)/u.exec(x);
22
+ return m?.groups === undefined ? undefined : Number(m.groups.n);
23
+ };
24
+
25
+ /**
26
+ * Optional boolean-ish prop schema: accepts a JSX boolean or an MDX string
27
+ * attribute; read it back with `attrTrue`/`attrFalse`.
28
+ */
29
+ export const BOOLISH_PROP = v.optional(v.union([v.boolean(), v.string()]));
30
+
31
+ /**
32
+ * String-or-number prop atom: MDX attributes arrive as strings but JSX
33
+ * expressions like `duration={300}` produce real numbers — both are valid.
34
+ * Required sites use it bare (`id: NUMISH`); optional sites wrap it.
35
+ */
36
+ export const NUMISH = v.union([v.string(), v.number()]);
37
+
38
+ /** Optional `title` — the heading attribute most containers share. */
39
+ export const TITLE_PROP = { title: v.optional(v.string()) } as const;
40
+
41
+ /** `href` + `lines` — the link-override pair shared by file-referencing props. */
42
+ export const LINK_LINES_PROPS = {
43
+ href: v.optional(v.string()),
44
+ lines: v.optional(v.string()),
45
+ } as const;
46
+
47
+ /** Optional `href`/`lines`/`path` — the full file-link attribute triple. */
48
+ export const FILE_LINK_PROPS = {
49
+ ...LINK_LINES_PROPS,
50
+ path: v.optional(v.string()),
51
+ } as const;
@@ -0,0 +1,139 @@
1
+ import * as v from "valibot";
2
+
3
+ import { defineComponent } from "../define.js";
4
+ import { nonEmpty, safeHref } from "../guards.js";
5
+ import { attrTrue, BOOLISH_PROP } from "./attrs.js";
6
+ import {
7
+ CaptionBar,
8
+ ListPanel,
9
+ ListRow,
10
+ MaybeLink,
11
+ Pill,
12
+ RowNote,
13
+ } from "./bits.js";
14
+ import { countByProp } from "./children.js";
15
+ import { Icon } from "./icon.js";
16
+ import { isSeverity, SEVERITY_LEVELS, SEVERITY_STYLES } from "./severity.js";
17
+ import {
18
+ CAPTION_TITLE_CLS,
19
+ COUNT_CHIP_CLS,
20
+ LOC_CLS,
21
+ MONO_CLS,
22
+ TEXT,
23
+ TRIM_CLS,
24
+ } from "./tones.js";
25
+
26
+ export const Vuln = defineComponent(
27
+ {
28
+ description:
29
+ "脆弱性1件。severity は critical|high|medium|low|info、id に CVE/GHSA 番号、package に対象パッケージ、affected に影響範囲、fix に修正バージョン、href でアドバイザリリンク、wontfix で対応外マーク。children は説明文",
30
+ schema: v.looseObject({
31
+ affected: v.optional(v.string()),
32
+ fix: v.optional(v.string()),
33
+ href: v.optional(v.string()),
34
+ id: v.optional(v.string()),
35
+ package: v.optional(v.string()),
36
+ severity: v.optional(v.picklist(SEVERITY_LEVELS), "medium"),
37
+ title: v.optional(v.string()),
38
+ wontfix: BOOLISH_PROP,
39
+ }),
40
+ },
41
+ ({
42
+ severity,
43
+ id,
44
+ package: pkg,
45
+ affected,
46
+ fix,
47
+ href,
48
+ title,
49
+ wontfix,
50
+ children,
51
+ }) => {
52
+ const s = SEVERITY_STYLES[severity];
53
+ const link = safeHref(href);
54
+ return (
55
+ <ListRow>
56
+ <Pill className={s.chip} icon={s.icon}>
57
+ {s.label}
58
+ </Pill>
59
+ <span className="min-w-0">
60
+ {nonEmpty(title) ? (
61
+ <span className="text-sm font-medium">{title}</span>
62
+ ) : null}
63
+ {nonEmpty(id) ? (
64
+ <MaybeLink
65
+ className={`${LOC_CLS} no-underline hover:underline ${nonEmpty(title) ? "ml-2" : ""}`}
66
+ href={link}
67
+ >
68
+ {id}
69
+ </MaybeLink>
70
+ ) : null}
71
+ </span>
72
+ {nonEmpty(pkg) ? <code className={MONO_CLS}>{pkg}</code> : null}
73
+ {nonEmpty(affected) ? (
74
+ <code className={`${MONO_CLS} ${TEXT.muted}`}>{affected}</code>
75
+ ) : null}
76
+ {nonEmpty(fix) ? (
77
+ <span
78
+ className={`inline-flex shrink-0 items-center gap-1 font-mono text-xs ${TEXT.muted}`}
79
+ >
80
+ <Icon className="h-3 w-3" name="lucide:arrow-right" />
81
+ <span className="font-medium text-emerald-600 dark:text-emerald-400">
82
+ {fix}
83
+ </span>
84
+ </span>
85
+ ) : null}
86
+ {attrTrue(wontfix) ? (
87
+ <span className={`${COUNT_CHIP_CLS} uppercase`}>wontfix</span>
88
+ ) : null}
89
+ <RowNote className={TRIM_CLS}>{children}</RowNote>
90
+ </ListRow>
91
+ );
92
+ }
93
+ );
94
+
95
+ export const Audit = defineComponent(
96
+ {
97
+ description:
98
+ "脆弱性・監査結果のコンテナ。<Vuln> を並べる。title はキャプション、tool はスキャナ名チップ (npm audit, osv, trivy …)。severity 別の件数を自動集計",
99
+ schema: v.looseObject({
100
+ title: v.optional(v.string()),
101
+ tool: v.optional(v.string()),
102
+ }),
103
+ },
104
+ ({ title, tool, children }) => {
105
+ const counts = countByProp(
106
+ children,
107
+ Vuln,
108
+ "severity",
109
+ isSeverity,
110
+ "medium"
111
+ );
112
+ const total = [...counts.values()].reduce((a, b) => a + b, 0);
113
+ const caption = nonEmpty(title) || nonEmpty(tool) || total > 0;
114
+ return (
115
+ <ListPanel>
116
+ {caption ? (
117
+ <CaptionBar className={CAPTION_TITLE_CLS}>
118
+ <Icon className="h-3.5 w-3.5" name="lucide:shield-alert" />
119
+ {nonEmpty(title) ? title : "Audit"}
120
+ {nonEmpty(tool) ? (
121
+ <span className={COUNT_CHIP_CLS}>{tool}</span>
122
+ ) : null}
123
+ <span className="ml-auto flex items-center gap-x-2 font-medium">
124
+ {SEVERITY_LEVELS.map((k) => {
125
+ const c = counts.get(k);
126
+ return c === undefined ? null : (
127
+ <Pill className={SEVERITY_STYLES[k].chip} key={k}>
128
+ {c} {SEVERITY_STYLES[k].label.toLowerCase()}
129
+ </Pill>
130
+ );
131
+ })}
132
+ </span>
133
+ </CaptionBar>
134
+ ) : null}
135
+ {children}
136
+ </ListPanel>
137
+ );
138
+ }
139
+ );