@braincrew-lab/langchain-canvas 0.1.14 → 0.2.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/dist/styles.css CHANGED
@@ -212,6 +212,16 @@
212
212
  .cv-body--flush { padding: 0; overflow: hidden; }
213
213
  .cv-fallback { color: var(--cv-muted); font-size: 14px; }
214
214
 
215
+ /* Browsing an old version: a preview, not an editing surface. Interaction is
216
+ blocked wholesale — renderers patch by artifact id, so edits made "in the
217
+ past" would silently overwrite the live version. */
218
+ .cv-body--history > :not(.cv-history-note) { pointer-events: none; user-select: none; }
219
+ .cv-history-note {
220
+ position: sticky; top: 0; z-index: 5; margin-bottom: 12px;
221
+ padding: 6px 12px; border-radius: 8px; font-size: 12.5px; font-weight: 600;
222
+ color: var(--cv-muted); background: var(--cv-surface); border: 1px solid var(--cv-border);
223
+ }
224
+
215
225
  /* --- inline editing (document / table / slides) ------------------------------- */
216
226
 
217
227
  .cv-edit-toolbar { display: flex; justify-content: flex-end; margin-bottom: 10px; }
@@ -404,6 +414,23 @@
404
414
  .cv-sheet-tools__filter { min-width: 96px; max-width: 160px; }
405
415
  .cv-sheet-tools__sort:focus, .cv-sheet-tools__filter:focus { outline: none; border-color: var(--cv-accent); }
406
416
  .cv-sheet-tools__hint { margin-left: auto; font-size: 11px; color: var(--cv-muted); }
417
+
418
+ /* selection formatting cluster — reads as one tight group between separators */
419
+ .cv-sheet-tools__fmt { display: inline-flex; align-items: center; gap: 3px; }
420
+ .cv-sheet-tools__fmt button:disabled,
421
+ .cv-sheet-tools button:disabled { opacity: 0.4; cursor: default; }
422
+ .cv-sheet-tools button:disabled:hover { border-color: var(--cv-border); color: var(--cv-text); }
423
+ .cv-sheet-tools__bold, .cv-sheet-tools__align { width: 28px; padding: 4px 0 !important; text-align: center; }
424
+ .cv-sheet-tools__bold--on,
425
+ .cv-sheet-tools__freeze--on {
426
+ background: var(--cv-accent-weak) !important; border-color: var(--cv-accent) !important;
427
+ color: var(--cv-accent) !important;
428
+ }
429
+ .cv-sheet-tools__color {
430
+ width: 28px; height: 26px; padding: 2px; border: 1px solid var(--cv-border);
431
+ border-radius: 6px; background: var(--cv-bg); cursor: pointer;
432
+ }
433
+ .cv-sheet-tools__color:disabled { opacity: 0.4; cursor: default; }
407
434
  .cv-sheet { position: relative; width: 100%; flex: 1; height: auto; min-height: 0; }
408
435
  .cv-sheet .fortune-container { height: 100% !important; width: 100% !important; }
409
436
  .cv-sheet--empty { display: grid; place-items: center; height: 200px; color: var(--cv-muted); font-size: 14px; }
@@ -613,6 +640,31 @@
613
640
  .cv-free__fmt input[type="number"] { width: 46px; height: 24px; border: 1px solid var(--cv-border); border-radius: 5px; background: var(--cv-bg); color: var(--cv-text); font-size: 12px; padding: 0 4px; }
614
641
  .cv-free__fmt input[type="color"] { width: 26px; height: 24px; padding: 0; border: 1px solid var(--cv-border); border-radius: 5px; background: none; cursor: pointer; }
615
642
 
