@wingleeio/mugen-markdown 0.2.0 → 0.4.0

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/index.cjs CHANGED
@@ -227,120 +227,6 @@ function resolveTheme(theme) {
227
227
  return resolved;
228
228
  }
229
229
  //#endregion
230
- //#region src/inline.ts
231
- /** A base format for body text at a given size/weight/colour. */
232
- function baseFormat(theme, opts = {}) {
233
- return {
234
- family: theme.fontFamily,
235
- monoFamily: theme.monoFamily,
236
- size: opts.size ?? theme.fontSize,
237
- weight: opts.weight ?? 400,
238
- italic: false,
239
- mono: false,
240
- underline: false,
241
- strike: false,
242
- color: opts.color ?? (theme.color !== "inherit" ? theme.color : void 0)
243
- };
244
- }
245
- /** Compose a measurable `Font` shorthand from a format. */
246
- function composeFont(fmt) {
247
- const family = fmt.mono ? fmt.monoFamily : fmt.family;
248
- return `${fmt.italic ? "italic " : ""}${fmt.weight} ${fmt.size}px ${family}`;
249
- }
250
- function pushRun(out, text, fmt) {
251
- if (text.length === 0) return;
252
- const run = {
253
- text,
254
- font: composeFont(fmt)
255
- };
256
- if (fmt.color != null) run.color = fmt.color;
257
- if (fmt.background != null) run.background = fmt.background;
258
- const decoration = [fmt.underline ? "underline" : "", fmt.strike ? "line-through" : ""].filter(Boolean).join(" ");
259
- if (decoration) run.decoration = decoration;
260
- if (fmt.href != null) {
261
- run.href = fmt.href;
262
- run.as = "a";
263
- } else if (fmt.mono) run.as = "code";
264
- out.push(run);
265
- }
266
- /**
267
- * Flatten phrasing content into styled runs. Recursive over the inline marks;
268
- * the result feeds a single `<RichText>` so the whole paragraph wraps as one
269
- * flow and measures exactly.
270
- */
271
- function flattenInline(nodes, fmt, theme, out) {
272
- for (const node of nodes) switch (node.type) {
273
- case "text":
274
- pushRun(out, node.value, fmt);
275
- break;
276
- case "strong":
277
- flattenInline(node.children, {
278
- ...fmt,
279
- weight: theme.strongWeight
280
- }, theme, out);
281
- break;
282
- case "emphasis":
283
- flattenInline(node.children, {
284
- ...fmt,
285
- italic: theme.emphasisItalic ? true : fmt.italic
286
- }, theme, out);
287
- break;
288
- case "delete":
289
- flattenInline(node.children, {
290
- ...fmt,
291
- strike: true
292
- }, theme, out);
293
- break;
294
- case "inlineCode":
295
- pushRun(out, node.value, {
296
- ...fmt,
297
- mono: true,
298
- size: Math.round(fmt.size * theme.inlineCode.sizeScale),
299
- color: theme.inlineCode.color !== "inherit" ? theme.inlineCode.color : fmt.color,
300
- background: theme.inlineCode.background
301
- });
302
- break;
303
- case "link":
304
- flattenInline(node.children, {
305
- ...fmt,
306
- href: node.url,
307
- color: theme.link.color,
308
- underline: theme.link.underline ? true : fmt.underline
309
- }, theme, out);
310
- break;
311
- case "linkReference":
312
- flattenInline(node.children, fmt, theme, out);
313
- break;
314
- case "break":
315
- out.push({
316
- text: "",
317
- break: true
318
- });
319
- break;
320
- case "image":
321
- if (node.alt) pushRun(out, node.alt, {
322
- ...fmt,
323
- color: theme.image.color
324
- });
325
- break;
326
- case "imageReference":
327
- if (node.alt) pushRun(out, node.alt, {
328
- ...fmt,
329
- color: theme.image.color
330
- });
331
- break;
332
- case "footnoteReference":
333
- pushRun(out, `[${node.label ?? node.identifier}]`, {
334
- ...fmt,
335
- color: theme.link.color
336
- });
337
- break;
338
- default:
339
- if ("children" in node && Array.isArray(node.children)) flattenInline(node.children, fmt, theme, out);
340
- break;
341
- }
342
- }
343
- //#endregion
344
230
  //#region src/primitives/rich-text.tsx
