@columenlabs/lumen-ui 0.1.196 → 0.1.197

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.
@@ -0,0 +1,3 @@
1
+ export default function ChatMarkdown({ children }: {
2
+ children: string;
3
+ }): import("react").JSX.Element;
@@ -0,0 +1,77 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import Markdown from 'react-markdown';
3
+ import remarkGfm from 'remark-gfm';
4
+ import { cn } from '../lib/utils.js';
5
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table.js';
6
+ const ALLOWED = [
7
+ 'p',
8
+ 'strong',
9
+ 'em',
10
+ 'del',
11
+ 'code',
12
+ 'ul',
13
+ 'ol',
14
+ 'li',
15
+ 'a',
16
+ 'pre',
17
+ 'br',
18
+ 'table',
19
+ 'thead',
20
+ 'tbody',
21
+ 'tr',
22
+ 'th',
23
+ 'td',
24
+ ];
25
+ // A number, with an optional sign, currency, thousands, decimals and a short unit (12, −3.5%,
26
+ // $1,204.10, 2.1×, 4.2K). A dash or an empty cell does not decide a column either way.
27
+ const NUMBER = /^[−+\-]?[$€£]?\d[\d,]*(\.\d+)?\s?(%|×|x|[KMBkmb])?$/;
28
+ const BLANK = /^(|—|–|-|n\/a)$/i;
29
+ function text(node) {
30
+ if (node.type === 'text')
31
+ return node.value ?? '';
32
+ return (node.children ?? []).map(text).join('');
33
+ }
34
+ const cells = (row) => (row.children ?? []).filter((c) => c.tagName === 'td' || c.tagName === 'th');
35
+ const rows = (section) => (section?.children ?? []).filter((c) => c.tagName === 'tr');
36
+ /**
37
+ * Marks each cell of a column that holds only numbers `dataNum`, so the column right-aligns on
38
+ * one edge, and gives every cell its text as `dataText` for its hover.
39
+ */
40
+ function rehypeNumberColumns() {
41
+ const visit = (node) => {
42
+ if (node.tagName === 'table') {
43
+ const head = node.children?.find((c) => c.tagName === 'thead');
44
+ const body = node.children?.find((c) => c.tagName === 'tbody');
45
+ const bodyRows = rows(body);
46
+ const width = Math.max(0, ...[...rows(head), ...bodyRows].map((r) => cells(r).length));
47
+ const numeric = Array.from({ length: width }, (_, i) => {
48
+ const values = bodyRows.map((r) => text(cells(r)[i] ?? { type: 'root' }).trim());
49
+ const filled = values.filter((v) => !BLANK.test(v));
50
+ return filled.length > 0 && filled.every((v) => NUMBER.test(v));
51
+ });
52
+ for (const r of [...rows(head), ...bodyRows]) {
53
+ cells(r).forEach((c, i) => {
54
+ c.properties = { ...c.properties, dataText: text(c).trim() };
55
+ if (numeric[i])
56
+ c.properties.dataNum = '';
57
+ });
58
+ }
59
+ return;
60
+ }
61
+ node.children?.forEach(visit);
62
+ };
63
+ return (tree) => visit(tree);
64
+ }
65
+ // A header keeps the label font; only its edge follows the numbers under it.
66
+ const cellClass = (p, head = false) => cn('max-w-xs overflow-hidden text-ellipsis', p['data-num'] !== undefined && (head ? 'text-right' : 'num text-right'));
67
+ const components = {
68
+ table: ({ children }) => (_jsx("div", { className: "not-prose my-2 rounded-md border", children: _jsx(Table, { className: "text-xs", children: children }) })),
69
+ thead: ({ children }) => _jsx(TableHeader, { children: children }),
70
+ tbody: ({ children }) => _jsx(TableBody, { children: children }),
71
+ tr: ({ children }) => _jsx(TableRow, { children: children }),
72
+ th: (p) => (_jsx(TableHead, { className: cn(cellClass(p, true), 'text-muted-foreground'), title: p['data-text'], children: p.children })),
73
+ td: (p) => (_jsx(TableCell, { className: cellClass(p), title: p['data-text'], children: p.children })),
74
+ };
75
+ export default function ChatMarkdown({ children }) {
76
+ return (_jsx(Markdown, { remarkPlugins: [remarkGfm], rehypePlugins: [rehypeNumberColumns], allowedElements: ALLOWED, unwrapDisallowed: true, components: components, children: children }));
77
+ }
@@ -21,8 +21,8 @@ import { Textarea } from '../ui/textarea.js';
21
21
  import { chatMarkdown, chatTurns, copyMarkdown, turnMarkdown } from './copy.js';