643
+ /* multi-select: marquee, selection state, floating action bar */
644
+ .cv-free { outline: none; } /* the canvas is focusable for keyboard nudge/shortcuts */
645
+ .cv-free:focus-visible { outline: 2px solid var(--cv-accent); outline-offset: -2px; border-radius: 4px; }
646
+ .cv-free__marquee {
647
+ position: absolute; z-index: 7; pointer-events: none;
648
+ border: 1px solid var(--cv-accent);
649
+ background: color-mix(in srgb, var(--cv-accent) 10%, transparent);
650
+ border-radius: 2px;
651
+ }
652
+ .cv-free__el--selected { outline: 1.5px solid var(--cv-accent); outline-offset: 1px; }
653
+ .cv-free__multibar {
654
+ position: absolute; z-index: 8; transform: translate(-50%, calc(-100% - 8px));
655
+ display: flex; align-items: center; gap: 2px;
656
+ padding: 3px; background: var(--cv-bg); border: 1px solid var(--cv-border);
657
+ border-radius: 8px; box-shadow: 0 6px 20px rgba(0, 0, 0, 0.16); cursor: default;
658
+ }
659
+ .cv-free__multibar--below { transform: translate(-50%, 8px); }
660
+ .cv-free__multibar button {
661
+ min-width: 24px; height: 24px; padding: 0 6px; border: none; border-radius: 5px;
662
+ background: transparent; color: var(--cv-text); cursor: pointer; font-size: 12px; white-space: nowrap;
663
+ }
664
+ .cv-free__multibar button:hover { background: var(--cv-surface); }
665
+ .cv-free__multibar button:disabled { opacity: 0.4; cursor: default; background: transparent; }
666
+ .cv-free__multibar-del:hover { background: #fee2e2 !important; color: #dc2626; }
667
+
616
668
  /* toolbar: background swatch + present button */
617
669
  .cv-deck__bg input[type="color"] {
618
670
  width: 30px; height: 30px; padding: 0;
@@ -656,6 +708,22 @@
656
708
  background: rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.08);
657
709
  border-radius: 10px; padding: 12px 16px; cursor: default; white-space: pre-wrap;
658
710
  }
711
+ /* Deck progress: a hairline track pinned to the bottom edge, plus a counter chip. */
712
+ .cv-present__progress {
713
+ position: absolute; left: 0; right: 0; bottom: 0; height: 3px;
714
+ background: rgba(255, 255, 255, 0.08);
715
+ }
716
+ .cv-present__progress-fill {
717
+ height: 100%; background: var(--cv-accent);
718
+ transition: width 0.28s ease;
719
+ }
720
+ @media (prefers-reduced-motion: reduce) { .cv-present__progress-fill { transition: none; } }
721
+ .cv-present__count {
722
+ position: absolute; top: 16px; right: 20px;
723
+ color: #9aa4b2; font-size: 13px; font-variant-numeric: tabular-nums;
724
+ background: rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.08);
725
+ border-radius: 999px; padding: 3px 10px; cursor: default;
726
+ }
659
727
 
660
728
  /* in-place editing cues */
661
729
  .cv-word .cv-doc { cursor: text; }
@@ -1086,6 +1154,36 @@
1086
1154
  color: var(--cv-muted);
1087
1155
  }
1088
1156
 
1157
+ /* --- PDF viewer ---------------------------------------------------------------- */
1158
+
1159
+ /* Fills the flush body outright — `flex: 1` alone collapsed the iframe to its
1160
+ content height (a thin strip of page) because `.cv-body` is a block, not a
1161
+ flex container. */
1162
+ .cv-pdf-panel { display: flex; flex-direction: column; height: 100%; min-height: 480px; }
1163
+ .cv-pdf-tools {
1164
+ display: flex; align-items: center; gap: 6px; padding: 6px 10px;
1165
+ border-bottom: 1px solid var(--cv-border); background: var(--cv-bg); flex: 0 0 auto;
1166
+ }
1167
+ .cv-pdf-tools button {
1168
+ font-size: 12px; font-weight: 600; padding: 4px 10px; border: 1px solid var(--cv-border);
1169
+ border-radius: 6px; background: var(--cv-bg); color: var(--cv-text); cursor: pointer;
1170
+ }
1171
+ .cv-pdf-tools button:hover { border-color: var(--cv-accent); color: var(--cv-accent); }
1172
+ .cv-pdf-tools__name {
1173
+ font-size: 12px; font-weight: 600; color: var(--cv-muted);
1174
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 55%;
1175
+ }
1176
+ .cv-pdf-tools__spacer { flex: 1; }
1177
+ .cv-pdf-tools__sep { width: 1px; height: 18px; background: var(--cv-border); margin: 0 2px; }
1178
+ .cv-pdf-tools__zoom { display: inline-flex; align-items: center; gap: 3px; }
1179
+ .cv-pdf-tools__zoom button { min-width: 26px; }
1180
+ .cv-pdf-tools__zoom-label {
1181
+ min-width: 52px; font-variant-numeric: tabular-nums; color: var(--cv-muted);
1182
+ }
1183
+ .cv-pdf { position: relative; flex: 1; min-height: 0; background: var(--cv-surface); }
1184
+ .cv-pdf__frame { display: block; width: 100%; height: 100%; border: 0; }
1185
+ .cv-pdf--empty { display: grid; place-items: center; min-height: 200px; color: var(--cv-muted); }
1186
+
1089
1187
  /* --- inline artifact card (for the transcript) -------------------------------- */