345
231
  function resolveRunFont(run, fallback) {
346
232
  const font = run.font ?? fallback;
@@ -348,6 +234,7 @@ function resolveRunFont(run, fallback) {
348
234
  (0, _wingleeio_mugen.assertMeasurableFont)(font);
349
235
  return font;
350
236
  }
237
+ const BOX_PLACEHOLDER = "‌";
351
238
  /** Split runs into hard-break-delimited segments, each a list of rich-inline items. */
352
239
  function segmentItems(runs, fallback) {
353
240
  const segments = [];
@@ -358,9 +245,19 @@ function segmentItems(runs, fallback) {
358
245
  cur = [];
359
246
  continue;
360
247
  }
361
- if (run.text.length === 0) continue;
248
+ if (run.advance != null) {
249
+ cur.push({
250
+ text: BOX_PLACEHOLDER,
251
+ font: resolveRunFont(run, fallback),
252
+ extraWidth: Math.max(0, run.advance),
253
+ break: "never"
254
+ });
255
+ continue;
256
+ }
257
+ const text = run.text ?? "";
258
+ if (text.length === 0) continue;
362
259
  const item = {
363
- text: run.text,
260
+ text,
364
261
  font: resolveRunFont(run, fallback)
365
262
  };
366
263
  if (run.letterSpacing != null) item.letterSpacing = run.letterSpacing;
@@ -425,6 +322,15 @@ function renderRichText(props) {
425
322
  };
426
323
  const children = props.runs.map((run, i) => {
427
324
  if (run.break) return (0, react.createElement)("br", { key: i });
325
+ if (run.advance != null) return (0, react.createElement)("span", {
326
+ key: i,
327
+ style: {
328
+ display: "inline-block",
329
+ verticalAlign: "middle",
330
+ lineHeight: 0,
331
+ whiteSpace: "nowrap"
332
+ }
333
+ }, run.content);
428
334
  const tag = run.as ?? (run.href != null ? "a" : "span");
429
335
  const elementProps = {
430
336
  key: i,
@@ -443,7 +349,7 @@ function renderRichText(props) {
443
349
  if (run.title != null) elementProps.title = run.title;
444
350
  if (run.onClick != null) elementProps.onClick = run.onClick;
445
351
  if (run.className != null) elementProps.className = run.className;
446
- return (0, react.createElement)(tag, elementProps, run.text);
352
+ return (0, react.createElement)(tag, elementProps, run.text ?? "");
447
353
  });
448
354
  return (0, react.createElement)("div", {
449
355
  className: props.className,
@@ -471,12 +377,173 @@ const RichText = (0, _wingleeio_mugen.markPrimitive)(renderRichText, {
471
377
  return max;
472
378
  }
473
379
  });
380
+ /**
381
+ * Measure a string's rendered advance in px for a given measurable font — the
382
+ * same ruler `RichText` measures with. Use it to size an inline box: a text
383
+ * "pill" reserves `measureInline(label, font) + horizontalPadding`.
384
+ */
385
+ function measureInline(text, font) {
386
+ if (text.length === 0) return 0;
387
+ (0, _wingleeio_mugen.assertMeasurableFont)(font);
388
+ return (0, _chenglou_pretext_rich_inline.measureRichInlineStats)(prepareCached([{
389
+ text,
390
+ font
391
+ }]), 1e7).maxLineWidth;
392
+ }
474
393
  /** Drop the rich-inline prepare cache (tests / memory pressure). */
475
394
  function clearRichTextCache() {
476
395
  prepCache.clear();
477
396
  cacheEpoch = -1;
478
397
  }
479
398
  //#endregion
399
+ //#region src/inline.ts
400
+ /** A base format for body text at a given size/weight/colour. */
401
+ function baseFormat(theme, opts = {}) {
402
+ return {
403
+ family: theme.fontFamily,
404
+ monoFamily: theme.monoFamily,
405
+ size: opts.size ?? theme.fontSize,
406
+ weight: opts.weight ?? 400,
407
+ italic: false,
408
+ mono: false,
409
+ underline: false,
410
+ strike: false,
411
+ color: opts.color ?? (theme.color !== "inherit" ? theme.color : void 0)
412
+ };
413
+ }
414
+ /** Compose a measurable `Font` shorthand from a format. */
415
+ function composeFont(fmt) {
416
+ const family = fmt.mono ? fmt.monoFamily : fmt.family;
417
+ return `${fmt.italic ? "italic " : ""}${fmt.weight} ${fmt.size}px ${family}`;
418
+ }
419
+ function pushRun(out, text, fmt) {
420
+ if (text.length === 0) return;
421
+ const run = {
422
+ text,
423
+ font: composeFont(fmt)
424
+ };
425
+ if (fmt.color != null) run.color = fmt.color;
426
+ if (fmt.background != null) run.background = fmt.background;
427
+ const decoration = [fmt.underline ? "underline" : "", fmt.strike ? "line-through" : ""].filter(Boolean).join(" ");
428
+ if (decoration) run.decoration = decoration;
429
+ if (fmt.href != null) {
430
+ run.href = fmt.href;
431
+ run.as = "a";
432
+ } else if (fmt.mono) run.as = "code";
433
+ out.push(run);
434
+ }
435
+ /** Build the context handed to an inline override. */
436
+ function makeInlineCtx(fmt, theme, inline) {
437
+ return {
438
+ theme,
439
+ fmt,
440
+ font: (overrides) => composeFont(overrides ? {
441
+ ...fmt,
442
+ ...overrides
443
+ } : fmt),
444
+ measure: (text, font) => measureInline(text, font),
445
+ runs: (nodes, fmtOverrides) => {
446
+ const sub = [];
447
+ flattenInline(nodes, fmtOverrides ? {
448
+ ...fmt,
449
+ ...fmtOverrides
450
+ } : fmt, theme, sub, inline);
451
+ return sub;
452
+ }
453
+ };
454
+ }
455
+ /**
456
+ * Flatten phrasing content into styled runs. Recursive over the inline marks;
457
+ * the result feeds a single `<RichText>` so the whole paragraph wraps as one
458
+ * flow and measures exactly. An `inline` override map can replace how any node
459
+ * type flattens — returning its own runs (e.g. a measured inline box) or `null`
460
+ * to fall through to the default.
461
+ */
462
+ function flattenInline(nodes, fmt, theme, out, inline) {
463
+ for (const node of nodes) {
464
+ if (inline != null) {
465
+ const override = inline[node.type];
466
+ if (override != null) {
467
+ const produced = override(node, makeInlineCtx(fmt, theme, inline));
468
+ if (produced != null) {
469
+ for (const run of produced) out.push(run);
470
+ continue;
471
+ }
472
+ }
473
+ }
474
+ switch (node.type) {
475
+ case "text":
476
+ pushRun(out, node.value, fmt);
477
+ break;
478
+ case "strong":
479
+ flattenInline(node.children, {
480
+ ...fmt,
481
+ weight: theme.strongWeight
482
+ }, theme, out, inline);
483
+ break;
484
+ case "emphasis":
485
+ flattenInline(node.children, {
486
+ ...fmt,
487
+ italic: theme.emphasisItalic ? true : fmt.italic
488
+ }, theme, out, inline);
489
+ break;
490
+ case "delete":
491
+ flattenInline(node.children, {
492
+ ...fmt,
493
+ strike: true
494
+ }, theme, out, inline);
495
+ break;
496
+ case "inlineCode":
497
+ pushRun(out, node.value, {
498
+ ...fmt,
499
+ mono: true,
500
+ size: Math.round(fmt.size * theme.inlineCode.sizeScale),
501
+ color: theme.inlineCode.color !== "inherit" ? theme.inlineCode.color : fmt.color,
502
+ background: theme.inlineCode.background
503
+ });
504
+ break;
505
+ case "link":
506
+ flattenInline(node.children, {
507
+ ...fmt,
508
+ href: node.url,
509
+ color: theme.link.color,
510
+ underline: theme.link.underline ? true : fmt.underline
511
+ }, theme, out, inline);
512
+ break;
513
+ case "linkReference":
514
+ flattenInline(node.children, fmt, theme, out, inline);
515
+ break;
516
+ case "break":
517
+ out.push({
518
+ text: "",
519
+ break: true
520
+ });
521
+ break;
522
+ case "image":
523
+ if (node.alt) pushRun(out, node.alt, {
524
+ ...fmt,
525
+ color: theme.image.color
526
+ });
527
+ break;
528
+ case "imageReference":
529
+ if (node.alt) pushRun(out, node.alt, {
530
+ ...fmt,
531
+ color: theme.image.color
532
+ });
533
+ break;
534
+ case "footnoteReference":
535
+ pushRun(out, `[${node.label ?? node.identifier}]`, {
536
+ ...fmt,
537
+ color: theme.link.color
538
+ });
539
+ break;
540
+ default:
541
+ if ("children" in node && Array.isArray(node.children)) flattenInline(node.children, fmt, theme, out, inline);
542
+ break;
543
+ }
544
+ }
545
+ }
546
+ //#endregion
480
547
  //#region src/highlight/languages.ts
481
548
  function words(s) {
482
549
  return new Set(s.split(" "));
@@ -1930,7 +1997,7 @@ function createContext(theme, components) {
1930
1997
  ...base
1931
1998
  };
1932
1999
  const out = [];
1933
- flattenInline(nodes, fmt, theme, out);
2000
+ flattenInline(nodes, fmt, theme, out, components.inline);
1934
2001
  return out;
1935
2002
  },
1936
2003
  inlineText(nodes, opts = {}) {
@@ -1940,7 +2007,7 @@ function createContext(theme, components) {
1940
2007
  ...opts.color != null ? { color: opts.color } : null
1941
2008
  });
1942
2009
  const out = [];
1943
- flattenInline(nodes, fmt, theme, out);
2010
+ flattenInline(nodes, fmt, theme, out, components.inline);
1944
2011
  return (0, react.createElement)(RichText, {
1945
2012
  runs: out,
1946
2013
  font: composeFont(fmt),
@@ -1963,6 +2030,256 @@ function renderMarkdown(source, options = {}) {
1963
2030
  return createContext(resolveTheme(options.theme), mergeComponents(options.components)).renderBlocks(ast.children);
1964
2031
  }
1965
2032
  //#endregion
2033
+ //#region src/primitives/fade.tsx
2034
+ /**
2035
+ * Streaming fade-in for `<Markdown fade>`.
2036
+ *
2037
+ * The same trick the code highlighter uses, pointed at motion instead of
2038
+ * colour: the markdown DOM commits and lays out instantly (so heights,
2039
+ * selection, and stick-to-bottom stay honest), and a background-coloured veil
2040
+ * is painted over just-arrived characters and dissolved — which *reads* as the
2041
+ * text fading in. Nothing about the row ever animates; layout is done the
2042
+ * moment the text lands.
2043
+ *
2044
+ * Unlike a list-level overlay, the veil canvas lives **inside** the markdown's
2045
+ * own box (`position: absolute; inset: 0`), so it scrolls with the content and
2046
+ * needs no viewport-geometry tracking. The painter idles (no rAF) until a
2047
+ * DOM mutation arrives, so leaving `fade` on for a settled block costs nothing.
2048
+ */
2049
+ const EMA_SEED_MS = 160;
2050
+ const MIN_FADE_MS = 120;
2051
+ const MAX_FADE_MS = 400;
2052
+ const MAX_VEILS = 32;
2053
+ function commonPrefixLength(a, b) {
2054
+ const n = Math.min(a.length, b.length);
2055
+ let i = 0;
2056
+ while (i < n && a.charCodeAt(i) === b.charCodeAt(i)) i++;
2057
+ return i;
2058
+ }
2059
+ function inChrome(node, container) {
2060
+ for (let p = node.parentElement; p != null && p !== container; p = p.parentElement) if (p.tagName === "BUTTON") return true;
2061
+ return false;
2062
+ }
2063
+ function contentTextFilter(container) {
2064
+ return { acceptNode: (n) => inChrome(n, container) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT };
2065
+ }
2066
+ /** `container.textContent`, minus interactive chrome (see {@link inChrome}). */
2067
+ function contentText(container) {
2068
+ const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, contentTextFilter(container));
2069
+ let s = "";
2070
+ for (let n = walker.nextNode(); n != null; n = walker.nextNode()) s += n.data;
2071
+ return s;
2072
+ }
2073
+ let scratch;
2074
+ function scratchCtx() {
2075
+ if (scratch === void 0) {
2076
+ const c = typeof document === "undefined" ? null : document.createElement("canvas");
2077
+ if (c == null) return scratch = null;
2078
+ c.width = 1;
2079
+ c.height = 1;
2080
+ scratch = c.getContext("2d", { willReadFrequently: true });
2081
+ }
2082
+ return scratch;
2083
+ }
2084
+ /** The opaque colour behind `el`: every ancestor's backgroundColor composited. */
2085
+ function effectiveBackground(el, cache) {
2086
+ const cached = cache.get(el);
2087
+ if (cached !== void 0) return cached;
2088
+ const layers = [];
2089
+ for (let cur = el; cur != null; cur = cur.parentElement) {
2090
+ const bg = getComputedStyle(cur).backgroundColor;
2091
+ if (bg && bg !== "transparent" && bg !== "rgba(0, 0, 0, 0)") layers.push(bg);
2092
+ }
2093
+ const s = scratchCtx();
2094
+ if (s == null) return "#808080";
2095
+ s.globalAlpha = 1;
2096
+ s.fillStyle = "#ffffff";
2097
+ s.fillRect(0, 0, 1, 1);
2098
+ for (let i = layers.length - 1; i >= 0; i--) {
2099
+ try {
2100
+ s.fillStyle = layers[i];
2101
+ } catch {
2102
+ continue;
2103
+ }
2104
+ s.fillRect(0, 0, 1, 1);
2105
+ }
2106
+ const d = s.getImageData(0, 0, 1, 1).data;
2107
+ const css = `rgb(${d[0]}, ${d[1]}, ${d[2]})`;
2108
+ cache.set(el, css);
2109
+ return css;
2110
+ }
2111
+ /**
2112
+ * Paints the dissolving veil over a single content element's newly-arrived text.
2113
+ * Driven by a MutationObserver: a rAF loop runs only while veils are alive, then
2114
+ * stops until the next mutation, so an idle (settled) block uses no frames.
2115
+ */
2116
+ var FadePainter = class {
2117
+ content = null;
2118
+ canvas = null;
2119
+ ctx = null;
2120
+ prevText = "";
2121
+ veils = [];
2122
+ ema = EMA_SEED_MS;
2123
+ lastAppend = 0;
2124
+ raf = 0;
2125
+ running = false;
2126
+ mo = null;
2127
+ attach(content, canvas) {
2128
+ if (typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches) return;
2129
+ const ctx = canvas.getContext("2d");
2130
+ if (ctx == null) return;
2131
+ this.content = content;
2132
+ this.canvas = canvas;
2133
+ this.ctx = ctx;
2134
+ this.prevText = contentText(content);
2135
+ this.mo = new MutationObserver(() => this.wake());
2136
+ this.mo.observe(content, {
2137
+ subtree: true,
2138
+ childList: true,
2139
+ characterData: true
2140
+ });
2141
+ }
2142
+ destroy() {
2143
+ this.mo?.disconnect();
2144
+ this.mo = null;
2145
+ if (this.raf !== 0) cancelAnimationFrame(this.raf);
2146
+ this.raf = 0;
2147
+ this.running = false;
2148
+ this.content = null;
2149
+ this.canvas = null;
2150
+ this.ctx = null;
2151
+ }
2152
+ wake() {
2153
+ if (this.running || this.content == null) return;
2154
+ this.running = true;
2155
+ this.raf = requestAnimationFrame(this.frame);
2156
+ }
2157
+ frame = () => {
2158
+ const content = this.content;
2159
+ const canvas = this.canvas;
2160
+ const ctx = this.ctx;
2161
+ if (content == null || canvas == null || ctx == null) {
2162
+ this.running = false;
2163
+ return;
2164
+ }
2165
+ const now = performance.now();
2166
+ const text = contentText(content);
2167
+ if (text !== this.prevText) {
2168
+ const prefix = commonPrefixLength(this.prevText, text);
2169
+ this.veils = this.veils.map((v) => ({
2170
+ ...v,
2171
+ start: Math.min(v.start, prefix),
2172
+ end: Math.min(v.end, prefix)
2173
+ })).filter((v) => v.end > v.start);
2174
+ if (text.length > prefix) {
2175
+ if (this.lastAppend > 0) this.ema = this.ema * .7 + Math.min(now - this.lastAppend, 1e3) * .3;
2176
+ this.lastAppend = now;
2177
+ this.veils.push({
2178
+ start: prefix,
2179
+ end: text.length,
2180
+ t0: now
2181
+ });
2182
+ if (this.veils.length > MAX_VEILS) this.veils.splice(0, this.veils.length - MAX_VEILS);
2183
+ }
2184
+ this.prevText = text;
2185
+ }
2186
+ const duration = Math.min(MAX_FADE_MS, Math.max(MIN_FADE_MS, this.ema * 3));
2187
+ const boost = 1 + .3 * Math.max(0, this.veils.length - 2);
2188
+ this.veils = this.veils.filter((v) => (now - v.t0) * boost < duration);
2189
+ const dpr = typeof devicePixelRatio === "number" && devicePixelRatio > 0 ? devicePixelRatio : 1;
2190
+ const w = content.clientWidth;
2191
+ const h = content.clientHeight;
2192
+ if (canvas.width !== Math.round(w * dpr) || canvas.height !== Math.round(h * dpr)) {
2193
+ canvas.width = Math.round(w * dpr);
2194
+ canvas.height = Math.round(h * dpr);
2195
+ }
2196
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
2197
+ ctx.clearRect(0, 0, w, h);
2198
+ if (this.veils.length > 0) {
2199
+ const origin = canvas.getBoundingClientRect();
2200
+ const bgCache = /* @__PURE__ */ new Map();
2201
+ const groups = /* @__PURE__ */ new Map();
2202
+ const minStart = this.veils.reduce((m, v) => Math.min(m, v.start), Infinity);
2203
+ const walker = document.createTreeWalker(content, NodeFilter.SHOW_TEXT, contentTextFilter(content));
2204
+ let base = 0;
2205
+ let node = walker.nextNode();
2206
+ while (node != null) {
2207
+ const len = node.data.length;
2208
+ if (len > 0 && base + len > minStart) for (let vi = 0; vi < this.veils.length; vi++) {
2209
+ const v = this.veils[vi];
2210
+ const s = Math.max(v.start - base, 0);
2211
+ const e = Math.min(v.end - base, len);
2212
+ if (e <= s) continue;
2213
+ const parent = node.parentElement;
2214
+ if (parent == null) continue;
2215
+ const bg = effectiveBackground(parent, bgCache);
2216
+ const key = `${vi}|${bg}`;
2217
+ let group = groups.get(key);
2218
+ if (group === void 0) {
2219
+ const p = Math.min(1, (now - v.t0) * boost / duration);
2220
+ group = {
2221
+ alpha: Math.pow(1 - p, 1.6),
2222
+ bg,
2223
+ path: new Path2D()
2224
+ };
2225
+ groups.set(key, group);
2226
+ }
2227
+ const range = document.createRange();
2228
+ range.setStart(node, s);
2229
+ range.setEnd(node, e);
2230
+ for (const r of range.getClientRects()) group.path.rect(r.left - origin.left - 1, r.top - origin.top - 1, r.width + 2, r.height + 2);
2231
+ }
2232
+ base += len;
2233
+ node = walker.nextNode();
2234
+ }
2235
+ for (const group of groups.values()) {
2236
+ ctx.globalAlpha = group.alpha;
2237
+ ctx.fillStyle = group.bg;
2238
+ ctx.fill(group.path);
2239
+ }
2240
+ ctx.globalAlpha = 1;
2241
+ }
2242
+ if (this.veils.length > 0) this.raf = requestAnimationFrame(this.frame);
2243
+ else {
2244
+ this.running = false;
2245
+ this.raf = 0;
2246
+ }
2247
+ };
2248
+ };
2249
+ function renderFadeMarkdown(props) {
2250
+ const contentRef = (0, react.useRef)(null);
2251
+ const canvasRef = (0, react.useRef)(null);
2252
+ (0, react.useEffect)(() => {
2253
+ const content = contentRef.current;
2254
+ const canvas = canvasRef.current;
2255
+ if (content == null || canvas == null) return;
2256
+ const painter = new FadePainter();
2257
+ painter.attach(content, canvas);
2258
+ return () => painter.destroy();
2259
+ }, []);
2260
+ return (0, react.createElement)("div", { style: { position: "relative" } }, (0, react.createElement)("div", { ref: contentRef }, props.children), (0, react.createElement)("canvas", {
2261
+ ref: canvasRef,
2262
+ "aria-hidden": true,
2263
+ style: {
2264
+ position: "absolute",
2265
+ inset: 0,
2266
+ width: "100%",
2267
+ height: "100%",
2268
+ pointerEvents: "none"
2269
+ }
2270
+ }));
2271
+ }
2272
+ /**
2273
+ * Wraps a measured markdown subtree with a streaming fade-in canvas. Its height
2274
+ * is exactly the subtree's height — the canvas overlay is out of flow and never
2275
+ * measured.
2276
+ */
2277
+ const FadeMarkdown = (0, _wingleeio_mugen.markPrimitive)(renderFadeMarkdown, {
2278
+ name: "FadeMarkdown",
2279
+ measure: (props, ctx) => (0, _wingleeio_mugen.measureChildren)(props.children, ctx),
2280
+ naturalWidth: (props, ctx) => (0, _wingleeio_mugen.naturalWidthOf)(props.children, ctx)
2281
+ });
2282
+ //#endregion
1966
2283
  //#region src/markdown.tsx
1967
2284
  /**
1968
2285
  * Render markdown as a tree of mugen primitives.
@@ -1985,7 +2302,9 @@ function renderMarkdown(source, options = {}) {
1985
2302
  * and a deep-partial `theme`.
1986
2303
  */
1987
2304
  function Markdown(props) {
1988
- return renderMarkdown(props.source, props);
2305
+ const content = renderMarkdown(props.source, props);
2306
+ if (props.fade && content != null) return (0, react.createElement)(FadeMarkdown, null, content);
2307
+ return content;
1989
2308
  }
1990
2309
  Markdown.displayName = "Markdown";
1991
2310
  //#endregion
@@ -2006,6 +2325,7 @@ function defineMarkdownComponents(components) {
2006
2325
  }
2007
2326
  //#endregion
2008
2327
  exports.CodeBlock = CodeBlock;
2328
+ exports.FadeMarkdown = FadeMarkdown;
2009
2329
  Object.defineProperty(exports, "HStack", {
2010
2330
  enumerable: true,
2011
2331
  get: function() {
@@ -2042,6 +2362,7 @@ Object.defineProperty(exports, "definePrimitive", {
2042
2362
  }
2043
2363
  });
2044
2364
  exports.flattenInline = flattenInline;
2365
+ exports.measureInline = measureInline;
2045
2366
  exports.parseMarkdown = parseMarkdown;
2046
2367
  exports.profileFor = profileFor;
2047
2368
  exports.registerLanguage = registerLanguage;