sketchmark 0.2.5 → 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
@@ -29,6 +29,7 @@ end
29
29
  - [DSL Reference](#dsl-reference)
30
30
  - [Diagram Header](#diagram-header)
31
31
  - [Node Shapes](#node-shapes)
32
+ - [Icons](#icon-shape)
32
33
  - [Edges](#edges)
33
34
  - [Groups](#groups)
34
35
  - [Bare Groups](#bare-groups)
@@ -37,6 +38,7 @@ end
37
38
  - [Charts](#charts)
38
39
  - [Markdown Blocks](#markdown-blocks)
39
40
  - [Themes](#themes)
41
+ - [Style Directive](#style-directive)
40
42
  - [Typography](#typography)
41
43
  - [Animation Steps](#animation-steps)
42
44
  - [Layout System](#layout-system)
@@ -127,11 +129,15 @@ end
127
129
  | Keyword | Example | Description |
128
130
  |---|---|---|
129
131
  | `title` | `title label="My Diagram"` | Title shown above the diagram |
132
+ | `description` | `description "A brief summary"` | Diagram description (metadata) |
130
133
  | `layout` | `layout row` | Root layout direction: `row`, `column`, `grid` |
131
134
  | `config gap` | `config gap=60` | Gap between root-level items (default: 80) |
132
135
  | `config margin` | `config margin=40` | Outer canvas margin (default: 60) |
133
136
  | `config theme` | `config theme=ocean` | Global palette (see [Theme Palettes](#theme-palettes)) |
134
137
  | `config font` | `config font=caveat` | Diagram-wide font (see [Font System](#font-system)) |
138
+ | `config title-color` | `config title-color=#333` | Title text color |
139
+ | `config title-size` | `config title-size=20` | Title font size in px |
140
+ | `config title-weight` | `config title-weight=700` | Title font weight |
135
141
 
136
142
  ---
137
143
 
@@ -147,6 +153,7 @@ cylinder id label="..."
147
153
  parallelogram id label="..."
148
154
  text id label="..."
149
155
  image id label="..." url="https://..."
156
+ icon id label="..." name="prefix:name"
150
157
  ```
151
158
 
152
159
  **Common properties:**
@@ -159,6 +166,9 @@ image id label="..." url="https://..."
159
166
  | `height` | `height=55` | Override auto-height in px |
160
167
  | `fill` | `fill="#e8f4ff"` | Background fill color |
161
168
  | `stroke` | `stroke="#0044cc"` | Border color |
169
+ | `stroke-width` | `stroke-width=2` | Border thickness in px |
170
+ | `stroke-dash` | `stroke-dash=5,3` | Dashed border pattern (dash, gap) |
171
+ | `opacity` | `opacity=0.5` | Element opacity (0 to 1) |
162
172
  | `color` | `color="#003399"` | Text color |
163
173
  | `font` | `font=caveat` | Font family or built-in name |
164
174
  | `font-size` | `font-size=12` | Label font size in px |
@@ -170,7 +180,9 @@ image id label="..." url="https://..."
170
180
 
171
181
  > **`text` shape:** No border or background. Long labels auto word-wrap. Use `width=` to control the wrap width.
172
182
 
173
- > **`image` shape:** Renders an image clipped to a rounded rect. Requires `url=` property.
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
+
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.
174
186
 
175
187
  **Example:**
176
188
  ```
@@ -178,11 +190,42 @@ box gateway label="API Gateway" theme=warning width=150 height=55
178
190
  circle user label="User" fill="#e8f4ff" stroke="#0044cc" color="#003399"
179
191
  cylinder db label="PostgreSQL" theme=success width=140 height=65
180
192
  image logo label="Logo" url="https://example.com/logo.png" width=80 height=80
193
+ icon db label="Database" name="mdi:database" color="#1976D2"
194
+ icon cloud name="mdi:cloud" width=64 height=64
181
195
  text caption label="This auto-wraps across multiple lines." width=300
182
196
  ```
183
197
 
184
198
  ---
185
199
 
200
+ ### Icon Shape
201
+
202
+ Render any of 200,000+ open source vector icons from [Iconify](https://iconify.design/).
203
+
204
+ ```
205
+ icon id [label="..."] name="prefix:name" [color="#hex"] [width=N] [height=N]
206
+ ```
207
+
208
+ | Property | Example | Description |
209
+ |---|---|---|
210
+ | `name` | `name="mdi:database"` | Icon identifier in `prefix:name` format. Defaults to `mdi` prefix if omitted |
211
+ | `color` | `color="#1976D2"` | Icon tint color |
212
+ | `stroke` | `stroke="#333"` | Optional border (not shown by default) |
213
+ | `label` | `label="DB"` | Label shown below the icon (defaults to id) |
214
+ | `width` | `width=64` | Icon width (default: 48) |
215
+ | `height` | `height=64` | Icon height (default: 48) |
216
+
217
+ Browse available icons at [icon-sets.iconify.design](https://icon-sets.iconify.design/). Common prefixes: `mdi` (Material Design), `lucide`, `heroicons`, `tabler`, `ph` (Phosphor), `ri` (Remix), `carbon`.
218
+
219
+ **Example:**
220
+ ```
221
+ icon db label="Database" name="mdi:database" color="#1976D2"
222
+ icon cloud label="Cloud" name="mdi:cloud-outline" color="#FF9800" width=64 height=64
223
+ icon lock name="mdi:lock" color="#E53935"
224
+ icon user name="lucide:user"
225
+ ```
226
+
227
+ ---
228
+
186
229
  ### Edges
187
230
 
188
231
  ```
@@ -483,6 +526,19 @@ Apply to any element: `box a theme=primary`, `group g theme=muted`, `note n them
483
526
 
484
527
  ---
485
528
 
529
+ ### Style Directive
530
+
531
+ Apply styles to any element after it's defined, by targeting its id:
532
+
533
+ ```
534
+ box a label="Hello"
535
+ style a fill="#ff0000" stroke="#cc0000" font-size=16
536
+ ```
537
+
538
+ This merges with any existing styles on the element. Useful for separating layout from styling.
539
+
540
+ ---
541
+
486
542
  ### Typography
487
543
 
488
544
  Typography properties work on all text-bearing elements.
@@ -540,6 +596,7 @@ All actions work on **all element types** — nodes, groups, tables, notes, char
540
596
  | Option | Description |
541
597
  |---|---|
542
598
  | `duration=600` | Animation duration in ms |
599
+ | `delay=100` | Delay before animation starts in ms |
543
600
  | `dx=100` | X offset for `move` |
544
601
  | `dy=-80` | Y offset for `move` |
545
602
  | `factor=1.5` | Scale multiplier |
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);