@tangle-network/ui 11.31.1 → 11.31.2
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
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @tangle-network/ui
|
|
2
2
|
|
|
3
|
+
## 11.31.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 7789ba9: The Markdown table card and link chips now out-rank `.tangle-prose` rules inside the renderer. A consumer that ships an older copy of those rules no longer turns the table into its own scroller, adds cell borders, left-aligns figure headers, or underlines and recolors chips.
|
|
8
|
+
|
|
3
9
|
## 11.31.1
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
|
@@ -32,10 +32,21 @@ function rowsOf(table) {
|
|
|
32
32
|
}
|
|
33
33
|
/** A column is numeric when every non-empty body cell is a figure. */
|
|
34
34
|
function numericColumns(body) {
|
|
35
|
+
return columnKinds(body).map((kind) => kind === "figure");
|
|
36
|
+
}
|
|
37
|
+
/** Text this long wraps; shorter text keeps its column on one line. */
|
|
38
|
+
const LONG_TEXT = 32;
|
|
39
|
+
/**
|
|
40
|
+
* How each column lays out: figures right-aligned, short text on one line,
|
|
41
|
+
* long text wrapping at a readable width, so a phone scrolls the table instead
|
|
42
|
+
* of crushing every column into a narrow stack.
|
|
43
|
+
*/
|
|
44
|
+
function columnKinds(body) {
|
|
35
45
|
const width = Math.max(0, ...body.map((row) => row.length));
|
|
36
46
|
return Array.from({ length: width }, (_, column) => {
|
|
37
47
|
const cells = body.map((row) => row[column] ?? "").filter(Boolean);
|
|
38
|
-
|
|
48
|
+
if (cells.length > 0 && cells.every((cell) => NUMERIC_CELL.test(cell))) return "figure";
|
|
49
|
+
return cells.some((cell) => cell.length > LONG_TEXT) ? "long" : "short";
|
|
39
50
|
});
|
|
40
51
|
}
|
|
41
52
|
function csvCell(value) {
|
|
@@ -46,6 +57,8 @@ function csvCell(value) {
|
|
|
46
57
|
function tableCsv(rows) {
|
|
47
58
|
return rows.map((row) => row.map(csvCell).join(",")).join("\n");
|
|
48
59
|
}
|
|
60
|
+
const PROSE_TABLE = "[.tangle-prose_&]:table! [.tangle-prose_&]:m-0! [.tangle-prose_&]:overflow-visible! [.tangle-prose_&]:text-sm!";
|
|
61
|
+
const PROSE_CELL = "[.tangle-prose_&]:px-3! [.tangle-prose_&]:py-2! [.tangle-prose_&]:border-t-0! [.tangle-prose_&]:border-l-0!";
|
|
49
62
|
const TableColumns = createContext([]);
|
|
50
63
|
const CellColumn = createContext(-1);
|
|
51
64
|
function CopyCsvButton({ csv }) {
|
|
@@ -88,14 +101,14 @@ function CopyCsvButton({ csv }) {
|
|
|
88
101
|
* scrolling inside the card when the columns outgrow a phone.
|
|
89
102
|
*/
|
|
90
103
|
function MarkdownTable({ node, className, children, ...props }) {
|
|
91
|
-
const { csv,
|
|
104
|
+
const { csv, kinds, rowCount } = useMemo(() => {
|
|
92
105
|
const { header, body } = node ? rowsOf(node) : {
|
|
93
106
|
header: [],
|
|
94
107
|
body: []
|
|
95
108
|
};
|
|
96
109
|
return {
|
|
97
110
|
csv: tableCsv([...header, ...body]),
|
|
98
|
-
|
|
111
|
+
kinds: columnKinds(body),
|
|
99
112
|
rowCount: body.length
|
|
100
113
|
};
|
|
101
114
|
}, [node]);
|
|
@@ -113,11 +126,11 @@ function MarkdownTable({ node, className, children, ...props }) {
|
|
|
113
126
|
"aria-label": "Table",
|
|
114
127
|
className: cn("overflow-x-auto", focusRingInset),
|
|
115
128
|
children: /* @__PURE__ */ jsx(TableColumns.Provider, {
|
|
116
|
-
value:
|
|
129
|
+
value: kinds,
|
|
117
130
|
children: /* @__PURE__ */ jsx("table", {
|
|
118
131
|
...props,
|
|
119
132
|
"data-markdown-table": "",
|
|
120
|
-
className: cn("w-full border-collapse text-sm text-foreground", className),
|
|
133
|
+
className: cn("w-full border-collapse text-sm text-foreground", PROSE_TABLE, className),
|
|
121
134
|
children
|
|
122
135
|
})
|
|
123
136
|
})
|
|
@@ -146,24 +159,24 @@ function MarkdownTableRow({ node: _node, children, ...props }) {
|
|
|
146
159
|
}) : child)
|
|
147
160
|
});
|
|
148
161
|
}
|
|
149
|
-
function
|
|
150
|
-
const
|
|
162
|
+
function useColumnKind() {
|
|
163
|
+
const kinds = useContext(TableColumns);
|
|
151
164
|
const column = useContext(CellColumn);
|
|
152
|
-
return column >= 0
|
|
165
|
+
return (column >= 0 ? kinds[column] : void 0) ?? "short";
|
|
153
166
|
}
|
|
154
167
|
function MarkdownTableHeaderCell({ node: _node, className, ...props }) {
|
|
155
|
-
const numeric =
|
|
168
|
+
const numeric = useColumnKind() === "figure";
|
|
156
169
|
return /* @__PURE__ */ jsx("th", {
|
|
157
170
|
...props,
|
|
158
|
-
className: cn("whitespace-nowrap border-b border-border px-3 py-2 text-left font-semibold text-foreground", numeric
|
|
171
|
+
className: cn("whitespace-nowrap border-b border-border px-3 py-2 text-left font-semibold text-foreground", PROSE_CELL, numeric ? "text-right [.tangle-prose_&]:text-right!" : "[.tangle-prose_&]:text-left!", className)
|
|
159
172
|
});
|
|
160
173
|
}
|
|
161
174
|
function MarkdownTableCell({ node: _node, className, ...props }) {
|
|
162
|
-
const
|
|
175
|
+
const kind = useColumnKind();
|
|
163
176
|
return /* @__PURE__ */ jsx("td", {
|
|
164
177
|
...props,
|
|
165
|
-
className: cn("border-b border-border/60 px-3 py-2 align-top",
|
|
178
|
+
className: cn("border-b border-border/60 px-3 py-2 align-top", PROSE_CELL, kind === "figure" ? "whitespace-nowrap text-right tabular-nums" : kind === "long" ? "min-w-[14rem]" : "whitespace-nowrap", className)
|
|
166
179
|
});
|
|
167
180
|
}
|
|
168
181
|
//#endregion
|
|
169
|
-
export { MarkdownTable, MarkdownTableBody, MarkdownTableCell, MarkdownTableHead, MarkdownTableHeaderCell, MarkdownTableRow, numericColumns, tableCsv };
|
|
182
|
+
export { MarkdownTable, MarkdownTableBody, MarkdownTableCell, MarkdownTableHead, MarkdownTableHeaderCell, MarkdownTableRow, columnKinds, numericColumns, tableCsv };
|
|
@@ -57,7 +57,7 @@ function MarkdownLink({ node: _node, children, className, ...props }) {
|
|
|
57
57
|
...props,
|
|
58
58
|
title: chip.title ?? props.title,
|
|
59
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),
|
|
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", "[.tangle-prose_&]:text-foreground! [.tangle-prose_&]:no-underline!", focusRing, className),
|
|
61
61
|
children: [/* @__PURE__ */ jsx(Icon, {
|
|
62
62
|
"aria-hidden": true,
|
|
63
63
|
className: "size-4 shrink-0 text-muted-foreground"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/ui",
|
|
3
|
-
"version": "11.31.
|
|
3
|
+
"version": "11.31.2",
|
|
4
4
|
"description": "Generic React UI components for Tangle products — primitives, chat, run, files, editor, markdown.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -150,7 +150,7 @@
|
|
|
150
150
|
"peerDependencies": {
|
|
151
151
|
"@hocuspocus/provider": ">=2.15.0 <5.0.0",
|
|
152
152
|
"@nanostores/react": ">=0.8.4 <3.0.0",
|
|
153
|
-
"@tangle-network/brand": "^1.
|
|
153
|
+
"@tangle-network/brand": "^1.20.0",
|
|
154
154
|
"@tiptap/core": ">=3.20.4 <4.0.0",
|
|
155
155
|
"@tiptap/extension-collaboration": ">=3.20.4 <4.0.0",
|
|
156
156
|
"@tiptap/extension-collaboration-caret": ">=3.7.0 <4.0.0",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
|
2
2
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
import { Markdown } from "./markdown";
|
|
4
|
-
import { numericColumns, tableCsv } from "./markdown-table";
|
|
4
|
+
import { columnKinds, numericColumns, tableCsv } from "./markdown-table";
|
|
5
5
|
|
|
6
6
|
const TABLE = [
|
|
7
7
|
"| Variant | Impressions | CTR % | Spend $ | Note |",
|
|
@@ -23,10 +23,13 @@ describe("Markdown tables", () => {
|
|
|
23
23
|
expect(headers.map((header) => header.textContent)).toEqual(["Variant", "Impressions", "CTR %", "Spend $", "Note"]);
|
|
24
24
|
expect(headers[0].className).toContain("text-left");
|
|
25
25
|
expect(headers[1].className).toContain("text-right");
|
|
26
|
+
// Inside `.tangle-prose`, a vendored header rule cannot left-align a figure column.
|
|
27
|
+
expect(headers[1].className).toContain("[.tangle-prose_&]:text-right!");
|
|
26
28
|
const cells = within(table).getAllByRole("cell");
|
|
27
29
|
expect(cells[1].className).toContain("text-right");
|
|
28
30
|
expect(cells[1].className).toContain("tabular-nums");
|
|
29
31
|
expect(cells[0].className).not.toContain("text-right");
|
|
32
|
+
expect(cells[0].className).toContain("whitespace-nowrap");
|
|
30
33
|
expect(cells[4].className).not.toContain("tabular-nums");
|
|
31
34
|
});
|
|
32
35
|
|
|
@@ -58,6 +61,7 @@ describe("Markdown tables", () => {
|
|
|
58
61
|
|
|
59
62
|
it("treats a column as numeric only when every filled cell is a figure", () => {
|
|
60
63
|
expect(numericColumns([["a", "1", "", "$2.50"], ["b", "x", "3", "-4%"]])).toEqual([false, false, true, true]);
|
|
64
|
+
expect(columnKinds([["Hero A", "1", "A long note that wraps across more than one line"]])).toEqual(["short", "figure", "long"]);
|
|
61
65
|
expect(tableCsv([["a,b", " c"]])).toBe('"a,b"," c"');
|
|
62
66
|
});
|
|
63
67
|
});
|
|
@@ -50,10 +50,23 @@ function rowsOf(table: Element): { header: string[][]; body: string[][] } {
|
|
|
50
50
|
|
|
51
51
|
/** A column is numeric when every non-empty body cell is a figure. */
|
|
52
52
|
export function numericColumns(body: string[][]): boolean[] {
|
|
53
|
+
return columnKinds(body).map((kind) => kind === "figure");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Text this long wraps; shorter text keeps its column on one line. */
|
|
57
|
+
const LONG_TEXT = 32;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* How each column lays out: figures right-aligned, short text on one line,
|
|
61
|
+
* long text wrapping at a readable width, so a phone scrolls the table instead
|
|
62
|
+
* of crushing every column into a narrow stack.
|
|
63
|
+
*/
|
|
64
|
+
export function columnKinds(body: string[][]): Array<"figure" | "short" | "long"> {
|
|
53
65
|
const width = Math.max(0, ...body.map((row) => row.length));
|
|
54
66
|
return Array.from({ length: width }, (_, column) => {
|
|
55
67
|
const cells = body.map((row) => row[column] ?? "").filter(Boolean);
|
|
56
|
-
|
|
68
|
+
if (cells.length > 0 && cells.every((cell) => NUMERIC_CELL.test(cell))) return "figure";
|
|
69
|
+
return cells.some((cell) => cell.length > LONG_TEXT) ? "long" : "short";
|
|
57
70
|
});
|
|
58
71
|
}
|
|
59
72
|
|
|
@@ -68,7 +81,14 @@ export function tableCsv(rows: string[][]): string {
|
|
|
68
81
|
return rows.map((row) => row.map(csvCell).join(",")).join("\n");
|
|
69
82
|
}
|
|
70
83
|
|
|
71
|
-
|
|
84
|
+
// Markdown wraps its output in `.tangle-prose`, and a consumer may ship an
|
|
85
|
+
// older, unlayered copy of those rules (display: block on tables, cell
|
|
86
|
+
// borders, left-aligned headers). Unlayered rules beat layered utilities at
|
|
87
|
+
// any specificity, so these scoped overrides are important.
|
|
88
|
+
const PROSE_TABLE = "[.tangle-prose_&]:table! [.tangle-prose_&]:m-0! [.tangle-prose_&]:overflow-visible! [.tangle-prose_&]:text-sm!";
|
|
89
|
+
const PROSE_CELL = "[.tangle-prose_&]:px-3! [.tangle-prose_&]:py-2! [.tangle-prose_&]:border-t-0! [.tangle-prose_&]:border-l-0!";
|
|
90
|
+
|
|
91
|
+
const TableColumns = createContext<Array<"figure" | "short" | "long">>([]);
|
|
72
92
|
const CellColumn = createContext(-1);
|
|
73
93
|
|
|
74
94
|
function CopyCsvButton({ csv }: { csv: string }) {
|
|
@@ -113,9 +133,9 @@ function CopyCsvButton({ csv }: { csv: string }) {
|
|
|
113
133
|
* scrolling inside the card when the columns outgrow a phone.
|
|
114
134
|
*/
|
|
115
135
|
export function MarkdownTable({ node, className, children, ...props }: ComponentProps<"table"> & ExtraProps) {
|
|
116
|
-
const { csv,
|
|
136
|
+
const { csv, kinds, rowCount } = useMemo(() => {
|
|
117
137
|
const { header, body } = node ? rowsOf(node) : { header: [], body: [] };
|
|
118
|
-
return { csv: tableCsv([...header, ...body]),
|
|
138
|
+
return { csv: tableCsv([...header, ...body]), kinds: columnKinds(body), rowCount: body.length };
|
|
119
139
|
}, [node]);
|
|
120
140
|
return (
|
|
121
141
|
<div className="not-prose my-4 overflow-hidden rounded-lg border border-border bg-card">
|
|
@@ -125,8 +145,8 @@ export function MarkdownTable({ node, className, children, ...props }: Component
|
|
|
125
145
|
</div>
|
|
126
146
|
{/* A keyboard reaches a sideways-scrolling table only when it can take focus. */}
|
|
127
147
|
<div tabIndex={0} role="region" aria-label="Table" className={cn("overflow-x-auto", focusRingInset)}>
|
|
128
|
-
<TableColumns.Provider value={
|
|
129
|
-
<table {...props} data-markdown-table="" className={cn("w-full border-collapse text-sm text-foreground", className)}>
|
|
148
|
+
<TableColumns.Provider value={kinds}>
|
|
149
|
+
<table {...props} data-markdown-table="" className={cn("w-full border-collapse text-sm text-foreground", PROSE_TABLE, className)}>
|
|
130
150
|
{children}
|
|
131
151
|
</table>
|
|
132
152
|
</TableColumns.Provider>
|
|
@@ -156,20 +176,21 @@ export function MarkdownTableRow({ node: _node, children, ...props }: ComponentP
|
|
|
156
176
|
);
|
|
157
177
|
}
|
|
158
178
|
|
|
159
|
-
function
|
|
160
|
-
const
|
|
179
|
+
function useColumnKind(): "figure" | "short" | "long" {
|
|
180
|
+
const kinds = useContext(TableColumns);
|
|
161
181
|
const column = useContext(CellColumn);
|
|
162
|
-
return column >= 0
|
|
182
|
+
return (column >= 0 ? kinds[column] : undefined) ?? "short";
|
|
163
183
|
}
|
|
164
184
|
|
|
165
185
|
export function MarkdownTableHeaderCell({ node: _node, className, ...props }: ComponentProps<"th"> & ExtraProps) {
|
|
166
|
-
const numeric =
|
|
186
|
+
const numeric = useColumnKind() === "figure";
|
|
167
187
|
return (
|
|
168
188
|
<th
|
|
169
189
|
{...props}
|
|
170
190
|
className={cn(
|
|
171
191
|
"whitespace-nowrap border-b border-border px-3 py-2 text-left font-semibold text-foreground",
|
|
172
|
-
|
|
192
|
+
PROSE_CELL,
|
|
193
|
+
numeric ? "text-right [.tangle-prose_&]:text-right!" : "[.tangle-prose_&]:text-left!",
|
|
173
194
|
className,
|
|
174
195
|
)}
|
|
175
196
|
/>
|
|
@@ -177,13 +198,15 @@ export function MarkdownTableHeaderCell({ node: _node, className, ...props }: Co
|
|
|
177
198
|
}
|
|
178
199
|
|
|
179
200
|
export function MarkdownTableCell({ node: _node, className, ...props }: ComponentProps<"td"> & ExtraProps) {
|
|
180
|
-
const
|
|
201
|
+
const kind = useColumnKind();
|
|
181
202
|
return (
|
|
182
203
|
<td
|
|
183
204
|
{...props}
|
|
184
205
|
className={cn(
|
|
185
206
|
"border-b border-border/60 px-3 py-2 align-top",
|
|
186
|
-
|
|
207
|
+
PROSE_CELL,
|
|
208
|
+
kind === "figure" ? "whitespace-nowrap text-right tabular-nums"
|
|
209
|
+
: kind === "long" ? "min-w-[14rem]" : "whitespace-nowrap",
|
|
187
210
|
className,
|
|
188
211
|
)}
|
|
189
212
|
/>
|
|
@@ -103,6 +103,8 @@ function MarkdownLink({ node: _node, children, className, ...props }: ComponentP
|
|
|
103
103
|
data-link-chip={chip.kind}
|
|
104
104
|
className={cn(
|
|
105
105
|
"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",
|
|
106
|
+
// An older, unlayered `.tangle-prose a` rule beats layered utilities.
|
|
107
|
+
"[.tangle-prose_&]:text-foreground! [.tangle-prose_&]:no-underline!",
|
|
106
108
|
focusRing,
|
|
107
109
|
className,
|
|
108
110
|
)}
|