@helping-ai-workflow/md2doc 2.6.1 → 2.7.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.
Files changed (2) hide show
  1. package/lib/md2doc.js +462 -3
  2. package/package.json +2 -2
package/lib/md2doc.js CHANGED
@@ -1364,6 +1364,31 @@ const html = `<!DOCTYPE html>
1364
1364
  cursor: pointer;
1365
1365
  }
1366
1366
  .lightbox-bar button:hover { background: rgba(255, 255, 255, 0.18); }
1367
+ .lightbox-bar button.is-active {
1368
+ background: rgba(147, 197, 253, 0.35);
1369
+ box-shadow: inset 0 0 0 1px #93c5fd;
1370
+ }
1371
+ .lightbox-sep {
1372
+ width: 1px;
1373
+ align-self: stretch;
1374
+ margin: 4px 2px;
1375
+ background: rgba(255, 255, 255, 0.25);
1376
+ }
1377
+ /* Annotation overlay: absolute twin of the artwork, same viewBox, so shapes
1378
+ live in image coordinates and ride every zoom for free. Must undo the
1379
+ .lightbox-canvas > * block/white-background defaults. */
1380
+ .lightbox-canvas > .lightbox-anno {
1381
+ position: absolute;
1382
+ inset: 0;
1383
+ width: 100%;
1384
+ height: 100%;
1385
+ background: transparent;
1386
+ pointer-events: none;
1387
+ }
1388
+ .lightbox-stage[data-anno-cursor="draw"],
1389
+ .lightbox-stage[data-anno-cursor="draw"] .lightbox-anno { cursor: crosshair; }
1390
+ .lightbox-anno g[data-anno-id] { cursor: move; }
1391
+ .lightbox-anno [data-anno-handle] { cursor: nwse-resize; }
1367
1392
  .lightbox-zoom-value {
1368
1393
  min-width: 56px;
1369
1394
  text-align: center;
@@ -1384,7 +1409,7 @@ const html = `<!DOCTYPE html>
1384
1409
  overscroll-behavior: contain;
1385
1410
  }
1386
1411
  .lightbox-stage[data-panning] { cursor: grabbing; }
1387
- .lightbox-canvas { margin: 0 auto; }
1412
+ .lightbox-canvas { margin: 0 auto; position: relative; }
1388
1413
  /* Sized in px by the runtime; the child follows, so the scroll extent grows
1389
1414
  with the zoom. A CSS transform would scale the pixels and leave the scroll
1390
1415
  area at the original size, stranding the edges out of reach. */
@@ -2158,6 +2183,21 @@ ${mermaidInitTag}` : ''}
2158
2183
  var lightboxNaturalH = 1;
2159
2184
  var lightboxReturnScroll = 0;
2160
2185
 
2186
+ // Annotation layer state. Shapes are keyed per source node in memory only:
2187
+ // close/reopen keeps them, reload starts clean (they are reading-session
2188
+ // scratch, and localStorage keys would go stale when the doc regenerates).
2189
+ var ANNO_NS = 'http://www.w3.org/2000/svg';
2190
+ var annoStore = (typeof WeakMap === 'function') ? new WeakMap() : null;
2191
+ var annoShapes = [];
2192
+ var annoSvg = null;
2193
+ var annoMode = null; // null | 'f' | 'e' | 'r' | 'm'
2194
+ var annoSelected = null;
2195
+ var annoDraft = null;
2196
+ var annoUndoStack = [];
2197
+ var annoRedoStack = [];
2198
+ var annoStrokeW = 3;
2199
+ var annoToolButtons = {};
2200
+
2161
2201
  function lightboxIsOpen() {
2162
2202
  return !!lightboxEl && !lightboxEl.hidden;
2163
2203
  }
@@ -2209,6 +2249,377 @@ ${mermaidInitTag}` : ''}
2209
2249
  setLightboxZoom(lightboxZoom * factor, anchorX, anchorY);
2210
2250
  }
2211
2251
 