1090
1188
 
1091
1189
  .cv-card {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@braincrew-lab/langchain-canvas",
3
- "version": "0.1.14",
3
+ "version": "0.2.0",
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,492 +0,0 @@
1
- import { resolveElements } from './chunk-6L3AL6W4.js';
2
- import { useArtifactPatch } from './chunk-TERWLUW3.js';
3
- import './chunk-KKLWKR5G.js';
4
- import { useState, useRef, useEffect } from 'react';
5
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
-
7
- function shapeStyle(el) {
8
- const fill = el.fill ?? "currentColor";
9
- if (el.shape === "ellipse") return { width: "100%", height: "100%", background: fill, borderRadius: "50%" };
10
- if (el.shape === "line") return { width: "100%", height: "100%", background: fill, borderRadius: 2 };
11
- return { width: "100%", height: "100%", background: fill, borderRadius: 8 };
12
- }
13
- var clamp = (v, min, max) => Math.max(min, Math.min(max, v));
14
- var dupSeq = 0;
15
- var dupId = (base) => `${base}_c${Date.now().toString(36)}${dupSeq++}`;
16
- var SNAP = 1.2;
17
- function snapAxis(pos, size, targets) {
18
- const anchors = [pos, pos + size / 2, pos + size];
19
- let best = null;
20
- for (const anchor of anchors) {
21
- for (const t of targets) {
22
- const delta = t - anchor;
23
- if (Math.abs(delta) <= SNAP && (!best || Math.abs(delta) < Math.abs(best.delta))) {
24
- best = { delta, guide: t };
25
- }
26
- }
27
- }
28
- return best ? { pos: pos + best.delta, guide: best.guide } : { pos, guide: null };
29
- }
30
- function FreeSlide({ elements, onChange, padding }) {
31
- const slideRef = useRef(null);
32
- const [els, setEls] = useState(elements);
33
- const [selected, setSelected] = useState(null);
34
- const [editingId, setEditingId] = useState(null);
35
- const [guides, setGuides] = useState({ x: null, y: null });
36
- const drag = useRef(null);
37
- useEffect(() => {
38
- if (!drag.current) setEls(elements);
39
- }, [elements]);
40
- const commit = (next) => {
41
- setEls(next);
42
- onChange(next);
43
- };
44
- const updateEl = (id, partial) => commit(els.map((el) => el.id === id ? { ...el, ...partial } : el));
45
- const duplicate = (el) => {
46
- const copy = {
47
- ...el,
48
- id: dupId(el.id),
49
- x: Math.min(el.x + 4, 100 - el.w),
50
- y: Math.min(el.y + 4, 100 - el.h)
51
- };
52
- commit([...els, copy]);
53
- setSelected(copy.id);
54
- };
55
- const zorder = (id, dir) => {
56
- const i = els.findIndex((e) => e.id === id);
57
- const j = i + dir;
58
- if (i < 0 || j < 0 || j >= els.length) return;
59
- const next = [...els];
60
- [next[i], next[j]] = [next[j], next[i]];
61
- commit(next);
62
- };
63
- const onDown = (e, el, mode) => {
64
- if (editingId === el.id && mode === "move") return;
65
- e.preventDefault();
66
- e.stopPropagation();
67
- setSelected(el.id);
68
- drag.current = { id: el.id, mode, sx: e.clientX, sy: e.clientY, orig: { ...el } };
69
- e.currentTarget.setPointerCapture(e.pointerId);
70
- };
71
- const onMove = (e) => {
72
- const d = drag.current;
73
- const rect = slideRef.current?.getBoundingClientRect();
74
- if (!d || !rect) return;
75
- const dx = (e.clientX - d.sx) / rect.width * 100;
76
- const dy = (e.clientY - d.sy) / rect.height * 100;
77
- if (d.mode === "resize") {
78
- setGuides({ x: null, y: null });
79
- setEls((prev) => prev.map((el) => el.id === d.id ? { ...el, w: clamp(d.orig.w + dx, 6, 100 - el.x), h: clamp(d.orig.h + dy, 5, 100 - el.y) } : el));
80
- return;
81
- }
82
- const others = els.filter((el) => el.id !== d.id);
83
- const xTargets = [0, 50, 100, ...others.flatMap((o) => [o.x, o.x + o.w / 2, o.x + o.w])];
84
- const yTargets = [0, 50, 100, ...others.flatMap((o) => [o.y, o.y + o.h / 2, o.y + o.h])];
85
- const rawX = clamp(d.orig.x + dx, 0, 100 - d.orig.w);
86
- const rawY = clamp(d.orig.y + dy, 0, 100 - d.orig.h);
87
- const sx = snapAxis(rawX, d.orig.w, xTargets);
88
- const sy = snapAxis(rawY, d.orig.h, yTargets);
89
- setGuides({ x: sx.guide, y: sy.guide });
90
- setEls(
91
- (prev) => prev.map(
92
- (el) => el.id === d.id ? { ...el, x: clamp(sx.pos, 0, 100 - el.w), y: clamp(sy.pos, 0, 100 - el.h) } : el
93
- )
94
- );
95
- };
96
- const onUp = () => {
97
- if (drag.current) {
98
- drag.current = null;
99
- setGuides({ x: null, y: null });
100
- onChange(els);
101
- }
102
- };
103
- useEffect(() => {
104
- if (!selected) return;
105
- const onKey = (e) => {
106
- if (editingId) return;
107
- const ae = document.activeElement;
108
- if (ae && (ae.tagName === "INPUT" || ae.tagName === "TEXTAREA" || ae.isContentEditable)) return;
109
- const el = els.find((x) => x.id === selected);
110
- if (!el) return;
111
- const step = e.shiftKey ? 5 : 1;
112
- const nudge = (ddx, ddy) => {
113
- e.preventDefault();
114
- commit(els.map((x) => x.id === selected ? { ...x, x: clamp(x.x + ddx, 0, 100 - x.w), y: clamp(x.y + ddy, 0, 100 - x.h) } : x));
115
- };
116
- if (e.key === "ArrowLeft") nudge(-step, 0);
117
- else if (e.key === "ArrowRight") nudge(step, 0);
118
- else if (e.key === "ArrowUp") nudge(0, -step);
119
- else if (e.key === "ArrowDown") nudge(0, step);
120
- else if (e.key === "Delete" || e.key === "Backspace") {
121
- e.preventDefault();
122
- commit(els.filter((x) => x.id !== selected));
123
- setSelected(null);
124
- } else if ((e.metaKey || e.ctrlKey) && (e.key === "d" || e.key === "D")) {
125
- e.preventDefault();
126
- duplicate(el);
127
- } else if (e.key === "Escape") {
128
- setSelected(null);
129
- }
130
- };
131
- window.addEventListener("keydown", onKey);
132
- return () => window.removeEventListener("keydown", onKey);
133
- }, [selected, editingId, els]);
134
- return /* @__PURE__ */ jsxs(
135
- "div",
136
- {
137
- className: "cv-free",
138
- ref: slideRef,
139
- style: padding ? { inset: `${padding}%` } : void 0,
140
- onPointerMove: onMove,
141
- onPointerUp: onUp,
142
- onPointerLeave: onUp,
143
- onClick: () => {
144
- setSelected(null);
145
- setEditingId(null);
146
- },
147
- children: [
148
- guides.x !== null && /* @__PURE__ */ jsx("span", { className: "cv-free__guide cv-free__guide--v", style: { left: `${guides.x}%` } }),
149
- guides.y !== null && /* @__PURE__ */ jsx("span", { className: "cv-free__guide cv-free__guide--h", style: { top: `${guides.y}%` } }),
150
- els.map((el) => /* @__PURE__ */ jsxs(
151
- "div",
152
- {
153
- className: `cv-free__el ${selected === el.id ? "is-selected" : ""}`,
154
- style: { left: `${el.x}%`, top: `${el.y}%`, width: `${el.w}%`, height: `${el.h}%` },
155
- onPointerDown: (e) => onDown(e, el, "move"),
156
- onDoubleClick: (e) => {
157
- if (el.type === "text") {
158
- e.stopPropagation();
159
- setEditingId(el.id);
160
- }
161
- },
162
- onClick: (e) => {
163
- e.stopPropagation();
164
- setSelected(el.id);
165
- },
166
- children: [
167
- el.type === "text" ? /* @__PURE__ */ jsx(
168
- "div",
169
- {
170
- className: "cv-free__text",
171
- contentEditable: editingId === el.id,
172
- suppressContentEditableWarning: true,
173
- style: {
174
- fontSize: el.fontSize ?? 24,
175
- fontWeight: el.bold ? 700 : 400,
176
- color: el.color,
177
- textAlign: el.align ?? "left"
178
- },
179
- onBlur: (e) => {
180
- setEditingId(null);
181
- updateEl(el.id, { text: e.currentTarget.textContent ?? "" });
182
- },
183
- children: el.text
184
- }
185
- ) : el.type === "shape" ? /* @__PURE__ */ jsx("div", { style: shapeStyle(el) }) : /* @__PURE__ */ jsx("img", { className: "cv-free__img", src: el.src, alt: "", draggable: false }),
186
- selected === el.id && el.type === "text" && /* @__PURE__ */ jsxs("div", { className: `cv-free__fmt ${el.y < 16 ? "cv-free__fmt--below" : ""}`, onPointerDown: (e) => e.stopPropagation(), onClick: (e) => e.stopPropagation(), children: [
187
- /* @__PURE__ */ jsx("button", { className: el.bold ? "is-on" : "", onClick: () => updateEl(el.id, { bold: !el.bold }), title: "Bold", children: /* @__PURE__ */ jsx("b", { children: "B" }) }),
188
- /* @__PURE__ */ jsx(
189
- "input",
190
- {
191
- type: "number",
192
- min: 8,
193
- max: 120,
194
- value: el.fontSize ?? 24,
195
- onChange: (e) => updateEl(el.id, { fontSize: Number(e.target.value) }),
196
- title: "Font size"
197
- }
198
- ),
199
- /* @__PURE__ */ jsx("input", { type: "color", value: el.color ?? "#1f2328", onChange: (e) => updateEl(el.id, { color: e.target.value }), title: "Text color" }),
200
- /* @__PURE__ */ jsx("button", { onClick: () => updateEl(el.id, { align: "left" }), title: "Align left", children: "\u27F8" }),
201
- /* @__PURE__ */ jsx("button", { onClick: () => updateEl(el.id, { align: "center" }), title: "Align center", children: "\u2261" }),
202
- /* @__PURE__ */ jsx("button", { onClick: () => updateEl(el.id, { align: "right" }), title: "Align right", children: "\u27F9" })
203
- ] }),
204
- selected === el.id && /* @__PURE__ */ jsxs(Fragment, { children: [
205
- /* @__PURE__ */ jsx("span", { className: "cv-free__resize", onPointerDown: (e) => onDown(e, el, "resize") }),
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" }),
208
- /* @__PURE__ */ jsx("button", { onClick: (e) => {
209
- e.stopPropagation();
210
- duplicate(el);
211
- }, title: "Duplicate", children: "\u29C9" }),
212
- /* @__PURE__ */ jsx("button", { onClick: (e) => {
213
- e.stopPropagation();
214
- zorder(el.id, 1);
215
- }, title: "Bring forward", children: "\u2191" }),
216
- /* @__PURE__ */ jsx("button", { onClick: (e) => {
217
- e.stopPropagation();
218
- zorder(el.id, -1);
219
- }, title: "Send back", children: "\u2193" }),
220
- /* @__PURE__ */ jsx("button", { className: "cv-free__ctl-del", onClick: (e) => {
221
- e.stopPropagation();
222
- commit(els.filter((x) => x.id !== el.id));
223
- }, title: "Delete", children: "\xD7" })
224
- ] })
225
- ] })
226
- ]
227
- },
228
- el.id
229
- ))
230
- ]
231
- }
232
- );
233
- }
234
- var THEMES = [
235
- { id: "light", label: "Light", bg: "#ffffff", text: "#1f2328" },
236
- { id: "paper", label: "Paper", bg: "#f7f5ef", text: "#2b2a26" },
237
- { id: "dark", label: "Dark", bg: "#14171f", text: "#f0f2f5" },
238
- { id: "midnight", label: "Midnight", bg: "#0b1020", text: "#e6e8ef" },
239
- { id: "navy", label: "Navy", bg: "#0d1b3e", text: "#eef2ff" },
240
- { id: "forest", label: "Forest", bg: "#0f2a22", text: "#e6f2ec" },
241
- { id: "sunset", label: "Sunset", bg: "#2b1a2e", text: "#ffe8d6" },
242
- { id: "mint", label: "Mint", bg: "#0f2a24", text: "#d7f5ec" }
243
- ];
244
- var SHAPES = [
245
- { id: "rect", label: "\u25AD Rectangle", box: { w: 30, h: 20 } },
246
- { id: "ellipse", label: "\u25EF Ellipse", box: { w: 24, h: 24 } },
247
- { id: "line", label: "\u2014 Line", box: { w: 40, h: 2 } }
248
- ];
249
- var LAYOUTS = {
250
- title: { label: "Title", els: [
251
- { type: "text", x: 12, y: 34, w: 76, h: 18, text: "Presentation title", fontSize: 54, bold: true, align: "center" },
252
- { type: "text", x: 20, y: 56, w: 60, h: 10, text: "Subtitle or one-line summary", fontSize: 24, align: "center" }
253
- ] },
254
- section: { label: "Section", els: [
255
- { type: "shape", shape: "rect", x: 10, y: 34, w: 8, h: 3, fill: "#5b5bd6" },
256
- { type: "text", x: 10, y: 40, w: 80, h: 16, text: "Section title", fontSize: 46, bold: true }
257
- ] },
258
- bullets: { label: "Bullets", els: [
259
- { type: "text", x: 10, y: 12, w: 80, h: 12, text: "Heading", fontSize: 36, bold: true },
260
- { type: "text", x: 10, y: 30, w: 80, h: 50, text: "\u2022 First key point\n\u2022 Second key point\n\u2022 Third key point", fontSize: 26 }
261
- ] },
262
- "two-column": { label: "Two column", els: [
263
- { type: "text", x: 8, y: 16, w: 40, h: 60, text: "Left column\n\n\u2022 point\n\u2022 point", fontSize: 24 },
264
- { type: "text", x: 52, y: 16, w: 40, h: 60, text: "Right column\n\n\u2022 point\n\u2022 point", fontSize: 24 }
265
- ] },
266
- quote: { label: "Quote", els: [
267
- { type: "text", x: 12, y: 30, w: 76, h: 30, text: "\u201CA short, memorable quote.\u201D", fontSize: 40, bold: true },
268
- { type: "text", x: 12, y: 62, w: 50, h: 8, text: "\u2014 Attribution", fontSize: 22 }
269
- ] }
270
- };
271
- var elementSeq = 0;
272
- var newElementId = () => `el_${Date.now().toString(36)}_${elementSeq++}`;
273
- function SlidesRenderer({ artifact }) {
274
- const slides = artifact.data.slides ?? [];
275
- const patch = useArtifactPatch(artifact.id);
276
- const [index, setIndex] = useState(0);
277
- const [dragIndex, setDragIndex] = useState(null);
278
- const [presenting, setPresenting] = useState(false);
279
- const imgRef = useRef(null);
280
- const bgRef = useRef(null);
281
- useEffect(() => {
282
- if (!presenting) return;
283
- const onKey = (e) => {
284
- if (e.key === "ArrowRight" || e.key === " ") setIndex((i) => Math.min(i + 1, slides.length - 1));
285
- else if (e.key === "ArrowLeft") setIndex((i) => Math.max(i - 1, 0));
286
- else if (e.key === "Escape") setPresenting(false);
287
- };
288
- window.addEventListener("keydown", onKey);
289
- return () => window.removeEventListener("keydown", onKey);
290
- }, [presenting]);
291
- if (slides.length === 0) {
292
- return /* @__PURE__ */ jsx("div", { className: "cv-deck cv-deck--empty", children: "No slides yet\u2026" });
293
- }
294
- const at = Math.min(index, slides.length - 1);
295
- const slide = slides[at];
296
- const slideStyle = {
297
- ...slide.background ? { background: slide.background } : {},
298
- ...slide.textColor ? { color: slide.textColor } : {}
299
- };
300
- const setSlides = (next) => patch({ slides: next });
301
- const update = (partial) => setSlides(slides.map((s, i) => i === at ? { ...s, ...partial } : s));
302
- const addSlide = () => {
303
- const next = [...slides];
304
- next.splice(at + 1, 0, { elements: [{ id: newElementId(), type: "text", x: 8, y: 10, w: 80, h: 14, text: "New slide", fontSize: 36, bold: true }] });
305
- setSlides(next);
306
- setIndex(at + 1);
307
- };
308
- const duplicateSlide = () => {
309
- const next = [...slides];
310
- next.splice(at + 1, 0, { ...slide, elements: resolveElements(slide).map((e) => ({ ...e })) });
311
- setSlides(next);
312
- setIndex(at + 1);
313
- };
314
- const deleteSlide = () => {
315
- if (slides.length === 1) return;
316
- setSlides(slides.filter((_, i) => i !== at));
317
- setIndex(Math.max(0, at - 1));
318
- };
319
- const moveSlide = (dir) => {
320
- const j = at + dir;
321
- if (j < 0 || j >= slides.length) return;
322
- const next = [...slides];
323
- [next[at], next[j]] = [next[j], next[at]];
324
- setSlides(next);
325
- setIndex(j);
326
- };
327
- const reorder = (from, to) => {
328
- if (from === to) return;
329
- const next = [...slides];
330
- const [moved] = next.splice(from, 1);
331
- next.splice(to, 0, moved);
332
- setSlides(next);
333
- setIndex(to);
334
- };
335
- const addElement = (el) => update({ elements: [...resolveElements(slide), { ...el, id: newElementId() }] });
336
- const addTextEl = () => addElement({ type: "text", x: 12, y: 16, w: 45, h: 16, text: "Text", fontSize: 24 });
337
- const addImageEl = (file) => {
338
- if (!file) return;
339
- const reader = new FileReader();
340
- reader.onload = () => addElement({ type: "image", x: 22, y: 22, w: 40, h: 34, src: String(reader.result) });
341
- reader.readAsDataURL(file);
342
- };
343
- const addShapeEl = (shape) => {
344
- const s = SHAPES.find((x) => x.id === shape);
345
- if (s) addElement({ type: "shape", shape, x: 20, y: 20, w: s.box.w, h: s.box.h, fill: "#5b5bd6" });
346
- };
347
- const applyLayout = (key) => {
348
- const l = LAYOUTS[key];
349
- if (l) update({ elements: l.els.map((e) => ({ ...e, id: newElementId() })) });
350
- };
351
- const setSlideBgImage = (file) => {
352
- if (!file) return;
353
- const reader = new FileReader();
354
- reader.onload = () => update({ background: `#000 url("${String(reader.result)}") center/cover no-repeat` });
355
- reader.readAsDataURL(file);
356
- };
357
- return /* @__PURE__ */ jsxs("div", { className: "cv-deck", children: [
358
- /* @__PURE__ */ jsxs("aside", { className: "cv-deck__rail cv-chrome", children: [
359
- slides.map((s, i) => /* @__PURE__ */ jsx(
360
- "div",
361
- {
362
- className: `cv-deck__thumb-wrap ${i === at ? "is-active" : ""} ${dragIndex === i ? "is-dragging" : ""}`,
363
- draggable: true,
364
- onDragStart: () => setDragIndex(i),
365
- onDragOver: (e) => e.preventDefault(),
366
- onDrop: () => {
367
- if (dragIndex !== null) reorder(dragIndex, i);
368
- setDragIndex(null);
369
- },
370
- onDragEnd: () => setDragIndex(null),
371
- children: /* @__PURE__ */ jsxs("button", { className: "cv-deck__thumb", onClick: () => setIndex(i), children: [
372
- /* @__PURE__ */ jsx("span", { className: "cv-deck__thumb-n", children: i + 1 }),
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(
374
- (el) => el.type === "text" ? /* @__PURE__ */ jsx(
375
- "span",
376
- {
377
- style: { position: "absolute", left: `${el.x}%`, top: `${el.y}%`, width: `${el.w}%`, fontSize: (el.fontSize ?? 24) * 0.12, fontWeight: el.bold ? 700 : 400, color: el.color ?? s.textColor, overflow: "hidden", whiteSpace: "pre-wrap" },
378
- children: el.text
379
- },
380
- el.id
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)
382
- ) }) })
383
- ] })
384
- },
385
- i
386
- )),
387
- /* @__PURE__ */ jsx("button", { className: "cv-deck__addslide", onClick: addSlide, children: "+ Add slide" })
388
- ] }),
389
- /* @__PURE__ */ jsxs("div", { className: "cv-deck__main", children: [
390
- /* @__PURE__ */ jsxs("div", { className: "cv-deck__toolbar cv-chrome", children: [
391
- /* @__PURE__ */ jsx("button", { onClick: addTextEl, title: "Add text box", children: "+ Text" }),
392
- /* @__PURE__ */ jsx("button", { onClick: () => imgRef.current?.click(), title: "Add image", children: "+ Image" }),
393
- /* @__PURE__ */ jsx("input", { ref: imgRef, type: "file", accept: "image/*", hidden: true, onChange: (e) => addImageEl(e.target.files?.[0]) }),
394
- /* @__PURE__ */ jsx("input", { ref: bgRef, type: "file", accept: "image/*", hidden: true, onChange: (e) => setSlideBgImage(e.target.files?.[0]) }),
395
- /* @__PURE__ */ jsxs(
396
- "select",
397
- {
398
- className: "cv-deck__theme",
399
- value: "",
400
- title: "Add a shape",
401
- onChange: (e) => {
402
- if (e.target.value) addShapeEl(e.target.value);
403
- e.currentTarget.value = "";
404
- },
405
- children: [
406
- /* @__PURE__ */ jsx("option", { value: "", children: "+ Shape" }),
407
- SHAPES.map((s) => /* @__PURE__ */ jsx("option", { value: s.id, children: s.label }, s.id))
408
- ]
409
- }
410
- ),
411
- /* @__PURE__ */ jsxs(
412
- "select",
413
- {
414
- className: "cv-deck__theme",
415
- value: "",
416
- title: "Apply a layout",
417
- onChange: (e) => {
418
- if (e.target.value) applyLayout(e.target.value);
419
- e.currentTarget.value = "";
420
- },
421
- children: [
422
- /* @__PURE__ */ jsx("option", { value: "", children: "Layout\u2026" }),
423
- Object.entries(LAYOUTS).map(([k, v]) => /* @__PURE__ */ jsx("option", { value: k, children: v.label }, k))
424
- ]
425
- }
426
- ),
427
- /* @__PURE__ */ jsxs(
428
- "select",
429
- {
430
- className: "cv-deck__theme",
431
- value: "",
432
- title: "Theme",
433
- onChange: (e) => {
434
- const t = THEMES.find((x) => x.id === e.target.value);
435
- if (t) update({ background: t.bg, textColor: t.text });
436
- e.currentTarget.value = "";
437
- },
438
- children: [
439
- /* @__PURE__ */ jsx("option", { value: "", children: "Theme\u2026" }),
440
- THEMES.map((t) => /* @__PURE__ */ jsx("option", { value: t.id, children: t.label }, t.id))
441
- ]
442
- }
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 }) }) }),
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
- ] }),
450
- /* @__PURE__ */ jsx("span", { className: "cv-deck__spacer" }),
451
- /* @__PURE__ */ jsx("button", { className: "cv-deck__present", onClick: () => setPresenting(true), title: "Present (full screen)", children: "\u25B6 Present" }),
452
- /* @__PURE__ */ jsx("button", { onClick: () => moveSlide(-1), title: "Move up", disabled: at === 0, children: "\u25B2" }),
453
- /* @__PURE__ */ jsx("button", { onClick: () => moveSlide(1), title: "Move down", disabled: at === slides.length - 1, children: "\u25BC" }),
454
- /* @__PURE__ */ jsx("button", { onClick: duplicateSlide, title: "Duplicate slide", children: "\u29C9" }),
455
- /* @__PURE__ */ jsx("button", { onClick: deleteSlide, title: "Delete slide", disabled: slides.length === 1, children: "\u{1F5D1}" })
456
- ] }),
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 }) }),
458
- /* @__PURE__ */ jsxs("div", { className: "cv-deck__nav cv-chrome", children: [
459
- /* @__PURE__ */ jsx("button", { disabled: at === 0, onClick: () => setIndex(at - 1), "aria-label": "Previous slide", children: "\u2039" }),
460
- /* @__PURE__ */ jsxs("span", { children: [
461
- at + 1,
462
- " / ",
463
- slides.length
464
- ] }),
465
- /* @__PURE__ */ jsx("button", { disabled: at === slides.length - 1, onClick: () => setIndex(at + 1), "aria-label": "Next slide", children: "\u203A" })
466
- ] }),
467
- /* @__PURE__ */ jsx(
468
- "textarea",
469
- {
470
- className: "cv-deck__notes cv-chrome",
471
- value: slide.notes ?? "",
472
- placeholder: "Speaker notes\u2026",
473
- onChange: (e) => update({ notes: e.target.value })
474
- }
475
- )
476
- ] }),
477
- presenting && /* @__PURE__ */ jsxs("div", { className: "cv-present", onClick: () => setIndex(Math.min(at + 1, slides.length - 1)), children: [
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(
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)
480
- ) }) }, at),
481
- slide.notes ? /* @__PURE__ */ jsx("div", { className: "cv-present__notes", onClick: (e) => e.stopPropagation(), children: slide.notes }) : null,
482
- /* @__PURE__ */ jsxs("div", { className: "cv-present__hint", children: [
483
- at + 1,
484
- " / ",
485
- slides.length,
486
- " \xB7 \u2190 \u2192 to navigate \xB7 Esc to exit"
487
- ] })
488
- ] })
489
- ] });
490
- }
491
-
492
- export { SlidesRenderer };