@tuidom/core 0.3.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.
@@ -3,17 +3,66 @@
3
3
  * Must be negative so it's never confused with a packed RGB value (0x000000–0xFFFFFF).
4
4
  */
5
5
  export declare const DEFAULT_COLOR = -1;
6
+ /**
7
+ * Полностью прозрачный цвет (`#RRGGBB00`, CSS `transparent`): при наложении
8
+ * ничего не меняет, а как собственный bg элемента — не красит. Сентинел, а не
9
+ * упаковка с альфой 0: старший байт 0 занят непрозрачными 24-битными числами.
10
+ */
11
+ export declare const TRANSPARENT_COLOR = -2;
6
12
  /** Pack three 8-bit channels into a single 24-bit integer. */
7
13
  export declare function packRgb(r: number, g: number, b: number): number;
14
+ /**
15
+ * Упаковывает цвет с альфой (0..255). Непрозрачный (`a === 255`) даёт обычное
16
+ * 24-битное число — то же, что {@link packRgb}; `a === 0` — {@link TRANSPARENT_COLOR};
17
+ * промежуточная альфа кладётся в старший байт (`0xAARRGGBB`, беззнаковое
18
+ * значение > 0xFFFFFF). Так весь существующий код с 24-битными литералами
19
+ * остаётся валидным без миграции, а полупрозрачность — отличима одним сравнением.
20
+ */
21
+ export declare function packRgba(r: number, g: number, b: number, a: number): number;
8
22
  /** Extract the red channel (bits 16–23). */
9
23
  export declare function unpackR(color: number): number;
10
24
  /** Extract the green channel (bits 8–15). */
11
25
  export declare function unpackG(color: number): number;
12
26
  /** Extract the blue channel (bits 0–7). */
13
27
  export declare function unpackB(color: number): number;
28
+ /**
29
+ * Альфа цвета: 255 у 24-битных чисел и сентинелов (`DEFAULT_COLOR` — цвет
30
+ * терминала, он непрозрачен), 0 у {@link TRANSPARENT_COLOR}, иначе старший байт.
31
+ */
32
+ export declare function unpackA(color: number): number;
33
+ /** true для полупрозрачного цвета (альфа 1..254) — того, что требует композитинга с подложкой. */
34
+ export declare function isTranslucent(color: number): boolean;
35
+ /**
36
+ * true, если число — легальное значение цвета: 24-битный RGB, упаковка с
37
+ * альфой, {@link DEFAULT_COLOR} или {@link TRANSPARENT_COLOR}. Сентинелы
38
+ * каскада (`INHERITED_*`) и мусор — false.
39
+ */
40
+ export declare function isColorValue(color: number): boolean;
14
41
  /**
15
42
  * Накладывает полупрозрачный `fg` на непрозрачную подложку `bg` и возвращает
16
43
  * непрозрачный результат: терминал альфы не умеет, поэтому композитинг
17
44
  * выполняется заранее (`alpha` — доля 0..1).
18
45
  */
19
46
  export declare function blendRgb(fg: number, bg: number, alpha: number): number;
47
+ /**
48
+ * Композитинг «`color` поверх `under`» (source-over): результат всегда
49
+ * непрозрачен или `DEFAULT_COLOR`. Непрозрачный `color` (и `DEFAULT_COLOR`)
50
+ * возвращается как есть — путь нулевой стоимости, одно сравнение;
51
+ * {@link TRANSPARENT_COLOR} оставляет подложку. Полупрозрачный цвет смешивается
52
+ * с `under` по альфе; если подложка — `DEFAULT_COLOR` (цвет терминала, нам
53
+ * неизвестен), смешивать не с чем — альфа отбрасывается, цвет ложится непрозрачным.
54
+ */
55
+ export declare function compositeOver(color: number, under: number): number;
56
+ /**
57
+ * Разбирает CSS-подобный hex: `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA`
58
+ * (решётка необязательна, регистр любой) в упакованный цвет (см.
59
+ * {@link packRgba}). Некорректная строка — throw: цвет темы с опечаткой
60
+ * должен падать на разборе, а не красить в чёрный.
61
+ */
62
+ export declare function parseHexColor(hex: string): number;
63
+ /**
64
+ * Обратное к {@link parseHexColor}: `#rrggbb` для непрозрачного, `#rrggbbaa`
65
+ * с альфой, `#00000000` для {@link TRANSPARENT_COLOR}. `DEFAULT_COLOR` hex не
66
+ * имеет — throw.
67
+ */
68
+ export declare function formatHexColor(color: number): string;
@@ -3,10 +3,34 @@
3
3
  * Must be negative so it's never confused with a packed RGB value (0x000000–0xFFFFFF).
