@svelterm/core 0.1.0 → 0.21.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.
Files changed (164) hide show
  1. package/CHANGELOG.md +425 -0
  2. package/README.md +42 -29
  3. package/dist/src/cli/build.d.ts +13 -0
  4. package/dist/src/cli/build.js +119 -0
  5. package/dist/src/cli/bundle.d.ts +25 -0
  6. package/dist/src/cli/bundle.js +61 -0
  7. package/dist/src/cli/dev.d.ts +10 -0
  8. package/dist/src/cli/dev.js +152 -0
  9. package/dist/src/cli/devtools.d.ts +9 -0
  10. package/dist/src/cli/devtools.js +47 -0
  11. package/dist/src/cli/init.d.ts +8 -0
  12. package/dist/src/cli/init.js +153 -0
  13. package/dist/src/cli/main.d.ts +9 -0
  14. package/dist/src/cli/main.js +52 -0
  15. package/dist/src/cli/svt-bin.d.ts +2 -0
  16. package/dist/src/cli/svt-bin.js +6 -0
  17. package/dist/src/cli/svt.d.ts +14 -0
  18. package/dist/src/cli/svt.js +76 -0
  19. package/dist/src/components/text-buffer.js +8 -5
  20. package/dist/src/css/animation-runner.d.ts +15 -6
  21. package/dist/src/css/animation-runner.js +80 -29
  22. package/dist/src/css/animation.d.ts +12 -0
  23. package/dist/src/css/animation.js +21 -0
  24. package/dist/src/css/calc.js +4 -3
  25. package/dist/src/css/color.d.ts +19 -0
  26. package/dist/src/css/color.js +371 -62
  27. package/dist/src/css/compute.d.ts +30 -3
  28. package/dist/src/css/compute.js +272 -33
  29. package/dist/src/css/defaults.d.ts +1 -1
  30. package/dist/src/css/defaults.js +9 -0
  31. package/dist/src/css/easing.d.ts +9 -0
  32. package/dist/src/css/easing.js +95 -0
  33. package/dist/src/css/incremental.d.ts +1 -1
  34. package/dist/src/css/incremental.js +2 -2
  35. package/dist/src/css/interpolate.d.ts +13 -0
  36. package/dist/src/css/interpolate.js +41 -0
  37. package/dist/src/css/parser.js +59 -3
  38. package/dist/src/css/pseudo-elements.d.ts +9 -0
  39. package/dist/src/css/pseudo-elements.js +97 -0
  40. package/dist/src/css/selector.d.ts +17 -2
  41. package/dist/src/css/selector.js +128 -13
  42. package/dist/src/css/specificity.js +17 -6
  43. package/dist/src/css/values.d.ts +6 -1
  44. package/dist/src/css/values.js +13 -6
  45. package/dist/src/debug/context.d.ts +13 -0
  46. package/dist/src/debug/context.js +11 -0
  47. package/dist/src/debug/css.d.ts +12 -0
  48. package/dist/src/debug/css.js +28 -0
  49. package/dist/src/debug/dom.d.ts +17 -0
  50. package/dist/src/debug/dom.js +92 -0
  51. package/dist/src/devtools/DevTools.compiled.js +327 -0
  52. package/dist/src/devtools/DevTools.css.js +1 -0
  53. package/dist/src/devtools/client.d.ts +36 -0
  54. package/dist/src/devtools/client.js +76 -0
  55. package/dist/src/framelog.d.ts +54 -0
  56. package/dist/src/framelog.js +99 -0
  57. package/dist/src/headless.js +12 -4
  58. package/dist/src/index.d.ts +65 -3
  59. package/dist/src/index.js +609 -81
  60. package/dist/src/input/checkable.d.ts +8 -0
  61. package/dist/src/input/checkable.js +66 -0
  62. package/dist/src/input/details.d.ts +6 -0
  63. package/dist/src/input/details.js +34 -0
  64. package/dist/src/input/focus.d.ts +6 -0
  65. package/dist/src/input/focus.js +27 -9
  66. package/dist/src/input/keyboard.d.ts +2 -2
  67. package/dist/src/input/keyboard.js +32 -5
  68. package/dist/src/input/label.d.ts +8 -0
  69. package/dist/src/input/label.js +53 -0
  70. package/dist/src/input/modal.d.ts +9 -0
  71. package/dist/src/input/modal.js +28 -0
  72. package/dist/src/input/mouse.d.ts +2 -2
  73. package/dist/src/input/mouse.js +15 -2
  74. package/dist/src/input/select.d.ts +12 -0
  75. package/dist/src/input/select.js +63 -0
  76. package/dist/src/input/selection.d.ts +48 -0
  77. package/dist/src/input/selection.js +150 -0
  78. package/dist/src/layout/engine.d.ts +2 -0
  79. package/dist/src/layout/engine.js +1084 -142
  80. package/dist/src/layout/flex.js +4 -4
  81. package/dist/src/layout/size.js +3 -2
  82. package/dist/src/layout/text.d.ts +3 -2
  83. package/dist/src/layout/text.js +96 -17
  84. package/dist/src/layout/unicode.d.ts +20 -0
  85. package/dist/src/layout/unicode.js +121 -0
  86. package/dist/src/render/animation-clock.d.ts +51 -0
  87. package/dist/src/render/animation-clock.js +213 -0
  88. package/dist/src/render/ansi-text.d.ts +26 -0
  89. package/dist/src/render/ansi-text.js +131 -0
  90. package/dist/src/render/ansi.d.ts +18 -0
  91. package/dist/src/render/ansi.js +64 -19
  92. package/dist/src/render/border.js +166 -17
  93. package/dist/src/render/buffer.d.ts +1 -0
  94. package/dist/src/render/buffer.js +5 -2
  95. package/dist/src/render/color-depth.d.ts +8 -0
  96. package/dist/src/render/color-depth.js +59 -0
  97. package/dist/src/render/context.d.ts +1 -0
  98. package/dist/src/render/context.js +17 -21
  99. package/dist/src/render/cursor-emit.d.ts +18 -0
  100. package/dist/src/render/cursor-emit.js +50 -0
  101. package/dist/src/render/diff.d.ts +12 -0
  102. package/dist/src/render/diff.js +120 -0
  103. package/dist/src/render/generation.d.ts +9 -0
  104. package/dist/src/render/generation.js +14 -0
  105. package/dist/src/render/graphics-layer.d.ts +27 -0
  106. package/dist/src/render/graphics-layer.js +86 -0
  107. package/dist/src/render/image.d.ts +27 -0
  108. package/dist/src/render/image.js +113 -0
  109. package/dist/src/render/incremental-paint.d.ts +7 -3
  110. package/dist/src/render/incremental-paint.js +52 -79
  111. package/dist/src/render/inline.d.ts +59 -0
  112. package/dist/src/render/inline.js +219 -0
  113. package/dist/src/render/kitty-graphics.d.ts +24 -0
  114. package/dist/src/render/kitty-graphics.js +58 -0
  115. package/dist/src/render/paint-text.js +68 -22
  116. package/dist/src/render/paint.d.ts +8 -1
  117. package/dist/src/render/paint.js +328 -30
  118. package/dist/src/render/png.d.ts +13 -0
  119. package/dist/src/render/png.js +145 -0
  120. package/dist/src/render/scrollbar.d.ts +8 -2
  121. package/dist/src/render/scrollbar.js +71 -14
  122. package/dist/src/render/snapshot.js +3 -1
  123. package/dist/src/renderer/default.d.ts +7 -0
  124. package/dist/src/renderer/default.js +11 -0
  125. package/dist/src/renderer/index.d.ts +8 -2
  126. package/dist/src/renderer/index.js +4 -2
  127. package/dist/src/renderer/node.d.ts +109 -0
  128. package/dist/src/renderer/node.js +165 -1
  129. package/dist/src/terminal/capabilities.d.ts +33 -0
  130. package/dist/src/terminal/capabilities.js +66 -0
  131. package/dist/src/terminal/clipboard.d.ts +9 -0
  132. package/dist/src/terminal/clipboard.js +39 -0
  133. package/dist/src/terminal/io.d.ts +82 -0
  134. package/dist/src/terminal/io.js +155 -0
  135. package/dist/src/terminal/screen.d.ts +3 -10
  136. package/dist/src/terminal/screen.js +5 -28
  137. package/dist/src/terminal/stdin-router.d.ts +8 -5
  138. package/dist/src/terminal/stdin-router.js +22 -11
  139. package/dist/src/utils/node-map.d.ts +24 -0
  140. package/dist/src/utils/node-map.js +75 -0
  141. package/dist/src/vite/config.d.ts +62 -0
  142. package/dist/src/vite/config.js +191 -0
  143. package/docs/compatibility.md +67 -0
  144. package/docs/debug/devtools.md +40 -0
  145. package/docs/debug/svt.md +50 -0
  146. package/docs/distribution.md +106 -0
  147. package/docs/elements.md +120 -0
  148. package/docs/getting-started.md +177 -0
  149. package/docs/guide/css.md +187 -0
  150. package/docs/guide/input.md +143 -0
  151. package/docs/guide/layout.md +171 -0
  152. package/docs/guide/theming.md +94 -0
  153. package/docs/how-it-works.md +115 -0
  154. package/docs/inline-mode.md +77 -0
  155. package/docs/layout.md +106 -0
  156. package/docs/motion.md +91 -0
  157. package/docs/reference/README.md +65 -0
  158. package/docs/reference/css/properties/border-corner.md +82 -0
  159. package/docs/reference/css/properties/border-style.md +168 -0
  160. package/docs/reference.md +226 -0
  161. package/docs/selectors.md +80 -0
  162. package/docs/terminal-css.md +149 -0
  163. package/docs/terminals.md +83 -0
  164. package/package.json +28 -7
