sketchmark 0.2.6 → 0.2.7

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 CHANGED
@@ -180,9 +180,9 @@ icon id label="..." name="prefix:name"
180
180
 
181
181
  > **`text` shape:** No border or background. Long labels auto word-wrap. Use `width=` to control the wrap width.
182
182
 
183
- > **`image` shape:** Renders an image clipped to a rounded rect. Requires `url=` property. Label and border only shown when explicitly set.
183
+ > **`image` shape:** Renders an image clipped to a rounded rect. Requires `url=` property. Label renders below the image. Border only shown when `stroke=` is set.
184
184
 
185
- > **`icon` shape:** Renders an icon from [Iconify](https://iconify.design/) (200,000+ open source icons). Requires `name=` property in `prefix:name` format (e.g. `mdi:database`). Defaults to `mdi` prefix if omitted. Use `color=` to tint the icon. Label and border only shown when explicitly set. Default size: 48x48.
185
+ > **`icon` shape:** Renders an icon from [Iconify](https://iconify.design/) (200,000+ open source icons). Requires `name=` property in `prefix:name` format (e.g. `mdi:database`). Defaults to `mdi` prefix if omitted. Use `color=` to tint the icon. Label renders below the icon. Border only shown when `stroke=` is set. Default size: 48x48.
186
186
 
187
187
  **Example:**
188
188
  ```
@@ -210,7 +210,7 @@ icon id [label="..."] name="prefix:name" [color="#hex"] [width=N] [height=N]
210
210
  | `name` | `name="mdi:database"` | Icon identifier in `prefix:name` format. Defaults to `mdi` prefix if omitted |
211
211
  | `color` | `color="#1976D2"` | Icon tint color |
212
212
  | `stroke` | `stroke="#333"` | Optional border (not shown by default) |
213
- | `label` | `label="DB"` | Optional label (not shown by default) |
213
+ | `label` | `label="DB"` | Label shown below the icon (defaults to id) |
214
214
  | `width` | `width=64` | Icon width (default: 48) |
215
215
  | `height` | `height=64` | Icon height (default: 48) |
216
216
 
package/dist/index.cjs CHANGED
@@ -424,7 +424,7 @@ function parse(src) {
424
424
  kind: "node",
425
425
  id,
426
426
  shape,
427
- label: props.label || (shape === "image" || shape === "icon" ? "" : id),
427
+ label: props.label || id,
428
428
  ...(groupId ? { groupId } : {}),
429
429
  ...(props.width ? { width: parseFloat(props.width) } : {}),
430
430
  ...(props.height ? { height: parseFloat(props.height) } : {}),
@@ -1509,10 +1509,13 @@ function sizeNode(n) {
1509
1509
  }
1510
1510
  break;
1511
1511
  }
1512
- case "icon":
1513
- n.w = n.w || 48;
1514
- n.h = n.h || (n.label !== n.id ? 64 : 48); // extra height for label
1512
+ case "icon": {
1513
+ const iconBase = 48;
1514
+ const labelH = n.label ? 20 : 0;
1515
+ n.w = n.w || Math.max(iconBase, n.label ? labelW : 0);
1516
+ n.h = n.h || (iconBase + labelH);
1515
1517
  break;
1518
+ }
1516
1519
  default:
1517
1520
  n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW));
1518
1521
  n.h = n.h || 52;
@@ -5116,27 +5119,32 @@ function renderShape$1(rc, n, palette) {
5116
5119
  const iconColor = s.color
5117
5120
  ? encodeURIComponent(String(s.color))
5118
5121
  : encodeURIComponent(String(palette.nodeStroke));
5119
- const iconSize = Math.min(n.w, n.h) - 4;
5122
+ // reserve bottom 20px for label when present
5123
+ const labelSpace = n.label ? 20 : 0;
5124
+ const iconAreaH = n.h - labelSpace;
5125
+ const iconSize = Math.min(n.w, iconAreaH) - 4;
5120
5126
  const iconUrl = `https://api.iconify.design/${prefix}/${name}.svg?color=${iconColor}&width=${iconSize}&height=${iconSize}`;
5121
5127
  const img = document.createElementNS(NS, "image");
5122
5128
  img.setAttribute("href", iconUrl);
5123
- img.setAttribute("x", String(n.x + 1));
5124
- img.setAttribute("y", String(n.y + 1));
5125
- img.setAttribute("width", String(n.w - 2));
5126
- img.setAttribute("height", String(n.h - 2));
5129
+ const iconX = n.x + (n.w - iconSize) / 2;
5130
+ const iconY = n.y + (iconAreaH - iconSize) / 2;
5131
+ img.setAttribute("x", String(iconX));
5132
+ img.setAttribute("y", String(iconY));
5133
+ img.setAttribute("width", String(iconSize));
5134
+ img.setAttribute("height", String(iconSize));
5127
5135
  img.setAttribute("preserveAspectRatio", "xMidYMid meet");
5128
5136
  if (s.opacity != null)
5129
5137
  img.setAttribute("opacity", String(s.opacity));
5130
- // clip-path for rounded corners (same as image)
5138
+ // clip-path for rounded corners
5131
5139
  const clipId = `clip-${n.id}`;
5132
5140
  const defs = document.createElementNS(NS, "defs");
5133
5141
  const clip = document.createElementNS(NS, "clipPath");
5134
5142
  clip.setAttribute("id", clipId);
5135
5143
  const rect = document.createElementNS(NS, "rect");
5136
- rect.setAttribute("x", String(n.x + 1));
5137
- rect.setAttribute("y", String(n.y + 1));
5138
- rect.setAttribute("width", String(n.w - 2));
5139
- rect.setAttribute("height", String(n.h - 2));
5144
+ rect.setAttribute("x", String(iconX));
5145
+ rect.setAttribute("y", String(iconY));
5146
+ rect.setAttribute("width", String(iconSize));
5147
+ rect.setAttribute("height", String(iconSize));
5140
5148
  rect.setAttribute("rx", "6");
5141
5149
  clip.appendChild(rect);
5142
5150
  defs.appendChild(clip);
@@ -5162,12 +5170,15 @@ function renderShape$1(rc, n, palette) {
5162
5170
  }
5163
5171
  case "image": {
5164
5172
  if (n.imageUrl) {
5173
+ // reserve bottom 20px for label when present
5174
+ const imgLabelSpace = n.label ? 20 : 0;
5175
+ const imgAreaH = n.h - imgLabelSpace;
5165
5176
  const img = document.createElementNS(NS, "image");
5166
5177
  img.setAttribute("href", n.imageUrl);
5167
5178
  img.setAttribute("x", String(n.x + 1));
5168
5179
  img.setAttribute("y", String(n.y + 1));
5169
5180
  img.setAttribute("width", String(n.w - 2));
5170
- img.setAttribute("height", String(n.h - 2));
5181
+ img.setAttribute("height", String(imgAreaH - 2));
5171
5182
  img.setAttribute("preserveAspectRatio", "xMidYMid meet");
5172
5183
  const clipId = `clip-${n.id}`;
5173
5184
  const defs = document.createElementNS(NS, "defs");
@@ -5177,7 +5188,7 @@ function renderShape$1(rc, n, palette) {
5177
5188
  rect.setAttribute("x", String(n.x + 1));
5178
5189
  rect.setAttribute("y", String(n.y + 1));
5179
5190
  rect.setAttribute("width", String(n.w - 2));
5180
- rect.setAttribute("height", String(n.h - 2));
5191
+ rect.setAttribute("height", String(imgAreaH - 2));
5181
5192
  rect.setAttribute("rx", "6");
5182
5193
  clip.appendChild(rect);
5183
5194
  defs.appendChild(clip);
@@ -5429,11 +5440,14 @@ function renderToSVG(sg, container, options = {}) {
5429
5440
  const nodeBodyBottom = n.y + n.h - pad;
5430
5441
  const nodeBodyMid = n.y + n.h / 2;
5431
5442
  const blockH = (lines.length - 1) * lineHeight;
5432
- const textCY = verticalAlign === "top"
5433
- ? nodeBodyTop + blockH / 2
5434
- : verticalAlign === "bottom"
5435
- ? nodeBodyBottom - blockH / 2
5436
- : nodeBodyMid;
5443
+ const isMediaShape = n.shape === "icon" || n.shape === "image";
5444
+ const textCY = isMediaShape
5445
+ ? n.y + n.h - 10 // label below the icon/image
5446
+ : verticalAlign === "top"
5447
+ ? nodeBodyTop + blockH / 2
5448
+ : verticalAlign === "bottom"
5449
+ ? nodeBodyBottom - blockH / 2
5450
+ : nodeBodyMid;
5437
5451
  if (n.label) {
5438
5452
  ng.appendChild(lines.length > 1
5439
5453
  ? mkMultilineText(lines, textX, textCY, fontSize, fontWeight, textColor, textAnchor, lineHeight, nodeFont, letterSpacing)
@@ -6144,7 +6158,10 @@ function renderShape(rc, ctx, n, palette, R) {
6144
6158
  const iconColor = s.color
6145
6159
  ? encodeURIComponent(String(s.color))
6146
6160
  : encodeURIComponent(String(palette.nodeStroke));
6147
- const iconSize = Math.min(n.w, n.h) - 4;
6161
+ // reserve bottom for label
6162
+ const iconLabelSpace = n.label ? 20 : 0;
6163
+ const iconAreaH = n.h - iconLabelSpace;
6164
+ const iconSize = Math.min(n.w, iconAreaH) - 4;
6148
6165
  const iconUrl = `https://api.iconify.design/${prefix}/${name}.svg?color=${iconColor}&width=${iconSize}&height=${iconSize}`;
6149
6166
  const img = new Image();
6150
6167
  img.crossOrigin = 'anonymous';
@@ -6152,23 +6169,23 @@ function renderShape(rc, ctx, n, palette, R) {
6152
6169
  ctx.save();
6153
6170
  if (s.opacity != null)
6154
6171
  ctx.globalAlpha = Number(s.opacity);
6155
- // clip-path for rounded corners (same as image)
6172
+ const iconX = n.x + (n.w - iconSize) / 2;
6173
+ const iconY = n.y + (iconAreaH - iconSize) / 2;
6156
6174
  ctx.beginPath();
6157
6175
  const r = 6;
6158
- ctx.moveTo(n.x + r, n.y);
6159
- ctx.lineTo(n.x + n.w - r, n.y);
6160
- ctx.quadraticCurveTo(n.x + n.w, n.y, n.x + n.w, n.y + r);
6161
- ctx.lineTo(n.x + n.w, n.y + n.h - r);
6162
- ctx.quadraticCurveTo(n.x + n.w, n.y + n.h, n.x + n.w - r, n.y + n.h);
6163
- ctx.lineTo(n.x + r, n.y + n.h);
6164
- ctx.quadraticCurveTo(n.x, n.y + n.h, n.x, n.y + n.h - r);
6165
- ctx.lineTo(n.x, n.y + r);
6166
- ctx.quadraticCurveTo(n.x, n.y, n.x + r, n.y);
6176
+ ctx.moveTo(iconX + r, iconY);
6177
+ ctx.lineTo(iconX + iconSize - r, iconY);
6178
+ ctx.quadraticCurveTo(iconX + iconSize, iconY, iconX + iconSize, iconY + r);
6179
+ ctx.lineTo(iconX + iconSize, iconY + iconSize - r);
6180
+ ctx.quadraticCurveTo(iconX + iconSize, iconY + iconSize, iconX + iconSize - r, iconY + iconSize);
6181
+ ctx.lineTo(iconX + r, iconY + iconSize);
6182
+ ctx.quadraticCurveTo(iconX, iconY + iconSize, iconX, iconY + iconSize - r);
6183
+ ctx.lineTo(iconX, iconY + r);
6184
+ ctx.quadraticCurveTo(iconX, iconY, iconX + r, iconY);
6167
6185
  ctx.closePath();
6168
6186
  ctx.clip();
6169
- ctx.drawImage(img, n.x + 1, n.y + 1, n.w - 2, n.h - 2);
6187
+ ctx.drawImage(img, iconX, iconY, iconSize, iconSize);
6170
6188
  ctx.restore();
6171
- // only draw border when stroke is explicitly set
6172
6189
  if (s.stroke) {
6173
6190
  rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: 'none' });
6174
6191
  }
@@ -6182,6 +6199,9 @@ function renderShape(rc, ctx, n, palette, R) {
6182
6199
  }
6183
6200
  case 'image': {
6184
6201
  if (n.imageUrl) {
6202
+ // reserve bottom for label
6203
+ const imgLblSpace = n.label ? 20 : 0;
6204
+ const imgAreaH = n.h - imgLblSpace;
6185
6205
  const img = new Image();
6186
6206
  img.crossOrigin = 'anonymous';
6187
6207
  img.onload = () => {
@@ -6191,15 +6211,15 @@ function renderShape(rc, ctx, n, palette, R) {
6191
6211
  ctx.moveTo(n.x + r, n.y);
6192
6212
  ctx.lineTo(n.x + n.w - r, n.y);
6193
6213
  ctx.quadraticCurveTo(n.x + n.w, n.y, n.x + n.w, n.y + r);
6194
- ctx.lineTo(n.x + n.w, n.y + n.h - r);
6195
- ctx.quadraticCurveTo(n.x + n.w, n.y + n.h, n.x + n.w - r, n.y + n.h);
6196
- ctx.lineTo(n.x + r, n.y + n.h);
6197
- ctx.quadraticCurveTo(n.x, n.y + n.h, n.x, n.y + n.h - r);
6214
+ ctx.lineTo(n.x + n.w, n.y + imgAreaH - r);
6215
+ ctx.quadraticCurveTo(n.x + n.w, n.y + imgAreaH, n.x + n.w - r, n.y + imgAreaH);
6216
+ ctx.lineTo(n.x + r, n.y + imgAreaH);
6217
+ ctx.quadraticCurveTo(n.x, n.y + imgAreaH, n.x, n.y + imgAreaH - r);
6198
6218
  ctx.lineTo(n.x, n.y + r);
6199
6219
  ctx.quadraticCurveTo(n.x, n.y, n.x + r, n.y);
6200
6220
  ctx.closePath();
6201
6221
  ctx.clip();
6202
- ctx.drawImage(img, n.x + 1, n.y + 1, n.w - 2, n.h - 2);
6222
+ ctx.drawImage(img, n.x + 1, n.y + 1, n.w - 2, imgAreaH - 2);
6203
6223
  ctx.restore();
6204
6224
  // only draw border when stroke is explicitly set
6205
6225
  if (s.stroke) {
@@ -6391,9 +6411,12 @@ function renderToCanvas(sg, canvas, options = {}) {
6391
6411
  const nodeBodyTop = n.y + pad;
6392
6412
  const nodeBodyBottom = n.y + n.h - pad;
6393
6413
  const blockH = (lines.length - 1) * lineHeight;
6394
- const textCY = vertAlign === 'top' ? nodeBodyTop + blockH / 2
6395
- : vertAlign === 'bottom' ? nodeBodyBottom - blockH / 2
6396
- : n.y + n.h / 2; // middle (default)
6414
+ const isMediaShape = n.shape === 'icon' || n.shape === 'image';
6415
+ const textCY = isMediaShape
6416
+ ? n.y + n.h - 10 // label below the icon/image
6417
+ : vertAlign === 'top' ? nodeBodyTop + blockH / 2
6418
+ : vertAlign === 'bottom' ? nodeBodyBottom - blockH / 2
6419
+ : n.y + n.h / 2; // middle (default)
6397
6420
  if (n.label) {
6398
6421
  if (lines.length > 1) {
6399
6422
  drawMultilineText(ctx, lines, textX, textCY, fontSize, fontWeight, textColor, textAlign, lineHeight, nodeFont, letterSpacing);