4
4
  */
5
5
  export const DEFAULT_COLOR = -1;
6
+ /**
7
+ * Полностью прозрачный цвет (`#RRGGBB00`, CSS `transparent`): при наложении
8
+ * ничего не меняет, а как собственный bg элемента — не красит. Сентинел, а не
9
+ * упаковка с альфой 0: старший байт 0 занят непрозрачными 24-битными числами.
10
+ */
11
+ export const TRANSPARENT_COLOR = -2;
12
+ /** Максимальное упакованное значение с альфой (`0xFE_FFFFFF`); всё выше — не цвет. */
13
+ const MAX_PACKED_COLOR = 0xfeffffff;
14
+ /** Множитель старшего байта — альфа хранится через умножение, а не `<<`, чтобы не уйти в отрицательные. */
15
+ const ALPHA_UNIT = 0x1000000;
6
16
  /** Pack three 8-bit channels into a single 24-bit integer. */
7
17
  export function packRgb(r, g, b) {
8
18
  return (r << 16) | (g << 8) | b;
9
19
  }
20
+ /**
21
+ * Упаковывает цвет с альфой (0..255). Непрозрачный (`a === 255`) даёт обычное
22
+ * 24-битное число — то же, что {@link packRgb}; `a === 0` — {@link TRANSPARENT_COLOR};
23
+ * промежуточная альфа кладётся в старший байт (`0xAARRGGBB`, беззнаковое
24
+ * значение > 0xFFFFFF). Так весь существующий код с 24-битными литералами
25
+ * остаётся валидным без миграции, а полупрозрачность — отличима одним сравнением.
26
+ */
27
+ export function packRgba(r, g, b, a) {
28
+ if (a >= 0xff)
29
+ return packRgb(r, g, b);
30
+ if (a <= 0)
31
+ return TRANSPARENT_COLOR;
32
+ return a * ALPHA_UNIT + packRgb(r, g, b);
33
+ }
10
34
  /** Extract the red channel (bits 16–23). */
