svg-terminal 1.0.0 → 1.1.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/README.md CHANGED
@@ -18,7 +18,7 @@ Generate animated SVG terminals from a declarative YAML config. The output is a
18
18
  ```bash
19
19
  npx svg-terminal init # writes terminal.yml
20
20
  npx svg-terminal generate # writes terminal.svg
21
- npx svg-terminal blocks # lists all 47 blocks
21
+ npx svg-terminal blocks # lists all 48 blocks
22
22
  ```
23
23
 
24
24
  Or as a GitHub Action — refresh your profile README on a schedule:
@@ -31,14 +31,14 @@ Or as a GitHub Action — refresh your profile README on a schedule:
31
31
  commit: true
32
32
  ```
33
33
 
34
- See the full [GitHub Action](#github-action) section below, the [block catalog](./examples/blocks/) (47 blocks, one preview each), and the [12-theme gallery](#themes).
34
+ See the full [GitHub Action](#github-action) section below, the [block catalog](./examples/blocks/) (48 blocks, one preview each), and the [12-theme gallery](#themes).
35
35
 
36
36
  ### What's in the box
37
37
 
38
38
  - **Declarative YAML config** — write blocks, pick a theme, run the CLI
39
- - **47 built-in blocks** — across identity, retro / fake-system, status, ASCII art, single-line animation, and humor categories. Browse the [block catalog](./examples/blocks/) for previews of each
39
+ - **48 built-in blocks** — across identity, retro / fake-system, status, ASCII art, single- and multi-line animation, and humor categories. Browse the [block catalog](./examples/blocks/) for previews of each
40
40
  - **12 built-in themes** — dracula, nord, monokai, amber, green-phosphor, cyberpunk, solarized-dark, win95, catppuccin, tokyo-night, gruvbox, high-contrast (with chrome to match)
41
- - **Single-line frame animation** — `BlockResult.animation = { frames, fps, loop }` powers the 9 animated blocks (spinners, clock, dice, progress bar, etc.). Multi-line is a known restriction
41
+ - **Frame animation** — `BlockResult.animation = { frames, fps, loop }` powers the 10 animated blocks (spinners, clock, dice, progress bar, etc.). Frames may be single- **or multi-line** as of #69 (`jumping-jack` is the reference multi-line block)
42
42
  - **Dynamic-block cache** — the 5 cacheable blocks (weather, github-stats, github-languages, quote, fun-fact) write to `.svg-terminal-cache.json`. Pair with `--frozen-cache` for offline CI builds
43
43
  - **Reduced-motion respected** — `@media (prefers-reduced-motion)` clamps the CSS fade-ins AND (since v0.17) the frame cycle. SMIL-driven typing reveal, cursor walk, and scroll-on-overflow remain animated; pair with `--static` for full stillness
44
44
  - **Schema-validated, XSS-safe** — strict zod schema on every config field; user-controllable values are escaped at SVG emit sites. See [SECURITY.md](./SECURITY.md)
@@ -125,7 +125,7 @@ Each is the same 2-block config (motd + neofetch) rendered against the named the
125
125
 
126
126
  ## Blocks
127
127
 
128
- Run `svg-terminal blocks` to list all 47 (cacheable ones marked `*`), or `svg-terminal blocks <name>` to print one block's config schema directly without grepping the source.
128
+ Run `svg-terminal blocks` to list all 48 (cacheable ones marked `*`), or `svg-terminal blocks <name>` to print one block's config schema directly without grepping the source.
129
129
 
130
130
  | Block | Description |
131
131
  |-------|-------------|
@@ -172,6 +172,7 @@ Run `svg-terminal blocks` to list all 47 (cacheable ones marked `*`), or `svg-te
172
172
  | `progress-bar` | Fake build progress bar that fills 0% → 100% |
173
173
  | `bouncing-dot` | Single glyph bouncing left ↔ right |
174
174
  | `dice-roll` | N d6 dice that tumble and land on a result |
