@braincrew-lab/langchain-canvas 0.1.11 → 0.1.13

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.
@@ -1,30 +1,32 @@
1
1
  import { useArtifactPatch } from './chunk-TERWLUW3.js';
2
2
  import './chunk-KKLWKR5G.js';
3
- import { useState, useMemo, useRef, useEffect } from 'react';
3
+ import { useState, useRef, useMemo, useEffect } from 'react';
4
4
  import * as echarts from 'echarts/core';
5
5
  import { BarChart, LineChart, PieChart } from 'echarts/charts';
6
6
  import { GridComponent, TooltipComponent, LegendComponent } from 'echarts/components';
7
7
  import { CanvasRenderer } from 'echarts/renderers';
8
- import { jsx, jsxs } from 'react/jsx-runtime';
8
+ import { jsxs, jsx } from 'react/jsx-runtime';
9
9
 
10
10
  echarts.use([BarChart, LineChart, PieChart, GridComponent, TooltipComponent, LegendComponent, CanvasRenderer]);
11
11
  var PALETTE = ["#6366f1", "#10b981", "#f59e0b", "#ef4444", "#0ea5e9", "#a855f7"];
12
12
  var seriesColor = (series, index) => series.color ?? PALETTE[index % PALETTE.length];
13
13
  var sliceColor = (options, index) => options?.colors?.[index] ?? PALETTE[index % PALETTE.length];
14
14
  var CHART_TYPES = ["bar", "line", "area", "pie"];
15
- function EChart({ option, height }) {
15
+ function EChart({ option, height, onInst }) {
16
16
  const boxRef = useRef(null);
17
17
  const instRef = useRef(null);
18
18
  useEffect(() => {
19
19
  if (!boxRef.current) return;
20
20
  const inst = echarts.init(boxRef.current, void 0, { renderer: "canvas" });
21
21
  instRef.current = inst;
22
+ onInst?.(inst);
22
23
  const ro = new ResizeObserver(() => inst.resize());
23
24
  ro.observe(boxRef.current);
24
25
  return () => {
25
26
  ro.disconnect();
26
27
  inst.dispose();
27
28
  instRef.current = null;
29
+ onInst?.(null);
28
30
  };
29
31
  }, []);
30
32
  useEffect(() => {
@@ -36,12 +38,28 @@ function ChartRenderer({ artifact }) {
36
38
  const { chart, rows, xKey, series, options, echartsOption } = artifact.data;
37
39
  const patch = useArtifactPatch(artifact.id);
38
40
  const [editing, setEditing] = useState(false);
41
+ const instRef = useRef(null);
42
+ const downloadPng = () => {
43
+ const inst = instRef.current;
44
+ if (!inst) return;
45
+ const url = inst.getDataURL({ type: "png", pixelRatio: 2, backgroundColor: "#ffffff" });
46
+ const a = document.createElement("a");
47
+ a.href = url;
48
+ a.download = `${(artifact.title || "chart").replace(/[^a-z0-9가-힣]+/gi, "-").replace(/^-+|-+$/g, "") || "chart"}.png`;
49
+ a.click();
50
+ };
39
51
  const option = useMemo(
40
52
  () => echartsOption ?? toEChartsOption(chart, rows, xKey, series, options),
41
53
  [echartsOption, chart, rows, xKey, series, options]
42
54
  );
43
55
  if (echartsOption) {
44
- return /* @__PURE__ */ jsx("div", { className: "cv-chart", children: /* @__PURE__ */ jsx(EChart, { option, height: 340 }) });
56
+ return /* @__PURE__ */ jsxs("div", { className: "cv-chart", children: [
57
+ /* @__PURE__ */ jsxs("div", { className: "cv-chart__toolbar cv-chrome", children: [
58
+ /* @__PURE__ */ jsx("span", { className: "cv-chart__spacer" }),
59
+ /* @__PURE__ */ jsx("button", { className: "cv-edit-btn", onClick: downloadPng, title: "Download as PNG", children: "\u2913 PNG" })
60
+ ] }),
61
+ /* @__PURE__ */ jsx(EChart, { option, height: 320, onInst: (i) => instRef.current = i })
62
+ ] });
45
63
  }
46
64
  if (rows.length === 0) {
47
65
  return /* @__PURE__ */ jsx("div", { className: "cv-chart cv-chart--empty", children: "Waiting for data\u2026" });
@@ -67,7 +85,18 @@ function ChartRenderer({ artifact }) {
67
85
  /* @__PURE__ */ jsx("input", { type: "checkbox", checked: !!options?.stacked, onChange: (e) => patch({ options: { ...options, stacked: e.target.checked } }) }),
68
86
  "Stacked"
69
87
  ] }),
88
+ /* @__PURE__ */ jsx(
89
+ "input",
90
+ {
91
+ className: "cv-chart__title-input",
92
+ value: options?.title ?? "",
93
+ placeholder: "Chart title\u2026",
94
+ onChange: (e) => patch({ options: { ...options, title: e.target.value || void 0 } }),
95
+ title: "Chart title"
96
+ }
97
+ ),
70
98
  /* @__PURE__ */ jsx("span", { className: "cv-chart__spacer" }),
99
+ /* @__PURE__ */ jsx("button", { className: "cv-edit-btn", onClick: downloadPng, title: "Download as PNG", children: "\u2913 PNG" }),
71
100
  /* @__PURE__ */ jsx("button", { className: `cv-edit-btn ${editing ? "is-primary" : ""}`, onClick: () => setEditing((v) => !v), children: editing ? "Done" : "Edit data" })
72
101
  ] }),
73
102
  editing && /* @__PURE__ */ jsxs("div", { className: "cv-chart__editor cv-chrome", children: [
@@ -92,7 +121,7 @@ function ChartRenderer({ artifact }) {
92
121
  ] }),
93
122
  /* @__PURE__ */ jsx(DataGrid, { rows, xKey, series, onCell: setCell, onAddRow: addRow, onRemoveRow: removeRow })
94
123
  ] }),