11
35
  export function unpackR(color) {
12
36
  return (color >> 16) & 0xff;
@@ -19,6 +43,31 @@ export function unpackG(color) {
19
43
  export function unpackB(color) {
20
44
  return color & 0xff;
21
45
  }
46
+ /**
47
+ * Альфа цвета: 255 у 24-битных чисел и сентинелов (`DEFAULT_COLOR` — цвет
48
+ * терминала, он непрозрачен), 0 у {@link TRANSPARENT_COLOR}, иначе старший байт.
49
+ */
50
+ export function unpackA(color) {
51
+ if (color === TRANSPARENT_COLOR)
52
+ return 0;
53
+ if (color <= 0xffffff)
54
+ return 0xff;
55
+ return color >>> 24;
56
+ }
57
+ /** true для полупрозрачного цвета (альфа 1..254) — того, что требует композитинга с подложкой. */
58
+ export function isTranslucent(color) {
59
+ return color > 0xffffff;
60
+ }
61
+ /**
62
+ * true, если число — легальное значение цвета: 24-битный RGB, упаковка с
63
+ * альфой, {@link DEFAULT_COLOR} или {@link TRANSPARENT_COLOR}. Сентинелы
64
+ * каскада (`INHERITED_*`) и мусор — false.
65
+ */
66
+ export function isColorValue(color) {
67
+ return (color === DEFAULT_COLOR ||
68
+ color === TRANSPARENT_COLOR ||
69
+ (Number.isInteger(color) && color >= 0 && color <= MAX_PACKED_COLOR));
70
+ }
22
71
  /**
23
72
  * Накладывает полупрозрачный `fg` на непрозрачную подложку `bg` и возвращает
24
73
  * непрозрачный результат: терминал альфы не умеет, поэтому композитинг
@@ -28,3 +77,55 @@ export function blendRgb(fg, bg, alpha) {
28
77
  const mix = (f, b) => Math.round(f * alpha + b * (1 - alpha));
29
78
  return packRgb(mix(unpackR(fg), unpackR(bg)), mix(unpackG(fg), unpackG(bg)), mix(unpackB(fg), unpackB(bg)));
30
79
  }
80
+ /**
81
+ * Композитинг «`color` поверх `under`» (source-over): результат всегда
82
+ * непрозрачен или `DEFAULT_COLOR`. Непрозрачный `color` (и `DEFAULT_COLOR`)
83
+ * возвращается как есть — путь нулевой стоимости, одно сравнение;
84
+ * {@link TRANSPARENT_COLOR} оставляет подложку. Полупрозрачный цвет смешивается
85
+ * с `under` по альфе; если подложка — `DEFAULT_COLOR` (цвет терминала, нам
86
+ * неизвестен), смешивать не с чем — альфа отбрасывается, цвет ложится непрозрачным.
87
+ */
88
+ export function compositeOver(color, under) {
89
+ if (color <= 0xffffff)
90
+ return color === TRANSPARENT_COLOR ? under : color;
91
+ const rgb = color & 0xffffff;
92
+ if (under < 0)
93
+ return rgb;
94
+ return blendRgb(rgb, under, (color >>> 24) / 0xff);
95
+ }
96
+ /**
97
+ * Разбирает CSS-подобный hex: `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA`
98
+ * (решётка необязательна, регистр любой) в упакованный цвет (см.
99
+ * {@link packRgba}). Некорректная строка — throw: цвет темы с опечаткой
100
+ * должен падать на разборе, а не красить в чёрный.
101
+ */
102
+ export function parseHexColor(hex) {
103
+ const digits = hex.startsWith("#") ? hex.slice(1) : hex;
104
+ const short = digits.length === 3 || digits.length === 4;
105
+ if (!(short || digits.length === 6 || digits.length === 8) || !/^[0-9a-fA-F]+$/.test(digits)) {
106
+ throw new Error(`parseHexColor: некорректный hex-цвет "${hex}"`);
107
+ }
108
+ const channel = (i) => {
109
+ if (short) {
110
+ const d = parseInt(digits[i], 16);
111
+ return d * 16 + d;
112
+ }
113
+ return parseInt(digits.slice(i * 2, i * 2 + 2), 16);
114
+ };
115
+ const hasAlpha = digits.length === 4 || digits.length === 8;
116
+ return packRgba(channel(0), channel(1), channel(2), hasAlpha ? channel(3) : 0xff);
117
+ }
118
+ /**
119
+ * Обратное к {@link parseHexColor}: `#rrggbb` для непрозрачного, `#rrggbbaa`
120
+ * с альфой, `#00000000` для {@link TRANSPARENT_COLOR}. `DEFAULT_COLOR` hex не
121
+ * имеет — throw.
122
+ */
123
+ export function formatHexColor(color) {
124
+ if (color === DEFAULT_COLOR)
125
+ throw new Error("formatHexColor: DEFAULT_COLOR не имеет hex-представления");
126
+ if (color === TRANSPARENT_COLOR)
127
+ return "#00000000";
128
+ const hex2 = (n) => n.toString(16).padStart(2, "0");
129
+ const rgb = `#${hex2(unpackR(color))}${hex2(unpackG(color))}${hex2(unpackB(color))}`;
130
+ return isTranslucent(color) ? `${rgb}${hex2(unpackA(color))}` : rgb;
131
+ }
@@ -72,4 +72,10 @@ export declare function mergeStyleVariants(style: TUIStyle, isActive: (selector:
72
72
  fg: StyleColor | undefined;
73
73
  bg: StyleColor | undefined;
74
74
  };
75
+ /**
76
+ * Резолв базовых fg/bg без when-вариантов и var-scope (утилита для тестов и
77
+ * простых потребителей; ядро идёт через `TUIElement.performStyleResolution`,
78
+ * та же модель): цвет с альфой композитится с унаследованным bg, fg — с
79
+ * итоговым bg, наружу уходят непрозрачные значения.
80
+ */
75
81
  export declare function resolveStyle(style: TUIStyle, inherited: ResolvedTUIStyle): ResolvedTUIStyle;
@@ -1,4 +1,4 @@
1
- import { DEFAULT_COLOR } from "../../common/colorUtils.js";
1
+ import { compositeOver, DEFAULT_COLOR } from "../../common/colorUtils.js";
2
2
  import { ROOT_VAR_SCOPE } from "./styleTokens.js";
3
3
  // ─── Inherited Color Sentinels ───
4
4
  // Sentinel values that resolve to the inherited fg/bg from the parent.
@@ -114,8 +114,16 @@ export function mergeStyleVariants(style, isActive) {
114
114
  }
115
115
  return { fg, bg };
116
116
  }
117
+ /**
118
+ * Резолв базовых fg/bg без when-вариантов и var-scope (утилита для тестов и
119
+ * простых потребителей; ядро идёт через `TUIElement.performStyleResolution`,
120
+ * та же модель): цвет с альфой композитится с унаследованным bg, fg — с
121
+ * итоговым bg, наружу уходят непрозрачные значения.
122
+ */
117
123
  export function resolveStyle(style, inherited) {
118
- const fg = style.fg !== undefined ? resolveStyleColor(style.fg, inherited.fg, inherited.bg) : inherited.fg;
119
- const bg = style.bg !== undefined ? resolveStyleColor(style.bg, inherited.fg, inherited.bg) : inherited.bg;
124
+ const ownBg = style.bg !== undefined ? resolveStyleColor(style.bg, inherited.fg, inherited.bg) : inherited.bg;
125
+ const bg = compositeOver(ownBg, inherited.bg);
126
+ const ownFg = style.fg !== undefined ? resolveStyleColor(style.fg, inherited.fg, inherited.bg) : inherited.fg;
127
+ const fg = compositeOver(ownFg, bg);
120
128
  return { fg, bg };
121
129
  }
@@ -143,6 +143,8 @@ export declare class TUIElement {
143
143
  private subtreeStyleDirty;
144
144
  private appliedFgValue;
145
145
  private appliedBgValue;
146
+ /** Красит ли элемент собственный фон: bg задан своим стилем и не TRANSPARENT_COLOR. */
147
+ private paintsOwnBackgroundValue;
146
148
  private styleStatesSet;
147
149
  private styleVarsValue;
148
150
  private varScopeRef;
@@ -283,8 +285,9 @@ export declare class TUIElement {
283
285
  * предков и дефолтов tuidom (STYLE_TOKEN_DEFAULTS). Обычное место — корень:
284
286
  * хост транслирует сюда палитру темы одним вызовом (hot-swap = повторный
285
287
  * вызов). Таблица заменяется целиком, null — снимает. Значения — только
286
- * конкретные числа (packed RGB | DEFAULT_COLOR); сентинелы INHERITED_*
287
- * нелегальны.
288
+ * конкретные числа (packed RGB, в том числе с альфой — `packRgba`/
289
+ * `parseHexColor`, | DEFAULT_COLOR | TRANSPARENT_COLOR); сентинелы
290
+ * INHERITED_* нелегальны.
288
291
  */
289
292
  setStyleVars(vars: Readonly<Record<string, number>> | null): void;
290
293
  /**
@@ -503,6 +506,10 @@ export declare class TUIElement {
503
506
  * флагов стиля: собственный фон — это новая поверхность, и атрибуты того,
504
507
  * что нарисовано под ней раньше в этом кадре (подчёркивание диагностики под
505
508
  * попапом, жирный токен под диалогом), на ней проступать не должны.
509
+ *
510
+ * Полупрозрачный собственный bg уже скомпозичен каскадом с унаследованным
511
+ * (см. {@link performStyleResolution}); `TRANSPARENT_COLOR` (`#RRGGBB00`)
512
+ * считается «фона нет» — как CSS `transparent`, элемент ничего не красит.
506
513
  */
507
514
  protected paintOwnBackground(context: RenderContext): void;
508
515
  /** true, если фон задан собственным стилем (см. {@link paintOwnBackground}). */
@@ -1,4 +1,4 @@
1
- import { DEFAULT_COLOR } from "../common/colorUtils.js";
1
+ import { compositeOver, isColorValue, TRANSPARENT_COLOR } from "../common/colorUtils.js";
2
2
  import { DisplayLine } from "../common/displayLine.js";
3
3
  import { BoxConstraints, Offset, Point, Rect, Size } from "../common/geometryPromitives.js";
4
4
  import { StyleFlags } from "../common/styleFlags.js";
@@ -217,6 +217,8 @@ export class TUIElement {
217
217
  // вопрос «задан ли цвет собственным стилем» (заливка фона, инспектор).
218
218
  appliedFgValue;
219
219
  appliedBgValue;
220
+ /** Красит ли элемент собственный фон: bg задан своим стилем и не TRANSPARENT_COLOR. */
221
+ paintsOwnBackgroundValue = false;
220
222
  // Активные состояния (hover/focus ведёт ядро, прочие — виджеты). Lazy:
221
223
  // у подавляющего большинства элементов состояний нет.
222
224
  styleStatesSet = null;
@@ -571,13 +573,19 @@ export class TUIElement {
571
573
  const vars = this.styleVarsValue !== null ? extendVarScope(context.vars, this.styleVarsValue) : context.vars;
572
574
  this.varScopeRef = vars;
573
575
  const describe = () => this.describeForStyleError();
574
- const fg = applied.fg !== undefined
575
- ? resolveStyleColor(applied.fg, context.fg, context.bg, vars, describe)
576
- : context.fg;
577
- const bg = applied.bg !== undefined
576
+ // Цвет с альфой композитится уже здесь, с унаследованным bg: в
577
+ // resolvedStyle и детям уходят непрозрачные значения, иначе заливка
578
+ // фона и текст с тем же bg наложились бы на ячейку дважды.
579
+ const ownBg = applied.bg !== undefined
578
580
  ? resolveStyleColor(applied.bg, context.fg, context.bg, vars, describe)
579
581
  : context.bg;
582
+ const bg = compositeOver(ownBg, context.bg);
583
+ const ownFg = applied.fg !== undefined
584
+ ? resolveStyleColor(applied.fg, context.fg, context.bg, vars, describe)
585
+ : context.fg;
586
+ const fg = compositeOver(ownFg, bg);
580
587
  this.resolvedStyleValue = { fg, bg };
588
+ this.paintsOwnBackgroundValue = applied.bg !== undefined && ownBg !== TRANSPARENT_COLOR;
581
589
  this.childStyleContext = this.buildChildStyleContext(context);
582
590
  }
583
591
  this.isStyleDirty = false;
@@ -636,15 +644,16 @@ export class TUIElement {
636
644
  * предков и дефолтов tuidom (STYLE_TOKEN_DEFAULTS). Обычное место — корень:
637
645
  * хост транслирует сюда палитру темы одним вызовом (hot-swap = повторный
638
646
  * вызов). Таблица заменяется целиком, null — снимает. Значения — только
639
- * конкретные числа (packed RGB | DEFAULT_COLOR); сентинелы INHERITED_*
640
- * нелегальны.
647
+ * конкретные числа (packed RGB, в том числе с альфой — `packRgba`/
648
+ * `parseHexColor`, | DEFAULT_COLOR | TRANSPARENT_COLOR); сентинелы
649
+ * INHERITED_* нелегальны.
641
650
  */
642
651
  setStyleVars(vars) {
643
652
  if (vars === this.styleVarsValue)
644
653
  return;
645
654
  if (vars !== null) {
646
655
  for (const key of Object.keys(vars)) {
647
- if (vars[key] < DEFAULT_COLOR) {
656
+ if (!isColorValue(vars[key])) {
648
657
  throw new Error(`${this.describeForStyleError()}.setStyleVars: токен "${key}" содержит сентинел/некорректное значение ${vars[key]} — таблицы принимают только конкретные цвета`);
649
658
  }
650
659
  }
@@ -1101,9 +1110,13 @@ export class TUIElement {
1101
1110
  * флагов стиля: собственный фон — это новая поверхность, и атрибуты того,
1102
1111
  * что нарисовано под ней раньше в этом кадре (подчёркивание диагностики под
1103
1112
  * попапом, жирный токен под диалогом), на ней проступать не должны.
1113
+ *
1114
+ * Полупрозрачный собственный bg уже скомпозичен каскадом с унаследованным
1115
+ * (см. {@link performStyleResolution}); `TRANSPARENT_COLOR` (`#RRGGBB00`)
1116
+ * считается «фона нет» — как CSS `transparent`, элемент ничего не красит.
1104
1117
  */
1105
1118
  paintOwnBackground(context) {
1106
- if (this.appliedBgValue === undefined)
1119
+ if (!this.paintsOwnBackgroundValue)
1107
1120
  return;
1108
1121
  const { fg, bg } = this.resolvedStyleValue;
1109
1122
  const { width, height } = this.layoutSize;
@@ -1115,7 +1128,7 @@ export class TUIElement {
1115
1128
  }
1116
1129
  /** true, если фон задан собственным стилем (см. {@link paintOwnBackground}). */
1117
1130
  get hasOwnBackground() {
1118
- return this.appliedBgValue !== undefined;
1131
+ return this.paintsOwnBackgroundValue;
1119
1132
  }
1120
1133
  /**
1121
1134
  * Сырые цвета после when-merge, до резолва токенов/сентинелов: что элемент
@@ -15,6 +15,12 @@ export interface CellPatch {
15
15
  }
16
16
  /**
17
17
  * 2D grid of terminal cells backed by a flat array for cache-friendly access.
18
+ *
19
+ * Ячейки хранят только непрозрачные цвета (или DEFAULT_COLOR): цвет с альфой,
20
+ * пришедший в {@link setCell}/{@link updateCell}, композитится с текущим
21
+ * содержимым ячейки на месте (см. `compositeOver`). Так порядок отрисовки —
22
+ * родитель, дети, оверлеи, патчи подсветок — становится порядком наложения
23
+ * слоёв, как в браузере, а бэкенды/снапшоты/SVG видят уже готовые цвета.
18
24
  */
19
25
  export declare class Grid {
20
26
  readonly size: Size;
@@ -1,9 +1,15 @@
1
- import { DEFAULT_COLOR } from "../common/colorUtils.js";
1
+ import { compositeOver, DEFAULT_COLOR } from "../common/colorUtils.js";
2
2
  import { Point, Rect, Size } from "../common/geometryPromitives.js";
3
3
  import { StyleFlags } from "../common/styleFlags.js";
4
4
  import { Cell } from "./cell.js";
5
5
  /**
6
6
  * 2D grid of terminal cells backed by a flat array for cache-friendly access.
7
+ *
8
+ * Ячейки хранят только непрозрачные цвета (или DEFAULT_COLOR): цвет с альфой,
9
+ * пришедший в {@link setCell}/{@link updateCell}, композитится с текущим
10
+ * содержимым ячейки на месте (см. `compositeOver`). Так порядок отрисовки —
11
+ * родитель, дети, оверлеи, патчи подсветок — становится порядком наложения
12
+ * слоёв, как в браузере, а бэкенды/снапшоты/SVG видят уже готовые цвета.
7
13
  */
8
14
  export class Grid {
9
15
  size;
@@ -55,8 +61,8 @@ export class Grid {
55
61
  /* v8 ignore stop */
56
62
  }
57
63
  cell.char = char;
58
- cell.fg = fg;
59
- cell.bg = bg;
64
+ cell.bg = compositeOver(bg, cell.bg);
65
+ cell.fg = compositeOver(fg, cell.bg);
60
66
  cell.style = style;
61
67
  cell.width = width;
62
68
  // For wide chars, set up the continuation cell
@@ -73,8 +79,8 @@ export class Grid {
73
79
  /* v8 ignore stop */
74
80
  }
75
81
  cont.char = "";
76
- cont.fg = fg;
77
- cont.bg = bg;
82
+ cont.fg = cell.fg;
83
+ cont.bg = cell.bg;
78
84
  cont.style = style;
79
85
  cont.width = 0;
80
86
  }
@@ -108,10 +114,13 @@ export class Grid {
108
114
  }
109
115
  if (patch.char !== undefined)
110
116
  cell.char = patch.char;
111
- if (patch.fg !== undefined)
112
- cell.fg = patch.fg;
117
+ // Композитинг в порядке отрисовки: полупрозрачный bg ложится на то, что
118
+ // уже лежит в ячейке в этом кадре, полупрозрачный fg — на итоговый bg.
119
+ // Непрозрачные цвета проходят как раньше (compositeOver — одно сравнение).
113
120
  if (patch.bg !== undefined)
114
- cell.bg = patch.bg;
121
+ cell.bg = compositeOver(patch.bg, cell.bg);
122
+ if (patch.fg !== undefined)
123
+ cell.fg = compositeOver(patch.fg, cell.bg);
115
124
  if (patch.style !== undefined)
116
125
  cell.style = patch.style;
117
126
  if (patch.width !== undefined)
@@ -132,10 +141,11 @@ export class Grid {
132
141
  }
133
142
  cont.char = "";
134
143
  cont.width = 0;
144
+ // Продолжение — та же ячейка: берёт уже скомпозиченные цвета головы.
135
145
  if (patch.fg !== undefined)
136
- cont.fg = patch.fg;
146
+ cont.fg = cell.fg;
137
147
  if (patch.bg !== undefined)
138
- cont.bg = patch.bg;
148
+ cont.bg = cell.bg;
139
149
  if (patch.style !== undefined)
140
150
  cont.style = patch.style;
141
151
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tuidom/core",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "Experimental terminal UI DOM — core: element tree, flex layout, grid rendering, input parsing. API is unstable.",
6
6
  "license": "MIT",