@@ -2,10 +2,10 @@ export function computeMainStart(justify, freeSpace, count, hasGrow) {
2
2
  if (hasGrow || count === 0)
3
3
  return 0;
4
4
  switch (justify) {
5
- case 'end': return freeSpace;
6
- case 'center': return Math.floor(freeSpace / 2);
7
- case 'space-around': return Math.floor(freeSpace / (count * 2));
8
- case 'space-evenly': return Math.floor(freeSpace / (count + 1));
5
+ case 'end': return Math.max(0, freeSpace);
6
+ case 'center': return Math.floor(freeSpace / 2); // allows negative for overflow
7
+ case 'space-around': return freeSpace > 0 ? Math.floor(freeSpace / (count * 2)) : 0;
8
+ case 'space-evenly': return freeSpace > 0 ? Math.floor(freeSpace / (count + 1)) : 0;
9
9
  default: return 0;
10
10
  }
11
11
  }
@@ -17,9 +17,10 @@ export function resolveSize(value, available) {
17
17
  }
18
18
  export function constrain(value, min, max) {
19
19
  let result = value;
20
- if (min != null)
21
- result = Math.max(result, min);
20
+ // Apply max first, then min per CSS spec, min wins when conflicting
22
21
  if (max != null)
23
22
  result = Math.min(result, max);
23
+ if (min != null)
24
+ result = Math.max(result, min);
24
25
  return result;
25
26
  }