2252
+ // ── Lightbox annotation layer ─────────────────────────────────────────────
2253
+ function annoGeomOf(shape) {
2254
+ if (shape.type === 'path') return { pts: shape.pts.map(function (p) { return [p[0], p[1]]; }) };
2255
+ var geom = {};
2256
+ Object.keys(shape).forEach(function (k) { if (k !== 'type') geom[k] = shape[k]; });
2257
+ return geom;
2258
+ }
2259
+
2260
+ function annoSetGeom(shape, geom) {
2261
+ if (shape.type === 'path') shape.pts = geom.pts.map(function (p) { return [p[0], p[1]]; });
2262
+ else Object.keys(geom).forEach(function (k) { shape[k] = geom[k]; });
2263
+ }
2264
+
2265
+ function annoBBox(shape) {
2266
+ if (shape.type === 'rect') return { x: shape.x, y: shape.y, w: shape.w, h: shape.h };
2267
+ if (shape.type === 'ellipse') return { x: shape.cx - shape.rx, y: shape.cy - shape.ry, w: shape.rx * 2, h: shape.ry * 2 };
2268
+ if (shape.type === 'line' || shape.type === 'arrow') {
2269
+ return {
2270
+ x: Math.min(shape.x1, shape.x2), y: Math.min(shape.y1, shape.y2),
2271
+ w: Math.abs(shape.x2 - shape.x1), h: Math.abs(shape.y2 - shape.y1),
2272
+ };
2273
+ }
2274
+ var xs = shape.pts.map(function (p) { return p[0]; });
2275
+ var ys = shape.pts.map(function (p) { return p[1]; });
2276
+ var x = Math.min.apply(null, xs);
2277
+ var y = Math.min.apply(null, ys);
2278
+ return { x: x, y: y, w: Math.max.apply(null, xs) - x, h: Math.max.apply(null, ys) - y };
2279
+ }
2280
+
2281
+ function annoApplyBBox(shape, from, to) {
2282
+ var sx = from.w > 0.01 ? to.w / from.w : 1;
2283
+ var sy = from.h > 0.01 ? to.h / from.h : 1;
2284
+ if (shape.type === 'rect') {
2285
+ shape.x = to.x; shape.y = to.y; shape.w = to.w; shape.h = to.h;
2286
+ } else if (shape.type === 'ellipse') {
2287
+ shape.cx = to.x + to.w / 2; shape.cy = to.y + to.h / 2;
2288
+ shape.rx = to.w / 2; shape.ry = to.h / 2;
2289
+ } else if (shape.type === 'line' || shape.type === 'arrow') {
2290
+ var x1 = to.x + (shape.x1 - from.x) * sx;
2291
+ var y1 = to.y + (shape.y1 - from.y) * sy;
2292
+ var x2 = to.x + (shape.x2 - from.x) * sx;
2293
+ var y2 = to.y + (shape.y2 - from.y) * sy;
2294
+ shape.x1 = x1; shape.y1 = y1; shape.x2 = x2; shape.y2 = y2;
2295
+ } else {
2296
+ shape.pts = shape.pts.map(function (p) {
2297
+ return [to.x + (p[0] - from.x) * sx, to.y + (p[1] - from.y) * sy];
2298
+ });
2299
+ }
2300
+ }
2301
+
2302
+ function annoPathD(pts) {
2303
+ return pts.map(function (p, i) {
2304
+ return (i === 0 ? 'M' : 'L') + p[0].toFixed(1) + ' ' + p[1].toFixed(1);
2305
+ }).join(' ');
2306
+ }
2307
+
2308
+ function annoShapeEl(shape) {
2309
+ var tag = (shape.type === 'line' || shape.type === 'arrow') ? 'line' : shape.type;
2310
+ var el = document.createElementNS(ANNO_NS, tag);
2311
+ if (shape.type === 'line' || shape.type === 'arrow') {
2312
+ el.setAttribute('x1', shape.x1.toFixed(1)); el.setAttribute('y1', shape.y1.toFixed(1));
2313
+ el.setAttribute('x2', shape.x2.toFixed(1)); el.setAttribute('y2', shape.y2.toFixed(1));
2314
+ el.setAttribute('stroke-linecap', 'round');
2315
+ if (shape.type === 'arrow') el.setAttribute('marker-end', 'url(#anno-arrow-head)');
2316
+ } else if (shape.type === 'rect') {
2317
+ el.setAttribute('x', shape.x.toFixed(1)); el.setAttribute('y', shape.y.toFixed(1));
2318
+ el.setAttribute('width', Math.max(0.1, shape.w).toFixed(1));
2319
+ el.setAttribute('height', Math.max(0.1, shape.h).toFixed(1));
2320
+ el.setAttribute('fill', 'transparent');
2321
+ } else if (shape.type === 'ellipse') {
2322
+ el.setAttribute('cx', shape.cx.toFixed(1)); el.setAttribute('cy', shape.cy.toFixed(1));
2323
+ el.setAttribute('rx', Math.max(0.1, shape.rx).toFixed(1));
2324
+ el.setAttribute('ry', Math.max(0.1, shape.ry).toFixed(1));
2325
+ el.setAttribute('fill', 'transparent');
2326
+ } else {
2327
+ el.setAttribute('d', annoPathD(shape.pts));
2328
+ el.setAttribute('fill', 'none');
2329
+ el.setAttribute('stroke-linejoin', 'round');
2330
+ el.setAttribute('stroke-linecap', 'round');
2331
+ }
2332
+ el.setAttribute('stroke', '#ef4444');
2333
+ el.setAttribute('stroke-width', String(annoStrokeW));
2334
+ return el;
2335
+ }
2336
+
2337
+ function annoEnsureDefs() {
2338
+ var defs = document.createElementNS(ANNO_NS, 'defs');
2339
+ var marker = document.createElementNS(ANNO_NS, 'marker');
2340
+ marker.setAttribute('id', 'anno-arrow-head');
2341
+ marker.setAttribute('markerWidth', '8');
2342
+ marker.setAttribute('markerHeight', '8');
2343
+ marker.setAttribute('refX', '6.4');
2344
+ marker.setAttribute('refY', '3');
2345
+ marker.setAttribute('orient', 'auto');
2346
+ var tip = document.createElementNS(ANNO_NS, 'path');
2347
+ tip.setAttribute('d', 'M0 0 L7 3 L0 6 Z');
2348
+ tip.setAttribute('fill', '#ef4444');
2349
+ marker.appendChild(tip);
2350
+ defs.appendChild(marker);
2351
+ annoSvg.appendChild(defs);
2352
+ }
2353
+
2354
+ function annoRedraw() {
2355
+ if (!annoSvg) return;
2356
+ while (annoSvg.firstChild) annoSvg.removeChild(annoSvg.firstChild);
2357
+ annoEnsureDefs();
2358
+ annoShapes.forEach(function (shape, index) {
2359
+ var g = document.createElementNS(ANNO_NS, 'g');
2360
+ g.setAttribute('data-anno-id', String(index));
2361
+ g.appendChild(annoShapeEl(shape));
2362
+ if (shape.type === 'path' || shape.type === 'line' || shape.type === 'arrow') {
2363
+ // A 3px stroke is an unclickable target — give strokes a fat invisible twin.
2364
+ var hit = document.createElementNS(ANNO_NS, shape.type === 'path' ? 'path' : 'line');
2365
+ if (shape.type === 'path') {
2366
+ hit.setAttribute('d', annoPathD(shape.pts));
2367
+ } else {
2368
+ hit.setAttribute('x1', shape.x1); hit.setAttribute('y1', shape.y1);
2369
+ hit.setAttribute('x2', shape.x2); hit.setAttribute('y2', shape.y2);
2370
+ }
2371
+ hit.setAttribute('fill', 'none');
2372
+ hit.setAttribute('stroke', 'transparent');
2373
+ hit.setAttribute('stroke-width', String(annoStrokeW * 5));
2374
+ hit.setAttribute('data-anno-hit', '');
2375
+ g.appendChild(hit);
2376
+ }
2377
+ annoSvg.appendChild(g);
2378
+ });
2379
+ if (annoSelected && annoShapes.indexOf(annoSelected) !== -1) {
2380
+ var box = annoBBox(annoSelected);
2381
+ var ui = document.createElementNS(ANNO_NS, 'rect');
2382
+ ui.setAttribute('data-anno-ui', '');
2383
+ ui.setAttribute('x', box.x); ui.setAttribute('y', box.y);
2384
+ ui.setAttribute('width', Math.max(0.1, box.w)); ui.setAttribute('height', Math.max(0.1, box.h));
2385
+ ui.setAttribute('fill', 'none');
2386
+ ui.setAttribute('stroke', '#3b82f6');
2387
+ ui.setAttribute('stroke-width', String(Math.max(1, annoStrokeW / 2)));
2388
+ ui.setAttribute('stroke-dasharray', (annoStrokeW * 2) + ' ' + annoStrokeW);
2389
+ annoSvg.appendChild(ui);
2390
+ var hs = annoStrokeW * 3;
2391
+ [[box.x, box.y], [box.x + box.w, box.y], [box.x, box.y + box.h], [box.x + box.w, box.y + box.h]]
2392
+ .forEach(function (corner, i) {
2393
+ var h = document.createElementNS(ANNO_NS, 'rect');
2394
+ h.setAttribute('data-anno-ui', '');
2395
+ h.setAttribute('data-anno-handle', String(i));
2396
+ h.setAttribute('x', corner[0] - hs / 2); h.setAttribute('y', corner[1] - hs / 2);
2397
+ h.setAttribute('width', hs); h.setAttribute('height', hs);
2398
+ h.setAttribute('fill', '#3b82f6');
2399
+ annoSvg.appendChild(h);
2400
+ });
2401
+ }
2402
+ }
2403
+
2404
+ function annoPushOp(op) {
2405
+ annoUndoStack.push(op);
2406
+ annoRedoStack.length = 0;
2407
+ }
2408
+
2409
+ function annoApplyOp(op, reverse) {
2410
+ if (op.kind === 'add') {
2411
+ if (reverse) annoShapes.splice(annoShapes.indexOf(op.shape), 1);
2412
+ else annoShapes.splice(Math.min(op.index, annoShapes.length), 0, op.shape);
2413
+ } else if (op.kind === 'del') {
2414
+ if (reverse) annoShapes.splice(Math.min(op.index, annoShapes.length), 0, op.shape);
2415
+ else annoShapes.splice(annoShapes.indexOf(op.shape), 1);
2416
+ } else if (op.kind === 'geom') {
2417
+ annoSetGeom(op.shape, reverse ? op.before : op.after);
2418
+ } else if (op.kind === 'clear') {
2419
+ if (reverse) op.shapes.forEach(function (s) { annoShapes.push(s); });
2420
+ else annoShapes.length = 0;
2421
+ }
2422
+ }
2423
+
2424
+ function annoUndo() {
2425
+ var op = annoUndoStack.pop();
2426
+ if (!op) return;
2427
+ annoApplyOp(op, true);
2428
+ annoRedoStack.push(op);
2429
+ annoSelected = null;
2430
+ annoRedraw();
2431
+ }
2432
+
2433
+ function annoRedo() {
2434
+ var op = annoRedoStack.pop();
2435
+ if (!op) return;
2436
+ annoApplyOp(op, false);
2437
+ annoUndoStack.push(op);
2438
+ annoSelected = null;
2439
+ annoRedraw();
2440
+ }
2441
+
2442
+ function annoClearAll() {
2443
+ if (!annoShapes.length) return;
2444
+ annoPushOp({ kind: 'clear', shapes: annoShapes.slice() });
2445
+ annoShapes.length = 0;
2446
+ annoSelected = null;
2447
+ annoRedraw();
2448
+ }
2449
+
2450
+ function annoDeleteSelected() {
2451
+ if (!annoSelected) return;
2452
+ var index = annoShapes.indexOf(annoSelected);
2453
+ if (index === -1) return;
2454
+ annoPushOp({ kind: 'del', shape: annoSelected, index: index });
2455
+ annoShapes.splice(index, 1);
2456
+ annoSelected = null;
2457
+ annoRedraw();
2458
+ }
2459
+
2460
+ function setAnnoMode(mode) {
2461
+ annoMode = (annoMode === mode) ? null : mode;
2462
+ if (annoMode !== 'm') annoSelected = null;
2463
+ Object.keys(annoToolButtons).forEach(function (key) {
2464
+ annoToolButtons[key].classList.toggle('is-active', key === annoMode);
2465
+ });
2466
+ if (annoSvg) annoSvg.style.pointerEvents = annoMode ? 'auto' : 'none';
2467
+ if (lightboxStage) {
2468
+ if (annoMode === 'f' || annoMode === 'e' || annoMode === 'r') {
2469
+ lightboxStage.setAttribute('data-anno-cursor', 'draw');
2470
+ } else {
2471
+ lightboxStage.removeAttribute('data-anno-cursor');
2472
+ }
2473
+ }
2474
+ annoRedraw();
2475
+ }
2476
+
2477
+ function annoPoint(event) {
2478
+ var rect = annoSvg.getBoundingClientRect();
2479
+ var scale = rect.width > 0 ? lightboxNaturalW / rect.width : 1;
2480
+ return { x: (event.clientX - rect.left) * scale, y: (event.clientY - rect.top) * scale };
2481
+ }
2482
+
2483
+ function annoCancelDraft() {
2484
+ if (!annoDraft) return;
2485
+ if (annoDraft.el && annoDraft.el.parentNode) annoDraft.el.parentNode.removeChild(annoDraft.el);
2486
+ annoDraft = null;
2487
+ }
2488
+
2489
+ function annoPointerDown(event) {
2490
+ if (!annoMode || event.button !== 0) return;
2491
+ event.preventDefault();
2492
+ event.stopPropagation();
2493
+ if (annoSvg.setPointerCapture) {
2494
+ try { annoSvg.setPointerCapture(event.pointerId); } catch (e) { /* detached */ }
2495
+ }
2496
+ var pt = annoPoint(event);
2497
+ if (annoMode === 'm') {
2498
+ var handle = event.target.closest ? event.target.closest('[data-anno-handle]') : null;
2499
+ var group = event.target.closest ? event.target.closest('g[data-anno-id]') : null;
2500
+ if (handle && annoSelected) {
2501
+ var corner = Number(handle.getAttribute('data-anno-handle'));
2502
+ var box = annoBBox(annoSelected);
2503
+ annoDraft = {
2504
+ kind: 'resize', shape: annoSelected, corner: corner,
2505
+ startBox: box, before: annoGeomOf(annoSelected), moved: false,
2506
+ anchor: {
2507
+ x: (corner === 0 || corner === 2) ? box.x + box.w : box.x,
2508
+ y: (corner === 0 || corner === 1) ? box.y + box.h : box.y,
2509
+ },
2510
+ };
2511
+ } else if (group) {
2512
+ annoSelected = annoShapes[Number(group.getAttribute('data-anno-id'))] || null;
2513
+ annoDraft = annoSelected ? {
2514
+ kind: 'move', shape: annoSelected, start: pt,
2515
+ startBox: annoBBox(annoSelected), before: annoGeomOf(annoSelected), moved: false,
2516
+ } : null;
2517
+ annoRedraw();
2518
+ } else {
2519
+ annoSelected = null;
2520
+ annoRedraw();
2521
+ }
2522
+ return;
2523
+ }
2524
+ var shape;
2525
+ if (annoMode === 'f') shape = { type: 'path', pts: [[pt.x, pt.y]] };
2526
+ else if (annoMode === 'e') shape = { type: 'ellipse', cx: pt.x, cy: pt.y, rx: 0, ry: 0 };
2527
+ else if (annoMode === 'l') shape = { type: 'line', x1: pt.x, y1: pt.y, x2: pt.x, y2: pt.y };
2528
+ else if (annoMode === 'a') shape = { type: 'arrow', x1: pt.x, y1: pt.y, x2: pt.x, y2: pt.y };
2529
+ else shape = { type: 'rect', x: pt.x, y: pt.y, w: 0, h: 0 };
2530
+ annoDraft = { kind: 'draw', shape: shape, start: pt, el: annoShapeEl(shape), clientDist: 0, lastClient: [event.clientX, event.clientY] };
2531
+ annoSvg.appendChild(annoDraft.el);
2532
+ }
2533
+
2534
+ function annoUpdateDraftEl() {
2535
+ var d = annoDraft;
2536
+ var fresh = annoShapeEl(d.shape);
2537
+ d.el.parentNode.replaceChild(fresh, d.el);
2538
+ d.el = fresh;
2539
+ }
2540
+
2541
+ function annoPointerMove(event) {
2542
+ if (!annoDraft) return;
2543
+ var pt = annoPoint(event);
2544
+ var d = annoDraft;
2545
+ if (d.kind === 'draw') {
2546
+ d.clientDist += Math.abs(event.clientX - d.lastClient[0]) + Math.abs(event.clientY - d.lastClient[1]);
2547
+ d.lastClient = [event.clientX, event.clientY];
2548
+ if (d.shape.type === 'path') {
2549
+ d.shape.pts.push([pt.x, pt.y]);
2550
+ } else if (d.shape.type === 'line' || d.shape.type === 'arrow') {
2551
+ d.shape.x2 = pt.x; d.shape.y2 = pt.y;
2552
+ } else if (d.shape.type === 'ellipse') {
2553
+ d.shape.cx = (d.start.x + pt.x) / 2; d.shape.cy = (d.start.y + pt.y) / 2;
2554
+ d.shape.rx = Math.abs(pt.x - d.start.x) / 2; d.shape.ry = Math.abs(pt.y - d.start.y) / 2;
2555
+ } else {
2556
+ d.shape.x = Math.min(d.start.x, pt.x); d.shape.y = Math.min(d.start.y, pt.y);
2557
+ d.shape.w = Math.abs(pt.x - d.start.x); d.shape.h = Math.abs(pt.y - d.start.y);
2558
+ }
2559
+ annoUpdateDraftEl();
2560
+ return;
2561
+ }
2562
+ d.moved = true;
2563
+ if (d.kind === 'move') {
2564
+ var box = d.startBox;
2565
+ annoApplyBBox(d.shape, annoBBox(d.shape), {
2566
+ x: box.x + (pt.x - d.start.x), y: box.y + (pt.y - d.start.y), w: box.w, h: box.h,
2567
+ });
2568
+ } else {
2569
+ var ax = d.anchor.x;
2570
+ var ay = d.anchor.y;
2571
+ annoApplyBBox(d.shape, annoBBox(d.shape), {
2572
+ x: Math.min(ax, pt.x), y: Math.min(ay, pt.y),
2573
+ w: Math.max(1, Math.abs(pt.x - ax)), h: Math.max(1, Math.abs(pt.y - ay)),
2574
+ });
2575
+ }
2576
+ annoRedraw();
2577
+ }
2578
+
2579
+ function annoPointerUp() {
2580
+ if (!annoDraft) return;
2581
+ var d = annoDraft;
2582
+ annoDraft = null;
2583
+ if (d.kind === 'draw') {
2584
+ if (d.el.parentNode) d.el.parentNode.removeChild(d.el);
2585
+ var tooSmall = d.clientDist < 4 ||
2586
+ (d.shape.type === 'path' && d.shape.pts.length < 2);
2587
+ if (!tooSmall) {
2588
+ annoPushOp({ kind: 'add', shape: d.shape, index: annoShapes.length });
2589
+ annoShapes.push(d.shape);
2590
+ }
2591
+ annoRedraw();
2592
+ return;
2593
+ }
2594
+ if (d.moved) {
2595
+ annoPushOp({ kind: 'geom', shape: d.shape, before: d.before, after: annoGeomOf(d.shape) });
2596
+ }
2597
+ annoRedraw();
2598
+ }
2599
+
2600
+ function annoSetup(sourceNode) {
2601
+ annoShapes = [];
2602
+ if (annoStore) {
2603
+ annoShapes = annoStore.get(sourceNode);
2604
+ if (!annoShapes) { annoShapes = []; annoStore.set(sourceNode, annoShapes); }
2605
+ }
2606
+ annoUndoStack = [];
2607
+ annoRedoStack = [];
2608
+ annoSelected = null;
2609
+ annoCancelDraft();
2610
+ annoStrokeW = Math.max(3, Math.round(Math.max(lightboxNaturalW, lightboxNaturalH) / 300));
2611
+ annoSvg = document.createElementNS(ANNO_NS, 'svg');
2612
+ annoSvg.setAttribute('class', 'lightbox-anno');
2613
+ annoSvg.setAttribute('viewBox', '0 0 ' + lightboxNaturalW + ' ' + lightboxNaturalH);
2614
+ annoSvg.addEventListener('pointerdown', annoPointerDown);
2615
+ annoSvg.addEventListener('pointermove', annoPointerMove);
2616
+ annoSvg.addEventListener('pointerup', annoPointerUp);
2617
+ annoSvg.addEventListener('pointercancel', annoPointerUp);
2618
+ lightboxCanvas.appendChild(annoSvg);
2619
+ annoMode = null;
2620
+ setAnnoMode(null);
2621
+ }
2622
+
2212
2623
  function buildLightbox() {
2213
2624
  if (lightboxEl) return;
2214
2625
  lightboxEl = document.createElement('div');
@@ -2221,7 +2632,7 @@ ${mermaidInitTag}` : ''}
2221
2632
  bar.className = 'lightbox-bar';
2222
2633
  var hint = document.createElement('span');
2223
2634
  hint.className = 'lightbox-hint';
2224
- hint.textContent = 'scroll to pan · shift+scroll sideways · ctrl+scroll to zoom · drag to pan · esc to close';
2635
+ hint.textContent = 'ctrl+scroll zoom · drag pan · f/e/r/l/a draw · m select · del remove · ctrl+z undo · esc close';
2225
2636
  lightboxZoomValue = document.createElement('span');
2226
2637
  lightboxZoomValue.className = 'lightbox-zoom-value';
2227
2638
  lightboxZoomValue.setAttribute('data-lightbox-zoom-value', '');
@@ -2231,6 +2642,25 @@ ${mermaidInitTag}` : ''}
2231
2642
  bar.appendChild(lightboxButton('+', 'Zoom in', function () { zoomLightboxBy(LIGHTBOX_STEP); }));
