dopecanvas 0.1.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/README.md +364 -0
- package/dist/dopecanvas.cjs +3 -0
- package/dist/dopecanvas.cjs.map +1 -0
- package/dist/dopecanvas.css +1 -0
- package/dist/dopecanvas.js +1280 -0
- package/dist/dopecanvas.js.map +1 -0
- package/dist/index.d.ts +344 -0
- package/package.json +83 -0
package/README.md
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
# DopeCanvas
|
|
2
|
+
|
|
3
|
+
**The office suite built for the AI era.**
|
|
4
|
+
|
|
5
|
+
DopeCanvas is a drop-in React component that replaces Word, Excel, and PowerPoint with a single, unified document canvas. It renders LLM-generated HTML as a paginated, editable document -- complete with tables, charts, rich formatting, and page breaks -- so your users work in a familiar office-like environment while your AI writes the code behind it.
|
|
6
|
+
|
|
7
|
+
The LLM sees HTML. The user sees a professional report. Both can edit it.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Why DopeCanvas?
|
|
12
|
+
|
|
13
|
+
### The problem with traditional office tools
|
|
14
|
+
|
|
15
|
+
Word, Excel, and PowerPoint are powerful individually, but painful together. Building a complex report means juggling three apps, copy-pasting between them, versioning each file separately, and hoping nothing breaks when you update a number in a spreadsheet that feeds into a chart in a slide deck that references a paragraph in a document.
|
|
16
|
+
|
|
17
|
+
### The problem with Notion-style tools
|
|
18
|
+
|
|
19
|
+
Notion solved the integration problem, but introduced new ones: limited formatting control, a learning curve that amounts to a new way of working, and -- critically -- **poor compatibility with LLM workflows**. You can't easily have an AI generate a Notion page with precise layout, formulas, and styling.
|
|
20
|
+
|
|
21
|
+
### The DopeCanvas solution
|
|
22
|
+
|
|
23
|
+
DopeCanvas takes a different approach entirely:
|
|
24
|
+
|
|
25
|
+
- **HTML is the document format.** LLMs already understand HTML natively. No custom schemas, no proprietary formats, no serialization loss. The AI writes HTML/CSS, and DopeCanvas renders it as a paginated, editable document.
|
|
26
|
+
- **One canvas, all capabilities.** Text, tables, charts, styled layouts -- everything lives in one document, on real pages with real page breaks. No switching between apps.
|
|
27
|
+
- **Users edit visually.** Click any text to edit it. Use the toolbar for formatting. It feels like Word, but it's powered by the web.
|
|
28
|
+
- **Programmatic API.** Load content, extract content, sync to databases. DopeCanvas is a framework, not just a viewer.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## How It Works
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
LLM writes HTML/CSS/JS
|
|
36
|
+
|
|
|
37
|
+
v
|
|
38
|
+
DopeCanvas.loadHTML()
|
|
39
|
+
|
|
|
40
|
+
v
|
|
41
|
+
Pagination Engine measures content,
|
|
42
|
+
distributes blocks across pages
|
|
43
|
+
|
|
|
44
|
+
v
|
|
45
|
+
User sees paginated, editable document
|
|
46
|
+
(white pages, margins, shadows, page numbers)
|
|
47
|
+
|
|
|
48
|
+
v
|
|
49
|
+
User clicks to edit (contentEditable)
|
|
50
|
+
Toolbar provides formatting (bold, italic, etc.)
|
|
51
|
+
|
|
|
52
|
+
v
|
|
53
|
+
DopeCanvas.getHTML() returns modified HTML
|
|
54
|
+
|
|
|
55
|
+
v
|
|
56
|
+
Sync to database, send back to LLM, export
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The key insight: **the LLM works with code, the user works with a document, and they're editing the same thing.**
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Quick Start
|
|
64
|
+
|
|
65
|
+
### Install
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npm install dopecanvas
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Basic Usage
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
import { DopeCanvas } from 'dopecanvas';
|
|
75
|
+
|
|
76
|
+
function App() {
|
|
77
|
+
// This HTML could come from an LLM, a database, or an API
|
|
78
|
+
const html = `
|
|
79
|
+
<h1 style="color: #1a1a2e; font-family: Georgia, serif;">
|
|
80
|
+
Quarterly Report
|
|
81
|
+
</h1>
|
|
82
|
+
<p style="line-height: 1.6;">
|
|
83
|
+
Revenue reached <strong>$48.2M</strong>, a 23% YoY increase.
|
|
84
|
+
</p>
|
|
85
|
+
<table style="width: 100%; border-collapse: collapse;">
|
|
86
|
+
<tr style="background: #1a1a2e; color: white;">
|
|
87
|
+
<th style="padding: 10px;">Metric</th>
|
|
88
|
+
<th style="padding: 10px;">Q3</th>
|
|
89
|
+
<th style="padding: 10px;">Q4</th>
|
|
90
|
+
</tr>
|
|
91
|
+
<tr>
|
|
92
|
+
<td style="padding: 8px; border-bottom: 1px solid #ddd;">Revenue</td>
|
|
93
|
+
<td style="padding: 8px; border-bottom: 1px solid #ddd;">$42.5M</td>
|
|
94
|
+
<td style="padding: 8px; border-bottom: 1px solid #ddd;">$48.2M</td>
|
|
95
|
+
</tr>
|
|
96
|
+
</table>
|
|
97
|
+
`;
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<DopeCanvas
|
|
101
|
+
html={html}
|
|
102
|
+
onContentChange={(updated) => {
|
|
103
|
+
console.log('User edited the document:', updated);
|
|
104
|
+
}}
|
|
105
|
+
/>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
That's it. You get a paginated, editable document with a formatting toolbar, page setup controls, and an API to read the content back.
|
|
111
|
+
|
|
112
|
+
### With Page Configuration
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
<DopeCanvas
|
|
116
|
+
html={html}
|
|
117
|
+
pageConfig={{
|
|
118
|
+
size: 'a4', // 'letter' | 'a4' | 'legal' | { width, height }
|
|
119
|
+
margins: {
|
|
120
|
+
top: 96, // pixels (96px = 1 inch at 96 DPI)
|
|
121
|
+
right: 96,
|
|
122
|
+
bottom: 96,
|
|
123
|
+
left: 96,
|
|
124
|
+
},
|
|
125
|
+
}}
|
|
126
|
+
onContentChange={(html) => saveToDatabase(html)}
|
|
127
|
+
onPageConfigChange={(config) => console.log('Page settings changed:', config)}
|
|
128
|
+
/>
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### With the Document API
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
import { DocumentAPI } from 'dopecanvas';
|
|
135
|
+
|
|
136
|
+
const api = new DocumentAPI();
|
|
137
|
+
|
|
138
|
+
// Load content programmatically
|
|
139
|
+
api.loadHTML('<h1>Hello World</h1><p>Generated by AI.</p>');
|
|
140
|
+
|
|
141
|
+
// Listen for user edits
|
|
142
|
+
api.onChange((html) => {
|
|
143
|
+
syncToDatabase(html);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Read content
|
|
147
|
+
const html = api.getHTML();
|
|
148
|
+
const text = api.getPlainText();
|
|
149
|
+
|
|
150
|
+
// Access specific elements for database sync
|
|
151
|
+
const tableContent = api.getElementContent('revenue-table');
|
|
152
|
+
api.setElementContent('summary', '<p>Updated by the system.</p>');
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## What Makes This LLM-Native
|
|
158
|
+
|
|
159
|
+
### HTML in, HTML out
|
|
160
|
+
|
|
161
|
+
Most document editors use proprietary internal models (ProseMirror schemas, Slate.js value trees, OOXML). These are hostile to LLMs -- the AI has to learn a custom format, and translation is lossy.
|
|
162
|
+
|
|
163
|
+
DopeCanvas uses **HTML as the document format**. Period. The LLM writes HTML with inline styles, classes, and any structure it wants. DopeCanvas renders it faithfully and returns it faithfully. No schema stripping, no attribute sanitization, no surprises.
|
|
164
|
+
|
|
165
|
+
### No editor framework baggage
|
|
166
|
+
|
|
167
|
+
We deliberately chose native `contentEditable` over TipTap, ProseMirror, or Slate.js. Those frameworks enforce schemas that strip CSS classes, inline styles, data attributes, and JavaScript -- destroying the LLM's output. With DopeCanvas, what the AI writes is exactly what renders.
|
|
168
|
+
|
|
169
|
+
### Structured content for structured prompts
|
|
170
|
+
|
|
171
|
+
Because the document is real HTML, you can:
|
|
172
|
+
- Ask the LLM to generate a report section and inject it at a specific element ID
|
|
173
|
+
- Extract a table from the document and send it back to the LLM for analysis
|
|
174
|
+
- Have the LLM update specific sections while preserving user edits elsewhere
|
|
175
|
+
- Round-trip between AI generation and human editing without format conversion
|
|
176
|
+
|
|
177
|
+
### Plain JavaScript = formulas, charts, and live analysis
|
|
178
|
+
|
|
179
|
+
This is the killer feature that falls out of using real HTML: **`<script>` tags just work.**
|
|
180
|
+
|
|
181
|
+
Other editors strip scripts during sanitization. DopeCanvas activates them. That means an LLM can generate a full analytical report with computed totals, interactive Chart.js visualizations, and cross-table data binding — using nothing but plain JavaScript — and it all runs live inside the document.
|
|
182
|
+
|
|
183
|
+
**Computed cells and formulas.** No formula engine needed. The LLM writes a `<script>` that reads data from table cells, computes totals/averages/growth percentages, and writes the results back. When the user edits a number, an `input` event listener recalculates everything instantly:
|
|
184
|
+
|
|
185
|
+
```html
|
|
186
|
+
<table id="revenue">
|
|
187
|
+
<tr><td>Q1</td><td class="num">$9.8M</td></tr>
|
|
188
|
+
<tr><td>Q2</td><td class="num">$11.2M</td></tr>
|
|
189
|
+
<tr class="total-row">
|
|
190
|
+
<td>Total</td><td class="num fx" id="total">$21.0M</td>
|
|
191
|
+
</tr>
|
|
192
|
+
</table>
|
|
193
|
+
|
|
194
|
+
<script>
|
|
195
|
+
(function () {
|
|
196
|
+
var table = document.getElementById('revenue');
|
|
197
|
+
function recalc() {
|
|
198
|
+
var rows = table.querySelectorAll('tr:not(.total-row)');
|
|
199
|
+
var sum = 0;
|
|
200
|
+
rows.forEach(function (tr) {
|
|
201
|
+
var text = tr.querySelector('.num').textContent;
|
|
202
|
+
sum += parseFloat(text.replace(/[^0-9.]/g, ''));
|
|
203
|
+
});
|
|
204
|
+
document.getElementById('total').textContent = '$' + sum.toFixed(1) + 'M';
|
|
205
|
+
}
|
|
206
|
+
recalc();
|
|
207
|
+
table.addEventListener('input', recalc);
|
|
208
|
+
})();
|
|
209
|
+
</script>
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Charts from table data.** The LLM generates a `<canvas>` element and a script that loads Chart.js from a CDN, reads values from the table, and renders a bar chart, line graph, or doughnut. When the user edits a cell, the chart animates to reflect the new data:
|
|
213
|
+
|
|
214
|
+
```html
|
|
215
|
+
<canvas id="chart" width="560" height="300"></canvas>
|
|
216
|
+
|
|
217
|
+
<script>
|
|
218
|
+
(function () {
|
|
219
|
+
function init() {
|
|
220
|
+
// Read data from the table, build chart
|
|
221
|
+
var chart = new Chart(document.getElementById('chart'), {
|
|
222
|
+
type: 'bar',
|
|
223
|
+
data: { labels: ['Q1','Q2','Q3','Q4'], datasets: [/*...*/] }
|
|
224
|
+
});
|
|
225
|
+
// Re-read table and update chart on every edit
|
|
226
|
+
document.getElementById('revenue').addEventListener('input', function () {
|
|
227
|
+
chart.data.datasets[0].data = readTableData();
|
|
228
|
+
chart.update();
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
// Load Chart.js dynamically, then init
|
|
232
|
+
var s = document.createElement('script');
|
|
233
|
+
s.src = 'https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js';
|
|
234
|
+
s.onload = init;
|
|
235
|
+
document.head.appendChild(s);
|
|
236
|
+
})();
|
|
237
|
+
</script>
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
**Why this matters.** The LLM doesn't need to learn a plugin API, a chart config schema, or a formula language. It writes the same JavaScript it already knows. Any library that runs in a browser — Chart.js, D3, MathJax, Mermaid — can be loaded and used. The document is a full web page with the UX of a Word doc.
|
|
241
|
+
|
|
242
|
+
The included `sample-report-charts.html` demo shows this in action: four Chart.js charts (bar, line, doughnut, horizontal bar) all driven by editable tables with live-updating totals, growth percentages, and KPI cards.
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## Features
|
|
247
|
+
|
|
248
|
+
### Pagination Engine
|
|
249
|
+
- Real page sizes: Letter (8.5 x 11"), A4 (210 x 297mm), Legal (8.5 x 14"), or custom dimensions
|
|
250
|
+
- Configurable margins (top, right, bottom, left)
|
|
251
|
+
- Automatic content measurement and distribution across pages
|
|
252
|
+
- CSS `break-before: page` / `break-after: page` for manual page breaks
|
|
253
|
+
- Page numbers
|
|
254
|
+
|
|
255
|
+
### Visual Document Rendering
|
|
256
|
+
- White pages on a scrollable gray background (like Word/Google Docs)
|
|
257
|
+
- Page shadows and margins
|
|
258
|
+
- Content rendered exactly as the HTML specifies -- gradients, flexbox, grid, anything
|
|
259
|
+
|
|
260
|
+
### Inline Editing
|
|
261
|
+
- Click any text to edit in place via `contentEditable`
|
|
262
|
+
- Table cells are individually editable
|
|
263
|
+
- All original CSS styling is preserved during editing
|
|
264
|
+
|
|
265
|
+
### Formatting Toolbar
|
|
266
|
+
- **Text**: Bold, Italic, Underline, Strikethrough, Font Size, Headings (H1-H6)
|
|
267
|
+
- **Color**: Text color, Highlight color
|
|
268
|
+
- **Alignment**: Left, Center, Right, Justify
|
|
269
|
+
- **Lists**: Ordered, Unordered, Indent, Outdent
|
|
270
|
+
- **Page**: Size selector, Margin controls
|
|
271
|
+
- **History**: Undo / Redo
|
|
272
|
+
- Contextual: toolbar adapts based on whether you're editing text, a table, or other elements
|
|
273
|
+
|
|
274
|
+
### Document API
|
|
275
|
+
- `loadHTML(html, css?)` -- Load content into the canvas
|
|
276
|
+
- `getHTML()` -- Get current content (reflects user edits)
|
|
277
|
+
- `getPlainText()` -- Get text content without markup
|
|
278
|
+
- `onChange(callback)` -- Subscribe to edit events
|
|
279
|
+
- `getPageCount()` / `setPageConfig()` -- Page operations
|
|
280
|
+
- `querySelectorAll()` / `getElementContent()` / `setElementContent()` -- Element-level access
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Architecture
|
|
285
|
+
|
|
286
|
+
```
|
|
287
|
+
dopecanvas/
|
|
288
|
+
src/
|
|
289
|
+
core/
|
|
290
|
+
PageLayoutEngine.ts -- Measures blocks, distributes across pages
|
|
291
|
+
EditableManager.ts -- contentEditable, MutationObserver, undo/redo
|
|
292
|
+
DocumentEngine.ts -- Orchestrator
|
|
293
|
+
types.ts -- PageConfig, PageSize, etc.
|
|
294
|
+
components/
|
|
295
|
+
DopeCanvas.tsx -- Main React component (toolbar + paged view)
|
|
296
|
+
PagedView.tsx -- Renders paginated pages
|
|
297
|
+
Page.tsx -- Single page frame
|
|
298
|
+
Toolbar/ -- Formatting and page setup controls
|
|
299
|
+
api/
|
|
300
|
+
DocumentAPI.ts -- External programmatic interface
|
|
301
|
+
hooks/
|
|
302
|
+
useDocumentEngine.ts -- React hook for the engine
|
|
303
|
+
useSelectionContext.ts -- Selection state tracking
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
**No dependencies beyond React.** No TipTap, no ProseMirror, no Slate, no draft-js. Just React, TypeScript, and the browser's native editing APIs.
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## Use Cases
|
|
311
|
+
|
|
312
|
+
- **AI report generation** -- LLM generates a financial report, user reviews and edits it visually, system syncs changes back
|
|
313
|
+
- **Template-based documents** -- Define HTML templates with placeholder IDs, fill them programmatically, let users customize
|
|
314
|
+
- **Collaborative AI editing** -- User writes in the canvas, sends sections to an LLM for rewriting, merges results back
|
|
315
|
+
- **Data-driven documents** -- Tables bound to database queries, charts generated from data, all in one paginated view
|
|
316
|
+
- **Print-ready output** -- Paginated layout with proper page breaks, margins, and formatting for PDF export
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## Roadmap
|
|
321
|
+
|
|
322
|
+
- [x] **Live formulas via JS** -- LLM-authored `<script>` tags compute totals, growth %, averages in real time
|
|
323
|
+
- [x] **Charts via JS** -- Chart.js / D3 / any library loaded from CDN, driven by editable table data
|
|
324
|
+
- [ ] **Table formulas (Excel-style)** -- Cell references (A1, B2:C5) with HyperFormula engine for spreadsheet power users
|
|
325
|
+
- [ ] **Database binding** -- Postgres/Convex tables backing document tables
|
|
326
|
+
- [ ] **Contextual toolbars** -- Different tools for text, tables, charts, images
|
|
327
|
+
- [ ] **Collaborative editing** -- Multi-user real-time editing via CRDT
|
|
328
|
+
- [x] **PDF export** -- High-fidelity print from the paginated layout
|
|
329
|
+
- [ ] **Drag and drop** -- Move blocks between pages
|
|
330
|
+
- [ ] **Image handling** -- Insert, resize, position images within the document
|
|
331
|
+
- [ ] **Presentation mode** -- Page-by-page slideshow from the same document
|
|
332
|
+
- [ ] **TOC** -- Generate table of content
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## Development
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
# Clone and install
|
|
340
|
+
git clone https://github.com/yourusername/dopecanvas.git
|
|
341
|
+
cd dopecanvas
|
|
342
|
+
npm install
|
|
343
|
+
|
|
344
|
+
# Start dev server
|
|
345
|
+
npm run dev
|
|
346
|
+
|
|
347
|
+
# Build
|
|
348
|
+
npm run build
|
|
349
|
+
|
|
350
|
+
# Type check
|
|
351
|
+
npx tsc --noEmit
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## See It In Action
|
|
357
|
+
|
|
358
|
+
Visit [dopeoffice.ai](https://dopeoffice.ai) to see DopeCanvas powering a full AI-native office suite. It's the reference implementation showing what's possible when you combine this framework with an LLM backend -- AI-generated reports, live editing, database-synced tables, and more.
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## License
|
|
363
|
+
|
|
364
|
+
MIT
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const s=require("react/jsx-runtime"),o=require("react"),A=({dimensions:a,margins:e,pageNumber:t,totalPages:i,children:n})=>s.jsxs("div",{className:"dopecanvas-page",style:{width:`${a.width}px`,height:`${a.height}px`,backgroundColor:"#ffffff",boxShadow:"0 2px 8px rgba(0, 0, 0, 0.15), 0 0 1px rgba(0, 0, 0, 0.1)",position:"relative",overflow:"hidden",flexShrink:0},children:[s.jsx("div",{className:"dopecanvas-page-content",style:{paddingTop:`${e.top}px`,paddingRight:`${e.right}px`,paddingBottom:`${e.bottom}px`,paddingLeft:`${e.left}px`,height:"100%",boxSizing:"border-box",overflow:"hidden"},children:n}),s.jsxs("div",{className:"dopecanvas-page-number",style:{position:"absolute",bottom:`${Math.max(e.bottom/3,16)}px`,left:0,right:0,textAlign:"center",fontSize:"11px",color:"#999",fontFamily:"system-ui, -apple-system, sans-serif",pointerEvents:"none",userSelect:"none"},children:[t," / ",i]})]}),w={letter:{width:816,height:1056},a4:{width:794,height:1123},legal:{width:816,height:1344}},N={top:96,right:96,bottom:96,left:96},L={size:"letter",margins:{...N}};function G(a){const e=[];return a.querySelectorAll("script").forEach(t=>{const i=document.createElement("script");Array.from(t.attributes).forEach(n=>i.setAttribute(n.name,n.value)),i.textContent=t.textContent||"",t.parentNode?.replaceChild(i,t),e.push(i)}),e}const U=({html:a,css:e,pageConfig:t,layoutEngine:i,editableManager:n,onContentChange:r,onPaginationChange:c})=>{const u=o.useRef(null),m=o.useRef(null),x=o.useRef(null),l=o.useRef(r);l.current=r;const[d,p]=o.useState([]),S=typeof t.size=="string"?w[t.size]:t.size,C=o.useCallback(()=>{if(!m.current)return;const g=m.current.querySelectorAll(".dopecanvas-block-wrapper"),y=[];g.forEach(h=>{const f=h.firstElementChild;f&&y.push(f.outerHTML)});const b=y.join(`
|
|
2
|
+
`);l.current?.(b)},[]),v=o.useCallback(()=>{if(!u.current)return;const g=u.current,y=i.getContentAreaWidth();if(g.style.width=`${y}px`,g.style.position="absolute",g.style.left="-9999px",g.style.top="0",g.style.visibility="hidden",g.innerHTML="",e){const T=document.createElement("style");T.textContent=e,g.appendChild(T)}const b=new DOMParser().parseFromString(a,"text/html");b.head.querySelectorAll('style, link[rel="stylesheet"]').forEach(T=>{b.body.insertBefore(T,b.body.firstChild)});const h=b.body.innerHTML,f=document.createElement("div");f.innerHTML=h,g.appendChild(f);const M=i.measureBlocks(f),k=M.map(T=>T.element.cloneNode(!0).outerHTML),H=i.paginate(M),_=H.pages.map(T=>({blocks:T.blockIndices.map(O=>k[O])}));g.innerHTML="",p(_),c?.(H)},[a,e,i,c]);return o.useEffect(()=>{v()},[v]),o.useEffect(()=>{const g=m.current;if(!g)return;x.current&&x.current.disconnect(),g.querySelectorAll(".dopecanvas-block-wrapper").forEach(M=>{const k=M.firstElementChild;k&&(k.tagName==="TABLE"?k.querySelectorAll("td, th").forEach(_=>{_.contentEditable="true"}):k.tagName==="SCRIPT"||k.tagName==="STYLE"||(k.contentEditable="true"))});const b=G(g);let h=null;const f=new MutationObserver(()=>{h&&clearTimeout(h),h=setTimeout(()=>{C()},200)});return f.observe(g,{childList:!0,subtree:!0,characterData:!0,attributes:!1}),x.current=f,()=>{f.disconnect(),b.forEach(M=>M.remove()),h&&clearTimeout(h)}},[d,C]),s.jsxs("div",{className:"dopecanvas-paged-view",style:V,children:[s.jsx("div",{ref:u,"aria-hidden":"true"}),s.jsxs("div",{ref:m,style:K,children:[e&&s.jsx("style",{dangerouslySetInnerHTML:{__html:e}}),d.map((g,y)=>s.jsx(A,{dimensions:S,margins:t.margins,pageNumber:y+1,totalPages:d.length,children:g.blocks.map((b,h)=>s.jsx("div",{className:"dopecanvas-block-wrapper",dangerouslySetInnerHTML:{__html:b}},`${y}-${h}`))},y)),d.length===0&&s.jsx(A,{dimensions:S,margins:t.margins,pageNumber:1,totalPages:1,children:s.jsx("div",{contentEditable:"true",style:{minHeight:"1em",outline:"none"},"data-placeholder":"Start typing..."})})]})]})},V={flex:1,overflow:"auto",backgroundColor:"#e8e8e8",display:"flex",flexDirection:"column",alignItems:"center"},K={display:"flex",flexDirection:"column",alignItems:"center",gap:"24px",padding:"24px 0"};function X(a){const[e,t]=o.useState("none");return o.useEffect(()=>a?a.onContextChange(n=>{t(n)}):void 0,[a]),e}function W(){const[a,e]=o.useState({bold:!1,italic:!1,underline:!1,strikethrough:!1,justifyLeft:!1,justifyCenter:!1,justifyRight:!1,justifyFull:!1}),t=o.useCallback(()=>{e({bold:document.queryCommandState("bold"),italic:document.queryCommandState("italic"),underline:document.queryCommandState("underline"),strikethrough:document.queryCommandState("strikethrough"),justifyLeft:document.queryCommandState("justifyLeft"),justifyCenter:document.queryCommandState("justifyCenter"),justifyRight:document.queryCommandState("justifyRight"),justifyFull:document.queryCommandState("justifyFull")})},[]);return o.useEffect(()=>(document.addEventListener("selectionchange",t),()=>{document.removeEventListener("selectionchange",t)}),[t]),a}const Z=({onExecCommand:a})=>{const e=W(),t=o.useRef(null),i=o.useCallback(()=>{const l=window.getSelection();l&&l.rangeCount>0&&(t.current=l.getRangeAt(0).cloneRange())},[]),n=o.useCallback(()=>{const l=t.current;if(!l)return;const d=window.getSelection();d&&(d.removeAllRanges(),d.addRange(l))},[]),r=o.useCallback((l,d)=>p=>{p.preventDefault(),a(l,d)},[a]),c=o.useCallback(l=>{n(),a("fontSize",l.target.value)},[a,n]),u=o.useCallback(l=>{n();const d=l.target.value;d==="p"?a("formatBlock","p"):a("formatBlock",d)},[a,n]),m=o.useCallback(l=>{n(),a("foreColor",l.target.value)},[a,n]),x=o.useCallback(l=>{n(),a("hiliteColor",l.target.value)},[a,n]);return s.jsxs("div",{style:Y,children:[s.jsxs("select",{onChange:u,defaultValue:"p",style:I,title:"Block Format",onMouseDown:i,children:[s.jsx("option",{value:"p",children:"Paragraph"}),s.jsx("option",{value:"h1",children:"Heading 1"}),s.jsx("option",{value:"h2",children:"Heading 2"}),s.jsx("option",{value:"h3",children:"Heading 3"}),s.jsx("option",{value:"h4",children:"Heading 4"}),s.jsx("option",{value:"h5",children:"Heading 5"}),s.jsx("option",{value:"h6",children:"Heading 6"})]}),s.jsx("div",{style:P}),s.jsxs("select",{onChange:c,defaultValue:"3",style:I,title:"Font Size",onMouseDown:i,children:[s.jsx("option",{value:"1",children:"8pt"}),s.jsx("option",{value:"2",children:"10pt"}),s.jsx("option",{value:"3",children:"12pt"}),s.jsx("option",{value:"4",children:"14pt"}),s.jsx("option",{value:"5",children:"18pt"}),s.jsx("option",{value:"6",children:"24pt"}),s.jsx("option",{value:"7",children:"36pt"})]}),s.jsx("div",{style:P}),s.jsx(D,{icon:"B",title:"Bold (Ctrl+B)",active:e.bold,onMouseDown:r("bold"),extraStyle:{fontWeight:"bold"}}),s.jsx(D,{icon:"I",title:"Italic (Ctrl+I)",active:e.italic,onMouseDown:r("italic"),extraStyle:{fontStyle:"italic"}}),s.jsx("div",{style:P}),s.jsxs("label",{style:F,title:"Text Color",onMouseDown:i,children:["A",s.jsx("input",{type:"color",defaultValue:"#000000",onChange:m,style:z})]}),s.jsxs("label",{style:F,title:"Highlight Color",onMouseDown:i,children:[s.jsx("span",{style:{backgroundColor:"#ffff00",padding:"0 2px"},children:"A"}),s.jsx("input",{type:"color",defaultValue:"#ffff00",onChange:x,style:z})]})]})},D=({icon:a,title:e,active:t,onMouseDown:i,extraStyle:n})=>s.jsx("button",{type:"button",title:e,onMouseDown:i,style:{width:"28px",height:"28px",borderWidth:"1px",borderStyle:"solid",borderColor:t?"#b0b5bd":"transparent",borderRadius:"3px",backgroundColor:t?"#d0d5dd":"transparent",cursor:"pointer",display:"flex",alignItems:"center",justifyContent:"center",fontSize:"13px",color:"#333",padding:0,fontFamily:"inherit",...n},dangerouslySetInnerHTML:{__html:a}}),Y={display:"flex",alignItems:"center",gap:"2px",flexWrap:"wrap"},I={height:"28px",borderWidth:"1px",borderStyle:"solid",borderColor:"#ccc",borderRadius:"3px",fontSize:"12px",padding:"0 4px",cursor:"pointer",backgroundColor:"#fff"},P={width:"1px",height:"20px",backgroundColor:"#ddd",margin:"0 4px"},F={position:"relative",width:"28px",height:"28px",display:"flex",alignItems:"center",justifyContent:"center",cursor:"pointer",fontSize:"13px",fontWeight:"bold"},z={position:"absolute",bottom:0,left:0,width:"100%",height:"4px",padding:0,borderWidth:0,borderStyle:"none",cursor:"pointer"},J=({pageConfig:a,pageCount:e,onPageConfigChange:t})=>{const i=o.useCallback(c=>{const u=c.target.value;t({size:u})},[t]),n=o.useCallback(c=>u=>{const m=Math.max(0,parseInt(u.target.value)||0);t({margins:{...a.margins,[c]:m}})},[a.margins,t]),r=typeof a.size=="string"?a.size:"custom";return s.jsxs("div",{style:Q,children:[s.jsxs("label",{style:ee,children:["Page:",s.jsxs("select",{value:r,onChange:i,style:te,children:[s.jsx("option",{value:"letter",children:"Letter (8.5 x 11)"}),s.jsx("option",{value:"a4",children:"A4 (210 x 297mm)"}),s.jsx("option",{value:"legal",children:"Legal (8.5 x 14)"})]})]}),s.jsx("div",{style:B}),s.jsx("span",{style:{fontSize:"12px",color:"#666"},children:"Margins (px):"}),s.jsx(j,{label:"T",value:a.margins.top,onChange:n("top")}),s.jsx(j,{label:"R",value:a.margins.right,onChange:n("right")}),s.jsx(j,{label:"B",value:a.margins.bottom,onChange:n("bottom")}),s.jsx(j,{label:"L",value:a.margins.left,onChange:n("left")}),s.jsx("div",{style:B}),s.jsxs("span",{style:{fontSize:"12px",color:"#666"},children:[e," ",e===1?"page":"pages"]})]})},j=({label:a,value:e,onChange:t})=>s.jsxs("label",{style:ne,title:`${a} margin`,children:[a,":",s.jsx("input",{type:"number",value:e,onChange:t,style:ie,min:0,max:300,step:12})]}),Q={display:"flex",alignItems:"center",gap:"6px",flexWrap:"wrap"},ee={display:"flex",alignItems:"center",gap:"4px",fontSize:"12px",color:"#666"},te={height:"26px",borderWidth:"1px",borderStyle:"solid",borderColor:"#ccc",borderRadius:"3px",fontSize:"12px",padding:"0 4px",cursor:"pointer",backgroundColor:"#fff"},B={width:"1px",height:"20px",backgroundColor:"#ddd",margin:"0 4px"},ne={display:"flex",alignItems:"center",gap:"2px",fontSize:"11px",color:"#666"},ie={width:"44px",height:"24px",borderWidth:"1px",borderStyle:"solid",borderColor:"#ccc",borderRadius:"3px",fontSize:"11px",textAlign:"center",padding:"0 2px"},se=({pageConfig:a,pageCount:e,onExecCommand:t,onPageConfigChange:i})=>s.jsxs("div",{style:ae,children:[s.jsx("div",{style:q,children:s.jsx(Z,{onExecCommand:t})}),s.jsx("div",{style:q,children:s.jsx(J,{pageConfig:a,pageCount:e,onPageConfigChange:i})})]}),ae={borderBottomWidth:"1px",borderBottomStyle:"solid",borderBottomColor:"#d0d0d0",backgroundColor:"#f8f8f8",padding:"4px 8px",display:"flex",flexDirection:"column",gap:"4px",flexShrink:0,zIndex:10},q={display:"flex",alignItems:"center",gap:"4px",minHeight:"32px"};class R{config;constructor(e=L){this.config={...e}}getConfig(){return{...this.config}}setConfig(e){e.size!==void 0&&(this.config.size=e.size),e.margins!==void 0&&(this.config.margins={...e.margins})}getPageDimensions(){return typeof this.config.size=="string"?w[this.config.size]:this.config.size}getContentAreaHeight(){return this.getPageDimensions().height-this.config.margins.top-this.config.margins.bottom}getContentAreaWidth(){return this.getPageDimensions().width-this.config.margins.left-this.config.margins.right}measureBlocks(e){const t=Array.from(e.children),i=[];for(let n=0;n<t.length;n++){const r=t[n],c=window.getComputedStyle(r),u=c.getPropertyValue("break-before")==="page"||c.getPropertyValue("page-break-before")==="always",m=c.getPropertyValue("break-after")==="page"||c.getPropertyValue("page-break-after")==="always",x=r.getBoundingClientRect(),l=parseFloat(c.marginTop)||0,d=parseFloat(c.marginBottom)||0,p=x.height+l+d;i.push({index:n,height:p,element:r,breakBefore:u,breakAfter:m})}return i}paginate(e){if(e.length===0)return{pages:[{blockIndices:[]}],pageCount:1};const t=this.getContentAreaHeight(),i=[];let n=[],r=0;for(let c=0;c<e.length;c++){const u=e[c];u.breakBefore&&n.length>0&&(i.push({blockIndices:n}),n=[],r=0),r+u.height>t&&n.length>0&&(i.push({blockIndices:n}),n=[],r=0),n.push(u.index),r+=u.height,u.breakAfter&&(i.push({blockIndices:n}),n=[],r=0)}return n.length>0&&i.push({blockIndices:n}),i.length===0&&i.push({blockIndices:[]}),{pages:i,pageCount:i.length}}}class E{observer=null;changeCallbacks=new Set;contextCallbacks=new Set;undoStack=[];redoStack=[];container=null;debounceTimer=null;selectionHandler=null;currentContext="none";static MAX_UNDO_STACK=100;static DEBOUNCE_MS=150;attach(e){this.detach(),this.container=e,this.makeChildrenEditable(e),this.pushUndoSnapshot(),this.observer=new MutationObserver(this.handleMutations),this.observer.observe(e,{childList:!0,subtree:!0,characterData:!0,attributes:!0,attributeFilter:["style","class"]}),this.selectionHandler=this.handleSelectionChange.bind(this),document.addEventListener("selectionchange",this.selectionHandler)}detach(){this.observer&&(this.observer.disconnect(),this.observer=null),this.selectionHandler&&(document.removeEventListener("selectionchange",this.selectionHandler),this.selectionHandler=null),this.debounceTimer&&(clearTimeout(this.debounceTimer),this.debounceTimer=null),this.container=null}makeChildrenEditable(e){const t=Array.from(e.children);for(const i of t)i.tagName==="TABLE"?this.makeTableCellsEditable(i):i.contentEditable="true"}makeTableCellsEditable(e){e.querySelectorAll("td, th").forEach(i=>{i.contentEditable="true"})}handleMutations=e=>{this.debounceTimer&&clearTimeout(this.debounceTimer),this.debounceTimer=setTimeout(()=>{this.pushUndoSnapshot(),this.notifyChange()},E.DEBOUNCE_MS)};handleSelectionChange(){const e=window.getSelection();if(!e||e.rangeCount===0||!this.container){this.setContext("none");return}const i=e.getRangeAt(0).startContainer;let n=i;for(;n&&n!==this.container;){if(n instanceof HTMLElement){const r=n.tagName;if(r==="TD"||r==="TH"||r==="TABLE"){this.setContext("table");return}if(r==="IMG"){this.setContext("image");return}if(n.dataset?.dopecanvasChart){this.setContext("chart");return}}n=n.parentNode}this.container.contains(i)?this.setContext("text"):this.setContext("none")}setContext(e){e!==this.currentContext&&(this.currentContext=e,this.contextCallbacks.forEach(t=>t(e)))}getContext(){return this.currentContext}pushUndoSnapshot(){if(!this.container)return;const e=this.container.innerHTML,t=this.undoStack[this.undoStack.length-1];t&&t.html===e||(this.undoStack.push({html:e,timestamp:Date.now()}),this.redoStack=[],this.undoStack.length>E.MAX_UNDO_STACK&&this.undoStack.shift())}undo(){if(!this.container||this.undoStack.length<=1)return!1;const e=this.undoStack.pop();this.redoStack.push(e);const t=this.undoStack[this.undoStack.length-1];return this.pauseObserver(()=>{this.container.innerHTML=t.html,this.makeChildrenEditable(this.container)}),this.notifyChange(),!0}redo(){if(!this.container||this.redoStack.length===0)return!1;const e=this.redoStack.pop();return this.undoStack.push(e),this.pauseObserver(()=>{this.container.innerHTML=e.html,this.makeChildrenEditable(this.container)}),this.notifyChange(),!0}pauseObserver(e){this.observer&&this.observer.disconnect(),e(),this.observer&&this.container&&this.observer.observe(this.container,{childList:!0,subtree:!0,characterData:!0,attributes:!0,attributeFilter:["style","class"]})}onChange(e){return this.changeCallbacks.add(e),()=>{this.changeCallbacks.delete(e)}}onContextChange(e){return this.contextCallbacks.add(e),()=>{this.contextCallbacks.delete(e)}}notifyChange(){this.changeCallbacks.forEach(e=>e())}execCommand(e,t){return document.execCommand(e,!1,t)}queryCommandState(e){return document.queryCommandState(e)}queryCommandValue(e){return document.queryCommandValue(e)}getHTML(){return this.container?this.container.innerHTML:""}getPlainText(){return this.container&&(this.container.innerText||this.container.textContent)||""}}const oe=({html:a="",css:e,pageConfig:t,onContentChange:i,onPageConfigChange:n,style:r})=>{const[c,u]=o.useState(t||L),[m,x]=o.useState({pages:[],pageCount:0}),l=o.useRef(a),d=o.useRef(i);d.current=i;const p=t||c,S=o.useMemo(()=>new R(p),[]),C=o.useMemo(()=>new E,[]);o.useEffect(()=>{S.setConfig(p)},[p,S]);const v=o.useCallback(h=>{l.current=h,d.current?.(h)},[]),g=o.useCallback(h=>{const f={...p,...h,margins:{...p.margins,...h.margins||{}}};u(f),S.setConfig(f),n?.(f)},[p,S,n]),y=o.useCallback(h=>{x(h)},[]),b=o.useCallback((h,f)=>{C.execCommand(h,f)},[C]);return s.jsxs("div",{className:"dopecanvas-root",style:{display:"flex",flexDirection:"column",height:"100%",width:"100%",fontFamily:"system-ui, -apple-system, BlinkMacSystemFont, sans-serif",...r},children:[s.jsx(se,{pageConfig:p,pageCount:m.pageCount,onExecCommand:b,onPageConfigChange:g}),s.jsx(U,{html:a,css:e,pageConfig:p,layoutEngine:S,editableManager:C,onContentChange:v,onPaginationChange:y})]})};class ${layoutEngine;editableManager;sourceHTML="";sourceCSS="";measureContainer=null;contentContainer=null;paginationResult={pages:[],pageCount:0};paginationCallbacks=new Set;changeCallbacks=new Set;constructor(e=L){this.layoutEngine=new R(e),this.editableManager=new E}getLayoutEngine(){return this.layoutEngine}getEditableManager(){return this.editableManager}getPaginationResult(){return this.paginationResult}getSourceHTML(){return this.sourceHTML}getPageConfig(){return this.layoutEngine.getConfig()}loadHTML(e,t){this.sourceHTML=e,this.sourceCSS=t||""}setMeasureContainer(e){this.measureContainer=e}setContentContainer(e){this.contentContainer&&this.editableManager.detach(),this.contentContainer=e}runPagination(){if(!this.measureContainer)return{result:{pages:[{blockIndices:[]}],pageCount:1},measurements:[]};const e=this.layoutEngine.getContentAreaWidth();this.measureContainer.style.width=`${e}px`,this.measureContainer.style.position="absolute",this.measureContainer.style.left="-9999px",this.measureContainer.style.top="0",this.measureContainer.style.visibility="hidden";let t=null;this.sourceCSS&&(t=document.createElement("style"),t.textContent=this.sourceCSS,this.measureContainer.appendChild(t));const i=document.createElement("div");i.innerHTML=this.sourceHTML,this.measureContainer.appendChild(i);const n=this.layoutEngine.measureBlocks(i);return this.paginationResult=this.layoutEngine.paginate(n),this.measureContainer.innerHTML="",this.paginationCallbacks.forEach(r=>r(this.paginationResult)),{result:this.paginationResult,measurements:n}}rePaginate(){if(!this.contentContainer)return this.paginationResult;const e=this.layoutEngine.measureBlocks(this.contentContainer);return this.paginationResult=this.layoutEngine.paginate(e),this.paginationCallbacks.forEach(t=>t(this.paginationResult)),this.paginationResult}attachEditing(e){this.contentContainer=e,this.editableManager.attach(e),this.editableManager.onChange(()=>{this.sourceHTML=e.innerHTML,this.changeCallbacks.forEach(t=>t(this.sourceHTML))})}setPageConfig(e){this.layoutEngine.setConfig(e)}onPagination(e){return this.paginationCallbacks.add(e),()=>{this.paginationCallbacks.delete(e)}}onChange(e){return this.changeCallbacks.add(e),()=>{this.changeCallbacks.delete(e)}}getHTML(){return this.contentContainer?this.contentContainer.innerHTML:this.sourceHTML}getPlainText(){return this.contentContainer&&(this.contentContainer.innerText||this.contentContainer.textContent)||""}destroy(){this.editableManager.detach(),this.paginationCallbacks.clear(),this.changeCallbacks.clear(),this.measureContainer=null,this.contentContainer=null}}class re{_html="";_css="";_pageConfig=null;_paginationResult={pages:[],pageCount:0};_changeCallbacks=new Set;_loadCallbacks=new Set;_pageConfigCallbacks=new Set;_getHTMLFn=null;_getPlainTextFn=null;loadHTML(e,t){this._html=e,this._css=t||"",this._loadCallbacks.forEach(i=>i(e,t))}getHTML(){return this._getHTMLFn?this._getHTMLFn():this._html}getPlainText(){if(this._getPlainTextFn)return this._getPlainTextFn();const e=document.createElement("div");return e.innerHTML=this._html,e.innerText||e.textContent||""}onChange(e){return this._changeCallbacks.add(e),()=>{this._changeCallbacks.delete(e)}}onLoad(e){return this._loadCallbacks.add(e),()=>{this._loadCallbacks.delete(e)}}onPageConfigChange(e){return this._pageConfigCallbacks.add(e),()=>{this._pageConfigCallbacks.delete(e)}}getPageCount(){return this._paginationResult.pageCount}getPageConfig(){return this._pageConfig}setPageConfig(e){this._pageConfig&&(this._pageConfig={...this._pageConfig,...e,margins:{...this._pageConfig.margins,...e.margins||{}}},this._pageConfigCallbacks.forEach(t=>t(this._pageConfig)))}querySelectorAll(e){const t=document.createElement("div");return t.innerHTML=this.getHTML(),Array.from(t.querySelectorAll(e))}getElementContent(e){const t=document.createElement("div");t.innerHTML=this.getHTML();const i=t.querySelector(`#${e}`);return i?i.innerHTML:null}setElementContent(e,t){const i=document.createElement("div");i.innerHTML=this.getHTML();const n=i.querySelector(`#${e}`);n&&(n.innerHTML=t,this.loadHTML(i.innerHTML,this._css))}_connectGetHTML(e){this._getHTMLFn=e}_connectGetPlainText(e){this._getPlainTextFn=e}_notifyChange(e){this._html=e,this._changeCallbacks.forEach(t=>t(e))}_updatePagination(e){this._paginationResult=e}_updatePageConfig(e){this._pageConfig=e}}function le(a={}){const{initialHTML:e="",initialCSS:t="",initialConfig:i=L}=a,n=o.useRef(new $(i)),[r,c]=o.useState({pages:[{blockIndices:[]}],pageCount:1}),[u,m]=o.useState(i),x=o.useCallback((C,v)=>{n.current.loadHTML(C,v)},[]),l=o.useCallback(C=>{n.current.setPageConfig(C),m(n.current.getPageConfig())},[]),d=o.useCallback(()=>{const{result:C}=n.current.runPagination();c(C)},[]),p=o.useCallback(()=>n.current.getHTML(),[]),S=o.useCallback(()=>n.current.getPlainText(),[]);return o.useEffect(()=>{e&&n.current.loadHTML(e,t)},[e,t]),o.useEffect(()=>n.current.onPagination(v=>{c(v)}),[]),o.useEffect(()=>()=>{n.current.destroy()},[]),{engine:n.current,paginationResult:r,pageConfig:u,loadHTML:x,setPageConfig:l,triggerPagination:d,getHTML:p,getPlainText:S}}exports.DEFAULT_MARGINS=N;exports.DEFAULT_PAGE_CONFIG=L;exports.DocumentAPI=re;exports.DocumentEngine=$;exports.DopeCanvas=oe;exports.EditableManager=E;exports.PAGE_SIZE_PRESETS=w;exports.PageLayoutEngine=R;exports.useDocumentEngine=le;exports.useFormattingState=W;exports.useSelectionContext=X;
|
|
3
|
+
//# sourceMappingURL=dopecanvas.cjs.map
|