175
+ | `jumping-jack` | Multi-line stick figure doing jumping jacks (reference multi-line animation) |
175
176
  | `palette-swatch` | One-line render of all 16 theme palette colors |
176
177
  | `semver-bump` | Current semver + bump preview (major/minor/patch) |
177
178
  | `ascii-calendar` | Current-month calendar grid with today highlighted |
@@ -230,7 +231,7 @@ accessibility:
230
231
  describe: false # default true — emit <desc> with full content
231
232
  ```
232
233
 
233
- **Reduced-motion caveat.** The SVG emits an inline `@media (prefers-reduced-motion: reduce)` rule, but it only applies to CSS animations. The typing reveal, cursor walk, scroll, and frame-cycle animations are SMIL (`<animate>` elements) and SMIL doesn't read the same CSS media query. Users who set the OS-level reduced-motion preference will still see full-speed animation. If that's a problem for your audience, generate with `--static` — same content, no motion at all.
234
+ **Reduced-motion caveat.** The SVG emits an inline `@media (prefers-reduced-motion: reduce)` rule, which applies to CSS animations the fade-ins and the frame cycle (single- and multi-line) honor it (migrated SMIL CSS in v0.17). The remaining SMIL holdouts — typing reveal, cursor walk, and scroll-on-overflow — don't read the same CSS media query, so users who set the OS-level reduced-motion preference still see those animate. If that's a problem for your audience, generate with `--static` — same content, no motion at all.
234
235
 
235
236
  ### Caching API responses
236
237
 
@@ -1,5 +1,5 @@
1
1
  // src/index.ts
2
- import { z as z50 } from "zod";
2
+ import { z as z51 } from "zod";
3
3
 
4
4
  // src/core/config.ts
5
5
  import { readFileSync } from "fs";
@@ -1284,22 +1284,39 @@ function buildRevealClip(clipId, startX, charWidth, charCount, fontSize, startTi
1284
1284
  const finalWidth = roundCoord(charCount * charWidth);
1285
1285
  return `<defs><clipPath id="${clipId}"><rect x="${startX}" y="${-fontSize}" width="${finalWidth}" height="${fontSize * 2}">${setHold("width", 0, startTime)}<animate attributeName="width" values="${values.join(";")}" keyTimes="${keyTimes.join(";")}" calcMode="discrete" begin="${startTime}ms" dur="${typingDuration}ms" fill="freeze"/></rect></clipPath></defs>`;
1286
1286
  }
1287
- function generateAnimatedOutputLine(y, frames, color, startTime, colorMap, chrome, fps, loop) {
1287
+ function generateAnimatedOutputLine(y, frames, color, startTime, colorMap, chrome, fps, loop, lineHeight) {
1288
1288
  const n = frames.length;
1289
1289
  const frameDurMs = 1e3 / fps;
1290
1290
  const cycleMs = Math.round(n * frameDurMs);
1291
1291
  const iter = loop ? "infinite" : "1";
1292
- const textElements = frames.map((frame, i) => {
1293
- const styled = hasMarkup(frame);
1294
- const textContent = styled ? generateStyledText(parseMarkup(frame, colorMap, color), color, chrome.dimOpacity) : escapeXml(frame);
1295
- const textFill = styled ? "" : ` fill="${escapeXml(color)}"`;
1296
- const delayMs = Math.round(i * frameDurMs);
1297
- const anim = `animation: frame-cycle-${n} ${cycleMs}ms linear ${delayMs}ms ${iter}`;
1298
- return `<text class="tt frame-cycle-${n}"${textFill} opacity="${i === 0 ? "1" : "0"}" style="${anim}">${textContent}</text>`;
1299
- }).join("");
1292
+ const height = Math.max(1, ...frames.map((f) => f.length));
1293
+ const animFor = (i) => `animation: frame-cycle-${n} ${cycleMs}ms linear ${Math.round(i * frameDurMs)}ms ${iter}`;
1294
+ const renderRow = (row) => {
1295
+ const styled = hasMarkup(row);
1296
+ return {
1297
+ fill: styled ? "" : ` fill="${escapeXml(color)}"`,
1298
+ content: styled ? generateStyledText(parseMarkup(row, colorMap, color), color, chrome.dimOpacity) : escapeXml(row)
1299
+ };
1300
+ };
1301
+ let body;
1302
+ if (height === 1) {
1303
+ body = frames.map((frame, i) => {
1304
+ const { fill, content } = renderRow(frame[0] ?? "");
1305
+ return `<text class="tt frame-cycle-${n}"${fill} opacity="${i === 0 ? "1" : "0"}" style="${animFor(i)}">${content}</text>`;
1306
+ }).join("");
1307
+ } else {
1308
+ body = frames.map((frame, i) => {
1309
+ const rows = [];
1310
+ for (let r = 0; r < height; r++) {
1311
+ const { fill, content } = renderRow(frame[r] ?? "");
1312
+ rows.push(`<text y="${roundCoord(r * lineHeight)}"${fill}>${content}</text>`);
1313
+ }
1314
+ return `<g class="tt frame-cycle-${n}" opacity="${i === 0 ? "1" : "0"}" style="${animFor(i)}">${rows.join("")}</g>`;
1315
+ }).join("");
1316
+ }
1300
1317
  return `
