@tuidom/elements 0.4.0 → 0.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tuidom/elements",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "type": "module",
5
5
  "description": "Experimental terminal UI DOM — widgets: lists, trees, menus, inputs, quick pick. API is unstable.",
6
6
  "license": "MIT",
@@ -28,6 +28,6 @@
28
28
  "./*": "./dist/*.js"
29
29
  },
30
30
  "dependencies": {
31
- "@tuidom/core": "0.4.0"
31
+ "@tuidom/core": "0.5.1"
32
32
  }
33
33
  }
@@ -1,64 +0,0 @@
1
- import { BoxConstraints, Size } from "@tuidom/core/common/geometryPromitives";
2
- import { RenderContext, TUIElement } from "@tuidom/core/dom/tuiElement";
3
- /** A view hosted in the bottom Panel (e.g. Problems, Output). */
4
- export interface PanelView {
5
- readonly id: string;
6
- readonly title: string;
7
- /** The view's content element; null renders {@link placeholder} instead. */
8
- content: TUIElement | null;
9
- /**
10
- * View-specific controls pinned to the right of the tab row (VS Code's
11
- * `MenuId.ViewTitle` area) — e.g. the Output channel selector.
12
- */
13
- actions?: TUIElement | null;
14
- /** Empty-state message shown when `content` is null (à la VS Code view welcome). */
15
- readonly placeholder?: string;
16
- }
17
- /**
18
- * The bottom **Panel** part (VS Code `ViewContainerLocation.Panel`): a top border
19
- * strip + a header row of view tabs (PROBLEMS, OUTPUT, …), a left border that
20
- * separates it from the sidebar, and the active view's content below. Views are
21
- * registered via {@link addView}; the active one is shown. A view with no content
22
- * element renders its {@link PanelView.placeholder} empty-state message.
23
- *
24
- * Tab labels are drawn dim (`panelTitle.inactiveForeground`); the active tab is
25
- * marked with an underline. Colours are pushed by the controller (`panel.*` /
26
- * `panelTitle.*`), mirroring how `EditorElement` receives its theme colours.
27
- */
28
- export declare class PanelContainerElement extends TUIElement {
29
- /** Fired when a tab is clicked (after the active view has switched). */
30
- onActivateView?: (id: string) => void;
31
- private views;
32
- private activeId;
33
- constructor();
34
- addView(view: PanelView): void;
35
- /** Replaces a registered view's title-row controls (null removes them). */
36
- setViewActions(id: string, actions: TUIElement | null): void;
37
- /** Replaces a registered view's content element (e.g. swapping a placeholder for the real view). */
38
- setViewContent(id: string, content: TUIElement | null): void;
39
- setActiveView(id: string): void;
40
- /**
41
- * Все вкладки живут в дереве постоянно; неактивные — hidden (root и стили
42
- * доходят до них всегда). Раньше getChildren() отдавал только активную, и
43
- * контент/actions, прицепленные до укоренения панели, оставались с
44
- * протухшим root — модель бага #204 (селектор каналов Output молча не
45
- * открывал выпадашку после restore сессии).
46
- */
47
- private syncChildren;
48
- getActiveViewId(): string | null;
49
- getViewIds(): string[];
50
- /**
51
- * Observable panel state for the inspector: which tabs exist, which is
52
- * active, and each tab's absolute hit geometry. Replaces the e2e helper that
53
- * re-derived tab coordinates from TAB_INDENT/TAB_PAD by hand — a test clicks
54
- * `tabs[i].centerX` on `tabRow` instead.
55
- */
56
- inspectState(): Record<string, unknown>;
57
- /** Правая граница таб-строки — за неё контролы вкладки заезжать не должны. */
58
- private tabsEnd;
59
- private activeView;
60
- /** Header tab layout: ` Title ` segments after the indent, with hit ranges. */
61
- private tabSegments;
62
- protected performLayout(constraints: BoxConstraints): Size;
63
- render(context: RenderContext): void;
64
- }
@@ -1,219 +0,0 @@
1
- import { BoxConstraints, Offset, Point, Rect, Size } from "@tuidom/core/common/geometryPromitives";
2
- import { StyleFlags } from "@tuidom/core/common/styleFlags";
3
- import { RenderContext, TUIElement } from "@tuidom/core/dom/tuiElement";
4
- /** One space of padding on each side of a tab title. */
5
- const TAB_PAD = 1;
6
- /** Indent of the tab strip from the left edge. */
7
- const TAB_INDENT = 1;
8
- /** Row the tab header sits on (below the top border strip). */
9
- const TAB_ROW = 1;
10
- /** First content row (below the top border strip + tab header). */
11
- const CONTENT_TOP = 2;
12
- /** Left indent of the content area / placeholder (aligns under the tab label). */
13
- const CONTENT_LEFT = 2;
14
- /**
15
- * The bottom **Panel** part (VS Code `ViewContainerLocation.Panel`): a top border
16
- * strip + a header row of view tabs (PROBLEMS, OUTPUT, …), a left border that
17
- * separates it from the sidebar, and the active view's content below. Views are
18
- * registered via {@link addView}; the active one is shown. A view with no content
19
- * element renders its {@link PanelView.placeholder} empty-state message.
20
- *
21
- * Tab labels are drawn dim (`panelTitle.inactiveForeground`); the active tab is
22
- * marked with an underline. Colours are pushed by the controller (`panel.*` /
23
- * `panelTitle.*`), mirroring how `EditorElement` receives its theme colours.
24
- */
25
- export class PanelContainerElement extends TUIElement {
26
- /** Fired when a tab is clicked (after the active view has switched). */
27
- onActivateView;
28
- views = [];
29
- activeId = null;
30
- constructor() {
31
- super();
32
- this.style = { bg: "panel.background" };
33
- this.addEventListener("mousedown", (event) => {
34
- if (event.button !== "left")
35
- return;
36
- // Событие, всплывшее из дочернего контрола (селектор в шапке), не наше:
37
- // без этой проверки клик по нему ещё и переключал бы вкладку.
38
- if (event.target !== this)
39
- return;
40
- const localY = event.screenY - this.globalPosition.y;
41
- if (localY !== TAB_ROW)
42
- return; // only the tab header row switches tabs
43
- const localX = event.screenX - this.globalPosition.x;
44
- const segment = this.tabSegments().find((s) => localX >= s.start && localX < s.end);
45
- if (segment === undefined)
46
- return;
47
- this.setActiveView(segment.id);
48
- this.onActivateView?.(segment.id);
49
- });
50
- }
51
- addView(view) {
52
- this.views.push(view);
53
- this.activeId ??= view.id;
54
- this.syncChildren();
55
- this.markDirty();
56
- }
57
- /** Replaces a registered view's title-row controls (null removes them). */
58
- setViewActions(id, actions) {
59
- const view = this.views.find((v) => v.id === id);
60
- if (view === undefined)
61
- return;
62
- view.actions = actions;
63
- this.syncChildren();
64
- this.markDirty();
65
- }
66
- /** Replaces a registered view's content element (e.g. swapping a placeholder for the real view). */
67
- setViewContent(id, content) {
68
- const view = this.views.find((v) => v.id === id);
69
- if (view === undefined)
70
- return;
71
- view.content = content;
72
- this.syncChildren();
73
- this.markDirty();
74
- }
75
- setActiveView(id) {
76
- if (this.views.every((v) => v.id !== id) || this.activeId === id)
77
- return;
78
- this.activeId = id;
79
- this.syncChildren();
80
- this.markDirty();
81
- }
82
- /**
83
- * Все вкладки живут в дереве постоянно; неактивные — hidden (root и стили
84
- * доходят до них всегда). Раньше getChildren() отдавал только активную, и
85
- * контент/actions, прицепленные до укоренения панели, оставались с
86
- * протухшим root — модель бага #204 (селектор каналов Output молча не
87
- * открывал выпадашку после restore сессии).
88
- */
89
- syncChildren() {
90
- const children = [];
91
- for (const view of this.views) {
92
- const isActive = view.id === this.activeId;
93
- if (view.actions != null) {
94
- view.actions.hidden = !isActive;
95
- children.push(view.actions);
96
- }
97
- if (view.content !== null) {
98
- view.content.hidden = !isActive;
99
- children.push(view.content);
100
- }
101
- }
102
- this.setChildren(children);
103
- }
104
- getActiveViewId() {
105
- return this.activeId;
106
- }
107
- getViewIds() {
108
- return this.views.map((v) => v.id);
109
- }
110
- /**
111
- * Observable panel state for the inspector: which tabs exist, which is
112
- * active, and each tab's absolute hit geometry. Replaces the e2e helper that
113
- * re-derived tab coordinates from TAB_INDENT/TAB_PAD by hand — a test clicks
114
- * `tabs[i].centerX` on `tabRow` instead.
115
- */
116
- inspectState() {
117
- const originX = this.globalPosition.x;
118
- let x = TAB_INDENT;
119
- const tabs = this.views.map((view) => {
120
- const width = view.title.length + TAB_PAD * 2;
121
- const start = x;
122
- x += width;
123
- return {
124
- id: view.id,
125
- title: view.title,
126
- active: view.id === this.activeId,
127
- x: originX + start,
128
- width,
129
- centerX: originX + start + Math.floor(width / 2),
130
- };
131
- });
132
- return { activeId: this.activeId, tabRow: this.globalPosition.y + TAB_ROW, tabs };
133
- }
134
- /** Правая граница таб-строки — за неё контролы вкладки заезжать не должны. */
135
- tabsEnd() {
136
- return this.views.reduce((x, view) => x + view.title.length + TAB_PAD * 2, TAB_INDENT);
137
- }
138
- activeView() {
139
- return this.views.find((v) => v.id === this.activeId);
140
- }
141
- /** Header tab layout: ` Title ` segments after the indent, with hit ranges. */
142
- tabSegments() {
143
- const segments = [];
144
- let x = TAB_INDENT;
145
- for (const view of this.views) {
146
- const width = view.title.length + TAB_PAD * 2;
147
- segments.push({ id: view.id, start: x, end: x + width });
148
- x += width;
149
- }
150
- return segments;
151
- }
152
- performLayout(constraints) {
153
- const containerSize = super.performLayout(constraints);
154
- // Контролы вкладки прижаты вправо на строке табов — как в шапке Panel у
155
- // VS Code. Ширину берём интринсиковую и не даём заехать на сами табы.
156
- const actions = this.activeView()?.actions;
157
- if (actions != null) {
158
- const actionsWidth = Math.min(actions.getMaxIntrinsicWidth(1), containerSize.width);
159
- const x = Math.max(this.tabsEnd(), containerSize.width - actionsWidth - TAB_INDENT);
160
- this.layoutChild(actions, x, TAB_ROW, BoxConstraints.tight(new Size(Math.max(0, containerSize.width - x), 1)));
161
- }
162
- const content = this.activeView()?.content;
163
- if (content != null) {
164
- const contentWidth = Math.max(0, containerSize.width - CONTENT_LEFT);
165
- const contentHeight = Math.max(0, containerSize.height - CONTENT_TOP);
166
- this.layoutChild(content, CONTENT_LEFT, CONTENT_TOP, BoxConstraints.tight(new Size(contentWidth, contentHeight)));
167
- }
168
- return containerSize;
169
- }
170
- render(context) {
171
- const { width, height } = this.layoutSize;
172
- // Fill the panel with its background first.
173
- for (let y = 0; y < height; y++) {
174
- for (let x = 0; x < width; x++) {
175
- context.setCell(x, y, { char: " ", bg: this.resolvedStyle.bg });
176
- }
177
- }
178
- // Top border strip (row 0).
179
- for (let x = 0; x < width; x++) {
180
- context.setCell(x, 0, { char: "─", fg: this.styleVar("panel.border"), bg: this.resolvedStyle.bg });
181
- }
182
- // Tab header (dim). The active tab is underlined — but only under the title
183
- // glyphs, leaving the surrounding padding un-underlined.
184
- const segments = this.tabSegments();
185
- for (let i = 0; i < this.views.length; i++) {
186
- const view = this.views[i];
187
- const segment = segments[i];
188
- const isActive = view.id === this.activeId;
189
- for (let x = segment.start; x < segment.end && x < width; x++) {
190
- const textIndex = x - segment.start - TAB_PAD;
191
- const isGlyph = textIndex >= 0 && textIndex < view.title.length;
192
- const char = isGlyph ? view.title[textIndex] : " ";
193
- const style = isActive && isGlyph ? StyleFlags.Underline : StyleFlags.None;
194
- context.setCell(x, TAB_ROW, {
195
- char,
196
- fg: this.styleVar("panelTitle.inactiveForeground"),
197
- bg: this.resolvedStyle.bg,
198
- style,
199
- });
200
- }
201
- }
202
- // View-specific controls in the title row (drawn after the tabs so they win
203
- // the shared row), then the active view's content below — renderChildren
204
- // рисует только видимых детей, скрытые вкладки пропускаются базой.
205
- this.renderChildren(context);
206
- // Placeholder empty-state message, если у активной вкладки нет контента.
207
- const active = this.activeView();
208
- if (active?.content == null && active?.placeholder !== undefined && height > CONTENT_TOP) {
209
- const message = active.placeholder;
210
- for (let i = 0; i < message.length && i + CONTENT_LEFT < width; i++) {
211
- context.setCell(i + CONTENT_LEFT, CONTENT_TOP, {
212
- char: message[i],
213
- fg: this.styleVar("panelTitle.inactiveForeground"),
214
- bg: this.resolvedStyle.bg,
215
- });
216
- }
217
- }
218
- }
219
- }