collabmd 0.1.26 → 0.1.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import { createDrawioLeaseRoomName } from '../../domain/drawio-room.js';
2
+ import { setDiagramActionButtonIcon } from '../domain/diagram-action-icons.js';
2
3
  import { resolveAppUrl } from '../domain/runtime-paths.js';
3
4
  import { vaultApiClient } from '../domain/vault-api-client.js';
4
5
 
@@ -113,22 +114,31 @@ export class DrawioEmbedController {
113
114
  this.hydrationIdleId = null;
114
115
  this.hydrationPaused = false;
115
116
  this.instanceCounter = 0;
117
+ this.maximizedEntry = null;
118
+ this.maximizedRoot = null;
116
119
 
117
120
  this._onMessage = this._onMessage.bind(this);
121
+ this._onKeyDown = this._onKeyDown.bind(this);
118
122
  this._onPreviewClick = this._onPreviewClick.bind(this);
119
123
 
120
124
  window.addEventListener('message', this._onMessage);
125
+ window.addEventListener('keydown', this._onKeyDown);
121
126
  this.previewElement?.addEventListener('click', this._onPreviewClick);
122
127
  }
123
128
 
124
129
  destroy() {
125
130
  window.removeEventListener('message', this._onMessage);
131
+ window.removeEventListener('keydown', this._onKeyDown);
126
132
  this.previewElement?.removeEventListener('click', this._onPreviewClick);
127
133
  cancelIdleRender(this.hydrationIdleId);
128
134
  this.hydrationIdleId = null;
129
135
  this.hydrationQueue = [];
136
+ this._exitMaximizedEntry();
137
+ document.body.classList.remove('drawio-maximized-open');
130
138
  this.embedEntries.forEach((entry) => entry.wrapper?.remove());
131
139
  this.embedEntries.clear();
140
+ this.maximizedRoot?.remove();
141
+ this.maximizedRoot = null;
132
142
  }
133
143
 