22
22
  import { composeAssistant, selectionRef, useComposeRequest, usePageSelection, useShownAssistantSources, } from './sources.js';
23
23
  import { useAssistantChat } from './use-assistant-chat.js';
24
- // The Markdown renderer loads with the first answer (lazy-part.tsx).
25
- const Markdown = lazyPart(() => import('react-markdown').then((m) => m.default));
24
+ // The Markdown renderer (tables included) loads with the first answer (lazy-part.tsx).
25
+ const ChatMarkdown = lazyPart(() => import('./chat-markdown.js').then((m) => m.default));
26
26
  /** The main model's context drawn full (tokens); the mark sits where the assistant compacts. */
27
27
  const CONTEXT_MAX = 200_000;
28
28
  const COMPACT_AT = 150_000;
@@ -256,7 +256,7 @@ function Item({ item, onSettle, busy, }) {
256
256
  case 'user':
257
257
  return (_jsxs("div", { className: "ml-8 self-end rounded-lg bg-muted px-3 py-2 text-sm whitespace-pre-wrap", children: [(item.tier || item.refs?.length) && (_jsxs("span", { className: "mb-1 flex flex-wrap gap-1", children: [item.tier && _jsx(ComposerChip, { sign: "@", label: tierName(item.tier) }), item.refs?.map((r, i) => (_jsx(ComposerChip, { sign: "#", label: r.label }, `${r.kind}:${r.label}:${i}`)))] })), item.text, item.context && (_jsxs("span", { className: "mt-1 block text-muted-foreground text-xs", children: ["on ", item.context] }))] }));
258
258
  case 'text':
259
- return (_jsxs("div", { className: "prose-sm text-sm leading-relaxed [&_code]:font-code [&_code]:text-xs [&_li]:ml-4 [&_ol]:list-decimal [&_p]:my-1.5 [&_ul]:list-disc", children: [_jsx(Markdown, { allowedElements: ['p', 'strong', 'em', 'code', 'ul', 'ol', 'li', 'a', 'pre', 'br'], unwrapDisallowed: true, children: item.text }), item.live && (_jsx("span", { className: "ml-0.5 inline-block h-3.5 w-1.5 animate-pulse bg-foreground/60 align-middle" }))] }));
259
+ return (_jsxs("div", { className: "prose-sm text-sm leading-relaxed [&_code]:font-code [&_code]:text-xs [&_li]:ml-4 [&_ol]:list-decimal [&_p]:my-1.5 [&_ul]:list-disc", children: [_jsx(ChatMarkdown, { children: item.text }), item.live && (_jsx("span", { className: "ml-0.5 inline-block h-3.5 w-1.5 animate-pulse bg-foreground/60 align-middle" }))] }));
260
260
  case 'step':
261
261
  return _jsx(Step, { step: item, live: !!item.live });
262
262
  case 'draft':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.196",
3
+ "version": "0.1.197",
4
4
  "description": "Lumen: shared React components (frame, views, widgets, charts, drawers).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -49,7 +49,7 @@
49
49
  "@codemirror/lang-javascript": "6.2.5",
50
50
  "@codemirror/lang-json": "6.0.2",
51
51
  "@codemirror/lang-sql": "6.10.0",
52
- "@columenlabs/lumen-ui-types": "0.1.196",
52
+ "@columenlabs/lumen-ui-types": "0.1.197",
53
53
  "@dnd-kit/core": "6.3.1",
54
54
  "@dnd-kit/sortable": "10.0.0",
55
55
  "@dnd-kit/utilities": "3.2.2",
@@ -80,6 +80,7 @@
80
80
  "react-markdown": "10.1.0",
81
81
  "react-resizable-panels": "^4.14.3",
82
82
  "recharts": "^3.10.1",
83
+ "remark-gfm": "4.0.1",
83
84
  "shiki": "4.5.0",
84
85
  "sonner": "^2.0.8",
85
86
  "tailwind-merge": "^3.7.0",