@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
- return cells.length > 0 && cells.every((cell) => NUMERIC_CELL.test(cell));
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, numeric, rowCount } = useMemo(() => {
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
- numeric: numericColumns(body),
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: numeric,
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 useNumericCell() {
150
- const numeric = useContext(TableColumns);
162
+ function useColumnKind() {
163
+ const kinds = useContext(TableColumns);
151
164
  const column = useContext(CellColumn);
152
- return column >= 0 && numeric[column] === true;
165
+ return (column >= 0 ? kinds[column] : void 0) ?? "short";
153
166
  }
154
167
  function MarkdownTableHeaderCell({ node: _node, className, ...props }) {
155
- const numeric = useNumericCell();
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 && "text-right", className)
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 numeric = useNumericCell();
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", numeric ? "whitespace-nowrap text-right tabular-nums" : "min-w-[8rem]", className)
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.1",
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.19.6",
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
- return cells.length > 0 && cells.every((cell) => NUMERIC_CELL.test(cell));
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
- const TableColumns = createContext<boolean[]>([]);
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, numeric, rowCount } = useMemo(() => {
136
+ const { csv, kinds, rowCount } = useMemo(() => {
117
137
  const { header, body } = node ? rowsOf(node) : { header: [], body: [] };
118
- return { csv: tableCsv([...header, ...body]), numeric: numericColumns(body), rowCount: body.length };
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={numeric}>
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 useNumericCell(): boolean {
160
- const numeric = useContext(TableColumns);
179
+ function useColumnKind(): "figure" | "short" | "long" {
180
+ const kinds = useContext(TableColumns);
161
181
  const column = useContext(CellColumn);
162
- return column >= 0 && numeric[column] === true;
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 = useNumericCell();
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
- numeric && "text-right",
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 numeric = useNumericCell();
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
- numeric ? "whitespace-nowrap text-right tabular-nums" : "min-w-[8rem]",
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
  )}