134
144
  detachForCommit() {
@@ -148,6 +158,8 @@ export class DrawioEmbedController {
148
158
  }
149
159
 
150
160
  reconcileEmbeds(previewElement) {
161
+ this._exitMaximizedEntry();
162
+
151
163
  const descriptors = Array.from(previewElement.querySelectorAll('.drawio-embed-placeholder[data-drawio-key]')).map((placeholder) => ({
152
164
  filePath: placeholder.dataset.drawioTarget || '',
153
165
  key: placeholder.dataset.drawioKey || '',
@@ -246,27 +258,28 @@ export class DrawioEmbedController {
246
258
  }
247
259
 
248
260
  const wrapper = document.createElement('div');
249
- wrapper.className = `drawio-embed${entry.mode === 'edit' ? ' is-direct-file' : ''}`;
261
+ wrapper.className = `drawio-embed diagram-preview-shell${entry.mode === 'edit' ? ' is-direct-file' : ''}`;
250
262
  wrapper.dataset.file = entry.filePath;
263
+ wrapper.dataset.drawioKey = entry.key;
251
264
 
252
265
  const header = document.createElement('div');
253
- header.className = 'drawio-embed-header';
266
+ header.className = 'drawio-embed-header diagram-preview-toolbar';
267
+
268
+ const icon = document.createElement('span');
269
+ icon.className = 'drawio-embed-icon';
270
+ icon.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="7" height="6" rx="1"/><rect x="14" y="4" width="7" height="6" rx="1"/><rect x="8.5" y="14" width="7" height="6" rx="1"/><path d="M10 7h4"/><path d="M17.5 10v2.5"/><path d="M6.5 10v2.5"/><path d="M6.5 12.5h11"/></svg>';
254
271
 
255
272
  const label = document.createElement('span');
256
273
  label.className = 'drawio-embed-label';
257
274
  label.textContent = entry.label.replace(/\.drawio$/i, '');
258
- header.appendChild(label);
275
+ header.append(icon, label);
259
276
 
260
277
  if (entry.mode !== 'edit') {
261
- const button = document.createElement('button');
262
- button.type = 'button';
263
- button.className = 'drawio-embed-btn';
264
- button.dataset.action = 'open-file';
265
- button.dataset.filePath = entry.filePath;
266
- button.textContent = 'Open';
267
- header.appendChild(button);
278
+ header.appendChild(this.createOpenButton(entry));
268
279
  }
269
280
 
281
+ header.appendChild(this.createMaximizeButton(entry, wrapper));
282
+
270
283
  const iframe = document.createElement('iframe');
271
284
  iframe.className = 'drawio-embed-iframe';
272
285
  iframe.loading = 'lazy';
@@ -287,24 +300,23 @@ export class DrawioEmbedController {
287
300
  }
288
301
 
289
302
  const wrapper = document.createElement('div');
290
- wrapper.className = 'drawio-embed is-static-preview';
303
+ wrapper.className = 'drawio-embed diagram-preview-shell is-static-preview';
291
304
  wrapper.dataset.file = entry.filePath;
305
+ wrapper.dataset.drawioKey = entry.key;
292
306
 
293
307
  const header = document.createElement('div');
294
- header.className = 'drawio-embed-header';
308
+ header.className = 'drawio-embed-header diagram-preview-toolbar';
309
+
310
+ const icon = document.createElement('span');
311
+ icon.className = 'drawio-embed-icon';
312
+ icon.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="7" height="6" rx="1"/><rect x="14" y="4" width="7" height="6" rx="1"/><rect x="8.5" y="14" width="7" height="6" rx="1"/><path d="M10 7h4"/><path d="M17.5 10v2.5"/><path d="M6.5 10v2.5"/><path d="M6.5 12.5h11"/></svg>';
295
313
 
296
314
  const label = document.createElement('span');
297
315
  label.className = 'drawio-embed-label';
298
316
  label.textContent = entry.label.replace(/\.drawio$/i, '');
299
- header.appendChild(label);
317
+ header.append(icon, label);
300
318
 
301
- const button = document.createElement('button');
302
- button.type = 'button';
303
- button.className = 'drawio-embed-btn';
304
- button.dataset.action = 'open-file';
305
- button.dataset.filePath = entry.filePath;
306
- button.textContent = 'Open';
307
- header.appendChild(button);
319
+ header.append(this.createOpenButton(entry), this.createMaximizeButton(entry, wrapper));
308
320
 
309
321
  const viewerShell = document.createElement('div');
310
322
  viewerShell.className = 'drawio-viewer-shell';
@@ -346,6 +358,18 @@ export class DrawioEmbedController {
346
358
  graphElement.setAttribute('role', 'button');
347
359
  graphElement.setAttribute('tabindex', '0');
348
360
  graphElement.setAttribute('aria-label', `Open ${entry.label.replace(/\.drawio$/i, '')}`);
361
+ graphElement.addEventListener('click', (event) => {
362
+ event.preventDefault();
363
+ this.onOpenFile?.(entry.filePath);
364
+ });
365
+ graphElement.addEventListener('keydown', (event) => {
366
+ if (event.key !== 'Enter' && event.key !== ' ') {
367
+ return;
368
+ }
369
+
370
+ event.preventDefault();
371
+ this.onOpenFile?.(entry.filePath);
372
+ });
349
373
  graphElement.dataset.mxgraph = JSON.stringify({
350
374
  'check-visible-state': false,
351
375
  center: true,
@@ -411,6 +435,146 @@ export class DrawioEmbedController {
411
435
 
412
436
  syncLayout() {}
413
437
 
438
+ createOpenButton(entry) {
439
+ const button = document.createElement('button');
440
+ button.type = 'button';
441
+ button.className = 'drawio-embed-btn ui-preview-action ui-preview-action--icon-only';
442
+ button.dataset.action = 'open-file';
443
+ button.dataset.filePath = entry.filePath;
444
+ button.title = 'Edit in draw.io';
445
+ button.setAttribute('aria-label', 'Edit in draw.io');
446
+ setDiagramActionButtonIcon(button, 'edit');
447
+ button.addEventListener('click', (event) => {
448
+ event.preventDefault();
449
+ this.onOpenFile?.(entry.filePath);
450
+ });
451
+ return button;
452
+ }
453
+
454
+ createMaximizeButton(entry, wrapper) {
455
+ const button = document.createElement('button');
456
+ button.type = 'button';
457
+ button.className = 'drawio-embed-btn ui-preview-action ui-preview-action--icon-only';
458
+ button.dataset.action = 'toggle-maximize';
459
+ button.dataset.filePath = entry.filePath;
460
+ button.dataset.drawioKey = entry.key;
461
+
462
+ const syncState = () => {
463
+ const isMaximized = this.maximizedEntry?.key === entry.key;
464
+ setDiagramActionButtonIcon(button, isMaximized ? 'restore' : 'maximize');
465
+ button.title = isMaximized ? 'Restore diagram size' : 'Maximize diagram';
466
+ button.setAttribute('aria-label', isMaximized ? 'Restore diagram size' : 'Maximize diagram');
467
+ };
468
+
469
+ button.addEventListener('click', (event) => {
470
+ event.preventDefault();
471
+ if (this.maximizedEntry?.key === entry.key) {
472
+ this._exitMaximizedEntry();
473
+ } else {
474
+ this._enterMaximizedEntry(entry, wrapper);
475
+ }
476
+ this._syncMaximizeButtons();
477
+ });
478
+
479
+ syncState();
480
+ return button;
481
+ }
482
+
483
+ _ensureMaximizedRoot() {
484
+ if (this.maximizedRoot?.isConnected && this.maximizedRoot.parentElement === document.body) {
485
+ return this.maximizedRoot;
486
+ }
487
+
488
+ let maximizedRoot = document.body.querySelector('[data-drawio-maximized-root="true"]');
489
+ if (!maximizedRoot) {
490
+ maximizedRoot = document.createElement('div');
491
+ maximizedRoot.dataset.drawioMaximizedRoot = 'true';
492
+ maximizedRoot.className = 'drawio-maximized-root';
493
+ document.body.appendChild(maximizedRoot);
494
+ }
495
+
496
+ this.maximizedRoot = maximizedRoot;
497
+ return maximizedRoot;
498
+ }
499
+
500
+ _enterMaximizedEntry(entry, wrapper = entry?.wrapper) {
501
+ if (!entry?.wrapper || entry.wrapper !== wrapper) {
502
+ return;
503
+ }
504
+
505
+ if (this.maximizedEntry?.key === entry.key) {
506
+ return;
507
+ }
508
+
509
+ this._exitMaximizedEntry();
510
+
511
+ const maximizedRoot = this._ensureMaximizedRoot();
512
+ const parent = entry.wrapper.parentElement;
513
+ if (!parent) {
514
+ return;
515
+ }
516
+
517
+ const spacer = document.createElement('div');
518
+ spacer.className = 'drawio-maximize-spacer';
519
+ spacer.style.height = `${Math.ceil(entry.wrapper.getBoundingClientRect().height)}px`;
520
+
521
+ entry.maximizeSpacer = spacer;
522
+ entry.restoreParent = parent;
523
+ entry.restoreNextSibling = entry.wrapper.nextSibling || null;
524
+ parent.insertBefore(spacer, entry.wrapper);
525
+
526
+ maximizedRoot.hidden = false;
527
+ maximizedRoot.appendChild(entry.wrapper);
528
+ entry.wrapper.classList.add('is-maximized');
529
+ document.body.classList.add('drawio-maximized-open');
530
+ this.maximizedEntry = entry;
531
+ }
532
+
533
+ _exitMaximizedEntry() {
534
+ const entry = this.maximizedEntry;
535
+ if (!entry?.wrapper) {
536
+ this.maximizedEntry = null;
537
+ document.body.classList.remove('drawio-maximized-open');
538
+ return;
539
+ }
540
+
541
+ const { maximizeSpacer, restoreNextSibling, restoreParent } = entry;
542
+ entry.wrapper.classList.remove('is-maximized');
543
+
544
+ if (maximizeSpacer?.parentElement) {
545
+ maximizeSpacer.replaceWith(entry.wrapper);
546
+ } else if (restoreParent?.isConnected) {
547
+ if (restoreNextSibling?.parentElement === restoreParent) {
548
+ restoreParent.insertBefore(entry.wrapper, restoreNextSibling);
549
+ } else {
550
+ restoreParent.appendChild(entry.wrapper);
551
+ }
552
+ }
553
+
554
+ entry.maximizeSpacer = null;
555
+ entry.restoreParent = null;
556
+ entry.restoreNextSibling = null;
557
+ this.maximizedEntry = null;
558
+ document.body.classList.remove('drawio-maximized-open');
559
+ if (this.maximizedRoot && this.maximizedRoot.childElementCount === 0) {
560
+ this.maximizedRoot.hidden = true;
561
+ }
562
+ }
563
+
564
+ _syncMaximizeButtons() {
565
+ this.embedEntries.forEach((entry) => {
566
+ const button = entry.wrapper?.querySelector('.drawio-embed-btn[data-action="toggle-maximize"]');
567
+ if (!(button instanceof HTMLElement)) {
568
+ return;
569
+ }
570
+
571
+ const isMaximized = this.maximizedEntry?.key === entry.key;
572
+ setDiagramActionButtonIcon(button, isMaximized ? 'restore' : 'maximize');
573
+ button.title = isMaximized ? 'Restore diagram size' : 'Maximize diagram';
574
+ button.setAttribute('aria-label', isMaximized ? 'Restore diagram size' : 'Maximize diagram');
575
+ });
576
+ }
577
+
414
578
  _findEntryByInstanceId(instanceId) {
415
579
  for (const entry of this.embedEntries.values()) {
416
580
  if (entry.instanceId === instanceId) {
@@ -434,7 +598,7 @@ export class DrawioEmbedController {
434
598
 
435
599
  if (payload.type === 'fallback-text') {
436
600
  if (entry.mode === 'view') {
437
- this.renderExportFallback(entry, 'Failed to render draw.io preview');
601
+ this.renderViewerFallback(entry, 'Failed to render draw.io preview');
438
602
  return;
439
603
  }
440
604
  this.onOpenTextFile?.(entry.filePath);
@@ -467,17 +631,12 @@ export class DrawioEmbedController {
467
631
  }
468
632
  return;
469
633
  }
634
+ }
470
635
 
471
- const openButton = event.target.closest('.drawio-embed-btn[data-action="open-file"]');
472
- if (openButton) {
473
- event.preventDefault();
474
- this.onOpenFile?.(openButton.dataset.filePath || '');
475
- }
476
-
477
- const viewerFrame = event.target.closest('.drawio-viewer-frame[data-action="open-file"]');
478
- if (viewerFrame) {
479
- event.preventDefault();
480
- this.onOpenFile?.(viewerFrame.dataset.filePath || '');
636
+ _onKeyDown(event) {
637
+ if (event.key === 'Escape') {
638
+ this._exitMaximizedEntry();
639
+ this._syncMaximizeButtons();
481
640
  }
482
641
  }
483
642
 
@@ -108,25 +108,28 @@
108
108
  flex-direction: column;
109
109
  }
110
110
 
111
+ .drawio-embed:hover {
112
+ box-shadow: var(--shadow-elevated-soft);
113
+ }
114
+
111
115
  .drawio-embed-header {
116
+ gap: var(--space-2);
117
+ }
118
+
119
+ .drawio-embed-icon {
112
120
  display: flex;
113
121
  align-items: center;
114
- justify-content: space-between;
115
- gap: var(--space-3);
116
- min-height: 38px;
117
- padding: var(--space-2) var(--space-3);
118
- border-bottom: 1px solid var(--color-border);
119
- background: var(--color-surface-raised);
122
+ flex-shrink: 0;
123
+ color: var(--color-primary);
120
124
  }
121
125
 
122
126
  .drawio-embed-label {
127
+ flex: 1;
123
128
  min-width: 0;
124
129
  overflow: hidden;
125
- color: var(--color-text-muted);
126
- font-size: var(--text-xs);
127
- font-weight: 600;
128
130
  text-overflow: ellipsis;
129
131
  white-space: nowrap;
132
+ font-weight: 500;
130
133
  }
131
134
 
132
135
  .drawio-embed-btn {
@@ -206,6 +209,60 @@
206
209
  margin-right: auto;
207
210
  }
208
211
 
212
+ .drawio-maximized-root {
213
+ position: fixed;
214
+ inset: 0;
215
+ z-index: 171;
216
+ pointer-events: none;
217
+ }
218
+
219
+ body.drawio-maximized-open {
220
+ overflow: hidden;
221
+ }
222
+
223
+ body.drawio-maximized-open::before {
224
+ content: '';
225
+ position: fixed;
226
+ inset: 0;
227
+ z-index: 170;
228
+ background: var(--surface-scrim);
229
+ backdrop-filter: blur(2px);
230
+ }
231
+
232
+ .drawio-maximized-root .drawio-embed.is-maximized {
233
+ pointer-events: auto;
234
+ }
235
+
236
+ .drawio-embed.is-maximized {
237
+ position: fixed;
238
+ inset: var(--space-4);
239
+ z-index: 172;
240
+ display: flex;
241
+ flex-direction: column;
242
+ width: auto;
243
+ max-width: none;
244
+ margin: 0;
245
+ box-sizing: border-box;
246
+ border-radius: var(--radius-lg-plus);
247
+ box-shadow: var(--shadow-media-modal);
248
+ }
249
+
250
+ .drawio-embed.is-maximized .drawio-embed-header {
251
+ flex-shrink: 0;
252
+ }
253
+
254
+ .drawio-embed.is-maximized .drawio-embed-iframe,
255
+ .drawio-embed.is-maximized .drawio-viewer-shell,
256
+ .drawio-embed.is-maximized .drawio-viewer-frame {
257
+ flex: 1;
258
+ min-height: 0;
259
+ height: 100%;
260
+ }
261
+
262
+ .drawio-embed.is-maximized .drawio-viewer-shell {
263
+ align-items: stretch;
264
+ }
265
+
209
266
  .excalidraw-embed-overlay-root {
210
267
  position: absolute;
211
268
  inset: 0;
@@ -367,10 +367,19 @@
367
367
  width: auto;
368
368
  }
369
369
 
370
+ .drawio-embed.is-maximized {
371
+ inset: var(--space-3);
372
+ width: auto;
373
+ }
374
+
370
375
  .excalidraw-embed.is-maximized .excalidraw-embed-iframe {
371
376
  height: auto !important;
372
377
  }
373
378
 
379
+ .drawio-embed.is-maximized .drawio-embed-iframe {
380
+ height: auto;
381
+ }
382
+
374
383
  .image-lightbox-toolbar {
375
384
  top: var(--space-3);
376
385
  left: var(--space-3);