2232
2643
  bar.appendChild(lightboxButton('Fit', 'Fit to window', function () { setLightboxZoom(lightboxFitZoom); }));
2233
2644
  bar.appendChild(lightboxButton('1:1', 'Actual size', function () { setLightboxZoom(1); }));
2645
+ var sep = document.createElement('span');
2646
+ sep.className = 'lightbox-sep';
2647
+ bar.appendChild(sep);
2648
+ [
2649
+ ['f', '✎', 'Freehand (f)'],
2650
+ ['e', '○', 'Ellipse (e)'],
2651
+ ['r', '▭', 'Rectangle (r)'],
2652
+ ['l', '╱', 'Line (l)'],
2653
+ ['a', '↗', 'Arrow (a)'],
2654
+ ['m', '✥', 'Select / move (m)'],
2655
+ ].forEach(function (tool) {
2656
+ var button = lightboxButton(tool[1], tool[2], function () { setAnnoMode(tool[0]); });
2657
+ button.setAttribute('data-anno-tool', tool[0]);
2658
+ annoToolButtons[tool[0]] = button;
2659
+ bar.appendChild(button);
2660
+ });
2661
+ var clearButton = lightboxButton('Clear', 'Clear all annotations', annoClearAll);
2662
+ clearButton.setAttribute('data-anno-clear', '');
2663
+ bar.appendChild(clearButton);
2234
2664
  bar.appendChild(lightboxButton('✕', 'Close', closeLightbox));
