@trebco/treb 30.8.2 → 30.10.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.
@@ -35,7 +35,7 @@ import type { SerializedValueType } from 'treb-base-types';
35
35
  import type { Sheet} from './workbook-sheet2';
36
36
  import { VisibleState } from './workbook-sheet2';
37
37
  import type { CellAnchor } from './drawing2/drawing2';
38
- import { type DOMContent, XMLUtils } from './xml-utils';
38
+ import { type GenericDOMElement, XMLUtils } from './xml-utils';
39
39
 
40
40
  // import { one_hundred_pixels } from './constants';
41
41
  import { ColumnWidthToPixels } from './column-width';
@@ -52,6 +52,55 @@ interface SharedFormula {
52
52
 
53
53
  interface SharedFormulaMap { [index: string]: SharedFormula }
54
54
 
55
+ interface CellElementType {
56
+ a$: {
57
+ r?: string;
58
+ t?: string;
59
+ s?: string;
60
+ };
61
+ v?: string|number|{
62
+ t$: string;
63
+ a$?: Record<string, string>; // DOMContent;
64
+ };
65
+ f?: string|{
66
+ t$: string;
67
+ a$?: {
68
+ si?: string;
69
+ t?: string;
70
+ ref?: string;
71
+ },
72
+ };
73
+ };
74
+
75
+ interface ConditionalFormatRule {
76
+ a$: {
77
+ type?: string;
78
+ dxfId?: string;
79
+ priority?: string;
80
+ operator?: string;
81
+ };
82
+ formula?: string|[number,number]|{t$: string};
83
+ colorScale?: {
84
+ cfvo?: {
85
+ a$: {
86
+ type?: string;
87
+ val?: string;
88
+ }
89
+ }[];
90
+ color?: {
91
+ a$: {
92
+ rgb?: string;
93
+ theme?: string;
94
+ tint?: string;
95
+ }
96
+ }[];
97
+ };
98
+ }
99
+
100
+ const ElementHasTextNode = (test: unknown): test is {t$: string} => {
101
+ return typeof test === 'object' && typeof (test as {$t: string}).$t !== 'undefined';
102
+ }
103
+
55
104
  export class Importer {
56
105
 
57
106
  // FIXME: need a way to share/pass parser flags
@@ -76,25 +125,7 @@ export class Importer {
76
125
 
77
126
  public ParseCell(
78
127
  sheet: Sheet,
79
- element: {
80
- a$: {
81
- r?: string;
82
- t?: string;
83
- s?: string;
84
- };
85
- v?: string|number|{
86
- t$: string;
87
- a$?: DOMContent;
88
- };
89
- f?: string|{
90
- t$: string;
91
- a$?: {
92
- si?: string;
93
- t?: string;
94
- ref?: string;
95
- },
96
- };
97
- },
128
+ element: CellElementType,
98
129
  shared_formulae: SharedFormulaMap,
99
130
  arrays: RangeType[],
100
131
  merges: RangeType[],
@@ -166,20 +197,31 @@ export class Importer {
166
197
  // doing it like this is sloppy (also does not work properly).
167
198
  value = '=' + formula.replace(/^_xll\./g, '');
168
199
 
169
- const parse_result = this.parser.Parse(formula); // l10n?
170
- if (parse_result.expression) {
171
- this.parser.Walk(parse_result.expression, (unit) => {
172
- if (unit.type === 'call') {
173
- if (/^_xll\./.test(unit.name)) {
174
- unit.name = unit.name.substring(5);
175
- }
176
- else if (/^_xlfn\./.test(unit.name)) {
177
- unit.name = unit.name.substring(6);
200
+ // drop the formula if it's a ref error, we can't handle this
201
+ if (/#REF/.test(formula)) {
202
+ value = formula;
203
+ }
204
+ else {
205
+ const parse_result = this.parser.Parse(formula); // l10n?
206
+ if (parse_result.expression) {
207
+ this.parser.Walk(parse_result.expression, (unit) => {
208
+ if (unit.type === 'call') {
209
+ if (/^_xll\./.test(unit.name)) {
210
+ unit.name = unit.name.substring(5);
211
+ }
212
+ if (/^_xlfn\./.test(unit.name)) {
213
+ console.info("xlfn:", unit.name);
214
+ unit.name = unit.name.substring(6);
215
+ }
216
+ if (/^_xlws\./.test(unit.name)) {
217
+ console.info("xlws:", unit.name);
218
+ unit.name = unit.name.substring(6);
219
+ }
178
220
  }
179
- }
180
- return true;
181
- });
182
- value = '=' + this.parser.Render(parse_result.expression, { missing: '' });
221
+ return true;
222
+ });
223
+ value = '=' + this.parser.Render(parse_result.expression, { missing: '' });
224
+ }
183
225
  }
184
226
 
185
227
  if (typeof element.f !== 'string') {
@@ -341,11 +383,13 @@ export class Importer {
341
383
 
342
384
  }
343
385
 
344
- public ParseConditionalFormat(address: RangeType|AddressType, rule: any): ConditionalFormat|ConditionalFormat[]|undefined {
386
+ public ParseConditionalFormat(address: RangeType|AddressType, rule: ConditionalFormatRule): ConditionalFormat|ConditionalFormat[]|undefined {
345
387
 
346
388
  const area = this.AddressToArea(address);
347
389
  const operators = ConditionalFormatOperators;
348
390
 
391
+ // console.info({rule});
392
+
349
393
  switch (rule.a$.type) {
350
394
  case 'duplicateValues':
351
395
  case 'uniqueValues':
@@ -425,7 +469,7 @@ export class Importer {
425
469
  if (rule.formula) {
426
470
 
427
471
  if (typeof rule.formula !== 'string') {
428
- if (rule.formula.t$) {
472
+ if (ElementHasTextNode(rule.formula)) {
429
473
 
430
474
  // the only case (to date) we've seen here is that the attribute
431
475
  // is "xml:space=preserve", which we can ignore (are you sure?)
@@ -436,7 +480,7 @@ export class Importer {
436
480
  }
437
481
  else {
438
482
  console.info("unexpected conditional expression", {rule});
439
-
483
+ rule.formula = '';
440
484
  }
441
485
  }
442
486
 
@@ -514,7 +558,7 @@ export class Importer {
514
558
  else if (color_element.a$.theme) {
515
559
  (color as ThemeColor).theme = Number(color_element.a$.theme) || 0;
516
560
  if (color_element.a$.tint) {
517
- (color as ThemeColor).tint = Math.round(color_element.a$.tint * 1000) / 1000;
561
+ (color as ThemeColor).tint = Math.round(Number(color_element.a$.tint) * 1000) / 1000;
518
562
  }
519
563
  }
520
564
 
@@ -589,7 +633,34 @@ export class Importer {
589
633
 
590
634
  const annotations: AnchoredAnnotation[] = [];
591
635
 
592
- const FindAll: (path: string) => any[] = XMLUtils.FindAll.bind(XMLUtils, sheet.sheet_data);
636
+ const FindAll: <T = GenericDOMElement>(path: string) => T[] = XMLUtils.FindAll.bind(XMLUtils, sheet.sheet_data);
637
+
638
+ // tab color
639
+
640
+ const tab_color_element = FindAll('worksheet/sheetPr/tabColor');
641
+
642
+ let tab_color: Color|undefined;
643
+
644
+ if (tab_color_element?.[0]) {
645
+
646
+ const element = tab_color_element[0];
647
+ if (element.a$?.theme) {
648
+ tab_color = { theme: Number(element.a$.theme) };
649
+ if (element.a$?.tint) {
650
+ tab_color.tint = Number(element.a$.tint);
651
+ }
652
+ }
653
+ if (element.a$?.rgb) {
654
+ const argb = element.a$.rgb;
655
+ tab_color = {
656
+ text: '#' + (
657
+ argb.length > 6 ?
658
+ argb.substr(argb.length - 6) :
659
+ argb),
660
+ };
661
+ }
662
+
663
+ }
593
664
 
594
665
  // conditionals
595
666
 
@@ -607,7 +678,7 @@ export class Importer {
607
678
  if (element.cfRule) {
608
679
  const rules = Array.isArray(element.cfRule) ? element.cfRule : [element.cfRule];
609
680
  for (const rule of rules) {
610
- const format = this.ParseConditionalFormat(area, rule);
681
+ const format = this.ParseConditionalFormat(area, rule as unknown as ConditionalFormatRule);
611
682
  if (format) {
612
683
  if (Array.isArray(format)) {
613
684
  conditional_formats.push(...format);
@@ -628,7 +699,7 @@ export class Importer {
628
699
  const merge_cells = FindAll('worksheet/mergeCells/mergeCell');
629
700
 
630
701
  for (const element of merge_cells) {
631
- if (element.a$.ref) {
702
+ if (element.a$?.ref) {
632
703
  const merge = sheet.TranslateAddress(element.a$.ref);
633
704
  if (is_range(merge)) {
634
705
  merges.push(ShiftRange(merge, -1, -1));
@@ -644,7 +715,7 @@ export class Importer {
644
715
  const ref = entry.a$?.sqref;
645
716
  const formula = entry.formula1;
646
717
 
647
- if (ref && formula && type === 'list') {
718
+ if (ref && formula && typeof formula === 'string' && type === 'list') {
648
719
  // let address: ICellAddress|undefined;
649
720
  let validation: DataValidation|undefined;
650
721
  let parse_result = this.parser.Parse(ref);
@@ -763,8 +834,8 @@ export class Importer {
763
834
 
764
835
  }
765
836
  else {
766
- reference = child.__location || '';
767
- text = child.__display || '';
837
+ reference = typeof child.__location === 'string' ? child.__location : '';
838
+ text = typeof child.__display === 'string' ? child.__display : '';
768
839
  }
769
840
 
770
841
  links.push({ address, reference, text });
@@ -831,11 +902,10 @@ export class Importer {
831
902
  row_heights[row_index - 1] = height;
832
903
  }
833
904
 
834
- let cells = row.c || [];
835
- if (!Array.isArray(cells)) { cells = [cells]; }
905
+ const cells = row.c ? Array.isArray(row.c) ? row.c : [row.c] : [];
836
906
 
837
907
  for (const element of cells) {
838
- const cell = this.ParseCell(sheet, element, shared_formulae, arrays, merges, links); // , validations);
908
+ const cell = this.ParseCell(sheet, element as unknown as CellElementType, shared_formulae, arrays, merges, links); // , validations);
839
909
  if (cell) {
840
910
  data.push(cell);
841
911
  }
@@ -1120,7 +1190,8 @@ export class Importer {
1120
1190
  type = 'treb-chart';
1121
1191
  func = 'Box.Plot';
1122
1192
  if (series?.length) {
1123
- args[0] = `Group(${series.map(s => `Series(${s.title || ''},,${s.values||''})` || '').join(', ')})`;
1193
+ args[0] = `Group(${series.map(s => `Series(${s.title || ''},,${s.values||''})`).join(', ')})`;
1194
+ console.info("S?", {series}, args[0])
1124
1195
  }
1125
1196
  args[1] = descriptor.chart.title;
1126
1197
  break;
@@ -1129,7 +1200,7 @@ export class Importer {
1129
1200
  type = 'treb-chart';
1130
1201
  func = 'Scatter.Line';
1131
1202
  if (series && series.length) {
1132
- args[0] = `Group(${series.map(s => `Series(${s.title || ''},${s.categories||''},${s.values||''})` || '').join(', ')})`;
1203
+ args[0] = `Group(${series.map(s => `Series(${s.title || ''},${s.categories||''},${s.values||''})`).join(', ')})`;
1133
1204
  }
1134
1205
  args[1] = descriptor.chart.title;
1135
1206
  break;
@@ -1165,7 +1236,7 @@ export class Importer {
1165
1236
 
1166
1237
  if (series) {
1167
1238
  if (series.length > 1) {
1168
- args[0] = `Group(${series.map(s => `Series(${s.title || ''},,${s.values||''})` || '').join(', ')})`;
1239
+ args[0] = `Group(${series.map(s => `Series(${s.title || ''},,${s.values||''})`).join(', ')})`;
1169
1240
  }
1170
1241
  else if (series.length === 1) {
1171
1242
  if (series[0].title) {
@@ -1289,6 +1360,7 @@ export class Importer {
1289
1360
  default_column_width,
1290
1361
  column_widths,
1291
1362
  row_heights,
1363
+ tab_color,
1292
1364
  row_styles,
1293
1365
  annotations,
1294
1366
  conditional_formats,
@@ -23,7 +23,7 @@ import type { ImportedSheetData } from 'treb-base-types';
23
23
  import type { SerializedModel } from 'treb-data-model';
24
24
 
25
25
  import { Exporter } from './export';
26
- import { Importer } from './import2';
26
+ import { Importer } from './import';
27
27
 
28
28
  const ctx: Worker = self as unknown as Worker;
29
29
  const exporter = new Exporter();
@@ -20,10 +20,10 @@
20
20
  */
21
21
 
22
22
  import { XMLParser } from 'fast-xml-parser';
23
- import { XMLUtils, XMLOptions, XMLOptions2 } from './xml-utils';
23
+ import { XMLUtils, XMLOptions2 } from './xml-utils';
24
24
 
25
25
  // const xmlparser = new XMLParser();
26
- const xmlparser1 = new XMLParser(XMLOptions);
26
+ // const xmlparser1 = new XMLParser(XMLOptions);
27
27
  const xmlparser2 = new XMLParser(XMLOptions2);
28
28
 
29
29
  // import * as he from 'he';
@@ -45,10 +45,10 @@ import type { SerializedNamed } from 'treb-data-model';
45
45
  export const ConditionalFormatOperators: Record<string, string> = {
46
46
  greaterThan: '>',
47
47
  greaterThanOrEqual: '>=',
48
- greaterThanOrEquals: '>=',
48
+ // greaterThanOrEquals: '>=',
49
49
  lessThan: '<',
50
50
  lessThanOrEqual: '<=',
51
- lessThanOrEquals: '<=',
51
+ // lessThanOrEquals: '<=',
52
52
  equal: '=',
53
53
  notEqual: '<>',
54
54
  };
@@ -514,7 +514,8 @@ export class Workbook {
514
514
  const data = this.zip.Get(reference.replace(/^../, 'xl'));
515
515
  if (!data) { return undefined; }
516
516
 
517
- const xml = xmlparser1.parse(data);
517
+ // const xml = xmlparser1.parse(data);
518
+ const xml = xmlparser2.parse(data);
518
519
 
519
520
  const result: ChartDescription = {
520
521
  type: ChartType.Unknown
@@ -532,8 +533,8 @@ export class Workbook {
532
533
  if (typeof node === 'string') {
533
534
  result.title = node;
534
535
  }
535
- else if (node.text__) {
536
- result.title = node.text__; // why is this not quoted, if the later one is quoted? is this a reference?
536
+ else if (node.t$) {
537
+ result.title = node.t$; // why is this not quoted, if the later one is quoted? is this a reference?
537
538
  }
538
539
  }
539
540
  else {
@@ -553,14 +554,14 @@ export class Workbook {
553
554
  series_nodes = [series_nodes];
554
555
  }
555
556
 
556
- // console.info("SN", series_nodes);
557
+ // console.info({SN: series_nodes});
557
558
 
558
559
  for (const series_node of series_nodes) {
559
560
 
560
561
  let index = series.length;
561
562
  const order_node = series_node['c:order'];
562
563
  if (order_node) {
563
- index = Number(order_node.__val||0) || 0;
564
+ index = Number(order_node.a$?.val||0) || 0;
564
565
  }
565
566
 
566
567
  const series_data: ChartSeries = {};
@@ -626,7 +627,7 @@ export class Workbook {
626
627
  result.type = ChartType.Bar;
627
628
  // console.info("BD", node);
628
629
  if (node['c:barDir']) {
629
- if (node['c:barDir'].__val === 'col') {
630
+ if (node['c:barDir'].a$?.val === 'col') {
630
631
  result.type = ChartType.Column;
631
632
  }
632
633
  }
@@ -682,7 +683,7 @@ export class Workbook {
682
683
 
683
684
  const ex_series = XMLUtils.FindAll(xml, 'cx:chartSpace/cx:chart/cx:plotArea/cx:plotAreaRegion/cx:series');
684
685
  if (ex_series?.length) {
685
- if (ex_series.every(test => test.__layoutId === 'boxWhisker')) {
686
+ if (ex_series.every(test => test.a$?.layoutId === 'boxWhisker')) {
686
687
  result.type = ChartType.Box;
687
688
  result.series = [];
688
689
  const data = XMLUtils.FindAll(xml, 'cx:chartSpace/cx:chartData/cx:data'); // /cx:data/cx:numDim/cx:f');
@@ -693,9 +694,9 @@ export class Workbook {
693
694
 
694
695
  const series: ChartSeries = {};
695
696
 
696
- const id = Number(entry['cx:dataId']?.['__val']);
697
+ const id = Number(entry['cx:dataId']?.a$?.val);
697
698
  for (const data_series of data) {
698
- if (Number(data_series.__id) === id) {
699
+ if (Number(data_series.a$?.id) === id) {
699
700
  series.values = data_series['cx:numDim']?.['cx:f'] || '';
700
701
  break;
701
702
  }
@@ -28,6 +28,26 @@ export interface DOMContent {
28
28
  [index: string]: string|DOMContent|string[]|DOMContent[]|number|number[]|undefined;
29
29
  }
30
30
 
31
+ // --- take 2 ------------------------------------------------------------------
32
+
33
+ export interface XMLKeys {
34
+ a$?: Record<string, string>;
35
+ t$?: string;
36
+ }
37
+
38
+ export type DOMElementType = number|number[]|string|string[]|(BaseDOM & XMLKeys)|(BaseDOM & XMLKeys)[];
39
+
40
+ export interface BaseDOM {
41
+ [key: string]: DOMElementType;
42
+ }
43
+
44
+ export type GenericDOMElement = BaseDOM & XMLKeys;
45
+
46
+ export const GenericDOMArray = (element: GenericDOMElement|GenericDOMElement[]): GenericDOMElement[] => element ? Array.isArray(element) ? element : [element] : [];
47
+
48
+ // -----------------------------------------------------------------------------
49
+
50
+
31
51
  /**
32
52
  * not sure why we have to do this, but filter attributes that
33
53
  * have value === undefined
@@ -76,6 +96,7 @@ export const PatchXMLBuilder = (options: Partial<XmlBuilderOptions>) => {
76
96
 
77
97
  //////////////////
78
98
 
99
+ /*
79
100
  export const XMLOptions: Partial<X2jOptions> = {
80
101
  ignoreAttributes: false,
81
102
  attributeNamePrefix: '__',
@@ -84,6 +105,7 @@ export const XMLOptions: Partial<X2jOptions> = {
84
105
  tagValueProcessor: XMLTagProcessor,
85
106
  ignoreDeclaration: true,
86
107
  };
108
+ */
87
109
 
88
110
  /**
89
111
  * group attributes under `a$`, and don't add attribute prefixes (should be
@@ -91,7 +113,6 @@ export const XMLOptions: Partial<X2jOptions> = {
91
113
  */
92
114
  export const XMLOptions2: Partial<X2jOptions> = {
93
115
  ignoreAttributes: false,
94
- // attrNodeName: 'a$', // FXP v4
95
116
  attributesGroupName: 'a$',
96
117
  attributeNamePrefix: '',
97
118
  textNodeName: 't$',
@@ -96,6 +96,8 @@ export abstract class BaseLayout {
96
96
 
97
97
  public header_size: Size = { width: 0, height: 0 };
98
98
 
99
+ public applied_theme_colors: string[] = [];
100
+
99
101
  /**
100
102
  * last rendered column. this is used to calculate the limits of
101
103
  * cell overflows, which may exceed actual data in the sheet.
@@ -1280,6 +1282,18 @@ export abstract class BaseLayout {
1280
1282
 
1281
1283
  }
1282
1284
 
1285
+ public ApplyThemeColors() {
1286
+
1287
+ // what's the best node for this? (...)
1288
+
1289
+ if (this.container) {
1290
+ for (const [index, entry] of this.applied_theme_colors.entries()) {
1291
+ this.container.style.setProperty(`--treb-applied-theme-color-${index + 1}`, entry);
1292
+ }
1293
+
1294
+ }
1295
+ }
1296
+
1283
1297
  /**
1284
1298
  * applies theme to nodes, as necessary
1285
1299
  */
@@ -1307,6 +1321,13 @@ export abstract class BaseLayout {
1307
1321
 
1308
1322
  this.dropdown_list.style.font = Style.Font(theme.grid_cell || {});
1309
1323
 
1324
+ // testing
1325
+
1326
+ this.applied_theme_colors = theme.theme_colors?.slice(4, 10) || [];
1327
+ if (this.container) {
1328
+ this.ApplyThemeColors();
1329
+ }
1330
+
1310
1331
  }
1311
1332
 
1312
1333
  public UpdateTotalSize(): void {
@@ -95,6 +95,8 @@ export class GridLayout extends BaseLayout {
95
95
 
96
96
  this.container.addEventListener('scroll', () => scroll_callback());
97
97
 
98
+ this.ApplyThemeColors();
99
+
98
100
  }
99
101
 
100
102
  public ResizeCursor(resize?: 'row'|'column'): void {
@@ -967,7 +967,7 @@ export class Grid extends GridBase {
967
967
  if (add_to_layout) {
968
968
  this.layout.AddAnnotation(annotation);
969
969
  if (annotation.data.layout) {
970
- this.EnsureAddress(annotation.data.layout.br.address, 1);
970
+ this.EnsureAddress(annotation.data.layout.br.address, 1, toll_events);
971
971
  }
972
972
  }
973
973
  else {
@@ -1190,7 +1190,7 @@ export class Grid extends GridBase {
1190
1190
  this.QueueLayoutUpdate();
1191
1191
 
1192
1192
  this.StyleDefaultFromTheme();
1193
-
1193
+
1194
1194
  if (render) {
1195
1195
  this.Repaint(false, false); // true, true);
1196
1196
  }
@@ -1403,6 +1403,10 @@ export class Grid extends GridBase {
1403
1403
 
1404
1404
  // this.tile_renderer.UpdateTheme(); // has reference
1405
1405
 
1406
+ if (this.tab_bar) {
1407
+ this.tab_bar.UpdateTheme();
1408
+ }
1409
+
1406
1410
  if (!initial) {
1407
1411
 
1408
1412
  this.UpdateLayout(); // in case we have changed font size
@@ -1454,7 +1458,7 @@ export class Grid extends GridBase {
1454
1458
 
1455
1459
  if (this.options.tab_bar) {
1456
1460
 
1457
- this.tab_bar = new TabBar(this.layout, this.model, this.view, this.options, view_node);
1461
+ this.tab_bar = new TabBar(this.layout, this.model, this.view, this.options, this.theme, view_node);
1458
1462
  this.tab_bar.Subscribe((event) => {
1459
1463
  switch (event.type) {
1460
1464
  case 'cancel':
@@ -5936,7 +5940,7 @@ export class Grid extends GridBase {
5936
5940
  /**
5937
5941
  * if the address is outside of current extent, expand
5938
5942
  */
5939
- private EnsureAddress(address: ICellAddress, step = 8): boolean {
5943
+ private EnsureAddress(address: ICellAddress, step = 8, toll_layout = false): boolean {
5940
5944
 
5941
5945
  let expanded = false;
5942
5946
 
@@ -5959,7 +5963,7 @@ export class Grid extends GridBase {
5959
5963
  expanded = true;
5960
5964
  }
5961
5965
 
5962
- if (expanded) {
5966
+ if (expanded && !toll_layout) {
5963
5967
  this.layout.UpdateTiles();
5964
5968
  this.layout.UpdateContentsSize();
5965
5969
  this.Repaint(true, true);
@@ -25,7 +25,7 @@ import type { BaseLayout } from '../layout/base_layout';
25
25
  import { MouseDrag } from './drag_mask';
26
26
  import type { GridOptions } from './grid_options';
27
27
  import { type ScaleEvent, ScaleControl } from './scale-control';
28
- import { DOMContext } from 'treb-base-types';
28
+ import { DOMContext, ResolveThemeColor, type Theme } from 'treb-base-types';
29
29
 
30
30
  export interface ActivateSheetEvent {
31
31
  type: 'activate-sheet';
@@ -100,6 +100,8 @@ export class TabBar extends EventSource<TabEvent> {
100
100
  timeout?: number;
101
101
  } = {};
102
102
 
103
+ private tab_color_cache: Map<number, { background: string, foreground: string }> = new Map();
104
+
103
105
  // tslint:disable-next-line: variable-name
104
106
  private _visible = false;
105
107
 
@@ -133,6 +135,7 @@ export class TabBar extends EventSource<TabEvent> {
133
135
  private model: DataModel,
134
136
  private view: ViewModel,
135
137
  private options: GridOptions,
138
+ private theme: Theme,
136
139
  // private container: HTMLElement,
137
140
  view_node: HTMLElement,
138
141
  ) {
@@ -236,10 +239,21 @@ export class TabBar extends EventSource<TabEvent> {
236
239
  if (active) {
237
240
  // tab.classList.add('treb-selected');
238
241
  tab.setAttribute('selected', '');
242
+
243
+ if (tab.dataset.background_color) {
244
+ tab.style.backgroundColor = `color-mix(in srgb, ${tab.dataset.background_color} 20%, var(--treb-tab-bar-active-tab-background, #fff))`;
245
+ tab.style.color = '';
246
+ }
239
247
  }
240
248
  else {
241
249
  // tab.classList.remove('treb-selected');
242
250
  tab.removeAttribute('selected');
251
+ if (tab.dataset.background_color) {
252
+ tab.style.backgroundColor = tab.dataset.background_color;
253
+ }
254
+ if (tab.dataset.foreground_color) {
255
+ tab.style.color = tab.dataset.foreground_color;
256
+ }
243
257
  }
244
258
  }
245
259
 
@@ -419,6 +433,11 @@ export class TabBar extends EventSource<TabEvent> {
419
433
 
420
434
  }
421
435
 
436
+ public UpdateTheme() {
437
+ this.tab_color_cache.clear();
438
+ this.Update();
439
+ }
440
+
422
441
  /**
423
442
  * update tabs from model.
424
443
  */
@@ -464,6 +483,24 @@ export class TabBar extends EventSource<TabEvent> {
464
483
  const tab = this.DOM.Create('li');
465
484
  tab.setAttribute('tabindex', '0');
466
485
 
486
+ if (sheet.tab_color) {
487
+ const id = sheet.id;
488
+ if (!this.tab_color_cache.has(id)) {
489
+ const background = ResolveThemeColor(this.theme, sheet.tab_color);
490
+ const foreground = ResolveThemeColor(this.theme, { offset: sheet.tab_color });
491
+ if (background && foreground) {
492
+ this.tab_color_cache.set(id, { background, foreground });
493
+ }
494
+ }
495
+ const color = this.tab_color_cache.get(id);
496
+ if (color) {
497
+ tab.style.backgroundColor = color.background;
498
+ tab.style.color = color.foreground;
499
+ tab.dataset.background_color = color.background;
500
+ tab.dataset.foreground_color = color.foreground;
501
+ }
502
+ }
503
+
467
504
  // tab.classList.add('tab');
468
505
  tab.style.order = (index * 2).toString();
469
506
  tab.role = 'tab';