@tangle-network/ui 11.30.1 → 11.31.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/CHANGELOG.md +17 -0
- package/dist/index.d.ts +8 -3
- package/dist/index.js +7 -2
- package/dist/markdown/code-block.d.ts +11 -4
- package/dist/markdown/code-block.js +239 -107
- package/dist/markdown/code-meta.d.ts +11 -0
- package/dist/markdown/code-meta.js +98 -0
- package/dist/markdown/markdown-table.d.ts +8 -0
- package/dist/markdown/markdown-table.js +168 -0
- package/dist/markdown/markdown.d.ts +17 -4
- package/dist/markdown/markdown.js +96 -34
- package/dist/markdown.d.ts +5 -3
- package/dist/markdown.js +4 -2
- package/dist/primitives/toolbar.d.ts +10 -10
- package/dist/primitives/toolbar.js +2 -2
- package/dist/run/approval-card.d.ts +46 -0
- package/dist/run/approval-card.js +162 -0
- package/dist/run/run-phase-list.d.ts +18 -0
- package/dist/run/run-phase-list.js +56 -0
- package/dist/run/task-list.d.ts +33 -0
- package/dist/run/task-list.js +138 -0
- package/dist/run.d.ts +4 -1
- package/dist/run.js +4 -1
- package/package.json +3 -4
- package/src/markdown/code-block.test.tsx +35 -26
- package/src/markdown/code-block.tsx +197 -110
- package/src/markdown/code-meta.ts +68 -0
- package/src/markdown/index.ts +10 -2
- package/src/markdown/markdown-table.test.tsx +61 -0
- package/src/markdown/markdown-table.tsx +191 -0
- package/src/markdown/markdown.test.tsx +27 -0
- package/src/markdown/markdown.tsx +126 -48
- package/src/primitives/toolbar.test.tsx +9 -10
- package/src/primitives/toolbar.tsx +12 -12
- package/src/run/approval-card.test.tsx +41 -0
- package/src/run/approval-card.tsx +146 -0
- package/src/run/index.ts +17 -0
- package/src/run/run-phase-list.test.tsx +25 -0
- package/src/run/run-phase-list.tsx +56 -0
- package/src/run/task-list.test.tsx +35 -0
- package/src/run/task-list.tsx +117 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
//#region src/markdown/code-meta.ts
|
|
2
|
+
/**
|
|
3
|
+
* Carries a fence's info string past the language (```ts title="src/x.ts"```)
|
|
4
|
+
* to the rendered `<code>` as `data-meta`, so the code block can name its file.
|
|
5
|
+
*/
|
|
6
|
+
function remarkCodeMeta() {
|
|
7
|
+
return (tree) => {
|
|
8
|
+
const walk = (node) => {
|
|
9
|
+
if (node.type === "code" && node.meta) node.data = {
|
|
10
|
+
...node.data,
|
|
11
|
+
hProperties: {
|
|
12
|
+
...node.data?.hProperties,
|
|
13
|
+
dataMeta: node.meta
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
node.children?.forEach(walk);
|
|
17
|
+
};
|
|
18
|
+
walk(tree);
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const LANGUAGE_BY_EXTENSION = {
|
|
22
|
+
ts: "typescript",
|
|
23
|
+
mts: "typescript",
|
|
24
|
+
cts: "typescript",
|
|
25
|
+
tsx: "tsx",
|
|
26
|
+
js: "javascript",
|
|
27
|
+
mjs: "javascript",
|
|
28
|
+
cjs: "javascript",
|
|
29
|
+
jsx: "jsx",
|
|
30
|
+
json: "json",
|
|
31
|
+
jsonc: "jsonc",
|
|
32
|
+
md: "markdown",
|
|
33
|
+
mdx: "mdx",
|
|
34
|
+
py: "python",
|
|
35
|
+
sh: "bash",
|
|
36
|
+
bash: "bash",
|
|
37
|
+
zsh: "bash",
|
|
38
|
+
yml: "yaml",
|
|
39
|
+
yaml: "yaml",
|
|
40
|
+
toml: "toml",
|
|
41
|
+
css: "css",
|
|
42
|
+
scss: "scss",
|
|
43
|
+
html: "html",
|
|
44
|
+
sql: "sql",
|
|
45
|
+
go: "go",
|
|
46
|
+
rs: "rust",
|
|
47
|
+
sol: "solidity",
|
|
48
|
+
rb: "ruby",
|
|
49
|
+
java: "java",
|
|
50
|
+
kt: "kotlin",
|
|
51
|
+
swift: "swift",
|
|
52
|
+
php: "php",
|
|
53
|
+
c: "c",
|
|
54
|
+
h: "c",
|
|
55
|
+
cpp: "cpp",
|
|
56
|
+
cs: "csharp",
|
|
57
|
+
graphql: "graphql",
|
|
58
|
+
gql: "graphql",
|
|
59
|
+
xml: "xml",
|
|
60
|
+
svg: "xml",
|
|
61
|
+
diff: "diff",
|
|
62
|
+
patch: "diff"
|
|
63
|
+
};
|
|
64
|
+
const FILE_PATH = /^(?:\.{0,2}\/)?(?:[\w@.-]+\/)*[\w@.-]*\.[A-Za-z0-9]+$|^(?:[\w@.-]+\/)+[\w@.-]+$/;
|
|
65
|
+
/** The language a file's extension implies, if the code block can highlight it. */
|
|
66
|
+
function languageForFile(filename) {
|
|
67
|
+
const base = filename.split("/").pop() ?? filename;
|
|
68
|
+
if (/^dockerfile$/i.test(base)) return "dockerfile";
|
|
69
|
+
const extension = base.includes(".") ? base.split(".").pop()?.toLowerCase() : void 0;
|
|
70
|
+
return extension ? LANGUAGE_BY_EXTENSION[extension] : void 0;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The language and file name of a fenced block, from its class and info
|
|
74
|
+
* string: ```ts title="src/x.ts"```, ```ts filename=src/x.ts```,
|
|
75
|
+
* ```ts src/x.ts```, ```ts:src/x.ts``` and ```src/x.ts``` all name the file.
|
|
76
|
+
*/
|
|
77
|
+
function fenceInfo(className, meta) {
|
|
78
|
+
const token = /(?:^|\s)language-(\S+)/.exec(className ?? "")?.[1];
|
|
79
|
+
let language = token;
|
|
80
|
+
let filename;
|
|
81
|
+
if (token?.includes(":")) [language, filename] = [token.slice(0, token.indexOf(":")), token.slice(token.indexOf(":") + 1)];
|
|
82
|
+
else if (token && FILE_PATH.test(token) && (token.includes("/") || languageForFile(token))) {
|
|
83
|
+
filename = token;
|
|
84
|
+
language = void 0;
|
|
85
|
+
}
|
|
86
|
+
if (!filename && typeof meta === "string") {
|
|
87
|
+
const named = /(?:^|\s)(?:title|filename|file|name)=(?:"([^"]+)"|'([^']+)'|(\S+))/.exec(meta);
|
|
88
|
+
const bare = meta.trim().split(/\s+/).find((part) => FILE_PATH.test(part));
|
|
89
|
+
filename = named?.[1] ?? named?.[2] ?? named?.[3] ?? bare;
|
|
90
|
+
}
|
|
91
|
+
if (!language && filename) language = languageForFile(filename);
|
|
92
|
+
return {
|
|
93
|
+
...language ? { language } : {},
|
|
94
|
+
...filename ? { filename } : {}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
//#endregion
|
|
98
|
+
export { fenceInfo, languageForFile, remarkCodeMeta };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import "react";
|
|
2
|
+
import "react-markdown";
|
|
3
|
+
//#region src/markdown/markdown-table.d.ts
|
|
4
|
+
/** A column is numeric when every non-empty body cell is a figure. */
|
|
5
|
+
export declare function numericColumns(body: string[][]): boolean[];
|
|
6
|
+
/** RFC 4180 CSV of a rendered Markdown table, header first. */
|
|
7
|
+
export declare function tableCsv(rows: string[][]): string;
|
|
8
|
+
//#endregion
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { focusRing, focusRingInset } from "../lib/focus.js";
|
|
2
|
+
import { cn } from "../lib/utils.js";
|
|
3
|
+
import { Children, createContext, isValidElement, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { Check, Copy } from "lucide-react";
|
|
6
|
+
//#region src/markdown/markdown-table.tsx
|
|
7
|
+
/** A figure: optional sign or currency, digits with separators, an optional unit. */
|
|
8
|
+
const NUMERIC_CELL = /^[(]?[-+−]?\s?[$€£¥₹]?\s?\d[\d,_ ]*(?:\.\d+)?\s?(?:%|[kKmMbB]|x|ms|s|h|pp)?[)]?$/;
|
|
9
|
+
/** The text of a hast subtree, as a reader sees it. */
|
|
10
|
+
function textOf(node) {
|
|
11
|
+
if (node.type === "text") return node.value;
|
|
12
|
+
if (node.type !== "element") return "";
|
|
13
|
+
return node.children.map(textOf).join("");
|
|
14
|
+
}
|
|
15
|
+
function rowsOf(table) {
|
|
16
|
+
const header = [];
|
|
17
|
+
const body = [];
|
|
18
|
+
const visit = (element, inHead) => {
|
|
19
|
+
for (const child of element.children) {
|
|
20
|
+
if (child.type !== "element") continue;
|
|
21
|
+
if (child.tagName === "tr") {
|
|
22
|
+
const cells = child.children.filter((cell) => cell.type === "element" && (cell.tagName === "th" || cell.tagName === "td")).map((cell) => textOf(cell).replace(/\s+/g, " ").trim());
|
|
23
|
+
(inHead ? header : body).push(cells);
|
|
24
|
+
} else visit(child, inHead || child.tagName === "thead");
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
visit(table, false);
|
|
28
|
+
return {
|
|
29
|
+
header,
|
|
30
|
+
body
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** A column is numeric when every non-empty body cell is a figure. */
|
|
34
|
+
function numericColumns(body) {
|
|
35
|
+
const width = Math.max(0, ...body.map((row) => row.length));
|
|
36
|
+
return Array.from({ length: width }, (_, column) => {
|
|
37
|
+
const cells = body.map((row) => row[column] ?? "").filter(Boolean);
|
|
38
|
+
return cells.length > 0 && cells.every((cell) => NUMERIC_CELL.test(cell));
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function csvCell(value) {
|
|
42
|
+
const safe = /^[=+\-@\t\r]/.test(value) && !NUMERIC_CELL.test(value) ? `'${value}` : value;
|
|
43
|
+
return /[",\n\r]/.test(safe) || safe !== safe.trim() ? `"${safe.replace(/"/g, "\"\"")}"` : safe;
|
|
44
|
+
}
|
|
45
|
+
/** RFC 4180 CSV of a rendered Markdown table, header first. */
|
|
46
|
+
function tableCsv(rows) {
|
|
47
|
+
return rows.map((row) => row.map(csvCell).join(",")).join("\n");
|
|
48
|
+
}
|
|
49
|
+
const TableColumns = createContext([]);
|
|
50
|
+
const CellColumn = createContext(-1);
|
|
51
|
+
function CopyCsvButton({ csv }) {
|
|
52
|
+
const [state, setState] = useState("idle");
|
|
53
|
+
const timer = useRef(null);
|
|
54
|
+
useEffect(() => () => {
|
|
55
|
+
if (timer.current) clearTimeout(timer.current);
|
|
56
|
+
}, []);
|
|
57
|
+
const copy = useCallback(async () => {
|
|
58
|
+
if (timer.current) clearTimeout(timer.current);
|
|
59
|
+
try {
|
|
60
|
+
await navigator.clipboard.writeText(csv);
|
|
61
|
+
setState("copied");
|
|
62
|
+
} catch {
|
|
63
|
+
setState("failed");
|
|
64
|
+
}
|
|
65
|
+
timer.current = setTimeout(() => setState("idle"), 2e3);
|
|
66
|
+
}, [csv]);
|
|
67
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("button", {
|
|
68
|
+
type: "button",
|
|
69
|
+
onClick: copy,
|
|
70
|
+
className: cn("inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-sm font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground", focusRing),
|
|
71
|
+
children: [state === "copied" ? /* @__PURE__ */ jsx(Check, {
|
|
72
|
+
"aria-hidden": true,
|
|
73
|
+
className: "size-4 text-[var(--surface-success-text)]"
|
|
74
|
+
}) : /* @__PURE__ */ jsx(Copy, {
|
|
75
|
+
"aria-hidden": true,
|
|
76
|
+
className: "size-4"
|
|
77
|
+
}), state === "copied" ? "Copied" : "Copy CSV"]
|
|
78
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
79
|
+
className: "sr-only",
|
|
80
|
+
role: "status",
|
|
81
|
+
"aria-live": "polite",
|
|
82
|
+
children: state === "copied" ? "Table copied as CSV" : state === "failed" ? "Copy failed" : ""
|
|
83
|
+
})] });
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* A Markdown table as a card: a header row, subtle row lines, figures
|
|
87
|
+
* right-aligned in tabular numerals, a copy-as-CSV control, and horizontal
|
|
88
|
+
* scrolling inside the card when the columns outgrow a phone.
|
|
89
|
+
*/
|
|
90
|
+
function MarkdownTable({ node, className, children, ...props }) {
|
|
91
|
+
const { csv, numeric, rowCount } = useMemo(() => {
|
|
92
|
+
const { header, body } = node ? rowsOf(node) : {
|
|
93
|
+
header: [],
|
|
94
|
+
body: []
|
|
95
|
+
};
|
|
96
|
+
return {
|
|
97
|
+
csv: tableCsv([...header, ...body]),
|
|
98
|
+
numeric: numericColumns(body),
|
|
99
|
+
rowCount: body.length
|
|
100
|
+
};
|
|
101
|
+
}, [node]);
|
|
102
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
103
|
+
className: "not-prose my-4 overflow-hidden rounded-lg border border-border bg-card",
|
|
104
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
105
|
+
className: "flex items-center justify-between gap-2 border-b border-border px-3 py-1",
|
|
106
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
107
|
+
className: "text-sm text-muted-foreground",
|
|
108
|
+
children: rowCount === 1 ? "1 row" : `${rowCount} rows`
|
|
109
|
+
}), /* @__PURE__ */ jsx(CopyCsvButton, { csv })]
|
|
110
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
111
|
+
tabIndex: 0,
|
|
112
|
+
role: "region",
|
|
113
|
+
"aria-label": "Table",
|
|
114
|
+
className: cn("overflow-x-auto", focusRingInset),
|
|
115
|
+
children: /* @__PURE__ */ jsx(TableColumns.Provider, {
|
|
116
|
+
value: numeric,
|
|
117
|
+
children: /* @__PURE__ */ jsx("table", {
|
|
118
|
+
...props,
|
|
119
|
+
className: cn("w-full border-collapse text-sm text-foreground", className),
|
|
120
|
+
children
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
})]
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function MarkdownTableHead({ node: _node, className, ...props }) {
|
|
127
|
+
return /* @__PURE__ */ jsx("thead", {
|
|
128
|
+
...props,
|
|
129
|
+
className: cn("bg-muted/50", className)
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
function MarkdownTableBody({ node: _node, className, ...props }) {
|
|
133
|
+
return /* @__PURE__ */ jsx("tbody", {
|
|
134
|
+
...props,
|
|
135
|
+
className: cn("[&>tr:last-child>td]:border-b-0", className)
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
function MarkdownTableRow({ node: _node, children, ...props }) {
|
|
139
|
+
let column = 0;
|
|
140
|
+
return /* @__PURE__ */ jsx("tr", {
|
|
141
|
+
...props,
|
|
142
|
+
children: Children.map(children, (child) => isValidElement(child) ? /* @__PURE__ */ jsx(CellColumn.Provider, {
|
|
143
|
+
value: column++,
|
|
144
|
+
children: child
|
|
145
|
+
}) : child)
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
function useNumericCell() {
|
|
149
|
+
const numeric = useContext(TableColumns);
|
|
150
|
+
const column = useContext(CellColumn);
|
|
151
|
+
return column >= 0 && numeric[column] === true;
|
|
152
|
+
}
|
|
153
|
+
function MarkdownTableHeaderCell({ node: _node, className, ...props }) {
|
|
154
|
+
const numeric = useNumericCell();
|
|
155
|
+
return /* @__PURE__ */ jsx("th", {
|
|
156
|
+
...props,
|
|
157
|
+
className: cn("whitespace-nowrap border-b border-border px-3 py-2 text-left font-semibold text-foreground", numeric && "text-right", className)
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
function MarkdownTableCell({ node: _node, className, ...props }) {
|
|
161
|
+
const numeric = useNumericCell();
|
|
162
|
+
return /* @__PURE__ */ jsx("td", {
|
|
163
|
+
...props,
|
|
164
|
+
className: cn("border-b border-border/60 px-3 py-2 align-top", numeric ? "whitespace-nowrap text-right tabular-nums" : "min-w-[8rem]", className)
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
//#endregion
|
|
168
|
+
export { MarkdownTable, MarkdownTableBody, MarkdownTableCell, MarkdownTableHead, MarkdownTableHeaderCell, MarkdownTableRow, numericColumns, tableCsv };
|
|
@@ -2,16 +2,29 @@ import { UrlTransform } from "react-markdown";
|
|
|
2
2
|
//#region src/markdown/markdown.d.ts
|
|
3
3
|
/** Converts a raw Markdown fragment to the ID rehype-sanitize emits for its heading. */
|
|
4
4
|
export declare function getSanitizedMarkdownHeadingIdFromRawFragment(rawFragment: string): string;
|
|
5
|
+
/** How a link to a known object renders: a chip with that object's icon. */
|
|
6
|
+
export interface MarkdownLinkChip {
|
|
7
|
+
kind: "file" | "image" | "asset" | "folder";
|
|
8
|
+
/** Chip text; the link's own text when omitted. */
|
|
9
|
+
label?: string;
|
|
10
|
+
/** Tooltip, such as the full path. */
|
|
11
|
+
title?: string;
|
|
12
|
+
}
|
|
13
|
+
/** Classifies a link by its transformed href; null keeps an ordinary link. */
|
|
14
|
+
export type MarkdownLinkChipResolver = (href: string) => MarkdownLinkChip | null | undefined;
|
|
5
15
|
export interface MarkdownProps {
|
|
6
16
|
children: string;
|
|
7
17
|
className?: string;
|
|
8
18
|
/** Transform parsed link and image URLs while retaining protocol safety checks. */
|
|
9
19
|
urlTransform?: UrlTransform;
|
|
20
|
+
/** Render links to known objects (a file, an asset) as chips that keep their href. */
|
|
21
|
+
linkChip?: MarkdownLinkChipResolver;
|
|
10
22
|
}
|
|
11
23
|
/**
|
|
12
|
-
* Renders Markdown content with GFM support
|
|
13
|
-
*
|
|
14
|
-
* or show a labelled placeholder
|
|
24
|
+
* Renders Markdown content with GFM support and XSS sanitisation: Shiki code
|
|
25
|
+
* blocks that name their file, tables as cards with right-aligned figures and
|
|
26
|
+
* a CSV copy, images that open full size or show a labelled placeholder, and
|
|
27
|
+
* optional chips for links to known objects.
|
|
15
28
|
*/
|
|
16
|
-
export declare const Markdown: import("react").MemoExoticComponent<({ children, className, urlTransform }: MarkdownProps) => import("react").JSX.Element>;
|
|
29
|
+
export declare const Markdown: import("react").MemoExoticComponent<({ children, className, urlTransform, linkChip }: MarkdownProps) => import("react").JSX.Element>;
|
|
17
30
|
//#endregion
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import { focusRing } from "../lib/focus.js";
|
|
1
2
|
import { cn } from "../lib/utils.js";
|
|
2
3
|
import { CodeBlock, CopyButton } from "./code-block.js";
|
|
4
|
+
import { fenceInfo, remarkCodeMeta } from "./code-meta.js";
|
|
5
|
+
import { MarkdownTable, MarkdownTableBody, MarkdownTableCell, MarkdownTableHead, MarkdownTableHeaderCell, MarkdownTableRow } from "./markdown-table.js";
|
|
3
6
|
import { createContext, memo, useContext, useEffect, useRef, useState } from "react";
|
|
4
|
-
import {
|
|
5
|
-
import { ImageOff } from "lucide-react";
|
|
7
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
import { FileImage, FileText, Folder, ImageOff, Package } from "lucide-react";
|
|
6
9
|
import ReactMarkdown, { defaultUrlTransform } from "react-markdown";
|
|
7
10
|
import remarkGfm from "remark-gfm";
|
|
8
|
-
import rehypeSanitize from "rehype-sanitize";
|
|
11
|
+
import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
|
|
9
12
|
import rehypeSlug from "rehype-slug";
|
|
10
13
|
import GithubSlugger from "github-slugger";
|
|
11
14
|
//#region src/markdown/markdown.tsx
|
|
@@ -28,13 +31,77 @@ function transformMarkdownUrl(url, key, node, urlTransform) {
|
|
|
28
31
|
if (key === "href" && transformed === url && url.startsWith("#")) return safeUrl("#" + getSanitizedMarkdownHeadingIdFromRawFragment(url.slice(1)));
|
|
29
32
|
return safeUrl(transformed);
|
|
30
33
|
}
|
|
34
|
+
const LINK_CHIP_ICONS = {
|
|
35
|
+
file: FileText,
|
|
36
|
+
image: FileImage,
|
|
37
|
+
asset: Package,
|
|
38
|
+
folder: Folder
|
|
39
|
+
};
|
|
40
|
+
const LinkChipContext = createContext(void 0);
|
|
31
41
|
const MarkdownLinkContext = createContext(false);
|
|
32
|
-
function MarkdownLink({ node: _node, ...props }) {
|
|
42
|
+
function MarkdownLink({ node: _node, children, className, ...props }) {
|
|
43
|
+
const resolveChip = useContext(LinkChipContext);
|
|
44
|
+
const chip = typeof props.href === "string" && props.href ? resolveChip?.(props.href) : null;
|
|
45
|
+
if (!chip) return /* @__PURE__ */ jsx(MarkdownLinkContext.Provider, {
|
|
46
|
+
value: true,
|
|
47
|
+
children: /* @__PURE__ */ jsx("a", {
|
|
48
|
+
...props,
|
|
49
|
+
className,
|
|
50
|
+
children
|
|
51
|
+
})
|
|
52
|
+
});
|
|
53
|
+
const Icon = LINK_CHIP_ICONS[chip.kind];
|
|
33
54
|
return /* @__PURE__ */ jsx(MarkdownLinkContext.Provider, {
|
|
34
55
|
value: true,
|
|
35
|
-
children: /* @__PURE__ */
|
|
56
|
+
children: /* @__PURE__ */ jsxs("a", {
|
|
57
|
+
...props,
|
|
58
|
+
title: chip.title ?? props.title,
|
|
59
|
+
"data-link-chip": chip.kind,
|
|
60
|
+
className: cn("not-prose inline-flex max-w-full items-center gap-1.5 rounded-md border border-border bg-muted/60 px-2 py-0.5 align-middle text-[0.9375em] font-medium leading-6 text-foreground no-underline transition-colors hover:border-[var(--border-strong)] hover:bg-muted", focusRing, className),
|
|
61
|
+
children: [/* @__PURE__ */ jsx(Icon, {
|
|
62
|
+
"aria-hidden": true,
|
|
63
|
+
className: "size-4 shrink-0 text-muted-foreground"
|
|
64
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
65
|
+
className: "min-w-0 truncate",
|
|
66
|
+
children: chip.label ?? children
|
|
67
|
+
})]
|
|
68
|
+
})
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const PreContext = createContext(false);
|
|
72
|
+
function MarkdownPre({ children }) {
|
|
73
|
+
return /* @__PURE__ */ jsx(PreContext.Provider, {
|
|
74
|
+
value: true,
|
|
75
|
+
children
|
|
36
76
|
});
|
|
37
77
|
}
|
|
78
|
+
function MarkdownCode({ node, className: codeClass, children: codeChildren, ...rest }) {
|
|
79
|
+
const fenced = useContext(PreContext);
|
|
80
|
+
const code = String(codeChildren).replace(/\n$/, "");
|
|
81
|
+
if (!fenced) {
|
|
82
|
+
const { "data-meta": _meta, ...attributes } = rest;
|
|
83
|
+
return /* @__PURE__ */ jsx("code", {
|
|
84
|
+
className: cn("px-1.5 py-0.5 rounded border border-border bg-muted text-foreground text-[0.85em] font-mono", codeClass),
|
|
85
|
+
...attributes,
|
|
86
|
+
children: codeChildren
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
const { language, filename } = fenceInfo(codeClass, node?.properties?.dataMeta);
|
|
90
|
+
return /* @__PURE__ */ jsx(CodeBlock, {
|
|
91
|
+
code,
|
|
92
|
+
language,
|
|
93
|
+
filename,
|
|
94
|
+
className: "my-4",
|
|
95
|
+
children: /* @__PURE__ */ jsx(CopyButton, { text: code })
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
const sanitizeSchema = {
|
|
99
|
+
...defaultSchema,
|
|
100
|
+
attributes: {
|
|
101
|
+
...defaultSchema.attributes,
|
|
102
|
+
code: [...defaultSchema.attributes?.code ?? [], "dataMeta"]
|
|
103
|
+
}
|
|
104
|
+
};
|
|
38
105
|
/**
|
|
39
106
|
* Renders a Markdown image that opens full size in a new tab, or a labelled
|
|
40
107
|
* placeholder when its URL is unsafe or fails to load, instead of the
|
|
@@ -92,39 +159,34 @@ function MarkdownImage({ node: _node, src, alt, className, ...props }) {
|
|
|
92
159
|
});
|
|
93
160
|
}
|
|
94
161
|
/**
|
|
95
|
-
* Renders Markdown content with GFM support
|
|
96
|
-
*
|
|
97
|
-
* or show a labelled placeholder
|
|
162
|
+
* Renders Markdown content with GFM support and XSS sanitisation: Shiki code
|
|
163
|
+
* blocks that name their file, tables as cards with right-aligned figures and
|
|
164
|
+
* a CSV copy, images that open full size or show a labelled placeholder, and
|
|
165
|
+
* optional chips for links to known objects.
|
|
98
166
|
*/
|
|
99
|
-
const Markdown = memo(({ children, className, urlTransform }) => {
|
|
167
|
+
const Markdown = memo(({ children, className, urlTransform, linkChip }) => {
|
|
100
168
|
return /* @__PURE__ */ jsx("div", {
|
|
101
169
|
className: cn("tangle-prose max-w-none text-sm", className),
|
|
102
|
-
children: /* @__PURE__ */ jsx(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
170
|
+
children: /* @__PURE__ */ jsx(LinkChipContext.Provider, {
|
|
171
|
+
value: linkChip,
|
|
172
|
+
children: /* @__PURE__ */ jsx(ReactMarkdown, {
|
|
173
|
+
remarkPlugins: [remarkGfm, remarkCodeMeta],
|
|
174
|
+
rehypePlugins: [rehypeSlug, [rehypeSanitize, sanitizeSchema]],
|
|
175
|
+
urlTransform: (url, key, node) => transformMarkdownUrl(url, key, node, urlTransform),
|
|
176
|
+
components: {
|
|
177
|
+
a: MarkdownLink,
|
|
178
|
+
img: MarkdownImage,
|
|
179
|
+
pre: MarkdownPre,
|
|
180
|
+
code: MarkdownCode,
|
|
181
|
+
table: MarkdownTable,
|
|
182
|
+
thead: MarkdownTableHead,
|
|
183
|
+
tbody: MarkdownTableBody,
|
|
184
|
+
tr: MarkdownTableRow,
|
|
185
|
+
th: MarkdownTableHeaderCell,
|
|
186
|
+
td: MarkdownTableCell
|
|
111
187
|
},
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const code = String(codeChildren).replace(/\n$/, "");
|
|
115
|
-
if (!language && !code.includes("\n")) return /* @__PURE__ */ jsx("code", {
|
|
116
|
-
className: cn("px-1.5 py-0.5 rounded border border-border bg-muted text-foreground text-[0.85em] font-mono", codeClass),
|
|
117
|
-
...rest,
|
|
118
|
-
children: codeChildren
|
|
119
|
-
});
|
|
120
|
-
return /* @__PURE__ */ jsx(CodeBlock, {
|
|
121
|
-
code,
|
|
122
|
-
language,
|
|
123
|
-
children: /* @__PURE__ */ jsx(CopyButton, { text: code })
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
},
|
|
127
|
-
children
|
|
188
|
+
children
|
|
189
|
+
})
|
|
128
190
|
})
|
|
129
191
|
});
|
|
130
192
|
});
|
package/dist/markdown.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
import { Markdown, MarkdownProps, getSanitizedMarkdownHeadingIdFromRawFragment } from "./markdown/markdown.js";
|
|
2
|
-
import { CodeBlock, CodeBlockProps, CopyButton } from "./markdown/code-block.js";
|
|
3
|
-
|
|
1
|
+
import { Markdown, MarkdownLinkChip, MarkdownLinkChipResolver, MarkdownProps, getSanitizedMarkdownHeadingIdFromRawFragment } from "./markdown/markdown.js";
|
|
2
|
+
import { CodeBlock, CodeBlockProps, CopyButton, codeBlockGrammar } from "./markdown/code-block.js";
|
|
3
|
+
import { fenceInfo } from "./markdown/code-meta.js";
|
|
4
|
+
import { numericColumns, tableCsv } from "./markdown/markdown-table.js";
|
|
5
|
+
export { CodeBlock, type CodeBlockProps, CopyButton, Markdown, type MarkdownLinkChip, type MarkdownLinkChipResolver, type MarkdownProps, codeBlockGrammar, fenceInfo, getSanitizedMarkdownHeadingIdFromRawFragment, numericColumns, tableCsv };
|
package/dist/markdown.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
import { CodeBlock, CopyButton } from "./markdown/code-block.js";
|
|
1
|
+
import { CodeBlock, CopyButton, codeBlockGrammar } from "./markdown/code-block.js";
|
|
2
|
+
import { fenceInfo } from "./markdown/code-meta.js";
|
|
3
|
+
import { numericColumns, tableCsv } from "./markdown/markdown-table.js";
|
|
2
4
|
import { Markdown, getSanitizedMarkdownHeadingIdFromRawFragment } from "./markdown/markdown.js";
|
|
3
|
-
export { CodeBlock, CopyButton, Markdown, getSanitizedMarkdownHeadingIdFromRawFragment };
|
|
5
|
+
export { CodeBlock, CopyButton, Markdown, codeBlockGrammar, fenceInfo, getSanitizedMarkdownHeadingIdFromRawFragment, numericColumns, tableCsv };
|
|
@@ -10,22 +10,22 @@ import * as React from "react";
|
|
|
10
10
|
* own visual language, so one row carried a native `<select>`, a custom input
|
|
11
11
|
* and a segmented control side by side.
|
|
12
12
|
*
|
|
13
|
-
* So
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* So on one `lg` line the filters keep their content width while it fits,
|
|
14
|
+
* search takes what is left between a 16rem floor and `max-w-sm`, and `actions`
|
|
15
|
+
* stays at its content width. The row is the one place those controls are
|
|
16
|
+
* composed, which is what keeps them looking like one set.
|
|
16
17
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* then stops search claiming more than it can use when there are few filters.
|
|
18
|
+
* Both earlier layouts failed one side. Filters at content width with no floor
|
|
19
|
+
* starved search: a 537px filter row in an 832px toolbar left it 89px. Splitting
|
|
20
|
+
* the row evenly clipped filters that would have fitted: at 1440px the Assets
|
|
21
|
+
* bar cut its third field at the scroll edge while search sat at 384px. With the
|
|
22
|
+
* floor, filters shrink and scroll only once search is down to 16rem.
|
|
23
23
|
*
|
|
24
24
|
* Below `lg` the row becomes a column and the filters wrap onto as many lines
|
|
25
25
|
* as they need. Scrolling them as one line hid every filter past the first
|
|
26
26
|
* behind the screen edge on a phone: at 390px the second select started at
|
|
27
27
|
* 403px and a third at 652px, with nothing to say more were there. From `lg`
|
|
28
|
-
* up they
|
|
28
|
+
* up they sit on one line as above.
|
|
29
29
|
*/
|
|
30
30
|
/**
|
|
31
31
|
* Slots only — `children` is omitted deliberately. A free child would render as
|
|
@@ -8,11 +8,11 @@ const Toolbar = React.forwardRef(({ className, search, filters, actions, ...prop
|
|
|
8
8
|
...props,
|
|
9
9
|
children: [
|
|
10
10
|
search && /* @__PURE__ */ jsx("div", {
|
|
11
|
-
className: "min-w-0 lg:max-w-sm lg:flex-1",
|
|
11
|
+
className: "min-w-0 lg:min-w-64 lg:max-w-sm lg:flex-1",
|
|
12
12
|
children: search
|
|
13
13
|
}),
|
|
14
14
|
filters && /* @__PURE__ */ jsx("div", {
|
|
15
|
-
className: "flex min-w-0 flex-wrap items-center gap-x-3 gap-y-2 py-1 lg:flex-
|
|
15
|
+
className: "flex min-w-0 flex-wrap items-center gap-x-3 gap-y-2 py-1 lg:flex-initial lg:flex-nowrap lg:gap-2 lg:overflow-x-auto",
|
|
16
16
|
children: filters
|
|
17
17
|
}),
|
|
18
18
|
actions && /* @__PURE__ */ jsx("div", {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { HTMLAttributes, ReactNode } from "react";
|
|
2
|
+
//#region src/run/approval-card.d.ts
|
|
3
|
+
export interface ApprovalDiffFile {
|
|
4
|
+
path: string;
|
|
5
|
+
change?: "added" | "modified" | "deleted";
|
|
6
|
+
/** Unknown counts render as a dash rather than a guessed zero. */
|
|
7
|
+
additions?: number;
|
|
8
|
+
deletions?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface ApprovalPlanStep {
|
|
11
|
+
label: ReactNode;
|
|
12
|
+
state: "done" | "current" | "upcoming";
|
|
13
|
+
}
|
|
14
|
+
export interface ApprovalCardProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
|
|
15
|
+
/** What the action does, in plain words. */
|
|
16
|
+
title: ReactNode;
|
|
17
|
+
/** Where it acts, such as "GitHub · owner/repo". */
|
|
18
|
+
context?: ReactNode;
|
|
19
|
+
/** This action's place in a declared plan. */
|
|
20
|
+
step?: {
|
|
21
|
+
index: number;
|
|
22
|
+
total: number;
|
|
23
|
+
};
|
|
24
|
+
/** The plan's steps, when the action belongs to one. */
|
|
25
|
+
plan?: readonly ApprovalPlanStep[];
|
|
26
|
+
/** Files the action writes, with line counts when known. */
|
|
27
|
+
diff?: readonly ApprovalDiffFile[];
|
|
28
|
+
/** Shown while line counts are being read. */
|
|
29
|
+
diffNote?: ReactNode;
|
|
30
|
+
/** The raw request, behind a Details toggle. */
|
|
31
|
+
details?: ReactNode;
|
|
32
|
+
status?: ReactNode;
|
|
33
|
+
actions?: ReactNode;
|
|
34
|
+
}
|
|
35
|
+
/** Files and +/− line counts for a write, with totals when every count is known. */
|
|
36
|
+
export declare function ApprovalDiffSummary({ files, note }: {
|
|
37
|
+
files: readonly ApprovalDiffFile[];
|
|
38
|
+
note?: ReactNode;
|
|
39
|
+
}): import("react").JSX.Element;
|
|
40
|
+
/**
|
|
41
|
+
* A request for a person to approve one action: what it does in plain words,
|
|
42
|
+
* where, its step in a plan, the files it writes, and the raw request behind
|
|
43
|
+
* a Details toggle. The caller owns the decision and supplies the actions.
|
|
44
|
+
*/
|
|
45
|
+
export declare function ApprovalCard({ title, context, step, plan, diff, diffNote, details, status, actions, className, ...props }: ApprovalCardProps): import("react").JSX.Element;
|
|
46
|
+
//#endregion
|