95
- /* @__PURE__ */ jsx(EChart, { option, height: editing ? 260 : 340 })
124
+ /* @__PURE__ */ jsx(EChart, { option, height: editing ? 260 : 340, onInst: (i) => instRef.current = i })
96
125
  ] });
97
126
  }
98
127
  function DataGrid({
@@ -128,9 +157,12 @@ function DataGrid({
128
157
  }
129
158
  function toEChartsOption(chart, rows, xKey, series, options) {
130
159
  const AXIS = "#9aa4b2";
160
+ const titleBlock = options?.title ? { title: { text: options.title, left: "center", top: 2, textStyle: { color: AXIS, fontSize: 15, fontWeight: 600 } } } : {};
161
+ const hasTitle = !!options?.title;
131
162
  if (chart === "pie") {
132
163
  const valueKey = series[0]?.key ?? "value";
133
164
  return {
165
+ ...titleBlock,
134
166
  tooltip: { trigger: "item" },
135
167
  legend: { bottom: 0, textStyle: { color: AXIS } },
136
168
  series: [
@@ -151,9 +183,10 @@ function toEChartsOption(chart, rows, xKey, series, options) {
151
183
  }
152
184
  const stacked = (chart === "bar" || chart === "area") && !!options?.stacked;
153
185
  return {
186
+ ...titleBlock,
154
187
  tooltip: { trigger: "axis" },
155
188
  legend: { bottom: 0, textStyle: { color: AXIS } },
156
- grid: { left: 8, right: 16, top: 24, bottom: 40, containLabel: true },
189
+ grid: { left: 8, right: 16, top: hasTitle ? 44 : 24, bottom: 40, containLabel: true },
157
190
  xAxis: {
158
191
  type: "category",
159
192
  data: rows.map((r) => String(r[xKey] ?? "")),
@@ -0,0 +1,123 @@
1
+ import { useArtifactPatch } from './chunk-TERWLUW3.js';
2
+ import './chunk-KKLWKR5G.js';
3
+ import { useState, useRef } from 'react';
4
+ import ReactMarkdown from 'react-markdown';
5
+ import remarkGfm from 'remark-gfm';
6
+ import { jsxs, jsx } from 'react/jsx-runtime';
7
+
8
+ function wordStats(text) {
9
+ const words = text.trim() ? text.trim().split(/\s+/).length : 0;
10
+ return { words, minutes: Math.max(1, Math.round(words / 200)) };
11
+ }
12
+ function DocumentRenderer({ artifact }) {
13
+ const { content } = artifact.data;
14
+ const patch = useArtifactPatch(artifact.id);
15
+ const [draft, setDraft] = useState(null);
16
+ const taRef = useRef(null);
17
+ const editing = draft !== null;
18
+ const commit = () => {
19
+ if (draft !== null && draft !== content) patch({ content: draft });
20
+ setDraft(null);
21
+ };
22
+ const apply = (fn) => {
23
+ const ta = taRef.current;
24
+ if (!ta || draft === null) return;
25
+ const { text, a, b } = fn(draft, ta.selectionStart, ta.selectionEnd);
26
+ setDraft(text);
27
+ requestAnimationFrame(() => {
28
+ ta.focus();
29
+ ta.setSelectionRange(a, b);
30
+ });
31
+ };
32
+ const wrap = (mark) => apply((full, s, e) => ({ text: full.slice(0, s) + mark + full.slice(s, e) + mark + full.slice(e), a: s + mark.length, b: e + mark.length }));
33
+ const link = () => apply((full, s, e) => {
34
+ const label = full.slice(s, e) || "link";
35
+ const ins = `[${label}](https://)`;
36
+ const urlAt = s + label.length + 3;
37
+ return { text: full.slice(0, s) + ins + full.slice(e), a: urlAt, b: urlAt + 8 };
38
+ });
39
+ const prefixLines = (prefix) => apply((full, s, e) => {
40
+ const start = full.lastIndexOf("\n", s - 1) + 1;
41
+ const nl = full.indexOf("\n", e);
42
+ const end = nl === -1 ? full.length : nl;
43
+ const block = full.slice(start, end).split("\n").map((l) => prefix + l).join("\n");
44
+ return { text: full.slice(0, start) + block + full.slice(end), a: start, b: start + block.length };
45
+ });
46
+ const { words, minutes } = wordStats(editing ? draft ?? "" : content);
47
+ return /* @__PURE__ */ jsxs("div", { className: "cv-word", children: [
48
+ editing && /* @__PURE__ */ jsxs("div", { className: "cv-doc-bar cv-chrome", onMouseDown: (e) => e.preventDefault(), children: [
49
+ /* @__PURE__ */ jsx("button", { title: "Bold", onMouseDown: (e) => {
50
+ e.preventDefault();
51
+ wrap("**");
52
+ }, children: /* @__PURE__ */ jsx("b", { children: "B" }) }),
53
+ /* @__PURE__ */ jsx("button", { title: "Italic", onMouseDown: (e) => {
54
+ e.preventDefault();
55
+ wrap("*");
56
+ }, children: /* @__PURE__ */ jsx("i", { children: "I" }) }),
57
+ /* @__PURE__ */ jsx("button", { title: "Inline code", onMouseDown: (e) => {
58
+ e.preventDefault();
59
+ wrap("`");
60
+ }, children: "<>" }),
61
+ /* @__PURE__ */ jsx("span", { className: "cv-doc-bar__sep" }),
62
+ /* @__PURE__ */ jsx("button", { title: "Heading 1", onMouseDown: (e) => {
63
+ e.preventDefault();
64
+ prefixLines("# ");
65
+ }, children: "H1" }),
66
+ /* @__PURE__ */ jsx("button", { title: "Heading 2", onMouseDown: (e) => {
67
+ e.preventDefault();
68
+ prefixLines("## ");
69
+ }, children: "H2" }),
70
+ /* @__PURE__ */ jsx("button", { title: "Bullet list", onMouseDown: (e) => {
71
+ e.preventDefault();
72
+ prefixLines("- ");
73
+ }, children: "\u2022" }),
74
+ /* @__PURE__ */ jsx("button", { title: "Numbered list", onMouseDown: (e) => {
75
+ e.preventDefault();
76
+ prefixLines("1. ");
77
+ }, children: "1." }),
78
+ /* @__PURE__ */ jsx("button", { title: "Quote", onMouseDown: (e) => {
79
+ e.preventDefault();
80
+ prefixLines("> ");
81
+ }, children: "\u275D" }),
82
+ /* @__PURE__ */ jsx("button", { title: "Link", onMouseDown: (e) => {
83
+ e.preventDefault();
84
+ link();
85
+ }, children: "\u{1F517}" }),
86
+ /* @__PURE__ */ jsx("span", { className: "cv-doc-bar__spacer" }),
87
+ /* @__PURE__ */ jsxs("span", { className: "cv-doc-bar__count", children: [
88
+ words,
89
+ " words \xB7 ",
90
+ minutes,
91
+ " min read"
92
+ ] }),
93
+ /* @__PURE__ */ jsx("button", { className: "cv-doc-bar__done", onMouseDown: (e) => {
94
+ e.preventDefault();
95
+ commit();
96
+ }, children: "Done" })
97
+ ] }),
98
+ /* @__PURE__ */ jsx("div", { className: "cv-word__page", children: editing ? /* @__PURE__ */ jsx(
99
+ "textarea",
100
+ {
101
+ ref: taRef,
102
+ className: "cv-doc-editor",
103
+ value: draft,
104
+ autoFocus: true,
105
+ onBlur: commit,
106
+ onChange: (e) => setDraft(e.target.value)
107
+ }
108
+ ) : /* @__PURE__ */ jsxs(
109
+ "article",
110
+ {
111
+ className: "cv-doc",
112
+ title: "Click to edit",
113
+ onClick: () => artifact.status !== "streaming" && setDraft(content),
114
+ children: [
115
+ /* @__PURE__ */ jsx(ReactMarkdown, { remarkPlugins: [remarkGfm], children: content }),
116
+ artifact.status === "streaming" && /* @__PURE__ */ jsx("span", { className: "cv-caret", "aria-hidden": true })
117
+ ]
118
+ }
119
+ ) })
120
+ ] });
121
+ }
122
+
123
+ export { DocumentRenderer };
@@ -27,7 +27,7 @@ function snapAxis(pos, size, targets) {
27
27
  }
28
28
  return best ? { pos: pos + best.delta, guide: best.guide } : { pos, guide: null };
29
29
  }
30
- function FreeSlide({ elements, onChange }) {
30
+ function FreeSlide({ elements, onChange, padding }) {
31
31
  const slideRef = useRef(null);
32
32
  const [els, setEls] = useState(elements);
33
33
  const [selected, setSelected] = useState(null);
@@ -136,6 +136,7 @@ function FreeSlide({ elements, onChange }) {
136
136
  {
137
137
  className: "cv-free",
138
138
  ref: slideRef,
139
+ style: padding ? { inset: `${padding}%` } : void 0,
139
140
  onPointerMove: onMove,
140
141
  onPointerUp: onUp,
141
142
  onPointerLeave: onUp,
@@ -200,10 +201,10 @@ function FreeSlide({ elements, onChange }) {
200
201
  /* @__PURE__ */ jsx("button", { onClick: () => updateEl(el.id, { align: "center" }), title: "Align center", children: "\u2261" }),
201
202
  /* @__PURE__ */ jsx("button", { onClick: () => updateEl(el.id, { align: "right" }), title: "Align right", children: "\u27F9" })
202
203
  ] }),
203
- selected === el.id && el.type === "shape" && /* @__PURE__ */ jsx("div", { className: `cv-free__fmt ${el.y < 16 ? "cv-free__fmt--below" : ""}`, onPointerDown: (e) => e.stopPropagation(), onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsx("input", { type: "color", value: el.fill ?? "#5b5bd6", onChange: (e) => updateEl(el.id, { fill: e.target.value }), title: "Fill color" }) }),
204
204
  selected === el.id && /* @__PURE__ */ jsxs(Fragment, { children: [
205
205
  /* @__PURE__ */ jsx("span", { className: "cv-free__resize", onPointerDown: (e) => onDown(e, el, "resize") }),
206
206
  /* @__PURE__ */ jsxs("div", { className: `cv-free__ctl ${el.y < 16 ? "cv-free__ctl--below" : ""}`, onPointerDown: (e) => e.stopPropagation(), children: [
207
+ el.type === "shape" && /* @__PURE__ */ jsx("input", { className: "cv-free__ctl-fill", type: "color", value: el.fill ?? "#5b5bd6", onChange: (e) => updateEl(el.id, { fill: e.target.value }), onClick: (e) => e.stopPropagation(), title: "Fill color" }),
207
208
  /* @__PURE__ */ jsx("button", { onClick: (e) => {
208
209
  e.stopPropagation();
209
210
  duplicate(el);
@@ -369,7 +370,7 @@ function SlidesRenderer({ artifact }) {
369
370
  onDragEnd: () => setDragIndex(null),
370
371
  children: /* @__PURE__ */ jsxs("button", { className: "cv-deck__thumb", onClick: () => setIndex(i), children: [
371
372
  /* @__PURE__ */ jsx("span", { className: "cv-deck__thumb-n", children: i + 1 }),
372
- /* @__PURE__ */ jsx("div", { className: "cv-deck__thumb-slide", style: s.background ? { background: s.background } : void 0, children: resolveElements(s).map(
373
+ /* @__PURE__ */ jsx("div", { className: "cv-deck__thumb-slide", style: s.background ? { background: s.background } : void 0, children: /* @__PURE__ */ jsx("div", { style: { position: "absolute", inset: `${s.padding ?? 0}%` }, children: resolveElements(s).map(
373
374
  (el) => el.type === "text" ? /* @__PURE__ */ jsx(
374
375
  "span",
375
376
  {
@@ -378,7 +379,7 @@ function SlidesRenderer({ artifact }) {
378
379
  },
379
380
  el.id
380
381
  ) : el.type === "shape" ? /* @__PURE__ */ jsx("div", { style: { position: "absolute", left: `${el.x}%`, top: `${el.y}%`, width: `${el.w}%`, height: `${el.h}%`, color: s.textColor, ...shapeStyle(el) } }, el.id) : /* @__PURE__ */ jsx("img", { src: el.src, alt: "", style: { position: "absolute", left: `${el.x}%`, top: `${el.y}%`, width: `${el.w}%`, height: `${el.h}%`, objectFit: "contain" } }, el.id)
381
- ) })
382
+ ) }) })
382
383
  ] })
383
384
  },
384
385
  i
@@ -442,6 +443,10 @@ function SlidesRenderer({ artifact }) {
442
443
  ),
443
444
  /* @__PURE__ */ jsx("label", { className: "cv-deck__bg", title: "Background color", children: /* @__PURE__ */ jsx("input", { type: "color", value: /^#/.test(slide.background ?? "") ? slide.background : "#ffffff", onChange: (e) => update({ background: e.target.value }) }) }),
444
445
  /* @__PURE__ */ jsx("button", { onClick: () => bgRef.current?.click(), title: "Background image", children: "\u{1F5BC} BG" }),
446
+ /* @__PURE__ */ jsxs("label", { className: "cv-deck__pad", title: "Content padding (% of slide)", children: [
447
+ "Pad",
448
+ /* @__PURE__ */ jsx("input", { type: "number", min: 0, max: 20, value: slide.padding ?? 0, onChange: (e) => update({ padding: Number(e.target.value) || void 0 }) })
449
+ ] }),
445
450
  /* @__PURE__ */ jsx("span", { className: "cv-deck__spacer" }),
446
451
  /* @__PURE__ */ jsx("button", { className: "cv-deck__present", onClick: () => setPresenting(true), title: "Present (full screen)", children: "\u25B6 Present" }),
447
452
  /* @__PURE__ */ jsx("button", { onClick: () => moveSlide(-1), title: "Move up", disabled: at === 0, children: "\u25B2" }),
@@ -449,7 +454,7 @@ function SlidesRenderer({ artifact }) {
449
454
  /* @__PURE__ */ jsx("button", { onClick: duplicateSlide, title: "Duplicate slide", children: "\u29C9" }),
450
455
  /* @__PURE__ */ jsx("button", { onClick: deleteSlide, title: "Delete slide", disabled: slides.length === 1, children: "\u{1F5D1}" })
451
456
  ] }),
452
- /* @__PURE__ */ jsx("div", { className: "cv-slide cv-slide--blank", style: slideStyle, children: /* @__PURE__ */ jsx(FreeSlide, { elements: resolveElements(slide), onChange: (elements) => update({ elements }) }) }),
457
+ /* @__PURE__ */ jsx("div", { className: "cv-slide cv-slide--blank", style: slideStyle, children: /* @__PURE__ */ jsx(FreeSlide, { elements: resolveElements(slide), onChange: (elements) => update({ elements }), padding: slide.padding }) }),
453
458
  /* @__PURE__ */ jsxs("div", { className: "cv-deck__nav cv-chrome", children: [
454
459
  /* @__PURE__ */ jsx("button", { disabled: at === 0, onClick: () => setIndex(at - 1), "aria-label": "Previous slide", children: "\u2039" }),
455
460
  /* @__PURE__ */ jsxs("span", { children: [
@@ -470,7 +475,7 @@ function SlidesRenderer({ artifact }) {
470
475
  )
471
476
  ] }),
472
477
  presenting && /* @__PURE__ */ jsxs("div", { className: "cv-present", onClick: () => setIndex(Math.min(at + 1, slides.length - 1)), children: [
473
- /* @__PURE__ */ jsx("div", { className: "cv-present__slide cv-present__fade cv-slide cv-slide--blank", style: slideStyle, children: /* @__PURE__ */ jsx("div", { className: "cv-free", children: resolveElements(slide).map(
478
+ /* @__PURE__ */ jsx("div", { className: "cv-present__slide cv-present__fade cv-slide cv-slide--blank", style: slideStyle, children: /* @__PURE__ */ jsx("div", { className: "cv-free", style: slide.padding ? { inset: `${slide.padding}%` } : void 0, children: resolveElements(slide).map(
474
479
  (el) => el.type === "text" ? /* @__PURE__ */ jsx("div", { className: "cv-free__el", style: { left: `${el.x}%`, top: `${el.y}%`, width: `${el.w}%`, height: `${el.h}%` }, children: /* @__PURE__ */ jsx("div", { className: "cv-free__text", style: { fontSize: el.fontSize ?? 24, fontWeight: el.bold ? 700 : 400, color: el.color, textAlign: el.align ?? "left", whiteSpace: "pre-wrap" }, children: el.text }) }, el.id) : el.type === "shape" ? /* @__PURE__ */ jsx("div", { className: "cv-free__el", style: { left: `${el.x}%`, top: `${el.y}%`, width: `${el.w}%`, height: `${el.h}%`, color: slide.textColor }, children: /* @__PURE__ */ jsx("div", { style: shapeStyle(el) }) }, el.id) : /* @__PURE__ */ jsx("div", { className: "cv-free__el", style: { left: `${el.x}%`, top: `${el.y}%`, width: `${el.w}%`, height: `${el.h}%` }, children: /* @__PURE__ */ jsx("img", { className: "cv-free__img", src: el.src, alt: "" }) }, el.id)
475
480
  ) }) }, at),
476
481
  slide.notes ? /* @__PURE__ */ jsx("div", { className: "cv-present__notes", onClick: (e) => e.stopPropagation(), children: slide.notes }) : null,
@@ -2,7 +2,7 @@ import { loadOptional } from './chunk-YZZSJJMQ.js';
2
2
  import { useCanvasStore } from './chunk-KKLWKR5G.js';
3
3
  import { lazy, useMemo, useState, useRef, useEffect, useCallback, Suspense } from 'react';
4
4
  import '@fortune-sheet/react/dist/index.css';
5
- import { jsx, jsxs } from 'react/jsx-runtime';
5
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
6
 
7
7
  // src/io/formula.ts
8
8
  var EMPTY = /* @__PURE__ */ new Map();
@@ -203,10 +203,34 @@ function TableRenderer({ artifact }) {
203
203
  };
204
204
  }, [dataKey, hasFormulas]);
205
205
  const hasSheet = !!artifact.data.sheet?.length;
206
+ const [sortCol, setSortCol] = useState("");
207
+ const [sortDir, setSortDir] = useState(1);
208
+ const [filter, setFilter] = useState("");
209
+ const [appliedFilter, setAppliedFilter] = useState("");
210
+ useEffect(() => {
211
+ const t = setTimeout(() => setAppliedFilter(filter), 300);
212
+ return () => clearTimeout(t);
213
+ }, [filter]);
214
+ const viewRows = useMemo(() => {
215
+ let r = rows;
216
+ const q = appliedFilter.trim().toLowerCase();
217
+ if (q) r = r.filter((row) => columns.some((c) => String(row[c.key] ?? "").toLowerCase().includes(q)));
218
+ if (sortCol) {
219
+ r = [...r].sort((a, b) => {
220
+ const av = a[sortCol];
221
+ const bv = b[sortCol];
222
+ const cmp = typeof av === "number" && typeof bv === "number" ? av - bv : String(av ?? "").localeCompare(String(bv ?? ""));
223
+ return cmp * sortDir;
224
+ });
225
+ }
226
+ return r;
227
+ }, [rows, columns, appliedFilter, sortCol, sortDir]);
228
+ const viewActive = !!appliedFilter.trim() || !!sortCol;
229
+ const wbKey = `${dataKey}:${viewActive ? `view-s${sortCol}${sortDir}-f${appliedFilter}` : hasSheet ? "sheet" : "rows"}`;
206
230
  const initialData = useMemo(
207
- () => artifact.data.sheet?.length ? artifact.data.sheet : toWorkbook(columns, rows, formulas),
231
+ () => viewActive ? toWorkbook(columns, viewRows, formulas) : hasSheet ? artifact.data.sheet : toWorkbook(columns, rows, formulas),
208
232
  // eslint-disable-next-line react-hooks/exhaustive-deps
209
- [dataKey, formulasReady]
233
+ [wbKey, formulasReady]
210
234
  );
211
235
  const applyEvent = useCanvasStore((s) => s.applyUserEvent);
212
236
  const persistTimer = useRef(null);
@@ -241,9 +265,36 @@ function TableRenderer({ artifact }) {
241
265
  /* @__PURE__ */ jsxs("div", { className: "cv-sheet-tools", children: [
242
266
  /* @__PURE__ */ jsx("button", { type: "button", onClick: () => insert("column"), children: "\uFF0B Column" }),
243
267
  /* @__PURE__ */ jsx("button", { type: "button", onClick: () => insert("row"), children: "\uFF0B Row" }),
268
+ columns.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
269
+ /* @__PURE__ */ jsx("span", { className: "cv-sheet-tools__sep" }),
270
+ /* @__PURE__ */ jsxs(
271
+ "select",
272
+ {
273
+ className: "cv-sheet-tools__sort",
274
+ value: sortCol,
275
+ title: "Sort by column",
276
+ onChange: (e) => setSortCol(e.target.value),
277
+ children: [
278
+ /* @__PURE__ */ jsx("option", { value: "", children: "Sort\u2026" }),
279
+ columns.map((c) => /* @__PURE__ */ jsx("option", { value: c.key, children: c.label ?? c.key }, c.key))
280
+ ]
281
+ }
282
+ ),
283
+ sortCol && /* @__PURE__ */ jsx("button", { type: "button", title: sortDir === 1 ? "Ascending" : "Descending", onClick: () => setSortDir((d) => d === 1 ? -1 : 1), children: sortDir === 1 ? "\u25B2" : "\u25BC" }),
284
+ /* @__PURE__ */ jsx(
285
+ "input",
286
+ {
287
+ className: "cv-sheet-tools__filter",
288
+ value: filter,
289
+ placeholder: "Filter\u2026",
290
+ onChange: (e) => setFilter(e.target.value),
291
+ title: "Filter rows"
292
+ }
293
+ )
294
+ ] }),
244
295
  /* @__PURE__ */ jsx("span", { className: "cv-sheet-tools__hint", children: "Right-click a header for more, or drag to edit" })
245
296
  ] }),
246
- /* @__PURE__ */ jsx("div", { className: "cv-sheet", ref: rootRef, children: /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx("div", { className: "cv-sheet--empty", children: "Loading\u2026" }), children: /* @__PURE__ */ jsx(Workbook, { ref: wbRef, data: initialData, onChange: handleChange }, dataKey) }) })
297
+ /* @__PURE__ */ jsx("div", { className: "cv-sheet", ref: rootRef, children: /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx("div", { className: "cv-sheet--empty", children: "Loading\u2026" }), children: /* @__PURE__ */ jsx(Workbook, { ref: wbRef, data: initialData, onChange: handleChange }, wbKey) }) })
247
298
  ] });
248
299
  }
249
300
 
package/dist/index.d.ts CHANGED
@@ -46,6 +46,8 @@ interface ChartSeries {
46
46
  interface ChartOptions {
47
47
  stacked?: boolean;
48
48
  yLabel?: string;
49
+ /** Chart title shown above the plot. */
50
+ title?: string;
49
51
  /** Per-slice colors for pie charts, index-aligned to `rows`. */
50
52
  colors?: string[];
51
53
  }
@@ -117,6 +119,9 @@ interface Slide {
117
119
  textColor?: string;
118
120
  /** Speaker notes (not shown on the slide; exported to the .pptx notes pane). */
119
121
  notes?: string;
122
+ /** Content padding as a percent of the slide width (a safe margin around the
123
+ * free canvas). Applied in the editor, present view, thumbnails, and export. */
124
+ padding?: number;
120
125
  }
121
126
  interface SlidesData {
122
127
  slides: Slide[];
package/dist/index.js CHANGED
@@ -2111,10 +2111,10 @@ function HtmlRenderer({ artifact: artifact2 }) {
2111
2111
  }
2112
2112
 
2113
2113
  // src/components/renderers/index.ts
2114
- var ChartRenderer = lazy(() => import('./ChartRenderer-4MWKP2J4.js').then((m) => ({ default: m.ChartRenderer })));
2115
- var DocumentRenderer = lazy(() => import('./DocumentRenderer-LJGP6N7E.js').then((m) => ({ default: m.DocumentRenderer })));
2116
- var TableRenderer = lazy(() => import('./TableRenderer-ZF7MVA7I.js').then((m) => ({ default: m.TableRenderer })));
2117
- var SlidesRenderer = lazy(() => import('./SlidesRenderer-7ICJQHYT.js').then((m) => ({ default: m.SlidesRenderer })));
2114
+ var ChartRenderer = lazy(() => import('./ChartRenderer-FX6ZN4NC.js').then((m) => ({ default: m.ChartRenderer })));
2115
+ var DocumentRenderer = lazy(() => import('./DocumentRenderer-KJ6FTGKH.js').then((m) => ({ default: m.DocumentRenderer })));
2116
+ var TableRenderer = lazy(() => import('./TableRenderer-Y4JFRSLU.js').then((m) => ({ default: m.TableRenderer })));
2117
+ var SlidesRenderer = lazy(() => import('./SlidesRenderer-SK5VQDIL.js').then((m) => ({ default: m.SlidesRenderer })));
2118
2118
  var builtinRenderers = {
2119
2119
  html: HtmlRenderer,
2120
2120
  document: DocumentRenderer,
@@ -2255,8 +2255,10 @@ async function slidesToPptx(data, _title) {
2255
2255
  const s = pptx.addSlide();
2256
2256
  if (slide.background && /^#[0-9a-f]{3,8}$/i.test(slide.background)) s.background = { color: slide.background.replace("#", "") };
2257
2257
  const tc = slide.textColor ? slide.textColor.replace("#", "") : void 0;
2258
+ const pad = (slide.padding ?? 0) / 100;
2259
+ const inset = (v) => pad + v / 100 * (1 - 2 * pad);
2258
2260
  for (const el of resolveElements(slide)) {
2259
- const box = { x: el.x / 100 * W, y: el.y / 100 * H, w: el.w / 100 * W, h: el.h / 100 * H };
2261
+ const box = { x: inset(el.x) * W, y: inset(el.y) * H, w: el.w / 100 * (1 - 2 * pad) * W, h: el.h / 100 * (1 - 2 * pad) * H };
2260
2262
  if (el.type === "text") {
2261
2263
  const color = el.color ? el.color.replace("#", "") : tc;
2262
2264
  s.addText(el.text ?? "", { ...box, fontSize: (el.fontSize ?? 24) * 0.75, bold: !!el.bold, align: el.align ?? "left", ...color ? { color } : {} });
@@ -2294,7 +2296,9 @@ function slidesToPrintHtml(data, title) {
2294
2296
  const src = safeSrc(el.src);
2295
2297
  return src ? `<img class="el" style="${box}" src="${escapeAttr(src)}"/>` : "";
2296
2298
  }).join("");
2297
- return `<section class="slide" style="background:${escapeAttr(bg)}">${els}</section>`;
2299
+ const pad = slide.padding ?? 0;
2300
+ const inner = pad ? `<div style="position:absolute;inset:${pad}%">${els}</div>` : els;
2301
+ return `<section class="slide" style="background:${escapeAttr(bg)}">${inner}</section>`;
2298
2302
  }).join("");
2299
2303
  return `<!doctype html><html><head><meta charset="utf-8"><title>${escapeXml(title)}</title><style>
2300
2304
  @page { size: 1280px 720px; margin: 0; }
package/dist/styles.css CHANGED
@@ -295,6 +295,21 @@
295
295
 
296
296
  /* --- Word viewer (document page) ---------------------------------------------- */
297
297
 
298
+ .cv-doc-bar {
299
+ position: sticky; top: 0; z-index: 5; display: flex; align-items: center; gap: 4px;
300
+ max-width: 816px; margin: 0 auto 12px; padding: 6px 8px;
301
+ border: 1px solid var(--cv-border); border-radius: 10px; background: var(--cv-surface);
302
+ flex-wrap: wrap;
303
+ }
304
+ .cv-doc-bar button {
305
+ border: 0; background: transparent; color: var(--cv-text); font: inherit; font-size: 13px;
306
+ min-width: 28px; height: 28px; padding: 0 8px; border-radius: 6px; cursor: pointer;
307
+ }
308
+ .cv-doc-bar button:hover { background: var(--cv-surface-2, rgba(120,120,140,0.12)); }
309
+ .cv-doc-bar__sep { width: 1px; height: 18px; background: var(--cv-border); margin: 0 4px; }
310
+ .cv-doc-bar__spacer { flex: 1; }
311
+ .cv-doc-bar__count { font-size: 12px; color: var(--cv-muted); white-space: nowrap; }
312
+ .cv-doc-bar__done { background: var(--cv-accent) !important; color: #fff; font-weight: 600; }
298
313
  .cv-word { background: #edeef1; padding: 28px 16px; min-height: 100%; }
299
314
  .cv-word .cv-edit-toolbar { max-width: 816px; margin: 0 auto 12px; }
300
315
  .cv-word__page {
@@ -381,6 +396,13 @@
381
396
  border-radius: 6px; background: var(--cv-bg); color: var(--cv-text); cursor: pointer;
382
397
  }
383
398
  .cv-sheet-tools button:hover { border-color: var(--cv-accent, #6366f1); color: var(--cv-accent, #6366f1); }
399
+ .cv-sheet-tools__sep { width: 1px; height: 18px; background: var(--cv-border); margin: 0 2px; }
400
+ .cv-sheet-tools__sort, .cv-sheet-tools__filter {
401
+ font: inherit; font-size: 12.5px; padding: 4px 8px; border-radius: 6px;
402
+ border: 1px solid var(--cv-border); background: var(--cv-bg); color: var(--cv-text);
403
+ }
404
+ .cv-sheet-tools__filter { min-width: 96px; max-width: 160px; }
405
+ .cv-sheet-tools__sort:focus, .cv-sheet-tools__filter:focus { outline: none; border-color: var(--cv-accent); }
384
406
  .cv-sheet-tools__hint { margin-left: auto; font-size: 11px; color: var(--cv-muted); }
385
407
  .cv-sheet { position: relative; width: 100%; flex: 1; height: auto; min-height: 0; }
386
408
  .cv-sheet .fortune-container { height: 100% !important; width: 100% !important; }
@@ -596,6 +618,13 @@
596
618
  width: 30px; height: 30px; padding: 0;
597
619
  border: 1px solid var(--cv-border); border-radius: 7px; background: none; cursor: pointer;
598
620
  }
621
+ .cv-deck__pad {
622
+ display: inline-flex; align-items: center; gap: 5px; font-size: 12px; color: var(--cv-muted);
623
+ }
624
+ .cv-deck__pad input {
625
+ width: 48px; font: inherit; font-size: 12.5px; padding: 4px 6px;
626
+ border: 1px solid var(--cv-border); border-radius: 6px; background: var(--cv-bg); color: var(--cv-text);
627
+ }
599
628
  .cv-deck__present {
600
629
  width: auto !important; padding: 0 12px !important;
601
630
  background: var(--cv-accent) !important; color: #fff !important;
@@ -690,6 +719,12 @@
690
719
  .cv-chart__types .cv-edit-btn { text-transform: capitalize; }
691
720
  .cv-chart__stack { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--cv-muted); cursor: pointer; }
692
721
  .cv-chart__spacer { flex: 1; }
722
+ .cv-chart__title-input {
723
+ font: inherit; font-size: 13px; padding: 5px 10px; border-radius: 7px;
724
+ border: 1px solid var(--cv-border); background: var(--cv-bg); color: var(--cv-text);
725
+ min-width: 120px; max-width: 220px;
726
+ }
727
+ .cv-chart__title-input:focus { outline: none; border-color: var(--cv-accent); }
693
728
 
694
729
  /* --- chart editor ------------------------------------------------------------- */
695
730
  .cv-chart__editor {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@braincrew-lab/langchain-canvas",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "A live canvas for LangChain agents — stream documents, charts, and rich artifacts into a React panel.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,40 +0,0 @@
1
- import { useArtifactPatch } from './chunk-TERWLUW3.js';
2
- import './chunk-KKLWKR5G.js';
3
- import { useState } from 'react';
4
- import ReactMarkdown from 'react-markdown';
5
- import remarkGfm from 'remark-gfm';
6
- import { jsx, jsxs } from 'react/jsx-runtime';
7
-
8
- function DocumentRenderer({ artifact }) {
9
- const { content } = artifact.data;
10
- const patch = useArtifactPatch(artifact.id);
11
- const [draft, setDraft] = useState(null);
12
- const editing = draft !== null;
13
- const commit = () => {
14
- if (draft !== null && draft !== content) patch({ content: draft });
15
- setDraft(null);
16
- };
17
- return /* @__PURE__ */ jsx("div", { className: "cv-word", children: /* @__PURE__ */ jsx("div", { className: "cv-word__page", children: editing ? /* @__PURE__ */ jsx(
18
- "textarea",
19
- {
20
- className: "cv-doc-editor",
21
- value: draft,
22
- autoFocus: true,
23
- onBlur: commit,
24
- onChange: (e) => setDraft(e.target.value)
25
- }
26
- ) : /* @__PURE__ */ jsxs(
27
- "article",
28
- {
29
- className: "cv-doc",
30
- title: "Click to edit",
31
- onClick: () => artifact.status !== "streaming" && setDraft(content),
32
- children: [
33
- /* @__PURE__ */ jsx(ReactMarkdown, { remarkPlugins: [remarkGfm], children: content }),
34
- artifact.status === "streaming" && /* @__PURE__ */ jsx("span", { className: "cv-caret", "aria-hidden": true })
35
- ]
36
- }
37
- ) }) });
38
- }
39
-
40
- export { DocumentRenderer };