@ngroznykh/papirus 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/papirus.js CHANGED
@@ -570,6 +570,14 @@ class SelectionManager extends EventEmitter {
570
570
  }
571
571
  return;
572
572
  }
573
+ const node = element;
574
+ if (typeof node.getBadgeAtPoint === "function") {
575
+ const badge = node.getBadgeAtPoint(point);
576
+ if (badge !== null) {
577
+ this.renderer.emit("nodeBadgeClick", element.id, badge.id);
578
+ return;
579
+ }
580
+ }
573
581
  if (event.ctrlKey || event.metaKey) {
574
582
  this.toggleSelection(element.id);
575
583
  } else {
@@ -5002,6 +5010,7 @@ class InteractionManager {
5002
5010
  this.overlayDragSession = null;
5003
5011
  this.handledOverlayMouseDown = false;
5004
5012
  this.renderer = options.renderer;
5013
+ this.navigationOnly = options.navigationOnly ?? false;
5005
5014
  this.inputHandler = new InputHandler({
5006
5015
  canvas: this.renderer.getCanvas(),
5007
5016
  screenToWorld: (x, y) => this.renderer.screenToWorld(x, y)
@@ -5153,13 +5162,15 @@ class InteractionManager {
5153
5162
  setupEvents(options) {
5154
5163
  this.overlayCleanup = this.renderer.addOverlayRenderer((ctx) => {
5155
5164
  this.selectionManager.renderSelectionRect(ctx);
5156
- this.dragManager.renderAlignmentGuides(ctx);
5157
- this.connectionManager.renderPreview(ctx);
5158
- this.connectionManager.renderHoverAnchors(ctx);
5159
- for (const id of this.selectionManager.selectedIds) {
5160
- const node = this.renderer.getNode(id);
5161
- if (node) {
5162
- node.renderResizeHandles(ctx);
5165
+ if (!this.navigationOnly) {
5166
+ this.dragManager.renderAlignmentGuides(ctx);
5167
+ this.connectionManager.renderPreview(ctx);
5168
+ this.connectionManager.renderHoverAnchors(ctx);
5169
+ for (const id of this.selectionManager.selectedIds) {
5170
+ const node = this.renderer.getNode(id);
5171
+ if (node) {
5172
+ node.renderResizeHandles(ctx);
5173
+ }
5163
5174
  }
5164
5175
  }
5165
5176
  });
@@ -5173,68 +5184,70 @@ class InteractionManager {
5173
5184
  this.inputHandler.on("pinch", (event) => this.handlePinch(event));
5174
5185
  this.inputHandler.on("keydown", (event) => this.handleKeyDown(event, options));
5175
5186
  this.inputHandler.on("keyup", (event) => this.handleKeyUp(event));
5176
- this.dragManager.on("dragstart", (nodeIds) => {
5177
- this.connectionManager.disableHover();
5178
- this.dragStartPositions.clear();
5179
- for (const id of nodeIds) {
5180
- const node = this.renderer.getNode(id);
5181
- if (node) {
5182
- this.dragStartPositions.set(id, { x: node.x, y: node.y });
5183
- }
5184
- }
5185
- });
5186
- this.dragManager.on("dragend", (nodeIds) => {
5187
- this.connectionManager.enableHover();
5188
- const nodePositions = /* @__PURE__ */ new Map();
5189
- for (const id of nodeIds) {
5190
- const node = this.renderer.getNode(id);
5191
- const before = this.dragStartPositions.get(id);
5192
- if (!node || !before) continue;
5193
- const after = { x: node.x, y: node.y };
5194
- if (before.x !== after.x || before.y !== after.y) {
5195
- nodePositions.set(id, { before, after });
5196
- }
5197
- }
5198
- if (nodePositions.size > 0) {
5199
- this.historyManager.execute(
5200
- new MoveNodesCommand((id) => this.renderer.getNode(id), nodePositions)
5201
- );
5202
- }
5203
- });
5204
- this.connectionManager.on("edgeReconnectStart", (edge, endpoint, original) => {
5205
- this.reconnectOrigins.set(edge.id, { endpoint, original: { ...original } });
5206
- });
5207
- this.connectionManager.on("edgeReconnect", (edge, endpoint) => {
5208
- const origin = this.reconnectOrigins.get(edge.id);
5209
- if (!origin || origin.endpoint !== endpoint) {
5210
- return;
5211
- }
5212
- const before = origin.original;
5213
- const after = endpoint === "start" ? edge.from : edge.to;
5214
- if (this.endpointsEqual(before, after)) {
5215
- this.reconnectOrigins.delete(edge.id);
5216
- return;
5217
- }
5218
- this.historyManager.execute({
5219
- execute: () => {
5220
- if (endpoint === "start") {
5221
- edge.from = { ...after };
5222
- } else {
5223
- edge.to = { ...after };
5187
+ if (!this.navigationOnly) {
5188
+ this.dragManager.on("dragstart", (nodeIds) => {
5189
+ this.connectionManager.disableHover();
5190
+ this.dragStartPositions.clear();
5191
+ for (const id of nodeIds) {
5192
+ const node = this.renderer.getNode(id);
5193
+ if (node) {
5194
+ this.dragStartPositions.set(id, { x: node.x, y: node.y });
5224
5195
  }
5225
- this.renderer.markDirty();
5226
- },
5227
- undo: () => {
5228
- if (endpoint === "start") {
5229
- edge.from = { ...before };
5230
- } else {
5231
- edge.to = { ...before };
5196
+ }
5197
+ });
5198
+ this.dragManager.on("dragend", (nodeIds) => {
5199
+ this.connectionManager.enableHover();
5200
+ const nodePositions = /* @__PURE__ */ new Map();
5201
+ for (const id of nodeIds) {
5202
+ const node = this.renderer.getNode(id);
5203
+ const before = this.dragStartPositions.get(id);
5204
+ if (!node || !before) continue;
5205
+ const after = { x: node.x, y: node.y };
5206
+ if (before.x !== after.x || before.y !== after.y) {
5207
+ nodePositions.set(id, { before, after });
5232
5208
  }
5233
- this.renderer.markDirty();
5209
+ }
5210
+ if (nodePositions.size > 0) {
5211
+ this.historyManager.execute(
5212
+ new MoveNodesCommand((id) => this.renderer.getNode(id), nodePositions)
5213
+ );
5234
5214
  }
5235
5215
  });
5236
- this.reconnectOrigins.delete(edge.id);
5237
- });
5216
+ this.connectionManager.on("edgeReconnectStart", (edge, endpoint, original) => {
5217
+ this.reconnectOrigins.set(edge.id, { endpoint, original: { ...original } });
5218
+ });
5219
+ this.connectionManager.on("edgeReconnect", (edge, endpoint) => {
5220
+ const origin = this.reconnectOrigins.get(edge.id);
5221
+ if (!origin || origin.endpoint !== endpoint) {
5222
+ return;
5223
+ }
5224
+ const before = origin.original;
5225
+ const after = endpoint === "start" ? edge.from : edge.to;
5226
+ if (this.endpointsEqual(before, after)) {
5227
+ this.reconnectOrigins.delete(edge.id);
5228
+ return;
5229
+ }
5230
+ this.historyManager.execute({
5231
+ execute: () => {
5232
+ if (endpoint === "start") {
5233
+ edge.from = { ...after };
5234
+ } else {
5235
+ edge.to = { ...after };
5236
+ }
5237
+ this.renderer.markDirty();
5238
+ },
5239
+ undo: () => {
5240
+ if (endpoint === "start") {
5241
+ edge.from = { ...before };
5242
+ } else {
5243
+ edge.to = { ...before };
5244
+ }
5245
+ this.renderer.markDirty();
5246
+ }
5247
+ });
5248
+ this.reconnectOrigins.delete(edge.id);
5249
+ });
5250
+ }
5238
5251
  this.historyManager.on("change", () => {
5239
5252
  this.renderer.markDirty();
5240
5253
  });
@@ -5242,14 +5255,16 @@ class InteractionManager {
5242
5255
  handleMouseDown(event) {
5243
5256
  this.handledScrollbarMouseDown = false;
5244
5257
  this.handledOverlayMouseDown = false;
5245
- if (this.resizeManager.handleMouseDown(event)) {
5246
- return;
5247
- }
5248
- if (this.connectionManager.tryStartReconnection(event)) {
5249
- return;
5250
- }
5251
- if (this.connectionManager.tryStartConnectionAtPoint(event)) {
5252
- return;
5258
+ if (!this.navigationOnly) {
5259
+ if (this.resizeManager.handleMouseDown(event)) {
5260
+ return;
5261
+ }
5262
+ if (this.connectionManager.tryStartReconnection(event)) {
5263
+ return;
5264
+ }
5265
+ if (this.connectionManager.tryStartConnectionAtPoint(event)) {
5266
+ return;
5267
+ }
5253
5268
  }
5254
5269
  const overlayDrag = this.renderer.beginOverlayDrag(event.screenX, event.screenY);
5255
5270
  if (overlayDrag) {
@@ -5283,6 +5298,9 @@ class InteractionManager {
5283
5298
  if (this.navigationManager.handleMouseDown(event)) {
5284
5299
  return;
5285
5300
  }
5301
+ if (this.navigationOnly) {
5302
+ return;
5303
+ }
5286
5304
  if (this.dragManager.handleMouseDown(event)) {
5287
5305
  return;
5288
5306
  }
@@ -5302,6 +5320,9 @@ class InteractionManager {
5302
5320
  }
5303
5321
  }
5304
5322
  const overScrollbar = this.renderer.updateScrollbarHover(event.screenX, event.screenY);
5323
+ this.renderer.updateBadgeHover(
5324
+ overScrollbar ? { x: -1e9, y: -1e9 } : { x: event.worldX, y: event.worldY }
5325
+ );
5305
5326
  if (this.overlayDragSession) {
5306
5327
  const moved = this.renderer.updateOverlayDrag(
5307
5328
  this.overlayDragSession,
@@ -5330,14 +5351,16 @@ class InteractionManager {
5330
5351
  if (overScrollbar) {
5331
5352
  return;
5332
5353
  }
5333
- if (this.resizeManager.handleMouseMove(event)) {
5334
- return;
5335
- }
5336
- if (this.connectionManager.handleMouseMove(event)) {
5337
- return;
5338
- }
5339
- if (this.dragManager.handleMouseMove(event)) {
5340
- return;
5354
+ if (!this.navigationOnly) {
5355
+ if (this.resizeManager.handleMouseMove(event)) {
5356
+ return;
5357
+ }
5358
+ if (this.connectionManager.handleMouseMove(event)) {
5359
+ return;
5360
+ }
5361
+ if (this.dragManager.handleMouseMove(event)) {
5362
+ return;
5363
+ }
5341
5364
  }
5342
5365
  if (this.selectionManager.selectionRectangle !== null) {
5343
5366
  this.selectionManager.updateSelectionRect({ x: event.worldX, y: event.worldY });
@@ -5356,14 +5379,16 @@ class InteractionManager {
5356
5379
  this.renderer.setScrollbarActiveAxis(null);
5357
5380
  return;
5358
5381
  }
5359
- if (this.resizeManager.handleMouseUp()) {
5360
- return;
5361
- }
5362
- if (this.connectionManager.handleMouseUp(event)) {
5363
- return;
5364
- }
5365
- if (this.dragManager.handleMouseUp(event)) {
5366
- return;
5382
+ if (!this.navigationOnly) {
5383
+ if (this.resizeManager.handleMouseUp()) {
5384
+ return;
5385
+ }
5386
+ if (this.connectionManager.handleMouseUp(event)) {
5387
+ return;
5388
+ }
5389
+ if (this.dragManager.handleMouseUp(event)) {
5390
+ return;
5391
+ }
5367
5392
  }
5368
5393
  if (this.selectionManager.selectionRectangle !== null) {
5369
5394
  this.selectionManager.endSelectionRect();
@@ -5389,6 +5414,9 @@ class InteractionManager {
5389
5414
  if (this.dragManager.handledMouseDown || this.resizeManager.handledMouseDown || this.connectionManager.connecting) {
5390
5415
  return;
5391
5416
  }
5417
+ if (this.navigationOnly) {
5418
+ return;
5419
+ }
5392
5420
  if (this.connectionManager.handleDoubleClick(event)) {
5393
5421
  return;
5394
5422
  }
@@ -5451,16 +5479,19 @@ class InteractionManager {
5451
5479
  handleKeyDown(event, options) {
5452
5480
  const isCtrlOrMeta = event.ctrlKey || event.metaKey;
5453
5481
  const key = event.code.startsWith("Key") ? event.code.slice(3).toLowerCase() : event.key.toLowerCase();
5482
+ this.navigationManager.handleKeyDown(event);
5483
+ if (this.handleViewportNavigationKey(event)) {
5484
+ return;
5485
+ }
5486
+ if (this.navigationOnly) {
5487
+ return;
5488
+ }
5454
5489
  if (isCtrlOrMeta && (key === "z" || key === "y")) {
5455
5490
  this.flushPendingPropertyChanges();
5456
5491
  }
5457
5492
  if (this.historyManager.handleKeyDown(event)) {
5458
5493
  return;
5459
5494
  }
5460
- this.navigationManager.handleKeyDown(event);
5461
- if (this.handleViewportNavigationKey(event)) {
5462
- return;
5463
- }
5464
5495
  if (this.keymap.deleteKeys.includes(event.key)) {
5465
5496
  event.preventDefault();
5466
5497
  this.deleteSelection();
@@ -6164,6 +6195,7 @@ class ContextMenuManager extends EventEmitter {
6164
6195
  iconEl.style.alignItems = "center";
6165
6196
  iconEl.style.justifyContent = "center";
6166
6197
  iconEl.style.textAlign = "center";
6198
+ iconEl.style.flexShrink = "0";
6167
6199
  if (!icon) {
6168
6200
  iconEl.textContent = "";
6169
6201
  return iconEl;
@@ -6171,6 +6203,14 @@ class ContextMenuManager extends EventEmitter {
6171
6203
  if (typeof icon === "string") {
6172
6204
  if (this.isSvgString(icon)) {
6173
6205
  iconEl.innerHTML = icon;
6206
+ } else if (this.options.iconToUrl) {
6207
+ const img = document.createElement("img");
6208
+ img.src = this.options.iconToUrl(icon);
6209
+ img.alt = "";
6210
+ img.style.width = "16px";
6211
+ img.style.height = "16px";
6212
+ img.style.objectFit = "contain";
6213
+ iconEl.appendChild(img);
6174
6214
  } else {
6175
6215
  iconEl.classList.add("material-symbols-outlined");
6176
6216
  iconEl.style.fontSize = "16px";
@@ -6180,6 +6220,14 @@ class ContextMenuManager extends EventEmitter {
6180
6220
  }
6181
6221
  if (icon.type === "svg" || icon.type === "html") {
6182
6222
  iconEl.innerHTML = icon.value;
6223
+ } else if (this.options.iconToUrl) {
6224
+ const img = document.createElement("img");
6225
+ img.src = this.options.iconToUrl(icon.value);
6226
+ img.alt = "";
6227
+ img.style.width = "16px";
6228
+ img.style.height = "16px";
6229
+ img.style.objectFit = "contain";
6230
+ iconEl.appendChild(img);
6183
6231
  } else {
6184
6232
  iconEl.classList.add("material-symbols-outlined");
6185
6233
  iconEl.style.fontSize = "16px";
@@ -6713,6 +6761,30 @@ class DiagramRenderer extends EventEmitter {
6713
6761
  }
6714
6762
  return void 0;
6715
6763
  }
6764
+ /**
6765
+ * Update badge hover state and canvas cursor based on pointer position.
6766
+ * Call from mousemove to show hover highlight and pointer cursor over badges.
6767
+ */
6768
+ updateBadgeHover(worldPoint) {
6769
+ const element = this.getElementAtPoint(worldPoint);
6770
+ let hoveredNodeId = null;
6771
+ let hoveredIndex = -1;
6772
+ const node = element;
6773
+ if (element && typeof node.getBadgeAtPoint === "function") {
6774
+ const badge = node.getBadgeAtPoint(worldPoint);
6775
+ if (badge !== null) {
6776
+ hoveredNodeId = node.id;
6777
+ hoveredIndex = badge.index;
6778
+ }
6779
+ }
6780
+ const cursor = hoveredNodeId !== null ? "pointer" : "";
6781
+ if (this.canvas.style.cursor !== cursor) {
6782
+ this.canvas.style.cursor = cursor;
6783
+ }
6784
+ for (const n of this._nodes.values()) {
6785
+ n.setBadgeHover(n.id === hoveredNodeId ? hoveredIndex : -1);
6786
+ }
6787
+ }
6716
6788
  /**
6717
6789
  * Mark the diagram as needing re-render
6718
6790
  */
@@ -8025,11 +8097,11 @@ function tintSvg(svgText, strokeColor, fillColor) {
8025
8097
  const all = [root, ...Array.from(root.querySelectorAll("*"))];
8026
8098
  for (const el of all) {
8027
8099
  const stroke = el.getAttribute("stroke");
8028
- if (strokeColor && stroke !== null && stroke.toLowerCase() !== "none") {
8100
+ const fill = el.getAttribute("fill");
8101
+ if (strokeColor && (stroke === null || stroke.toLowerCase() !== "none")) {
8029
8102
  el.setAttribute("stroke", strokeColor);
8030
8103
  }
8031
- const fill = el.getAttribute("fill");
8032
- if (fillColor && fill !== null && fill.toLowerCase() !== "none") {
8104
+ if (fillColor && (fill === null || fill.toLowerCase() !== "none")) {
8033
8105
  el.setAttribute("fill", fillColor);
8034
8106
  }
8035
8107
  const style = el.getAttribute("style");
@@ -8196,6 +8268,9 @@ class NodeImage {
8196
8268
  };
8197
8269
  }
8198
8270
  }
8271
+ const BADGE_SIZE = 15;
8272
+ const BADGE_OFFSET = 4;
8273
+ const BADGE_GAP = 4;
8199
8274
  function isValidAnchorId(id) {
8200
8275
  return /^(top|right|bottom|left):\d+$/.test(id);
8201
8276
  }
@@ -8217,7 +8292,10 @@ class Node extends Element {
8217
8292
  styleClass: options.styleClass
8218
8293
  });
8219
8294
  this._ports = [];
8295
+ this._badges = [];
8220
8296
  this._anchorCache = null;
8297
+ this._badgeImageCache = /* @__PURE__ */ new Map();
8298
+ this._hoveredBadgeIndex = -1;
8221
8299
  this._defaultSize = { width: options.width, height: options.height };
8222
8300
  this._nodeStyle = { ...DEFAULT_NODE_STYLE, ...options.style };
8223
8301
  this._showPortsAlways = options.showPortsAlways ?? false;
@@ -8242,6 +8320,10 @@ class Node extends Element {
8242
8320
  this.addPort(portOptions);
8243
8321
  }
8244
8322
  }
8323
+ if (options.badges !== void 0 && options.badges.length > 0) {
8324
+ this._badges = options.badges.map((b) => ({ id: b.id, iconUrl: b.iconUrl }));
8325
+ this.ensureBadgeImagesLoaded();
8326
+ }
8245
8327
  }
8246
8328
  /**
8247
8329
  * Node style
@@ -8307,6 +8389,46 @@ class Node extends Element {
8307
8389
  }
8308
8390
  this.markDirty();
8309
8391
  }
8392
+ /**
8393
+ * Badges shown in top-left corner of node (e.g. interactive property icons)
8394
+ */
8395
+ get badges() {
8396
+ return this._badges;
8397
+ }
8398
+ set badges(value) {
8399
+ this._badges = Array.isArray(value) ? value.map((b) => ({ id: b.id, iconUrl: b.iconUrl })) : [];
8400
+ this.ensureBadgeImagesLoaded();
8401
+ this.markDirty();
8402
+ }
8403
+ /**
8404
+ * Set which badge index is under the pointer (-1 for none). Used for hover highlight and cursor.
8405
+ */
8406
+ setBadgeHover(index) {
8407
+ if (this._hoveredBadgeIndex === index) return;
8408
+ this._hoveredBadgeIndex = index;
8409
+ this.markDirty();
8410
+ }
8411
+ /**
8412
+ * Return badge at world point, or null if point is not over a badge.
8413
+ */
8414
+ getBadgeAtPoint(worldPoint) {
8415
+ if (this._badges.length === 0) {
8416
+ return null;
8417
+ }
8418
+ const bounds = this.getBounds();
8419
+ const contentBounds = this.getLabelContainerBounds(bounds);
8420
+ const localX = worldPoint.x - (contentBounds.x + BADGE_OFFSET);
8421
+ const localY = worldPoint.y - (contentBounds.y + BADGE_OFFSET);
8422
+ for (let i = 0; i < this._badges.length; i++) {
8423
+ const badge = this._badges[i];
8424
+ if (badge === void 0) continue;
8425
+ const x = i * (BADGE_SIZE + BADGE_GAP);
8426
+ if (localX >= x && localX <= x + BADGE_SIZE && localY >= 0 && localY <= BADGE_SIZE) {
8427
+ return { id: badge.id, index: i };
8428
+ }
8429
+ }
8430
+ return null;
8431
+ }
8310
8432
  /**
8311
8433
  * Add a port to this node
8312
8434
  */
@@ -8526,6 +8648,7 @@ class Node extends Element {
8526
8648
  ctx.setLineDash([]);
8527
8649
  ctx.lineDashOffset = 0;
8528
8650
  let bounds = this.getBounds();
8651
+ this.renderBadges(ctx, bounds);
8529
8652
  const iconBoxSize = this._icon ? this.getIconBoxSize() : void 0;
8530
8653
  if (this._label) {
8531
8654
  this._label.setAutoMaxWidth(this.getLabelContainerBounds(bounds).width);
@@ -8549,6 +8672,67 @@ class Node extends Element {
8549
8672
  this.renderLabel(ctx, labelBounds);
8550
8673
  this.renderPorts(ctx);
8551
8674
  }
8675
+ ensureBadgeImagesLoaded() {
8676
+ for (const badge of this._badges) {
8677
+ const url = badge.iconUrl;
8678
+ if (!url || this._badgeImageCache.has(url)) {
8679
+ continue;
8680
+ }
8681
+ const img = new Image();
8682
+ img.decoding = "async";
8683
+ this._badgeImageCache.set(url, { img, loaded: false });
8684
+ img.onload = () => {
8685
+ const entry = this._badgeImageCache.get(url);
8686
+ if (entry) {
8687
+ entry.loaded = true;
8688
+ this.markDirty();
8689
+ }
8690
+ };
8691
+ img.onerror = () => {
8692
+ this.markDirty();
8693
+ };
8694
+ img.src = url;
8695
+ }
8696
+ }
8697
+ renderBadges(ctx, bounds) {
8698
+ if (this._badges.length === 0) {
8699
+ return;
8700
+ }
8701
+ const contentBounds = this.getLabelContainerBounds(bounds);
8702
+ const x0 = contentBounds.x + BADGE_OFFSET;
8703
+ const y0 = contentBounds.y + BADGE_OFFSET;
8704
+ const radius = 2;
8705
+ for (let i = 0; i < this._badges.length; i++) {
8706
+ const badge = this._badges[i];
8707
+ if (badge === void 0) continue;
8708
+ const x = x0 + i * (BADGE_SIZE + BADGE_GAP);
8709
+ const isHovered = this._hoveredBadgeIndex === i;
8710
+ if (isHovered) {
8711
+ ctx.save();
8712
+ ctx.fillStyle = "rgba(0, 0, 0, 0.08)";
8713
+ ctx.beginPath();
8714
+ ctx.roundRect(x, y0, BADGE_SIZE, BADGE_SIZE, radius);
8715
+ ctx.fill();
8716
+ ctx.restore();
8717
+ }
8718
+ const entry = this._badgeImageCache.get(badge.iconUrl);
8719
+ if (entry?.loaded && entry.img.naturalWidth > 0) {
8720
+ ctx.save();
8721
+ const img = entry.img;
8722
+ const sw = img.naturalWidth;
8723
+ const sh = img.naturalHeight;
8724
+ const scale = Math.min(BADGE_SIZE / sw, BADGE_SIZE / sh, 1);
8725
+ const dw = sw * scale;
8726
+ const dh = sh * scale;
8727
+ const dx = x + (BADGE_SIZE - dw) / 2;
8728
+ const dy = y0 + (BADGE_SIZE - dh) / 2;
8729
+ ctx.imageSmoothingEnabled = true;
8730
+ ctx.imageSmoothingQuality = "high";
8731
+ ctx.drawImage(img, 0, 0, sw, sh, dx, dy, dw, dh);
8732
+ ctx.restore();
8733
+ }
8734
+ }
8735
+ }
8552
8736
  /**
8553
8737
  * Minimal size required to fit current contents
8554
8738
  */