@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/bits.tsx
ADDED
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import { nonEmpty } from "../guards.js";
|
|
4
|
+
import { indexChildren } from "./child-index.js";
|
|
5
|
+
import { linkTarget, useFileLink } from "./file-link.js";
|
|
6
|
+
import { Icon } from "./icon.js";
|
|
7
|
+
import {
|
|
8
|
+
BORDER_CLS,
|
|
9
|
+
CHIP_BORDER_CLS,
|
|
10
|
+
commentStripCls,
|
|
11
|
+
LOC_CLS,
|
|
12
|
+
SURFACE_CLS,
|
|
13
|
+
TEXT,
|
|
14
|
+
TEXT_MICRO,
|
|
15
|
+
TEXT_SUB,
|
|
16
|
+
TONE_TEXT,
|
|
17
|
+
TRIM_CLS,
|
|
18
|
+
} from "./tones.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Bordered panel shape framing a document block (code, diffs, graphs,
|
|
22
|
+
* tables). Exported for panels needing a non-figure tag (`<nav>`, `<section>`)
|
|
23
|
+
* or extra attributes; plain panels should use `<Panel>`.
|
|
24
|
+
*/
|
|
25
|
+
export const PANEL_CLS = `not-prose my-6 overflow-hidden rounded-lg border ${BORDER_CLS}`;
|
|
26
|
+
|
|
27
|
+
export const Panel = (props: {
|
|
28
|
+
children?: ReactNode;
|
|
29
|
+
/** Extra classes (e.g. `mdxr-json`) merged after the panel shape. */
|
|
30
|
+
className?: string;
|
|
31
|
+
}): ReactElement => (
|
|
32
|
+
<figure className={`${PANEL_CLS} ${props.className ?? ""}`}>
|
|
33
|
+
{props.children}
|
|
34
|
+
</figure>
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// Lookup maps (not `items-${x}` interpolation) so Tailwind's static class
|
|
38
|
+
// scan sees the literals.
|
|
39
|
+
const ROW_ALIGN = {
|
|
40
|
+
baseline: "items-baseline",
|
|
41
|
+
center: "items-center",
|
|
42
|
+
} as const;
|
|
43
|
+
const ROW_GAP_X = {
|
|
44
|
+
"2": "gap-x-2",
|
|
45
|
+
"2.5": "gap-x-2.5",
|
|
46
|
+
"3": "gap-x-3",
|
|
47
|
+
} as const;
|
|
48
|
+
|
|
49
|
+
/** One padded row inside a `ListPanel`. */
|
|
50
|
+
export const ListRow = (props: {
|
|
51
|
+
/** `items-*` — default `items-baseline`. */
|
|
52
|
+
align?: keyof typeof ROW_ALIGN;
|
|
53
|
+
children?: ReactNode;
|
|
54
|
+
/** Non-conflicting extras, e.g. `opacity-60`. */
|
|
55
|
+
className?: string;
|
|
56
|
+
/** `gap-x-*` — default `gap-x-3`. */
|
|
57
|
+
gapX?: keyof typeof ROW_GAP_X;
|
|
58
|
+
}): ReactElement => (
|
|
59
|
+
<div
|
|
60
|
+
className={`flex flex-wrap ${ROW_ALIGN[props.align ?? "baseline"]} ${ROW_GAP_X[props.gapX ?? "3"]} gap-y-1 px-4 py-2.5 ${props.className ?? ""}`}
|
|
61
|
+
>
|
|
62
|
+
{props.children}
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Click-to-copy button. The `mdxr-copy`/`mdxr-copy-idle`/`mdxr-copy-done`
|
|
68
|
+
* classes are client-JS hooks (assets.ts toggles `.copied` on the button) —
|
|
69
|
+
* rename them and copy stops working.
|
|
70
|
+
*/
|
|
71
|
+
export const CopyButton = (props: {
|
|
72
|
+
/** Extra button classes (default `opacity-60`). */
|
|
73
|
+
className?: string;
|
|
74
|
+
/** Clipboard payload (`data-copy`). */
|
|
75
|
+
copy: string;
|
|
76
|
+
/** Done-state color classes (default light+dark emerald). */
|
|
77
|
+
doneClassName?: string;
|
|
78
|
+
/** Tooltip + accessible label. */
|
|
79
|
+
title: string;
|
|
80
|
+
}): ReactElement => (
|
|
81
|
+
<button
|
|
82
|
+
aria-label={props.title}
|
|
83
|
+
className={`mdxr-copy cursor-pointer ${props.className ?? "opacity-60"}`}
|
|
84
|
+
data-copy={props.copy}
|
|
85
|
+
title={props.title}
|
|
86
|
+
type="button"
|
|
87
|
+
>
|
|
88
|
+
<span className="mdxr-copy-idle inline-flex">
|
|
89
|
+
<Icon className="h-3.5 w-3.5" name="lucide:copy" />
|
|
90
|
+
</span>
|
|
91
|
+
<span
|
|
92
|
+
className={`mdxr-copy-done hidden items-center ${props.doneClassName ?? TONE_TEXT.emerald}`}
|
|
93
|
+
>
|
|
94
|
+
<Icon className="h-3.5 w-3.5" name="lucide:check" />
|
|
95
|
+
</span>
|
|
96
|
+
</button>
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Bordered secondary-button shape for labeled copy/save actions (Ask,
|
|
101
|
+
* Board). `transition-colors` only — `transition-all` would animate layout
|
|
102
|
+
* properties too.
|
|
103
|
+
*/
|
|
104
|
+
export const ACTION_BUTTON_CLS = `inline-flex cursor-pointer items-center rounded-md border ${CHIP_BORDER_CLS} bg-white px-2.5 py-1 text-xs font-medium ${TEXT.chip} transition-colors hover:bg-neutral-100 active:translate-y-px dark:bg-neutral-800 dark:hover:bg-neutral-700`;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Idle/done label pair inside a labeled action button. The
|
|
108
|
+
* `mdxr-copy-idle`/`mdxr-copy-done` classes are client-JS hooks — the
|
|
109
|
+
* document script toggles `.copied` on the parent button to swap them.
|
|
110
|
+
*/
|
|
111
|
+
export const CopyFeedback = (props: {
|
|
112
|
+
/** Done-state text (e.g. `Copied`, `Saved`). */
|
|
113
|
+
done: string;
|
|
114
|
+
/** Idle-state icon, e.g. `lucide:clipboard-list`. */
|
|
115
|
+
icon: string;
|
|
116
|
+
/** Idle-state text (e.g. `Copy answers`). */
|
|
117
|
+
label: string;
|
|
118
|
+
}): ReactElement => (
|
|
119
|
+
<>
|
|
120
|
+
<span className="mdxr-copy-idle inline-flex items-center gap-1.5">
|
|
121
|
+
<Icon className="h-3.5 w-3.5" name={props.icon} />
|
|
122
|
+
{props.label}
|
|
123
|
+
</span>
|
|
124
|
+
<span
|
|
125
|
+
className={`mdxr-copy-done hidden items-center gap-1.5 ${TONE_TEXT.emerald}`}
|
|
126
|
+
>
|
|
127
|
+
<Icon className="h-3.5 w-3.5" name="lucide:check" />
|
|
128
|
+
{props.done}
|
|
129
|
+
</span>
|
|
130
|
+
</>
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
/** Round status/kind chip — icon + label on a `TONE` background. */
|
|
134
|
+
export const Pill = (props: {
|
|
135
|
+
children?: ReactNode;
|
|
136
|
+
/** Tone/color classes appended after the pill shape. */
|
|
137
|
+
className?: string;
|
|
138
|
+
/** Iconify name, e.g. `lucide:check`. */
|
|
139
|
+
icon?: string;
|
|
140
|
+
}): ReactElement => (
|
|
141
|
+
<span
|
|
142
|
+
className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium ${props.className ?? ""}`}
|
|
143
|
+
>
|
|
144
|
+
{props.icon === undefined ? null : (
|
|
145
|
+
<Icon className="h-3 w-3" name={props.icon} />
|
|
146
|
+
)}
|
|
147
|
+
{props.children}
|
|
148
|
+
</span>
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const CAPTION_BASE = `${SURFACE_CLS} px-4 py-2 text-xs ${TEXT.muted}`;
|
|
152
|
+
const CAPTION_BORDER = `border-b ${BORDER_CLS}`;
|
|
153
|
+
|
|
154
|
+
/** CaptionBar's strip classes, for headers that are not a `<figcaption>`
|
|
155
|
+
* (Ask's section header). */
|
|
156
|
+
export const CAPTION_CLS = `${CAPTION_BORDER} ${CAPTION_BASE}`;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Header strip on framed blocks (code, diffs, tables). `border` (default on)
|
|
160
|
+
* draws the divider to the body; `className` adds layout like
|
|
161
|
+
* `flex items-center gap-2` or weight like `font-medium`/`font-mono`.
|
|
162
|
+
*/
|
|
163
|
+
export const CaptionBar = (props: {
|
|
164
|
+
border?: boolean;
|
|
165
|
+
children?: ReactNode;
|
|
166
|
+
className?: string;
|
|
167
|
+
}): ReactElement => (
|
|
168
|
+
<figcaption
|
|
169
|
+
className={`${props.border === false ? "" : `${CAPTION_BORDER} `}${CAPTION_BASE} ${props.className ?? ""}`}
|
|
170
|
+
>
|
|
171
|
+
{props.children}
|
|
172
|
+
</figcaption>
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
/** PANEL_CLS + row dividers — the bordered container for row-style lists. */
|
|
176
|
+
const LIST_CLS = `divide-y divide-neutral-200 dark:divide-neutral-800 ${PANEL_CLS}`;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Bordered `divide-y` container for row-style lists (Deps, Files, Tests…).
|
|
180
|
+
* `as` picks the semantic tag; figures are the default because captioned
|
|
181
|
+
* lists pair this with `CaptionBar` (a figcaption). `title` renders that
|
|
182
|
+
* caption — only sensible on the default figure.
|
|
183
|
+
*/
|
|
184
|
+
export const ListPanel = (props: {
|
|
185
|
+
as?: "div" | "dl" | "figure" | "section";
|
|
186
|
+
children?: ReactNode;
|
|
187
|
+
className?: string;
|
|
188
|
+
/** Plain-text list title rendered as a `CaptionBar`. */
|
|
189
|
+
title?: string;
|
|
190
|
+
}): ReactElement => {
|
|
191
|
+
const Tag = props.as ?? "figure";
|
|
192
|
+
return (
|
|
193
|
+
<Tag className={`${LIST_CLS} ${props.className ?? ""}`}>
|
|
194
|
+
{nonEmpty(props.title) ? (
|
|
195
|
+
<CaptionBar border={false} className="font-medium">
|
|
196
|
+
{props.title}
|
|
197
|
+
</CaptionBar>
|
|
198
|
+
) : null}
|
|
199
|
+
{props.children}
|
|
200
|
+
</Tag>
|
|
201
|
+
);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Truncated name + optional muted sub-line — the label cell of chart rows
|
|
206
|
+
* (Waterfall spans, Gantt tasks/milestones).
|
|
207
|
+
*/
|
|
208
|
+
export const RowLabel = (props: {
|
|
209
|
+
name: ReactNode;
|
|
210
|
+
/** Faint second line (`owner · note`); empty/absent hides it. */
|
|
211
|
+
sub?: string;
|
|
212
|
+
}): ReactElement => (
|
|
213
|
+
<div className="min-w-0">
|
|
214
|
+
<div className="truncate text-sm">{props.name}</div>
|
|
215
|
+
{nonEmpty(props.sub) ? (
|
|
216
|
+
<div className={`truncate ${TEXT_SUB} ${TEXT.faint}`}>{props.sub}</div>
|
|
217
|
+
) : null}
|
|
218
|
+
</div>
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Rotating disclosure chevron for tree-line `<details>` (Tree, Json).
|
|
223
|
+
* `mdxr-chev` is the CSS hook that spins it open — keep the name.
|
|
224
|
+
*/
|
|
225
|
+
export const FoldChev = (props: {
|
|
226
|
+
/** Non-conflicting extras, e.g. group-hover colors. */
|
|
227
|
+
className?: string;
|
|
228
|
+
}): ReactElement => (
|
|
229
|
+
<Icon
|
|
230
|
+
className={`mdxr-chev h-3 w-3 shrink-0 self-center ${TEXT.faint} ${props.className ?? ""}`}
|
|
231
|
+
name="lucide:chevron-right"
|
|
232
|
+
/>
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
/** Muted icon centered on a list row (Files/Deps/Changes/Search). */
|
|
236
|
+
export const RowIcon = (props: {
|
|
237
|
+
/** Non-conflicting extras. */
|
|
238
|
+
className?: string;
|
|
239
|
+
/** Iconify name, e.g. `lucide:search`. */
|
|
240
|
+
name: string;
|
|
241
|
+
}): ReactElement => (
|
|
242
|
+
<Icon
|
|
243
|
+
className={`h-3.5 w-3.5 shrink-0 self-center ${TEXT.faint} ${props.className ?? ""}`}
|
|
244
|
+
name={props.name}
|
|
245
|
+
/>
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Inline kind/status tag — icon + label colored via `className`
|
|
250
|
+
* (e.g. `text-emerald-600 dark:text-emerald-400`); no background, unlike `Pill`.
|
|
251
|
+
*/
|
|
252
|
+
export const Tag = (props: {
|
|
253
|
+
children?: ReactNode;
|
|
254
|
+
/** Tone/extra classes appended after the tag shape. */
|
|
255
|
+
className?: string;
|
|
256
|
+
/** Iconify name. */
|
|
257
|
+
icon: string;
|
|
258
|
+
}): ReactElement => (
|
|
259
|
+
<span
|
|
260
|
+
className={`inline-flex shrink-0 items-center gap-1 text-xs font-medium ${props.className ?? ""}`}
|
|
261
|
+
>
|
|
262
|
+
<Icon className="h-3.5 w-3.5" name={props.icon} />
|
|
263
|
+
{props.children}
|
|
264
|
+
</span>
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
/** Top-level titled section — the `my-6` wrapper with an optional h3 heading. */
|
|
268
|
+
export const Section = (props: {
|
|
269
|
+
children?: ReactNode;
|
|
270
|
+
/** Heading text; empty/absent hides the h3. */
|
|
271
|
+
title?: string;
|
|
272
|
+
}): ReactElement => (
|
|
273
|
+
<section className="my-6">
|
|
274
|
+
{nonEmpty(props.title) ? <h3 className="mt-0">{props.title}</h3> : null}
|
|
275
|
+
{props.children}
|
|
276
|
+
</section>
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
/** Small numbered square used by list items indexed via `useChildIndex`. */
|
|
280
|
+
export const NumBadge = (props: { n: number }): ReactElement => (
|
|
281
|
+
<span
|
|
282
|
+
className={`flex h-5 w-5 items-center justify-center rounded-md border ${CHIP_BORDER_CLS} font-mono ${TEXT_MICRO} font-medium ${TEXT.faint}`}
|
|
283
|
+
>
|
|
284
|
+
{props.n}
|
|
285
|
+
</span>
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Optional MDX body region — renders nothing without children and trims the
|
|
290
|
+
* outer margins of the first/last child (TRIM_CLS). `className` carries the
|
|
291
|
+
* spacing/text look, e.g. `mt-1.5 text-sm`.
|
|
292
|
+
*/
|
|
293
|
+
export const TrimBody = (props: {
|
|
294
|
+
children?: ReactNode;
|
|
295
|
+
className?: string;
|
|
296
|
+
}): ReactElement | null =>
|
|
297
|
+
props.children === undefined ? null : (
|
|
298
|
+
<div className={`${TRIM_CLS} ${props.className ?? ""}`}>
|
|
299
|
+
{props.children}
|
|
300
|
+
</div>
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Bordered card for counted list items (Finding, Hypothesis): index badge +
|
|
305
|
+
* a pill slot + optional title + prose children.
|
|
306
|
+
*/
|
|
307
|
+
export const IndexedCard = (props: {
|
|
308
|
+
/** Right-aligned extra in the header row (e.g. a file LocLink). */
|
|
309
|
+
aside?: ReactNode;
|
|
310
|
+
children?: ReactNode;
|
|
311
|
+
/** 1-based index from `useChildIndex`; 0 hides the badge. */
|
|
312
|
+
n: number;
|
|
313
|
+
/** Status pill (usually `<Pill>`). */
|
|
314
|
+
pill: ReactNode;
|
|
315
|
+
title?: string;
|
|
316
|
+
}): ReactElement => (
|
|
317
|
+
<article className={`rounded-lg border px-4 py-3 ${BORDER_CLS}`}>
|
|
318
|
+
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
|
|
319
|
+
{props.n > 0 ? <NumBadge n={props.n} /> : null}
|
|
320
|
+
{props.pill}
|
|
321
|
+
{nonEmpty(props.title) ? (
|
|
322
|
+
<span className="text-sm font-medium">{props.title}</span>
|
|
323
|
+
) : null}
|
|
324
|
+
{props.aside === undefined ? null : (
|
|
325
|
+
<span className="ml-auto">{props.aside}</span>
|
|
326
|
+
)}
|
|
327
|
+
</div>
|
|
328
|
+
<TrimBody className="mt-1.5 text-sm">{props.children}</TrimBody>
|
|
329
|
+
</article>
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Section wrapper for counted item lists (Findings, Hypotheses): optional
|
|
334
|
+
* heading, a `N items · n status` summary line, then indexed children.
|
|
335
|
+
*/
|
|
336
|
+
export const CountedList = (props: {
|
|
337
|
+
children?: ReactNode;
|
|
338
|
+
/** prop value → count, e.g. `{confirmed: 2, inferred: 1}`. */
|
|
339
|
+
counts: ReadonlyMap<string, number>;
|
|
340
|
+
/** Display label for a count key (rendered lowercased by callers). */
|
|
341
|
+
labelOf: (key: string) => string;
|
|
342
|
+
/** `[singular, plural]` noun for the total line. */
|
|
343
|
+
noun: readonly [string, string];
|
|
344
|
+
/** Fixed display order for the per-key counts. */
|
|
345
|
+
order: readonly string[];
|
|
346
|
+
title?: string;
|
|
347
|
+
}): ReactElement => {
|
|
348
|
+
const total = [...props.counts.values()].reduce((a, b) => a + b, 0);
|
|
349
|
+
return (
|
|
350
|
+
<Section title={props.title}>
|
|
351
|
+
{total > 0 ? (
|
|
352
|
+
<div
|
|
353
|
+
className={`mb-2 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs ${TEXT.muted}`}
|
|
354
|
+
>
|
|
355
|
+
<span>
|
|
356
|
+
{total} {props.noun[total === 1 ? 0 : 1]}
|
|
357
|
+
</span>
|
|
358
|
+
{props.order.map((k) => {
|
|
359
|
+
const count = props.counts.get(k);
|
|
360
|
+
return count === undefined ? null : (
|
|
361
|
+
<span key={k}>
|
|
362
|
+
· {count} {props.labelOf(k)}
|
|
363
|
+
</span>
|
|
364
|
+
);
|
|
365
|
+
})}
|
|
366
|
+
</div>
|
|
367
|
+
) : null}
|
|
368
|
+
<div className="not-prose space-y-3">{indexChildren(props.children)}</div>
|
|
369
|
+
</Section>
|
|
370
|
+
);
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Hover "+" on a code/diff row (`<Comments>`) — opens the comment form
|
|
375
|
+
* under that line. `data-comment-add` carries the line number, `data-side`/
|
|
376
|
+
* `data-file` the diff anchor; `.mdxr-add` positions and reveals the
|
|
377
|
+
* button (BASE_CSS). All are client-JS hooks — rename them and adding
|
|
378
|
+
* comments stops working.
|
|
379
|
+
*/
|
|
380
|
+
export const AddCommentButton = (props: {
|
|
381
|
+
file?: string;
|
|
382
|
+
line: number;
|
|
383
|
+
side?: "new" | "old";
|
|
384
|
+
}): ReactElement => (
|
|
385
|
+
<button
|
|
386
|
+
aria-label={`Add a comment on line ${props.line}`}
|
|
387
|
+
className="mdxr-add"
|
|
388
|
+
data-comment-add={props.line}
|
|
389
|
+
data-file={props.file}
|
|
390
|
+
data-side={props.side === "old" ? "old" : undefined}
|
|
391
|
+
title="Add a comment"
|
|
392
|
+
type="button"
|
|
393
|
+
>
|
|
394
|
+
<Icon className="h-3 w-3" name="lucide:plus" />
|
|
395
|
+
</button>
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Reply affordance at the end of a comment thread. `data-comment-reply`
|
|
400
|
+
* is a client-JS hook (doc-events.ts swaps it for the comment form) —
|
|
401
|
+
* rename it and replies stop opening.
|
|
402
|
+
*/
|
|
403
|
+
export const CommentReplyButton = (): ReactElement => (
|
|
404
|
+
<button
|
|
405
|
+
className={`inline-flex cursor-pointer items-center gap-1 rounded-md border ${CHIP_BORDER_CLS} bg-white px-2 py-0.5 text-xs font-medium ${TEXT.muted} transition-colors hover:bg-neutral-100 active:translate-y-px dark:bg-neutral-900 dark:hover:bg-neutral-800`}
|
|
406
|
+
data-comment-reply=""
|
|
407
|
+
type="button"
|
|
408
|
+
>
|
|
409
|
+
<Icon className="h-3 w-3" name="lucide:reply" />
|
|
410
|
+
Reply
|
|
411
|
+
</button>
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Full-width strip inserted between code/diff rows to hold a comment
|
|
416
|
+
* thread (`<Comments>`) — GitHub's inline review-thread row: white
|
|
417
|
+
* background, edge-to-edge top border. `bleed` stretches the strip across
|
|
418
|
+
* a parent's `px-4` padding (annotated code blocks sit inside one).
|
|
419
|
+
* `anchor` lands on `data-strip-*`: replies posted into the strip inherit
|
|
420
|
+
* it and the markdown serializer emits it on the new `<Comment>`.
|
|
421
|
+
* `data-comment-strip`/`data-thread-tools` are client-JS hooks.
|
|
422
|
+
*/
|
|
423
|
+
export const CommentStrip = (props: {
|
|
424
|
+
/** Anchor replies inherit (file/lines/side). */
|
|
425
|
+
anchor?: { file?: string; lines?: string; side?: "new" | "old" };
|
|
426
|
+
bleed?: boolean;
|
|
427
|
+
children?: ReactNode;
|
|
428
|
+
}): ReactElement => (
|
|
429
|
+
<div
|
|
430
|
+
className={commentStripCls(props.bleed)}
|
|
431
|
+
data-comment-strip=""
|
|
432
|
+
data-strip-file={props.anchor?.file}
|
|
433
|
+
data-strip-lines={props.anchor?.lines}
|
|
434
|
+
data-strip-side={props.anchor?.side === "old" ? "old" : undefined}
|
|
435
|
+
>
|
|
436
|
+
{props.children}
|
|
437
|
+
<div className="mdxr-thread-tools" data-thread-tools="">
|
|
438
|
+
<CommentReplyButton />
|
|
439
|
+
</div>
|
|
440
|
+
</div>
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* File/editor link that may not resolve: renders an `<a>` when `href` is
|
|
445
|
+
* set (with `linkTarget`), else a `<span>` — identical classes either way.
|
|
446
|
+
* Link-only utilities (`hover:underline`, `text-inherit`) are inert on the
|
|
447
|
+
* span, so a single `className` covers both branches.
|
|
448
|
+
*/
|
|
449
|
+
export const MaybeLink = (props: {
|
|
450
|
+
children?: ReactNode;
|
|
451
|
+
className?: string;
|
|
452
|
+
href?: string;
|
|
453
|
+
}): ReactElement =>
|
|
454
|
+
props.href === undefined ? (
|
|
455
|
+
<span className={props.className}>{props.children}</span>
|
|
456
|
+
) : (
|
|
457
|
+
<a
|
|
458
|
+
className={props.className}
|
|
459
|
+
href={props.href}
|
|
460
|
+
{...linkTarget(props.href)}
|
|
461
|
+
>
|
|
462
|
+
{props.children}
|
|
463
|
+
</a>
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Trailing note slot inside a `ListRow` — the muted free-text annotation at
|
|
468
|
+
* the end of a row (Files/Deps/Changes/…). Renders nothing without children.
|
|
469
|
+
*/
|
|
470
|
+
export const RowNote = (props: {
|
|
471
|
+
children?: ReactNode;
|
|
472
|
+
/** Non-conflicting extras, e.g. margin-trimming `[&>*]:…` selectors. */
|
|
473
|
+
className?: string;
|
|
474
|
+
}): ReactElement | null =>
|
|
475
|
+
props.children === undefined ? null : (
|
|
476
|
+
<span
|
|
477
|
+
className={`min-w-0 flex-1 text-sm ${TEXT.muted} ${props.className ?? ""}`}
|
|
478
|
+
>
|
|
479
|
+
{props.children}
|
|
480
|
+
</span>
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Bordered inline chip shell shared by the reference components —
|
|
485
|
+
* FileRef/SymbolRef/Cmd (via CodeChip), Ref's link chip, Graph's standalone
|
|
486
|
+
* node chip. `font-mono` stays with the callers that want mono content.
|
|
487
|
+
*/
|
|
488
|
+
export const CODE_CHIP_CLS = `not-prose mx-0.5 inline-flex items-center gap-1.5 rounded-md border ${CHIP_BORDER_CLS} bg-neutral-100 px-1.5 py-0.5 align-baseline text-[0.85em] ${TEXT.code} dark:bg-neutral-800`;
|
|
489
|
+
|
|
490
|
+
/** Bordered inline code chip — the `<code>` shell used by FileRef/SymbolRef/Cmd. */
|
|
491
|
+
export const CodeChip = (props: { children?: ReactNode }): ReactElement => (
|
|
492
|
+
<code className={`font-mono ${CODE_CHIP_CLS}`}>{props.children}</code>
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
/** `path` plus its `:lines` suffix — the file-location label. */
|
|
496
|
+
export const PathLabel = (props: {
|
|
497
|
+
lines?: string;
|
|
498
|
+
/** Classes for the `:{lines}` suffix (e.g. `opacity-60`); plain text otherwise. */
|
|
499
|
+
linesClassName?: string;
|
|
500
|
+
path: string;
|
|
501
|
+
}): ReactElement => {
|
|
502
|
+
const suffix = nonEmpty(props.lines) ? `:${props.lines}` : "";
|
|
503
|
+
return (
|
|
504
|
+
<>
|
|
505
|
+
{props.path}
|
|
506
|
+
{props.linesClassName === undefined || suffix === "" ? (
|
|
507
|
+
suffix
|
|
508
|
+
) : (
|
|
509
|
+
<span className={props.linesClassName}>{suffix}</span>
|
|
510
|
+
)}
|
|
511
|
+
</>
|
|
512
|
+
);
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* File-location link: `path:lines` in muted mono, linked to the editor when
|
|
517
|
+
* the path resolves (`useFileLink`). The standard companion of `FlowStep`,
|
|
518
|
+
* `TraceFrame`, `Test` rows.
|
|
519
|
+
*/
|
|
520
|
+
export const LocLink = (props: {
|
|
521
|
+
/** Overrides the muted-mono look. */
|
|
522
|
+
className?: string;
|
|
523
|
+
/** Explicit link override (the `href` catalog prop). */
|
|
524
|
+
href?: string;
|
|
525
|
+
lines?: string;
|
|
526
|
+
path: string;
|
|
527
|
+
}): ReactElement => {
|
|
528
|
+
const link = useFileLink(props.path, props.lines, props.href);
|
|
529
|
+
return (
|
|
530
|
+
<MaybeLink
|
|
531
|
+
className={props.className ?? `${LOC_CLS} no-underline hover:underline`}
|
|
532
|
+
href={link}
|
|
533
|
+
>
|
|
534
|
+
<PathLabel lines={props.lines} path={props.path} />
|
|
535
|
+
</MaybeLink>
|
|
536
|
+
);
|
|
537
|
+
};
|
package/src/ui/board.tsx
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import * as v from "valibot";
|
|
3
|
+
|
|
4
|
+
import { defineComponent, flattenChildren, textOf } from "../define.js";
|
|
5
|
+
import { nonEmpty } from "../guards.js";
|
|
6
|
+
import { TITLE_PROP } from "./attrs.js";
|
|
7
|
+
import { ACTION_BUTTON_CLS, CopyFeedback, Section, TrimBody } from "./bits.js";
|
|
8
|
+
import { isEl } from "./children.js";
|
|
9
|
+
import { CHIP_PROPS, ChipRow } from "./chips.js";
|
|
10
|
+
import { Icon } from "./icon.js";
|
|
11
|
+
import { STATUS_ICON_CLS, STATUS_ICONS, STATUS_PROP } from "./status-badge.js";
|
|
12
|
+
import type { Status } from "./status-badge.js";
|
|
13
|
+
import { BORDER_CLS, COUNT_CHIP_CLS, TEXT } from "./tones.js";
|
|
14
|
+
|
|
15
|
+
const DOT: Record<Status, string> = {
|
|
16
|
+
blocked: "bg-red-500",
|
|
17
|
+
doing: "bg-sky-500",
|
|
18
|
+
done: "bg-emerald-500",
|
|
19
|
+
todo: "bg-neutral-400",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Lane-to-lane move buttons — the keyboard/touch alternative to drag & drop
|
|
24
|
+
* (HTML5 DnD never reaches touch browsers). `data-board-move` is a client-JS
|
|
25
|
+
* hook: doc-events.ts moves the card ±1 lane on click and disables the
|
|
26
|
+
* button at the board's edges via `disabled`.
|
|
27
|
+
*/
|
|
28
|
+
const MoveButton = (props: {
|
|
29
|
+
/** `"-1"` previous lane, `"1"` next lane. */
|
|
30
|
+
dir: "-1" | "1";
|
|
31
|
+
}): ReactElement => (
|
|
32
|
+
<button
|
|
33
|
+
aria-label={
|
|
34
|
+
props.dir === "-1" ? "Move to previous lane" : "Move to next lane"
|
|
35
|
+
}
|
|
36
|
+
className="mdxr-move"
|
|
37
|
+
data-board-move={props.dir}
|
|
38
|
+
title={props.dir === "-1" ? "Move left" : "Move right"}
|
|
39
|
+
type="button"
|
|
40
|
+
>
|
|
41
|
+
<Icon
|
|
42
|
+
className="h-3.5 w-3.5"
|
|
43
|
+
name={props.dir === "-1" ? "lucide:chevron-left" : "lucide:chevron-right"}
|
|
44
|
+
/>
|
|
45
|
+
</button>
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
export const BoardCard = defineComponent(
|
|
49
|
+
{
|
|
50
|
+
description:
|
|
51
|
+
"カンバンのカード。title は必須。priority/effort/owner/due で既存チップを並べられる。children は補足テキスト。ドラッグまたは両端の矢印ボタンでレーン間を移動できる",
|
|
52
|
+
schema: v.looseObject({
|
|
53
|
+
...CHIP_PROPS,
|
|
54
|
+
status: STATUS_PROP,
|
|
55
|
+
title: v.string(),
|
|
56
|
+
}),
|
|
57
|
+
},
|
|
58
|
+
({ title, priority, effort, owner, due, status, children }) => {
|
|
59
|
+
// Plain-text payload for the markdown serializer — element children
|
|
60
|
+
// (links, emphasis) flatten to their visible text.
|
|
61
|
+
const text = children === undefined ? "" : textOf(children).trim();
|
|
62
|
+
return (
|
|
63
|
+
<div
|
|
64
|
+
className={`rounded-md border bg-white p-2.5 shadow-sm dark:bg-neutral-950 ${BORDER_CLS}`}
|
|
65
|
+
data-board-card=""
|
|
66
|
+
data-card-due={due}
|
|
67
|
+
data-card-effort={effort}
|
|
68
|
+
data-card-owner={owner}
|
|
69
|
+
data-card-priority={priority}
|
|
70
|
+
data-card-status={status}
|
|
71
|
+
data-card-text={nonEmpty(text) ? text : undefined}
|
|
72
|
+
data-card-title={title}
|
|
73
|
+
draggable
|
|
74
|
+
>
|
|
75
|
+
<div className="flex items-start gap-1.5">
|
|
76
|
+
<Icon
|
|
77
|
+
className={`mdxr-grip mt-0.5 h-3.5 w-3.5 shrink-0 ${TEXT.ghost}`}
|
|
78
|
+
name="lucide:grip-vertical"
|
|
79
|
+
/>
|
|
80
|
+
{status === undefined ? null : (
|
|
81
|
+
<Icon
|
|
82
|
+
className={`mt-0.5 h-3.5 w-3.5 shrink-0 ${STATUS_ICON_CLS[status]}`}
|
|
83
|
+
label={status}
|
|
84
|
+
name={STATUS_ICONS[status]}
|
|
85
|
+
/>
|
|
86
|
+
)}
|
|
87
|
+
<div className="min-w-0 flex-1 text-sm leading-snug font-medium">
|
|
88
|
+
{title}
|
|
89
|
+
</div>
|
|
90
|
+
<span className="mdxr-card-moves -mt-0.5 -mr-1 flex shrink-0 items-center">
|
|
91
|
+
<MoveButton dir="-1" />
|
|
92
|
+
<MoveButton dir="1" />
|
|
93
|
+
</span>
|
|
94
|
+
</div>
|
|
95
|
+
<TrimBody className={`mt-1 text-xs ${TEXT.muted}`}>{children}</TrimBody>
|
|
96
|
+
<ChipRow due={due} effort={effort} owner={owner} priority={priority} />
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
export const Lane = defineComponent(
|
|
103
|
+
{
|
|
104
|
+
description:
|
|
105
|
+
"カンバンの列。<Board> の子として使う。title は列名、status でヘッダの色点 (todo|doing|done|blocked)。子の <BoardCard> 数がバッジになる。空の列もドロップ先になる",
|
|
106
|
+
schema: v.looseObject({
|
|
107
|
+
status: STATUS_PROP,
|
|
108
|
+
title: v.string(),
|
|
109
|
+
}),
|
|
110
|
+
},
|
|
111
|
+
({ title, status, children }) => {
|
|
112
|
+
const count = flattenChildren(children).filter((c) =>
|
|
113
|
+
isEl(c, BoardCard)
|
|
114
|
+
).length;
|
|
115
|
+
return (
|
|
116
|
+
<section
|
|
117
|
+
className="w-64 shrink-0 rounded-xl bg-neutral-100/70 p-2 dark:bg-neutral-900/70"
|
|
118
|
+
data-board-lane=""
|
|
119
|
+
data-lane-status={status}
|
|
120
|
+
data-lane-title={title}
|
|
121
|
+
>
|
|
122
|
+
<header
|
|
123
|
+
className={`flex items-center gap-2 px-1.5 py-1.5 text-xs font-semibold ${TEXT.body}`}
|
|
124
|
+
>
|
|
125
|
+
{status === undefined ? null : (
|
|
126
|
+
<span
|
|
127
|
+
aria-hidden
|
|
128
|
+
className={`h-2 w-2 shrink-0 rounded-full ${DOT[status]}`}
|
|
129
|
+
/>
|
|
130
|
+
)}
|
|
131
|
+
<span className="min-w-0 flex-1 truncate">{title}</span>
|
|
132
|
+
<span className={`mdxr-lane-count ${COUNT_CHIP_CLS}`}>{count}</span>
|
|
133
|
+
</header>
|
|
134
|
+
<div className="min-h-8 space-y-2" data-board-cards="">
|
|
135
|
+
{children}
|
|
136
|
+
</div>
|
|
137
|
+
</section>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
export const Board = defineComponent(
|
|
143
|
+
{
|
|
144
|
+
description:
|
|
145
|
+
"カンバンボードのコンテナ。<Lane> を横に並べる (はみ出しは横スクロール)。カードはドラッグまたは矢印ボタンで移動でき、移動後の状態を <Board> マークアップとしてコピーできる",
|
|
146
|
+
schema: v.looseObject(TITLE_PROP),
|
|
147
|
+
},
|
|
148
|
+
({ title, children }) => (
|
|
149
|
+
<Section title={title}>
|
|
150
|
+
<div className="mdxr-board" data-board="" data-board-title={title}>
|
|
151
|
+
<div className="not-prose flex items-start gap-3 overflow-x-auto pb-1">
|
|
152
|
+
{children}
|
|
153
|
+
</div>
|
|
154
|
+
<div className="mdxr-board-tools not-prose mt-2 flex items-center justify-between gap-3">
|
|
155
|
+
<span className={`text-xs ${TEXT.faint}`}>
|
|
156
|
+
Drag cards or use the arrow buttons, then copy the updated markup.
|
|
157
|
+
</span>
|
|
158
|
+
<button
|
|
159
|
+
className={ACTION_BUTTON_CLS}
|
|
160
|
+
data-board-copy=""
|
|
161
|
+
type="button"
|
|
162
|
+
>
|
|
163
|
+
<CopyFeedback
|
|
164
|
+
done="Copied"
|
|
165
|
+
icon="lucide:clipboard-list"
|
|
166
|
+
label="Copy markdown"
|
|
167
|
+
/>
|
|
168
|
+
</button>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
</Section>
|
|
172
|
+
)
|
|
173
|
+
);
|