1301
1318
  <g transform="translate(0, ${y})"${fadeInStyle(startTime)}>
1302
- ${textElements}
1319
+ ${body}
1303
1320
  </g>`;
1304
1321
  }
1305
1322
  function generateOutputLine(y, content, color, startTime, colorMap, chrome, pinWidth, fontSize) {
@@ -1351,7 +1368,8 @@ function generateAllLines(frames, terminal, lineHeight, colors, chrome, animatio
1351
1368
  colorMap,
1352
1369
  chromeConfig,
1353
1370
  frame.framesFps ?? 4,
1354
- frame.framesLoop ?? true
1371
+ frame.framesLoop ?? true,
1372
+ lineHeight
1355
1373
  )
1356
1374
  );
1357
1375
  } else {
@@ -1374,12 +1392,26 @@ function generateAllLines(frames, terminal, lineHeight, colors, chrome, animatio
1374
1392
  return Array.from(processedLines.entries()).sort((a, b) => a[0] - b[0]).map(([, content]) => content).join("\n");
1375
1393
  }
1376
1394
 
1395
+ // src/core/strict-mode.ts
1396
+ var STRICT = false;
1397
+ function setStrict(enabled) {
1398
+ STRICT = enabled;
1399
+ }
1400
+ function isStrict() {
1401
+ return STRICT;
1402
+ }
1403
+
1377
1404
  // src/core/svg-generator.ts
1405
+ function animationHeight(frames) {
1406
+ return Math.max(1, ...frames.map((f) => f.length));
1407
+ }
1378
1408
  function countTotalLines(sequences) {
1379
1409
  let total = 0;
1380
1410
  for (const seq of sequences) {
1381
1411
  if (seq.type === "command") {
1382
1412
  total += 1;
1413
+ } else if (seq.frames && seq.frames.length > 0) {
1414
+ total += animationHeight(seq.frames);
1383
1415
  } else {
1384
1416
  total += seq.content.split("\n").length;
1385
1417
  }
@@ -1625,12 +1657,19 @@ function createAnimationFrames(sequences, terminal, maxVisibleLines, scrollDurat
1625
1657
  continue;
1626
1658
  }
1627
1659
  if (seq.frames && seq.frames.length > 0) {
1628
- buffer.push({ type: "output" });
1660
+ const height = animationHeight(seq.frames);
1661
+ if (height > maxVisibleLines) {
1662
+ const msg = `[svg-terminal] An animated block is ${height} rows tall but only ${maxVisibleLines} row(s) fit the terminal \u2014 the overflow is clipped. Use window.autoHeight (default) or a taller window.height / maxHeight. (#124)`;
1663
+ if (isStrict()) throw new Error(msg);
1664
+ console.warn(msg);
1665
+ }
1666
+ for (let r = 0; r < height; r++) buffer.push({ type: "output" });
1629
1667
  frames.push({
1630
1668
  time: currentTime,
1631
1669
  type: "add-output",
1632
- lineIndex: buffer.length - 1,
1633
- content: seq.frames[0],
1670
+ lineIndex: buffer.length - height,
1671
+ // top row of the reserved band
1672
+ content: seq.frames[0].join("\n"),
1634
1673
  // frame 0 acts as the static fallback
1635
1674
  color: seq.color,
1636
1675
  frames: seq.frames,
@@ -2170,7 +2209,63 @@ import { z as z7 } from "zod";
2170
2209
  import { z as z6 } from "zod";
2171
2210
 
2172
2211
  // src/core/http.ts
2212
+ import { isIP } from "net";
2173
2213
  var DEFAULT_FETCH_TIMEOUT = 1e4;
2214
+ var MAX_REDIRECTS = 5;
2215
+ function ipv4Blocked(ip) {
2216
+ const parts = ip.split(".").map(Number);
2217
+ if (parts.length !== 4 || parts.some((p) => !Number.isInteger(p) || p < 0 || p > 255)) {
2218
+ return false;
2219
+ }
2220
+ const [a, b] = parts;
2221
+ if (a === 0) return true;
2222
+ if (a === 10) return true;
2223
+ if (a === 127) return true;
2224
+ if (a === 169 && b === 254) return true;
2225
+ if (a === 172 && b >= 16 && b <= 31) return true;
2226
+ if (a === 192 && b === 168) return true;
2227
+ if (a === 100 && b >= 64 && b <= 127) return true;
2228
+ if (a === 255 && b === 255 && parts[2] === 255 && parts[3] === 255) return true;
2229
+ return false;
2230
+ }
2231
+ function ipv6Blocked(ip) {
2232
+ const v = ip.toLowerCase();
2233
+ if (v === "::1" || v === "::") return true;
2234
+ if (/^fe[89ab]/.test(v)) return true;
2235
+ if (/^f[cd]/.test(v)) return true;
2236
+ const dotted = v.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
2237
+ if (dotted) return ipv4Blocked(dotted[1]);
2238
+ const hex = v.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
2239
+ if (hex) {
2240
+ const hi = parseInt(hex[1], 16);
2241
+ const lo = parseInt(hex[2], 16);
2242
+ return ipv4Blocked(`${hi >> 8 & 255}.${hi & 255}.${lo >> 8 & 255}.${lo & 255}`);
2243
+ }
2244
+ return false;
2245
+ }
2246
+ function isBlockedHost(hostname) {
2247
+ const host = hostname.replace(/^\[|\]$/g, "").replace(/\.$/, "").toLowerCase();
2248
+ if (host === "localhost" || host.endsWith(".localhost")) return true;
2249
+ const kind = isIP(host);
2250
+ if (kind === 4) return ipv4Blocked(host);
2251
+ if (kind === 6) return ipv6Blocked(host);
2252
+ return false;
2253
+ }
2254
+ function fetchBlockReason(url) {
2255
+ let parsed;
2256
+ try {
2257
+ parsed = new URL(url);
2258
+ } catch {
2259
+ return "unparseable URL";
2260
+ }
2261
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
2262
+ return `unsupported scheme "${parsed.protocol}"`;
2263
+ }
2264
+ if (isBlockedHost(parsed.hostname)) {
2265
+ return "private / loopback / link-local address";
2266
+ }
2267
+ return null;
2268
+ }
2174
2269
  function safeUrlForLog(url) {
2175
2270
  try {
2176
2271
  const u = new URL(url);
@@ -2207,20 +2302,47 @@ async function readCappedText(response, url) {
2207
2302
  }
2208
2303
  return new TextDecoder().decode(Buffer.concat(chunks));
2209
2304
  }
2210
- var USER_AGENT = `svg-terminal/${true ? "1.0.0" : "0.0.0-dev"}`;
2305
+ var USER_AGENT = `svg-terminal/${true ? "1.1.1" : "0.0.0-dev"}`;
2211
2306
  async function fetchWithTimeout(url, timeoutMs = DEFAULT_FETCH_TIMEOUT) {
2307
+ const blocked = fetchBlockReason(url);
2308
+ if (blocked) {
2309
+ console.warn(`[svg-terminal] Refused to fetch ${safeUrlForLog(url)}: ${blocked}`);
2310
+ return null;
2311
+ }
2212
2312
  const controller = new AbortController();
2213
2313
  const timer = setTimeout(() => controller.abort(), timeoutMs);
2214
2314
  try {
2215
- const response = await fetch(url, {
2216
- signal: controller.signal,
2217
- headers: { "User-Agent": USER_AGENT }
2218
- });
2219
- if (!response.ok) {
2220
- console.warn(`[svg-terminal] HTTP ${response.status} from ${safeUrlForLog(url)}`);
2221
- return null;
2315
+ let currentUrl = url;
2316
+ for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
2317
+ const response = await fetch(currentUrl, {
2318
+ signal: controller.signal,
2319
+ headers: { "User-Agent": USER_AGENT },
2320
+ redirect: "manual"
2321
+ });
2322
+ if (response.status >= 300 && response.status < 400 && response.headers.has("location")) {
2323
+ let next;
2324
+ try {
2325
+ next = new URL(response.headers.get("location"), currentUrl).toString();
2326
+ } catch {
2327
+ console.warn(`[svg-terminal] Bad redirect Location from ${safeUrlForLog(currentUrl)}`);
2328
+ return null;
2329
+ }
2330
+ const blockedHop = fetchBlockReason(next);
2331
+ if (blockedHop) {
2332
+ console.warn(`[svg-terminal] Refused redirect to ${safeUrlForLog(next)}: ${blockedHop}`);
2333
+ return null;
2334
+ }
2335
+ currentUrl = next;
2336
+ continue;
2337
+ }
2338
+ if (!response.ok) {
2339
+ console.warn(`[svg-terminal] HTTP ${response.status} from ${safeUrlForLog(currentUrl)}`);
2340
+ return null;
2341
+ }
2342
+ return response;
2222
2343
  }
2223
- return response;
2344
+ console.warn(`[svg-terminal] Too many redirects (>${MAX_REDIRECTS}) fetching ${safeUrlForLog(url)}`);
2345
+ return null;
2224
2346
  } catch (error) {
2225
2347
  const message = error instanceof Error ? error.message : String(error);
2226
2348
  if (message.includes("abort")) {
@@ -4339,6 +4461,34 @@ var tocBlock = {
4339
4461
  }
4340
4462
  };
4341
4463
 
4464
+ // src/blocks/jumping-jack.ts
4465
+ import { z as z50 } from "zod";
4466
+ var POSE_OPEN = ["\\o/", " | ", "/ \\"];
4467
+ var POSE_SHUT = [" o ", "/|\\", " | "];
4468
+ var jumpingJackSchema = z50.object({
4469
+ fps: z50.number().int().min(1).max(30).optional(),
4470
+ command: z50.string().optional(),
4471
+ color: z50.string().optional()
4472
+ }).strict();
4473
+ var jumpingJackBlock = {
4474
+ name: "jumping-jack",
4475
+ description: "A multi-line stick figure doing jumping jacks",
4476
+ configSchema: jumpingJackSchema,
4477
+ render(_context, config) {
4478
+ const fps = config["fps"] ?? 2;
4479
+ const command = config["command"] ?? "exercise";
4480
+ const color = config["color"] ?? "yellow";
4481
+ const paint = (rows) => rows.map((r) => `[[fg:${color}]]${r}[[/fg]]`);
4482
+ const frames = [paint(POSE_OPEN), paint(POSE_SHUT)];
4483
+ return {
4484
+ command,
4485
+ lines: frames[0],
4486
+ // static fallback (first pose, all 3 rows)
4487
+ animation: { frames, fps }
4488
+ };
4489
+ }
4490
+ };
4491
+
4342
4492
  // src/blocks/index.ts
4343
4493
  function registerBuiltinBlocks() {
4344
4494
  registerBlocks([
@@ -4388,15 +4538,15 @@ function registerBuiltinBlocks() {
4388
4538
  paletteSwatchBlock,
4389
4539
  semverBumpBlock,
4390
4540
  asciiCalendarBlock,
4391
- tocBlock
4541
+ tocBlock,
4542
+ jumpingJackBlock
4392
4543
  ]);
4393
4544
  }
4394
4545
 
4395
4546
  // src/index.ts
4396
4547
  registerBuiltinBlocks();
4397
- var STRICT_BLOCK_CONFIG = false;
4398
4548
  function setStrictBlockConfig(enabled) {
4399
- STRICT_BLOCK_CONFIG = enabled;
4549
+ setStrict(enabled);
4400
4550
  }
4401
4551
  function validateBlockEntry(block, entry, index) {
4402
4552
  const cfg = entry.config ?? {};
@@ -4404,7 +4554,7 @@ function validateBlockEntry(block, entry, index) {
4404
4554
  try {
4405
4555
  block.configSchema.parse(cfg);
4406
4556
  } catch (err) {
4407
- if (err instanceof z50.ZodError) {
4557
+ if (err instanceof z51.ZodError) {
4408
4558
  const issues = err.issues.map((i) => {
4409
4559
  const path = i.path.length ? i.path.join(".") : "<root>";
4410
4560
  return ` ${path}: ${i.message}`;
@@ -4435,7 +4585,7 @@ ${issues}`
4435
4585
  const known = block.allowedKeys.join(", ");
4436
4586
  const msg = `Unknown config key(s) [${list}] for block "${block.name}" at blocks[${index}]
4437
4587
  Known keys: ${known}`;
4438
- if (STRICT_BLOCK_CONFIG) {
4588
+ if (isStrict()) {
4439
4589
  throw new BlockConfigError(block.name, index, msg);
4440
4590
  }
4441
4591
  console.warn(`[svg-terminal] warning: ${msg}`);
@@ -4470,7 +4620,9 @@ async function generate(userConfig, options = {}) {
4470
4620
  pause: resolvePause(entry.pause ?? result.pause),
4471
4621
  pinWidth: result.pinWidth,
4472
4622
  ...result.animation ? {
4473
- frames: result.animation.frames.map((f) => f.join("\n")),
4623
+ // Carry frames as string[][] (rows preserved) straight through the
4624
+ // timeline — no lossy join/split round-trip (#69, nexus B-PIPELINE).
4625
+ frames: result.animation.frames,
4474
4626
  framesFps: Math.min(30, Math.max(1, result.animation.fps ?? 4)),
4475
4627
  framesLoop: result.animation.loop ?? true
4476
4628
  } : {}
@@ -4600,4 +4752,4 @@ export {
4600
4752
  inspectCache,
4601
4753
  generateStatic
4602
4754
  };
4603
- //# sourceMappingURL=chunk-IVINEQLU.js.map
4755
+ //# sourceMappingURL=chunk-24NH6UUG.js.map
package/dist/cli.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  mergeConfig,
12
12
  setStrictBlockConfig,
13
13
  themes
14
- } from "./chunk-IVINEQLU.js";
14
+ } from "./chunk-24NH6UUG.js";
15
15
 
16
16
  // src/cli.ts
17
17
  import { writeFileSync, watch as fsWatch } from "fs";
@@ -127,7 +127,7 @@ function isZodOptional(t) {
127
127
  }
128
128
 
129
129
  // src/cli.ts
130
- var VERSION = true ? "1.0.0" : "0.0.0-dev";
130
+ var VERSION = true ? "1.1.1" : "0.0.0-dev";
131
131
  var args = process.argv.slice(2);
132
132
  var command = args[0];
133
133
  function getFlag(name) {
package/dist/index.d.ts CHANGED
@@ -211,8 +211,11 @@ interface Sequence {
211
211
  pause?: number;
212
212
  /** Delay before this sequence starts in ms */
213
213
  delay?: number;
214
- /** Optional multi-frame payload — output sequences only. Each entry is one frame's content (newline-separated lines). Triggers frame-cycle rendering. */
215
- frames?: string[];
214
+ /** Optional multi-frame payload — output sequences only. Each entry is one
215
+ * frame; each frame is an array of rows. Multi-line frames are supported
216
+ * (#69) — single-row frames (the common case) are `[oneLine]`. Triggers
217
+ * frame-cycle rendering. */
218
+ frames?: string[][];
216
219
  /** Frames per second when `frames` is set (default 4, capped at 30). */
217
220
  framesFps?: number;
218
221
  /** Loop frames forever when set (default true). */
@@ -233,8 +236,9 @@ interface AnimationFrame {
233
236
  typingDuration?: number;
234
237
  scrollLines?: number;
235
238
  bufferStart?: number;
236
- /** Multi-frame payload — present only on add-output frames spawned from animated blocks. */
237
- frames?: string[];
239
+ /** Multi-frame payload — present only on add-output frames spawned from
240
+ * animated blocks. Each frame is an array of rows (#69 multi-line). */
241
+ frames?: string[][];
238
242
  framesFps?: number;
239
243
  framesLoop?: boolean;
240
244
  /** Width-pinning opt-in from BlockResult.pinWidth. */
@@ -712,7 +716,13 @@ interface GenerateOptions {
712
716
  */
713
717
  onCacheEvent?: (event: CacheEventType, key: string) => void;
714
718
  }
715
- /** Enable strict mode globally — unknown block-config keys throw instead of warning. */
719
+ /**
720
+ * Enable strict mode globally — soft warnings (unknown block-config keys for
721
+ * schemaless blocks; an over-tall animated band, #124) become hard errors.
722
+ * The flag lives in `./core/strict-mode.js` so `svg-generator.ts` can read it
723
+ * without importing this module (which would be circular). Re-exported here as
724
+ * `setStrictBlockConfig` for back-compat with the CLI + library consumers.
725
+ */
716
726
  declare function setStrictBlockConfig(enabled: boolean): void;
717
727
  /**
718
728
  * Generate an animated SVG terminal from a declarative config.
package/dist/index.js CHANGED
@@ -47,7 +47,7 @@ import {
47
47
  themes,
48
48
  tokyoNight,
49
49
  win95
50
- } from "./chunk-IVINEQLU.js";
50
+ } from "./chunk-24NH6UUG.js";
51
51
  export {
52
52
  BlockConfigError,
53
53
  ConfigError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svg-terminal",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "Generate animated SVG terminals for GitHub READMEs from a declarative YAML config. 47 built-in blocks, 12 themes, zero runtime deps in the output.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -59,13 +59,14 @@
59
59
  "node": ">=22.0.0"
60
60
  },
61
61
  "devDependencies": {
62
+ "@eslint/js": "^10.0.1",
62
63
  "@types/js-yaml": "^4.0.9",
63
64
  "@types/node": "^22.19.19",
64
65
  "@vitest/coverage-v8": "^4.1.7",
65
- "eslint": "^9.39.4",
66
+ "eslint": "^10.4.1",
66
67
  "tsup": "^8.0.0",
67
68
  "tsx": "^4.22.3",
68
- "typescript": "^5.7.0",
69
+ "typescript": "^6.0.3",
69
70
  "typescript-eslint": "^8.60.0",
70
71
  "vitest": "^4.1.7"
71
72
  },