2235
2665
 
2236
2666
  lightboxStage = document.createElement('div');
@@ -2261,6 +2691,8 @@ ${mermaidInitTag}` : ''}
2261
2691
 
2262
2692
  lightboxStage.addEventListener('mousedown', function (event) {
2263
2693
  if (event.button !== 0) return;
2694
+ // With a tool armed the overlay owns pointer events over the artwork.
2695
+ if (annoMode && event.target && event.target.closest && event.target.closest('.lightbox-anno')) return;
2264
2696
  panning = true;
2265
2697
  panDistance = 0;
2266
2698
  panX = event.clientX;
@@ -2291,6 +2723,7 @@ ${mermaidInitTag}` : ''}
2291
2723
  });
2292
2724
  lightboxStage.addEventListener('dblclick', function (event) {
2293
2725
  event.preventDefault();
2726
+ if (annoMode && event.target && event.target.closest && event.target.closest('.lightbox-anno')) return;
2294
2727
  setLightboxZoom(Math.abs(lightboxZoom - 1) < 0.001 ? lightboxFitZoom : 1);
2295
2728
  });
2296
2729
  }
@@ -2331,6 +2764,7 @@ ${mermaidInitTag}` : ''}
2331
2764
  lightboxCanvas.removeChild(lightboxCanvas.firstChild);
2332
2765
  }
2333
2766
  lightboxCanvas.appendChild(clone);
2767
+ annoSetup(node);
2334
2768
 
2335
2769
  lightboxReturnScroll = window.scrollY;
2336
2770
  suppressAnchorCapture = true;
@@ -2347,6 +2781,10 @@ ${mermaidInitTag}` : ''}
2347
2781
  if (!lightboxIsOpen()) return;