@@ -1,7 +1,8 @@
1
- export declare function wrapText(text: string, width: number): string[];
1
+ export type WordBreak = 'normal' | 'break-all';
2
+ export declare function wrapText(text: string, width: number, wordBreak?: WordBreak): string[];
2
3
  export declare function truncateText(text: string, width: number): string;
3
4
  export declare function truncateMiddle(text: string, width: number): string;
4
- export declare function measureText(text: string, availWidth: number): {
5
+ export declare function measureText(text: string, availWidth: number, wordBreak?: WordBreak): {
5
6
  width: number;
6
7
  height: number;
7
8
  };
@@ -1,6 +1,20 @@
1
- export function wrapText(text, width) {
1
+ import { graphemes, charWidth, stringWidth } from './unicode.js';
2
+ /** True when every unit is a single-cell ASCII character. */
3
+ function isPlainAscii(text) {
4
+ for (let i = 0; i < text.length; i++) {
5
+ if (text.charCodeAt(i) < 0x20 || text.charCodeAt(i) > 0x7e)
6
+ return false;
7
+ }
8
+ return true;
9
+ }
10
+ export function wrapText(text, width, wordBreak = 'normal') {
2
11
  if (text === '')
3
12
  return [''];
13
+ if (isPlainAscii(text))
14
+ return wrapAscii(text, width, wordBreak);
15
+ return wrapGraphemes(text, width, wordBreak);
16
+ }
17
+ function wrapAscii(text, width, wordBreak) {
4
18
  if (text.length <= width)
5
19
  return [text];
6
20
  const lines = [];
@@ -10,43 +24,108 @@ export function wrapText(text, width) {
10
24
  lines.push(remaining);
11
25
  break;
12
26
  }
13
- // Find last space within width
14
- let breakAt = remaining.lastIndexOf(' ', width);
27
+ // break-all wraps at any character; normal prefers the last space
28
+ const breakAt = wordBreak === 'break-all' ? -1 : remaining.lastIndexOf(' ', width);
15
29
  if (breakAt <= 0) {
16
- // No space found hard break at width
17
- breakAt = width;
18
- lines.push(remaining.substring(0, breakAt));
19
- remaining = remaining.substring(breakAt);
30
+ // Hard break at width (break-all, or no space found)
31
+ lines.push(remaining.substring(0, width));
32
+ remaining = remaining.substring(width);
20
33
  }
21
34
  else {
22
35
  lines.push(remaining.substring(0, breakAt));
23
36
  remaining = remaining.substring(breakAt + 1); // skip the space
24
37
  }
38
+ if (wordBreak === 'break-all')
39
+ remaining = remaining.replace(/^ /, '');
40
+ }
41
+ return lines;
42
+ }
43
+ /** Wrap by cell width over grapheme clusters — wide glyphs never split. */
44
+ function wrapGraphemes(text, width, wordBreak) {
45
+ if (stringWidth(text) <= width)
46
+ return [text];
47
+ const cells = graphemes(text).map(g => ({ g, w: charWidth(g) }));
48
+ const lines = [];
49
+ let line = '';
50
+ let lineWidth = 0;
51
+ let lastSpaceIndex = -1; // index into `line` string of the last space
52
+ for (const { g, w } of cells) {
53
+ if (lineWidth + w > width && lineWidth > 0) {
54
+ if (wordBreak === 'normal' && lastSpaceIndex > 0 && g !== ' ') {
55
+ // Move the partial word down to the next line
56
+ const carried = line.slice(lastSpaceIndex + 1);
57
+ lines.push(line.slice(0, lastSpaceIndex));
58
+ line = carried;
59
+ lineWidth = stringWidth(carried);
60
+ }
61
+ else {
62
+ lines.push(line);
63
+ line = '';
64
+ lineWidth = 0;
65
+ }
66
+ if (g === ' ')
67
+ continue; // breaks eat the space
68
+ }
69
+ line += g;
70
+ lineWidth += w;
71
+ if (g === ' ')
72
+ lastSpaceIndex = line.length - 1;
73
+ else if (lastSpaceIndex >= line.length)
74
+ lastSpaceIndex = -1;
25
75
  }
76
+ if (line !== '')
77
+ lines.push(line);
26
78
  return lines;
27
79
  }
28
80
  export function truncateText(text, width) {
29
81
  if (width <= 0)
30
82
  return '';
31
- if (text.length <= width)
83
+ if (stringWidth(text) <= width)
32
84
  return text;
33
85
  if (width === 1)
34
86
  return '…';
35
- return text.substring(0, width - 1) + '';
87
+ let out = '';
88
+ let used = 0;
89
+ for (const g of graphemes(text)) {
90
+ const w = charWidth(g);
91
+ if (used + w > width - 1)
92
+ break;
93
+ out += g;
94
+ used += w;
95
+ }
96
+ return out + '…';
36
97
  }
37
98
  export function truncateMiddle(text, width) {
38
99
  if (width <= 0)
39
100
  return '';
40
- if (text.length <= width)
101
+ if (stringWidth(text) <= width)
41
102
  return text;
42
103
  if (width <= 3)
43
- return text.substring(0, width - 1) + '…';
44
- const half = Math.floor((width - 1) / 2);
45
- const endLen = width - 1 - half;
46
- return text.substring(0, half) + '…' + text.substring(text.length - endLen);
104
+ return truncateText(text, width);
105
+ const cells = graphemes(text).map(g => ({ g, w: charWidth(g) }));
106
+ const budget = width - 1;
107
+ const headBudget = Math.floor(budget / 2);
108
+ let head = '';
109
+ let used = 0;
110
+ for (const { g, w } of cells) {
111
+ if (used + w > headBudget)
112
+ break;
113
+ head += g;
114
+ used += w;
115
+ }
116
+ const tailBudget = budget - used;
117
+ let tail = '';
118
+ let tailUsed = 0;
119
+ for (let i = cells.length - 1; i >= 0; i--) {
120
+ if (tailUsed + cells[i].w > tailBudget)
121
+ break;
122
+ tail = cells[i].g + tail;
123
+ tailUsed += cells[i].w;
124
+ }
125
+ return head + '…' + tail;
47
126
  }
48
- export function measureText(text, availWidth) {
49
- const lines = wrapText(text, availWidth);
50
- const maxLineWidth = lines.reduce((max, line) => Math.max(max, line.length), 0);
127
+ export function measureText(text, availWidth, wordBreak = 'normal') {
128
+ const lines = wrapText(text, availWidth, wordBreak);
129
+ const maxLineWidth = lines.reduce((max, line) => Math.max(max, stringWidth(line)), 0);
51
130
  return { width: maxLineWidth, height: lines.length };
52
131
  }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Grapheme segmentation and terminal cell widths. Terminals allot two
3
+ * columns to East Asian wide/fullwidth characters and (by modern
4
+ * convention) emoji; combining marks ride their base; a handful of
5
+ * characters occupy no column at all.
6
+ */
7
+ /** Split into user-perceived characters (grapheme clusters). */
8
+ export declare function graphemes(text: string): string[];
9
+ /**
10
+ * The cell width of one grapheme cluster: the max of its code points'
11
+ * widths — a ZWJ emoji sequence is as wide as its widest member, a
12
+ * combining sequence as wide as its base.
13
+ */
14
+ export declare function charWidth(grapheme: string): number;
15
+ /** The code-unit index of the grapheme boundary after `index`. */
16
+ export declare function nextGraphemeBoundary(text: string, index: number): number;
17
+ /** The code-unit index of the grapheme boundary before `index`. */
18
+ export declare function prevGraphemeBoundary(text: string, index: number): number;
19
+ /** The total cell width of a string. */
20
+ export declare function stringWidth(text: string): number;
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Grapheme segmentation and terminal cell widths. Terminals allot two
3
+ * columns to East Asian wide/fullwidth characters and (by modern
4
+ * convention) emoji; combining marks ride their base; a handful of
5
+ * characters occupy no column at all.
6
+ */
7
+ const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
8
+ /** Split into user-perceived characters (grapheme clusters). */
9
+ export function graphemes(text) {
10
+ if (text === '')
11
+ return [];
12
+ const out = [];
13
+ for (const segment of segmenter.segment(text))
14
+ out.push(segment.segment);
15
+ return out;
16
+ }
17
+ /**
18
+ * East Asian wide/fullwidth ranges plus wide-rendering emoji blocks.
19
+ * Compact interval table over code points; derived from UAX #11 and the
20
+ * ranges terminals treat as wide in practice.
21
+ */
22
+ const WIDE_RANGES = [
23
+ [0x1100, 0x115f], // Hangul Jamo leading consonants
24
+ [0x2e80, 0x303e], // CJK Radicals … CJK Symbols and Punctuation
25
+ [0x3041, 0x33ff], // Hiragana … CJK Compatibility
26
+ [0x3400, 0x4dbf], // CJK Extension A
27
+ [0x4e00, 0x9fff], // CJK Unified Ideographs
28
+ [0xa000, 0xa4cf], // Yi
29
+ [0xa960, 0xa97f], // Hangul Jamo Extended-A
30
+ [0xac00, 0xd7a3], // Hangul Syllables
31
+ [0xf900, 0xfaff], // CJK Compatibility Ideographs
32
+ [0xfe10, 0xfe19], // Vertical forms
33
+ [0xfe30, 0xfe6f], // CJK Compatibility Forms, Small Form Variants
34
+ [0xff00, 0xff60], // Fullwidth Forms
35
+ [0xffe0, 0xffe6], // Fullwidth signs
36
+ [0x1f004, 0x1f004], // Mahjong red dragon
37
+ [0x1f0cf, 0x1f0cf], // Playing card joker
38
+ [0x1f18e, 0x1f18e], // AB button
39
+ [0x1f191, 0x1f19a], // Squared CL … VS
40
+ [0x1f200, 0x1f2ff], // Enclosed ideographic supplement
41
+ [0x1f300, 0x1f64f], // Misc symbols & pictographs, emoticons
42
+ [0x1f680, 0x1f6ff], // Transport & map symbols
43
+ [0x1f900, 0x1f9ff], // Supplemental symbols & pictographs
44
+ [0x1fa70, 0x1faff], // Symbols & pictographs extended-A
45
+ [0x20000, 0x2fffd], // CJK Extension B …
46
+ [0x30000, 0x3fffd], // CJK Extension G …
47
+ ];
48
+ /** Zero-column code points: combining marks handled via category check. */
49
+ const ZERO_WIDTH = new Set([
50
+ 0x200b, // zero width space
51
+ 0x200c, // zero width non-joiner
52
+ 0x200d, // zero width joiner
53
+ 0xfeff, // BOM / zero width no-break space
54
+ ]);
55
+ const COMBINING_RE = /\p{Mn}|\p{Me}/u;
56
+ function codePointWidth(cp) {
57
+ if (ZERO_WIDTH.has(cp))
58
+ return 0;
59
+ const char = String.fromCodePoint(cp);
60
+ if (COMBINING_RE.test(char))
61
+ return 0;
62
+ for (const [start, end] of WIDE_RANGES) {
63
+ if (cp >= start && cp <= end)
64
+ return 2;
65
+ if (cp < start)
66
+ break;
67
+ }
68
+ return 1;
69
+ }
70
+ /**
71
+ * The cell width of one grapheme cluster: the max of its code points'
72
+ * widths — a ZWJ emoji sequence is as wide as its widest member, a
73
+ * combining sequence as wide as its base.
74
+ */
75
+ export function charWidth(grapheme) {
76
+ let width = 0;
77
+ for (const char of grapheme) {
78
+ width = Math.max(width, codePointWidth(char.codePointAt(0)));
79
+ }
80
+ return width;
81
+ }
82
+ /** The code-unit index of the grapheme boundary after `index`. */
83
+ export function nextGraphemeBoundary(text, index) {
84
+ if (index >= text.length)
85
+ return text.length;
86
+ for (const segment of segmenter.segment(text)) {
87
+ const end = segment.index + segment.segment.length;
88
+ if (end > index)
89
+ return end;
90
+ }
91
+ return text.length;
92
+ }
93
+ /** The code-unit index of the grapheme boundary before `index`. */
94
+ export function prevGraphemeBoundary(text, index) {
95
+ if (index <= 0)
96
+ return 0;
97
+ let previous = 0;
98
+ for (const segment of segmenter.segment(text)) {
99
+ if (segment.index >= index)
100
+ break;
101
+ previous = segment.index;
102
+ }
103
+ return previous;
104
+ }
105
+ /** The total cell width of a string. */
106
+ export function stringWidth(text) {
107
+ // Fast path: ASCII (the overwhelmingly common case)
108
+ let ascii = true;
109
+ for (let i = 0; i < text.length; i++) {
110
+ if (text.charCodeAt(i) > 0x7e) {
111
+ ascii = false;
112
+ break;
113
+ }
114
+ }
115
+ if (ascii)
116
+ return text.length;
117
+ let width = 0;
118
+ for (const grapheme of graphemes(text))
119
+ width += charWidth(grapheme);
120
+ return width;
121
+ }
@@ -0,0 +1,51 @@
1
+ import { TermNode } from '../renderer/node.js';
2
+ import { type KeyframeResolution } from '../css/animation.js';
3
+ import type { KeyframeStop } from '../css/parser.js';
4
+ import type { ResolvedStyle } from '../css/compute.js';
5
+ export type { KeyframeResolution } from '../css/animation.js';
6
+ /**
7
+ * Drives CSS animations: discovers animated elements after style
8
+ * resolution, applies the current keyframe onto their resolved styles,
9
+ * and ticks a frame timer while any animation is live. The consumer
10
+ * wires `onFrame` to re-apply and repaint.
11
+ */
12
+ export declare class AnimationClock {
13
+ private now;
14
+ private active;
15
+ private transitions;
16
+ /** Last-seen target values per transitioned node, as CSS property → value. */
17
+ private transitionTargets;
18
+ private timer;
19
+ onFrame?: () => void;
20
+ constructor(now?: () => number);
21
+ get activeCount(): number;
22
+ /** Whether this node's animation needs re-layout each frame (vs repaint only). */
23
+ touchesLayout(node: TermNode): boolean;
24
+ /**
25
+ * Track transitioned elements and start a one-shot transition when a
26
+ * tracked property's target value changed since the last sync. When
27
+ * `resolvedIds` is given (incremental restyle), only subtrees rooted
28
+ * at those nodes are compared — other nodes' styles may carry
29
+ * mid-animation values that are not new targets.
30
+ */
31
+ syncTransitions(root: TermNode, styles: Map<number, ResolvedStyle>, resolvedIds?: Set<number>): void;
32
+ /** Reconcile active animations with the tree's current resolved styles. */
33
+ sync(root: TermNode, styles: Map<number, ResolvedStyle>, keyframes: Map<string, KeyframeStop[]>, resolution?: KeyframeResolution): void;
34
+ /**
35
+ * Apply the current animation state of each animation onto the styles
36
+ * map. Returns the nodes touched, each flagged with whether it needs
37
+ * re-layout; finished animations hold their final keyframe and are
38
+ * pruned.
39
+ */
40
+ apply(styles: Map<number, ResolvedStyle>): {
41
+ node: TermNode;
42
+ touchesLayout: boolean;
43
+ }[];
44
+ private applyEntries;
45
+ stop(): void;
46
+ private discover;
47
+ private discoverTransitions;
48
+ /** Snapshot the node's target values; start a transition on any change. */
49
+ private trackTransitionTargets;
50
+ private updateTimer;
51
+ }
@@ -0,0 +1,213 @@
1
+ import { AnimationRunner } from '../css/animation-runner.js';
2
+ import { resolveKeyframeStops } from '../css/animation.js';
3
+ import { parseEasing } from '../css/easing.js';
4
+ /** The runner's easing for a CSS timing-function value; invalid → linear. */
5
+ function easingFor(value) {
6
+ return parseEasing(value) ?? (t => t);
7
+ }
8
+ /** Parse a transition-property value into the tracked-property filter. */
9
+ function transitionedProperties(value) {
10
+ const names = new Set();
11
+ for (const raw of value.split(',')) {
12
+ const name = raw.trim();
13
+ if (name === 'all')
14
+ return { all: true, names };
15
+ names.add(name === 'background' ? 'background-color' : name);
16
+ }
17
+ return { all: false, names };
18
+ }
19
+ function cellValue(value) {
20
+ return typeof value === 'number' && value >= 0 ? `${value}cell` : null;
21
+ }
22
+ /** Properties transitions can animate, read from the resolved style as CSS values. */
23
+ const TRANSITIONABLE = [
24
+ { css: 'color', read: s => s.fg },
25
+ { css: 'background-color', read: s => s.bg },
26
+ { css: 'width', read: s => cellValue(s.width) },
27
+ { css: 'height', read: s => cellValue(s.height) },
28
+ { css: 'padding-top', read: s => cellValue(s.paddingTop) },
29
+ { css: 'padding-right', read: s => cellValue(s.paddingRight) },
30
+ { css: 'padding-bottom', read: s => cellValue(s.paddingBottom) },
31
+ { css: 'padding-left', read: s => cellValue(s.paddingLeft) },
32
+ { css: 'margin-top', read: s => cellValue(s.marginTop) },
33
+ { css: 'margin-right', read: s => cellValue(s.marginRight) },
34
+ { css: 'margin-bottom', read: s => cellValue(s.marginBottom) },
35
+ { css: 'margin-left', read: s => cellValue(s.marginLeft) },
36
+ { css: 'gap', read: s => cellValue(s.gap) },
37
+ { css: 'top', read: s => cellValue(s.top) },
38
+ { css: 'right', read: s => cellValue(s.right) },
39
+ { css: 'bottom', read: s => cellValue(s.bottom) },
40
+ { css: 'left', read: s => cellValue(s.left) },
41
+ ];
42
+ /** Repaint cadence while animations run — colours interpolate, so ~30fps. */
43
+ const FRAME_INTERVAL_MS = 33;
44
+ /**
45
+ * Drives CSS animations: discovers animated elements after style
46
+ * resolution, applies the current keyframe onto their resolved styles,
47
+ * and ticks a frame timer while any animation is live. The consumer
48
+ * wires `onFrame` to re-apply and repaint.
49
+ */
50
+ export class AnimationClock {
51
+ now;
52
+ active = new Map();
53
+ transitions = new Map();
54
+ /** Last-seen target values per transitioned node, as CSS property → value. */
55
+ transitionTargets = new Map();
56
+ timer = null;
57
+ onFrame;
58
+ constructor(now = Date.now) {
59
+ this.now = now;
60
+ }
61
+ get activeCount() {
62
+ return this.active.size + this.transitions.size;
63
+ }
64
+ /** Whether this node's animation needs re-layout each frame (vs repaint only). */
65
+ touchesLayout(node) {
66
+ return (this.active.get(node.id)?.runner.touchesLayout
67
+ || this.transitions.get(node.id)?.runner.touchesLayout) ?? false;
68
+ }
69
+ /**
70
+ * Track transitioned elements and start a one-shot transition when a
71
+ * tracked property's target value changed since the last sync. When
72
+ * `resolvedIds` is given (incremental restyle), only subtrees rooted
73
+ * at those nodes are compared — other nodes' styles may carry
74
+ * mid-animation values that are not new targets.
75
+ */
76
+ syncTransitions(root, styles, resolvedIds) {
77
+ const seen = new Set();
78
+ this.discoverTransitions(root, styles, resolvedIds === undefined, resolvedIds, seen);
79
+ for (const id of this.transitionTargets.keys()) {
80
+ if (!seen.has(id)) {
81
+ this.transitionTargets.delete(id);
82
+ this.transitions.delete(id);
83
+ }
84
+ }
85
+ this.updateTimer();
86
+ }
87
+ /** Reconcile active animations with the tree's current resolved styles. */
88
+ sync(root, styles, keyframes, resolution) {
89
+ const seen = new Set();
90
+ this.discover(root, styles, keyframes, seen, resolution);
91
+ for (const id of this.active.keys()) {
92
+ if (!seen.has(id))
93
+ this.active.delete(id);
94
+ }
95
+ this.updateTimer();
96
+ }
97
+ /**
98
+ * Apply the current animation state of each animation onto the styles
99
+ * map. Returns the nodes touched, each flagged with whether it needs
100
+ * re-layout; finished animations hold their final keyframe and are
101
+ * pruned.
102
+ */
103
+ apply(styles) {
104
+ const dirty = [];
105
+ this.applyEntries(this.active, styles, dirty);
106
+ this.applyEntries(this.transitions, styles, dirty);
107
+ this.updateTimer();
108
+ return dirty;
109
+ }
110
+ applyEntries(entries, styles, dirty) {
111
+ for (const [id, anim] of entries) {
112
+ const style = styles.get(id);
113
+ if (!style)
114
+ continue;
115
+ const elapsed = this.now() - anim.start;
116
+ anim.runner.apply(style, elapsed);
117
+ dirty.push({ node: anim.node, touchesLayout: anim.runner.touchesLayout });
118
+ if (anim.runner.isFinished(elapsed))
119
+ entries.delete(id);
120
+ }
121
+ }
122
+ stop() {
123
+ if (this.timer !== null) {
124
+ clearInterval(this.timer);
125
+ this.timer = null;
126
+ }
127
+ }
128
+ discover(node, styles, keyframes, seen, resolution) {
129
+ if (node.nodeType === 'element') {
130
+ const style = styles.get(node.id);
131
+ const name = style?.animationName;
132
+ const stops = name ? keyframes.get(name) : undefined;
133
+ if (style && name && stops && style.animationDuration > 0) {
134
+ const existing = this.active.get(node.id);
135
+ if (!existing || existing.name !== name || existing.duration !== style.animationDuration) {
136
+ const resolved = resolution
137
+ ? resolveKeyframeStops(stops, resolution, node.id)
138
+ : stops;
139
+ this.active.set(node.id, {
140
+ node,
141
+ runner: new AnimationRunner(resolved, style.animationDuration, style.animationIterationCount, easingFor(style.animationTimingFunction)),
142
+ name,
143
+ duration: style.animationDuration,
144
+ start: this.now(),
145
+ });
146
+ }
147
+ seen.add(node.id);
148
+ }
149
+ }
150
+ for (const child of node.children)
151
+ this.discover(child, styles, keyframes, seen, resolution);
152
+ }
153
+ discoverTransitions(node, styles, parentResolved, resolvedIds, seen) {
154
+ const subtreeResolved = parentResolved || (resolvedIds?.has(node.id) ?? false);
155
+ if (node.nodeType === 'element') {
156
+ const style = styles.get(node.id);
157
+ if (style?.transitionProperty && style.transitionDuration > 0) {
158
+ seen.add(node.id);
159
+ if (subtreeResolved)
160
+ this.trackTransitionTargets(node, style);
161
+ }
162
+ }
163
+ for (const child of node.children) {
164
+ this.discoverTransitions(child, styles, subtreeResolved, resolvedIds, seen);
165
+ }
166
+ }
167
+ /** Snapshot the node's target values; start a transition on any change. */
168
+ trackTransitionTargets(node, style) {
169
+ const included = transitionedProperties(style.transitionProperty);
170
+ const targets = {};
171
+ for (const prop of TRANSITIONABLE) {
172
+ if (!included.all && !included.names.has(prop.css))
173
+ continue;
174
+ const value = prop.read(style);
175
+ if (value !== null)
176
+ targets[prop.css] = value;
177
+ }
178
+ const previous = this.transitionTargets.get(node.id);
179
+ this.transitionTargets.set(node.id, targets);
180
+ if (!previous)
181
+ return; // first sight — the initial style never transitions
182
+ const fromDecls = [];
183
+ const toDecls = [];
184
+ for (const [property, target] of Object.entries(targets)) {
185
+ const before = previous[property];
186
+ if (before !== undefined && before !== target) {
187
+ fromDecls.push({ property, value: before });
188
+ toDecls.push({ property, value: target });
189
+ }
190
+ }
191
+ if (fromDecls.length === 0)
192
+ return;
193
+ const stops = [
194
+ { offset: 0, declarations: fromDecls },
195
+ { offset: 1, declarations: toDecls },
196
+ ];
197
+ this.transitions.set(node.id, {
198
+ node,
199
+ runner: new AnimationRunner(stops, style.transitionDuration, 1, easingFor(style.transitionTimingFunction)),
200
+ name: '',
201
+ duration: style.transitionDuration,
202
+ start: this.now(),
203
+ });
204
+ }
205
+ updateTimer() {
206
+ if (this.activeCount > 0 && this.timer === null) {
207
+ this.timer = setInterval(() => this.onFrame?.(), FRAME_INTERVAL_MS);
208
+ }
209
+ else if (this.activeCount === 0) {
210
+ this.stop();
211
+ }
212
+ }
213
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Minimal ANSI/SGR parser for the <svt-ansi> passthrough element:
3
+ * pre-styled tool output (git diff, ls --color, build logs) renders as
4
+ * styled cells. SGR sequences apply; every other escape sequence is
5
+ * dropped. Content is `pre` — lines split on newline, no wrapping.
6
+ */
7
+ export interface AnsiCell {
8
+ char: string;
9
+ fg: string;
10
+ bg: string;
11
+ bold: boolean;
12
+ dim: boolean;
13
+ italic: boolean;
14
+ underline: boolean;
15
+ strikethrough: boolean;
16
+ inverse: boolean;
17
+ }
18
+ /** The xterm 256-palette entry as #rrggbb (16–255; 0–15 use names/brights). */
19
+ export declare function palette256(index: number): string;
20
+ /** Parse ANSI-styled text into lines of styled cells. */
21
+ export declare function parseAnsiText(text: string): AnsiCell[][];
22
+ /** Width/height of parsed content (pre semantics — longest line, no wrap). */
23
+ export declare function measureAnsiText(text: string): {
24
+ width: number;
25
+ height: number;
26
+ };