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 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