2348
2782
  lightboxEl.hidden = true;
2349
2783
  document.body.removeAttribute('data-lightbox-open');
2784
+ annoCancelDraft();
2785
+ annoSvg = null;
2786
+ annoMode = null;
2787
+ setAnnoMode(null);
2350
2788
  while (lightboxCanvas.firstChild) {
2351
2789
  lightboxCanvas.removeChild(lightboxCanvas.firstChild);
2352
2790
  }
@@ -2372,7 +2810,28 @@ ${mermaidInitTag}` : ''}
2372
2810
 
2373
2811
  document.addEventListener('keydown', function (event) {
2374
2812
  if (!lightboxIsOpen()) return;
2375
- if (event.key === 'Escape') { event.preventDefault(); closeLightbox(); return; }
2813
+ var key = event.key.toLowerCase();
2814
+ if ((event.ctrlKey || event.metaKey) && key === 'z' && !event.shiftKey) {
2815
+ event.preventDefault(); annoUndo(); return;
2816
+ }
2817
+ if ((event.ctrlKey || event.metaKey) && (key === 'y' || (key === 'z' && event.shiftKey))) {
2818
+ event.preventDefault(); annoRedo(); return;
2819
+ }
2820
+ if (event.ctrlKey || event.metaKey || event.altKey) return;
2821
+ if (event.key === 'Escape') {
2822
+ event.preventDefault();
2823
+ // Layered: cancel an in-progress draw, then drop the selection, then close.
2824
+ if (annoDraft) { annoCancelDraft(); annoRedraw(); return; }
2825
+ if (annoSelected) { annoSelected = null; annoRedraw(); return; }
2826
+ closeLightbox();
2827
+ return;
2828
+ }
2829
+ if (key === 'f' || key === 'e' || key === 'r' || key === 'l' || key === 'a' || key === 'm') {
2830
+ event.preventDefault(); setAnnoMode(key); return;
2831
+ }
2832
+ if (event.key === 'Delete' || event.key === 'Backspace') {
2833
+ event.preventDefault(); annoDeleteSelected(); return;
2834
+ }
2376
2835
  if (event.key === '+' || event.key === '=') { event.preventDefault(); zoomLightboxBy(LIGHTBOX_STEP); return; }
2377
2836
  if (event.key === '-' || event.key === '_') { event.preventDefault(); zoomLightboxBy(1 / LIGHTBOX_STEP); return; }
2378
2837
  if (event.key === '0') { event.preventDefault(); setLightboxZoom(lightboxFitZoom); return; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helping-ai-workflow/md2doc",
3
- "version": "2.6.1",
3
+ "version": "2.7.0",
4
4
  "description": "Markdown → HTML / PDF renderer with WaveDrom, Mermaid, and Graphviz support",
5
5
  "keywords": [
6
6
  "markdown",
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "scripts": {
38
38
  "preinstall": "node scripts/preinstall.js",
39
- "test": "node test/md2doc.test.js && node test/images.test.js && node test/scroll-anchor.test.js && node test/lightbox.test.js && node test/reader-panels.test.js && node test/cli.test.js && node test/code-operator.test.js"
39
+ "test": "node test/md2doc.test.js && node test/images.test.js && node test/scroll-anchor.test.js && node test/lightbox.test.js && node test/lightbox-anno.test.js && node test/reader-panels.test.js && node test/cli.test.js && node test/code-operator.test.js"
40
40
  },
41
41
  "repository": {
42
42
  "type": "git",