@teammates/consolonia 0.2.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 (104) hide show
  1. package/README.md +48 -0
  2. package/dist/__tests__/ansi.test.d.ts +1 -0
  3. package/dist/__tests__/ansi.test.js +520 -0
  4. package/dist/__tests__/chat-view.test.d.ts +4 -0
  5. package/dist/__tests__/chat-view.test.js +480 -0
  6. package/dist/__tests__/drawing.test.d.ts +4 -0
  7. package/dist/__tests__/drawing.test.js +426 -0
  8. package/dist/__tests__/input.test.d.ts +5 -0
  9. package/dist/__tests__/input.test.js +911 -0
  10. package/dist/__tests__/layout.test.d.ts +4 -0
  11. package/dist/__tests__/layout.test.js +689 -0
  12. package/dist/__tests__/pixel.test.d.ts +1 -0
  13. package/dist/__tests__/pixel.test.js +674 -0
  14. package/dist/__tests__/render.test.d.ts +1 -0
  15. package/dist/__tests__/render.test.js +400 -0
  16. package/dist/__tests__/styled.test.d.ts +4 -0
  17. package/dist/__tests__/styled.test.js +149 -0
  18. package/dist/__tests__/widgets.test.d.ts +5 -0
  19. package/dist/__tests__/widgets.test.js +924 -0
  20. package/dist/ansi/esc.d.ts +61 -0
  21. package/dist/ansi/esc.js +85 -0
  22. package/dist/ansi/output.d.ts +66 -0
  23. package/dist/ansi/output.js +192 -0
  24. package/dist/ansi/strip.d.ts +16 -0
  25. package/dist/ansi/strip.js +74 -0
  26. package/dist/app.d.ts +68 -0
  27. package/dist/app.js +297 -0
  28. package/dist/drawing/clip.d.ts +23 -0
  29. package/dist/drawing/clip.js +67 -0
  30. package/dist/drawing/context.d.ts +77 -0
  31. package/dist/drawing/context.js +275 -0
  32. package/dist/index.d.ts +48 -0
  33. package/dist/index.js +63 -0
  34. package/dist/input/escape-matcher.d.ts +27 -0
  35. package/dist/input/escape-matcher.js +253 -0
  36. package/dist/input/events.d.ts +49 -0
  37. package/dist/input/events.js +17 -0
  38. package/dist/input/index.d.ts +15 -0
  39. package/dist/input/index.js +14 -0
  40. package/dist/input/matcher.d.ts +23 -0
  41. package/dist/input/matcher.js +14 -0
  42. package/dist/input/mouse-matcher.d.ts +27 -0
  43. package/dist/input/mouse-matcher.js +142 -0
  44. package/dist/input/paste-matcher.d.ts +23 -0
  45. package/dist/input/paste-matcher.js +104 -0
  46. package/dist/input/processor.d.ts +51 -0
  47. package/dist/input/processor.js +145 -0
  48. package/dist/input/raw-mode.d.ts +13 -0
  49. package/dist/input/raw-mode.js +24 -0
  50. package/dist/input/text-matcher.d.ts +14 -0
  51. package/dist/input/text-matcher.js +32 -0
  52. package/dist/layout/box.d.ts +33 -0
  53. package/dist/layout/box.js +92 -0
  54. package/dist/layout/column.d.ts +21 -0
  55. package/dist/layout/column.js +90 -0
  56. package/dist/layout/control.d.ts +73 -0
  57. package/dist/layout/control.js +215 -0
  58. package/dist/layout/row.d.ts +21 -0
  59. package/dist/layout/row.js +95 -0
  60. package/dist/layout/stack.d.ts +18 -0
  61. package/dist/layout/stack.js +64 -0
  62. package/dist/layout/types.d.ts +27 -0
  63. package/dist/layout/types.js +4 -0
  64. package/dist/pixel/background.d.ts +16 -0
  65. package/dist/pixel/background.js +16 -0
  66. package/dist/pixel/box-pattern.d.ts +38 -0
  67. package/dist/pixel/box-pattern.js +57 -0
  68. package/dist/pixel/buffer.d.ts +25 -0
  69. package/dist/pixel/buffer.js +51 -0
  70. package/dist/pixel/color.d.ts +48 -0
  71. package/dist/pixel/color.js +92 -0
  72. package/dist/pixel/foreground.d.ts +31 -0
  73. package/dist/pixel/foreground.js +64 -0
  74. package/dist/pixel/pixel.d.ts +21 -0
  75. package/dist/pixel/pixel.js +38 -0
  76. package/dist/pixel/symbol.d.ts +38 -0
  77. package/dist/pixel/symbol.js +192 -0
  78. package/dist/render/regions.d.ts +54 -0
  79. package/dist/render/regions.js +102 -0
  80. package/dist/render/render-target.d.ts +42 -0
  81. package/dist/render/render-target.js +118 -0
  82. package/dist/styled.d.ts +113 -0
  83. package/dist/styled.js +176 -0
  84. package/dist/widgets/border.d.ts +34 -0
  85. package/dist/widgets/border.js +121 -0
  86. package/dist/widgets/chat-view.d.ts +239 -0
  87. package/dist/widgets/chat-view.js +993 -0
  88. package/dist/widgets/interview.d.ts +87 -0
  89. package/dist/widgets/interview.js +187 -0
  90. package/dist/widgets/markdown.d.ts +87 -0
  91. package/dist/widgets/markdown.js +611 -0
  92. package/dist/widgets/panel.d.ts +19 -0
  93. package/dist/widgets/panel.js +35 -0
  94. package/dist/widgets/scroll-view.d.ts +43 -0
  95. package/dist/widgets/scroll-view.js +182 -0
  96. package/dist/widgets/styled-text.d.ts +38 -0
  97. package/dist/widgets/styled-text.js +183 -0
  98. package/dist/widgets/syntax.d.ts +37 -0
  99. package/dist/widgets/syntax.js +670 -0
  100. package/dist/widgets/text-input.d.ts +121 -0
  101. package/dist/widgets/text-input.js +618 -0
  102. package/dist/widgets/text.d.ts +34 -0
  103. package/dist/widgets/text.js +168 -0
  104. package/package.json +45 -0
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Static text display widget.
3
+ *
4
+ * Supports word wrapping, text alignment (left/center/right), and
5
+ * multi-line content. Automatically invalidates on text or style changes.
6
+ */
7
+ import { Control } from "../layout/control.js";
8
+ export class Text extends Control {
9
+ _text;
10
+ _style;
11
+ _wrap;
12
+ _align;
13
+ /** Cached wrapped lines from the last measure/render pass. */
14
+ _lines = [];
15
+ constructor(options = {}) {
16
+ super();
17
+ this._text = options.text ?? "";
18
+ this._style = options.style ?? {};
19
+ this._wrap = options.wrap ?? false;
20
+ this._align = options.align ?? "left";
21
+ }
22
+ // ── Properties ────────────────────────────────────────────────
23
+ get text() {
24
+ return this._text;
25
+ }
26
+ set text(value) {
27
+ if (this._text !== value) {
28
+ this._text = value;
29
+ this.invalidate();
30
+ }
31
+ }
32
+ get style() {
33
+ return this._style;
34
+ }
35
+ set style(value) {
36
+ this._style = value;
37
+ this.invalidate();
38
+ }
39
+ get wrap() {
40
+ return this._wrap;
41
+ }
42
+ set wrap(value) {
43
+ if (this._wrap !== value) {
44
+ this._wrap = value;
45
+ this.invalidate();
46
+ }
47
+ }
48
+ get align() {
49
+ return this._align;
50
+ }
51
+ set align(value) {
52
+ if (this._align !== value) {
53
+ this._align = value;
54
+ this.invalidate();
55
+ }
56
+ }
57
+ // ── Layout ────────────────────────────────────────────────────
58
+ measure(constraint) {
59
+ if (this._text.length === 0) {
60
+ this._lines = [];
61
+ return { width: 0, height: 0 };
62
+ }
63
+ const rawLines = this._text.split("\n");
64
+ if (!this._wrap) {
65
+ this._lines = rawLines;
66
+ const longestLine = rawLines.reduce((max, line) => Math.max(max, line.length), 0);
67
+ return {
68
+ width: Math.min(longestLine, constraint.maxWidth),
69
+ height: Math.min(rawLines.length, constraint.maxHeight),
70
+ };
71
+ }
72
+ // Word-wrap mode: wrap to maxWidth
73
+ const maxW = constraint.maxWidth;
74
+ if (maxW <= 0) {
75
+ this._lines = [];
76
+ return { width: 0, height: 0 };
77
+ }
78
+ const wrapped = wrapLines(rawLines, maxW);
79
+ this._lines = wrapped;
80
+ const longestWrapped = wrapped.reduce((max, line) => Math.max(max, line.length), 0);
81
+ return {
82
+ width: Math.min(longestWrapped, constraint.maxWidth),
83
+ height: Math.min(wrapped.length, constraint.maxHeight),
84
+ };
85
+ }
86
+ render(ctx) {
87
+ const bounds = this.bounds;
88
+ if (!bounds || this._lines.length === 0)
89
+ return;
90
+ const availW = bounds.width;
91
+ for (let i = 0; i < this._lines.length && i < bounds.height; i++) {
92
+ const line = this._lines[i];
93
+ const x = alignOffset(line.length, availW, this._align);
94
+ ctx.drawText(bounds.x + x, bounds.y + i, line, this._style);
95
+ }
96
+ }
97
+ }
98
+ // ── Helper: word wrapping ──────────────────────────────────────────
99
+ /**
100
+ * Wrap an array of raw lines to fit within `maxWidth` columns.
101
+ *
102
+ * Rules:
103
+ * - Break on spaces when possible.
104
+ * - If a single word exceeds `maxWidth`, hard-break it at the width boundary.
105
+ * - Preserve explicit line breaks (already split by caller).
106
+ */
107
+ function wrapLines(rawLines, maxWidth) {
108
+ const result = [];
109
+ for (const rawLine of rawLines) {
110
+ if (rawLine.length === 0) {
111
+ result.push("");
112
+ continue;
113
+ }
114
+ const words = rawLine.split(" ");
115
+ let current = "";
116
+ for (let wi = 0; wi < words.length; wi++) {
117
+ const word = words[wi];
118
+ if (word.length === 0) {
119
+ // Consecutive spaces: add a space to current if it fits
120
+ if (current.length < maxWidth) {
121
+ current += " ";
122
+ }
123
+ continue;
124
+ }
125
+ // If the word itself exceeds maxWidth, hard-break it
126
+ if (word.length > maxWidth) {
127
+ // Flush current line first
128
+ if (current.length > 0) {
129
+ result.push(current);
130
+ current = "";
131
+ }
132
+ // Split the long word into chunks
133
+ for (let ci = 0; ci < word.length; ci += maxWidth) {
134
+ const chunk = word.slice(ci, ci + maxWidth);
135
+ if (ci + maxWidth < word.length) {
136
+ result.push(chunk);
137
+ }
138
+ else {
139
+ current = chunk;
140
+ }
141
+ }
142
+ continue;
143
+ }
144
+ if (current.length === 0) {
145
+ current = word;
146
+ }
147
+ else if (current.length + 1 + word.length <= maxWidth) {
148
+ current += ` ${word}`;
149
+ }
150
+ else {
151
+ result.push(current);
152
+ current = word;
153
+ }
154
+ }
155
+ result.push(current);
156
+ }
157
+ return result;
158
+ }
159
+ // ── Helper: alignment offset ───────────────────────────────────────
160
+ function alignOffset(textLen, availWidth, align) {
161
+ if (align === "center") {
162
+ return Math.max(0, Math.floor((availWidth - textLen) / 2));
163
+ }
164
+ if (align === "right") {
165
+ return Math.max(0, availWidth - textLen);
166
+ }
167
+ return 0;
168
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@teammates/consolonia",
3
+ "version": "0.2.0",
4
+ "description": "Terminal UI rendering engine inspired by Consolonia. Pixel-level compositing with ANSI output.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "dev": "tsc --watch",
20
+ "test": "vitest run",
21
+ "test:coverage": "vitest run --coverage",
22
+ "test:watch": "vitest",
23
+ "typecheck": "tsc --noEmit"
24
+ },
25
+ "keywords": [
26
+ "teammates",
27
+ "terminal",
28
+ "tui",
29
+ "ansi",
30
+ "consolonia"
31
+ ],
32
+ "license": "MIT",
33
+ "devDependencies": {
34
+ "@types/node": "^25.5.0",
35
+ "@vitest/coverage-v8": "^4.1.0",
36
+ "typescript": "^5.5.0",
37
+ "vitest": "^4.1.0"
38
+ },
39
+ "engines": {
40
+ "node": ">=20.0.0"
41
+ },
42
+ "dependencies": {
43
+ "marked": "^17.0.4"
44
+ }
45
+ }