@stocksharp/trading-controls 1.1.1 → 1.3.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 (77) hide show
  1. package/README.md +95 -16
  2. package/dist/esm/active-orders-widget.js +38 -25
  3. package/dist/esm/active-orders-widget.js.map +1 -1
  4. package/dist/esm/black-scholes.js +147 -0
  5. package/dist/esm/black-scholes.js.map +1 -0
  6. package/dist/esm/control-types.js +4 -0
  7. package/dist/esm/control-types.js.map +1 -1
  8. package/dist/esm/index.js +18 -0
  9. package/dist/esm/index.js.map +1 -1
  10. package/dist/esm/log-monitor-widget.js +265 -0
  11. package/dist/esm/log-monitor-widget.js.map +1 -0
  12. package/dist/esm/log-tree.js +96 -0
  13. package/dist/esm/log-tree.js.map +1 -0
  14. package/dist/esm/option-desk-widget.js +322 -0
  15. package/dist/esm/option-desk-widget.js.map +1 -0
  16. package/dist/esm/pnl-curve.js +129 -0
  17. package/dist/esm/pnl-curve.js.map +1 -0
  18. package/dist/esm/statistics-widget.js +194 -0
  19. package/dist/esm/statistics-widget.js.map +1 -0
  20. package/dist/esm/strategies-widget.js +348 -0
  21. package/dist/esm/strategies-widget.js.map +1 -0
  22. package/dist/esm/trade-history-widget.js +14 -6
  23. package/dist/esm/trade-history-widget.js.map +1 -1
  24. package/dist/esm/tradefeed-widget.js +3 -3
  25. package/dist/esm/tradefeed-widget.js.map +1 -1
  26. package/dist/esm/trading-host.js +1 -0
  27. package/dist/esm/trading-host.js.map +1 -1
  28. package/dist/sstradingcontrols.js +1358 -78
  29. package/dist/sstradingcontrols.js.map +4 -4
  30. package/dist/types/active-orders-widget.d.ts +11 -3
  31. package/dist/types/active-orders-widget.d.ts.map +1 -1
  32. package/dist/types/black-scholes.d.ts +28 -0
  33. package/dist/types/black-scholes.d.ts.map +1 -0
  34. package/dist/types/control-types.d.ts +4 -0
  35. package/dist/types/control-types.d.ts.map +1 -1
  36. package/dist/types/index.d.ts +16 -1
  37. package/dist/types/index.d.ts.map +1 -1
  38. package/dist/types/log-monitor-widget.d.ts +42 -0
  39. package/dist/types/log-monitor-widget.d.ts.map +1 -0
  40. package/dist/types/log-tree.d.ts +34 -0
  41. package/dist/types/log-tree.d.ts.map +1 -0
  42. package/dist/types/option-desk-widget.d.ts +68 -0
  43. package/dist/types/option-desk-widget.d.ts.map +1 -0
  44. package/dist/types/pnl-curve.d.ts +43 -0
  45. package/dist/types/pnl-curve.d.ts.map +1 -0
  46. package/dist/types/statistics-widget.d.ts +29 -0
  47. package/dist/types/statistics-widget.d.ts.map +1 -0
  48. package/dist/types/strategies-widget.d.ts +63 -0
  49. package/dist/types/strategies-widget.d.ts.map +1 -0
  50. package/dist/types/trade-history-widget.d.ts +3 -1
  51. package/dist/types/trade-history-widget.d.ts.map +1 -1
  52. package/dist/types/trading-data.d.ts +9 -0
  53. package/dist/types/trading-data.d.ts.map +1 -1
  54. package/dist/types/trading-host.d.ts +1 -0
  55. package/dist/types/trading-host.d.ts.map +1 -1
  56. package/package.json +27 -2
  57. package/screenshots/log-monitor.png +0 -0
  58. package/screenshots/option-desk.png +0 -0
  59. package/screenshots/panels.jpg +0 -0
  60. package/screenshots/statistics.png +0 -0
  61. package/screenshots/strategies.png +0 -0
  62. package/src/active-orders-widget.ts +49 -25
  63. package/src/black-scholes.ts +199 -0
  64. package/src/control-types.ts +4 -0
  65. package/src/index.ts +41 -0
  66. package/src/log-monitor-widget.ts +312 -0
  67. package/src/log-tree.ts +131 -0
  68. package/src/option-desk-widget.ts +422 -0
  69. package/src/pnl-curve.ts +204 -0
  70. package/src/statistics-widget.ts +226 -0
  71. package/src/strategies-widget.ts +435 -0
  72. package/src/trade-history-widget.ts +19 -6
  73. package/src/tradefeed-widget.ts +3 -3
  74. package/src/trading-data.ts +22 -0
  75. package/src/trading-host.ts +8 -0
  76. package/styles/trading-controls.css +356 -0
  77. package/translation-keys.json +70 -1
@@ -12,7 +12,7 @@
12
12
  // The table is `DataGrid` from `@stocksharp/grids`: the columns below are this
13
13
  // blotter's single declaration and the <thead> is left empty for the grid to
14
14
  // fill.
15
- import { formatPrice, formatQty, formatTime } from './formatters.js';
15
+ import { formatPrice, formatQty } from './formatters.js';
16
16
  import { makeElement, makeIconButton, makePanelId, makePanelRoot } from './dom.js';
17
17
  import { ControlTypes } from './control-types.js';
18
18
  import { makeGridMenu } from './grid-menu.js';
@@ -24,6 +24,10 @@ import { DataGrid, GridColumn } from '@stocksharp/grids/source/data-grid';
24
24
  /// portfolio and its lifecycle all arrive through it.
25
25
  export interface TradeHistoryDeps {
26
26
  host: TradingHost;
27
+ /// A blotter over a finished run - a backtest report, an archived session.
28
+ /// There is no account to load from and nothing to reload, so the rows come
29
+ /// in through `update` and the refresh gesture is not rendered.
30
+ readOnly?: boolean;
27
31
  }
28
32
 
29
33
  export class TradeHistoryWidget {
@@ -44,7 +48,7 @@ export class TradeHistoryWidget {
44
48
  // host, so a missing host has to fail here rather than render a panel
45
49
  // captioned with raw English keys.
46
50
  const host = assertHost(deps?.host, 'TradeHistoryWidget');
47
- const root = TradeHistoryWidget._buildRoot(host);
51
+ const root = TradeHistoryWidget._buildRoot(host, deps?.readOnly === true);
48
52
  root.id = makePanelId(TradeHistoryWidget.TYPE);
49
53
  hostEl.appendChild(root);
50
54
  return new TradeHistoryWidget(root, state || {}, deps);
@@ -52,7 +56,7 @@ export class TradeHistoryWidget {
52
56
 
53
57
  // The panel's markup. The host stylesheet reads this structure, and a
54
58
  // docking host lifts `.panel-header`'s children into its tab strip.
55
- static _buildRoot(host: TradingHost): HTMLElement {
59
+ static _buildRoot(host: TradingHost, readOnly = false): HTMLElement {
56
60
  const title = host.t('TradeHistory');
57
61
  return makePanelRoot('trade-history-panel', title, [
58
62
  makeElement('div', 'panel-header', {}, [
@@ -67,7 +71,7 @@ export class TradeHistoryWidget {
67
71
  ]),
68
72
  ]),
69
73
  makeElement('div', 'panel-rail', { role: 'toolbar', 'aria-label': host.t('TradeHistoryActions') }, [
70
- makeIconButton('bt-icon-btn panel-refresh-btn', host.t('Refresh'), 'bi-arrow-clockwise', {}),
74
+ ...(readOnly ? [] : [makeIconButton('bt-icon-btn panel-refresh-btn', host.t('Refresh'), 'bi-arrow-clockwise', {})]),
71
75
  makeIconButton('bt-icon-btn panel-export-btn', host.t('ExportToExcel'), 'bi-file-earmark-spreadsheet', {}),
72
76
  ]),
73
77
  ]),
@@ -157,6 +161,15 @@ export class TradeHistoryWidget {
157
161
  }
158
162
  }
159
163
 
164
+ /// Show these rows. The push path, for a consumer that already holds the
165
+ /// trades - a finished run's report - rather than an account to load them
166
+ /// from. `refresh` is the pull path and the two do not mix: whichever ran
167
+ /// last is what the grid shows.
168
+ update(rows: TradeRow[]): void {
169
+ this._rows = rows || [];
170
+ this._grid?.setRows(this._rows);
171
+ }
172
+
160
173
  // Export the table to .xlsx in the currently rendered (sorted) order.
161
174
  _export(): void {
162
175
  this._grid?.download('trades', this._host.t('TradeHistory'));
@@ -184,8 +197,8 @@ export class TradeHistoryWidget {
184
197
  header: label('Time'),
185
198
  exportable: true,
186
199
  value: (t) => t.executedAt || t.time,
187
- render: (t) => formatTime(t.executedAt || t.time),
188
- exportValue: (t) => formatTime(t.executedAt || t.time),
200
+ render: (t) => presentation.timeText(t.executedAt || t.time!),
201
+ exportValue: (t) => presentation.timeText(t.executedAt || t.time!),
189
202
  },
190
203
  {
191
204
  key: 'symbol',
@@ -20,7 +20,7 @@
20
20
  // geometry comes out of `tradefeed-bubbles.ts` as numbers and its colours out
21
21
  // of the host's `canvasPalette()`, so the package still paints nothing from a
22
22
  // palette of its own.
23
- import { formatPrice, formatQty, formatTime } from './formatters.js';
23
+ import { formatPrice, formatQty } from './formatters.js';
24
24
  import { makeElement, makeIconButton, makePanelId, makePanelRoot } from './dom.js';
25
25
  import { ControlTypes } from './control-types.js';
26
26
  import { makeGridMenu } from './grid-menu.js';
@@ -372,7 +372,7 @@ export class TradeFeedWidget {
372
372
  header: label('Time'),
373
373
  exportable: true,
374
374
  value: (t) => String(t.time || t.executedAt || ''),
375
- render: (t) => formatTime(t.time || t.executedAt),
375
+ render: (t) => this._host.presentation.timeText(t.time || t.executedAt!),
376
376
  },
377
377
  {
378
378
  key: 'symbol',
@@ -708,7 +708,7 @@ export class TradeFeedWidget {
708
708
 
709
709
  const lines: HTMLElement[] = [];
710
710
  if (this._extraSymbols.size > 0) lines.push(this._tooltipLine(this._host.t('Symbol'), bubble.symbol, ''));
711
- lines.push(this._tooltipLine(this._host.t('Time'), formatTime(bubble.time), ''));
711
+ lines.push(this._tooltipLine(this._host.t('Time'), this._host.presentation.timeText(bubble.time), ''));
712
712
  lines.push(this._tooltipLine(aggregated ? this._host.t('VWAP') : this._host.t('Price'), formatPrice(bubble.price), ''));
713
713
  lines.push(this._tooltipLine(aggregated ? this._host.t('Total qty') : this._host.t('Qty'), formatQty(bubble.quantity), ''));
714
714
  lines.push(this._tooltipLine(this._host.t('Side'), presentation.sideText(side), presentation.sideClass(side)));
@@ -100,6 +100,28 @@ export interface TradeRow {
100
100
  }
101
101
 
102
102
  /// One tradable instrument, as the instrument search returns it.
103
+ /// One statistic a strategy reports about itself.
104
+ ///
105
+ /// The desktop grid reflects these off the strategy's parameter objects; a browser is handed
106
+ /// them already resolved. `category` groups and `order` sorts - both are the registry's, not
107
+ /// the reader's, and neither is the translated text beside it.
108
+ export interface StatisticRow {
109
+ /// Stable identity, and what the row is addressed by. The parameter's own kind.
110
+ key: string;
111
+ /// Grouping key. Stable across languages.
112
+ category: string;
113
+ /// What that group is called here.
114
+ categoryText?: string;
115
+ /// Where the parameter sits among its peers. Banded by category by the registry.
116
+ order: number;
117
+ /// Localized caption.
118
+ name: string;
119
+ /// Localized explanation, shown on hover.
120
+ description?: string;
121
+ /// A number, a moment, or a word - whatever the parameter measures. Null until measured.
122
+ value?: number | string | Date | null;
123
+ }
124
+
103
125
  export interface InstrumentRow {
104
126
  /// Qualified symbol — `BTC@IMEX`. The display form drops the venue.
105
127
  symbol?: string;
@@ -86,6 +86,13 @@ export interface TradingPresentation {
86
86
  typeText(type: OrderType, limitPrice: number, stopPrice: number): string;
87
87
  /// Localized order status.
88
88
  statusText(status: OrderStatus): string;
89
+ /// A moment, worded. The host owns this for the same reason it owns a side or a
90
+ /// status: what a time should read as is not a property of the data. A live tape wants
91
+ /// the time of day in the reader's own zone; a blotter over a finished run wants the
92
+ /// date too, and in the zone the run was recorded in, or every row reads as the same
93
+ /// unplaceable day. `formatTime` is this package's tape answer, exported so a host that
94
+ /// wants exactly that need not restate it.
95
+ timeText(value: string | number | Date): string;
89
96
  /// CSS class colouring a cell by side. The shipped stylesheet styles
90
97
  /// `side-buy` and `side-sell`; a host on its own palette may answer with
91
98
  /// its own names and style those instead.
@@ -271,6 +278,7 @@ export function assertHost(host: TradingHost, controlName: string): TradingHost
271
278
  _required(host.presentation.isBuy, 'host.presentation.isBuy', 'function', controlName);
272
279
  _required(host.presentation.typeText, 'host.presentation.typeText', 'function', controlName);
273
280
  _required(host.presentation.statusText, 'host.presentation.statusText', 'function', controlName);
281
+ _required(host.presentation.timeText, 'host.presentation.timeText', 'function', controlName);
274
282
  _required(host.presentation.sideClass, 'host.presentation.sideClass', 'function', controlName);
275
283
  _required(host.presentation.pnlClass, 'host.presentation.pnlClass', 'function', controlName);
276
284
  _required(host.presentation.canvasPalette, 'host.presentation.canvasPalette', 'function', controlName);
@@ -1378,3 +1378,359 @@
1378
1378
  }
1379
1379
 
1380
1380
  .oe-estimate.oe-estimate-error { color: var(--t-red); }
1381
+
1382
+ /* Statistics: a figure column, right-aligned and lined up on the digits so a column of
1383
+ numbers can be compared down rather than read across. */
1384
+ .statistic-value {
1385
+ text-align: right;
1386
+ font-family: var(--t-mono);
1387
+ font-variant-numeric: tabular-nums;
1388
+ }
1389
+
1390
+ /* ---- Log monitor -------------------------------------------------------- */
1391
+
1392
+ /* Two panes side by side: the sources, which are as wide as their names, and the messages,
1393
+ which take everything left. Both scroll in their own right so a long log does not push the
1394
+ tree off the panel. */
1395
+ .log-monitor-body {
1396
+ display: flex;
1397
+ min-height: 0;
1398
+ overflow: hidden;
1399
+ }
1400
+
1401
+ .log-sources {
1402
+ flex: 0 0 14rem;
1403
+ min-height: 0;
1404
+ overflow: auto;
1405
+ display: flex;
1406
+ flex-direction: column;
1407
+ border-right: 1px solid var(--t-border);
1408
+ background: var(--t-panel);
1409
+ }
1410
+
1411
+ /* A source reads as a row, not as a button: the whole width is the target, the caption sits
1412
+ where its depth puts it, and only the selected one is filled. */
1413
+ .log-source {
1414
+ flex: 0 0 auto;
1415
+ border: 0;
1416
+ background: transparent;
1417
+ color: var(--t-text);
1418
+ font: inherit;
1419
+ text-align: left;
1420
+ padding: .25rem .5rem;
1421
+ cursor: pointer;
1422
+ white-space: nowrap;
1423
+ overflow: hidden;
1424
+ text-overflow: ellipsis;
1425
+ }
1426
+
1427
+ .log-source:hover { background: var(--t-hover); }
1428
+
1429
+ .log-source.is-selected {
1430
+ background: var(--t-hover);
1431
+ color: var(--t-text-bright);
1432
+ box-shadow: inset 2px 0 0 var(--t-accent);
1433
+ }
1434
+
1435
+ .log-messages {
1436
+ flex: 1 1 auto;
1437
+ min-width: 0;
1438
+ min-height: 0;
1439
+ display: flex;
1440
+ flex-direction: column;
1441
+ overflow: hidden;
1442
+ }
1443
+
1444
+ .log-toolbar {
1445
+ flex: 0 0 auto;
1446
+ display: flex;
1447
+ align-items: center;
1448
+ gap: .3rem;
1449
+ padding: .3rem .5rem;
1450
+ border-bottom: 1px solid var(--t-border);
1451
+ background: var(--t-header);
1452
+ }
1453
+
1454
+ /* One letter per level, lit when that level is shown. Off is dimmed rather than hidden, so the
1455
+ row of them reads as a set of switches and not as a shrinking list. */
1456
+ .log-level-toggle {
1457
+ border: 1px solid var(--t-border);
1458
+ background: var(--t-panel);
1459
+ color: var(--t-text-dim);
1460
+ font-family: var(--t-mono);
1461
+ font-size: .72rem;
1462
+ line-height: 1;
1463
+ width: 1.4rem;
1464
+ height: 1.4rem;
1465
+ border-radius: var(--t-radius);
1466
+ cursor: pointer;
1467
+ opacity: .45;
1468
+ transition: opacity var(--t-transition), color var(--t-transition);
1469
+ }
1470
+
1471
+ .log-level-toggle.is-on { opacity: 1; color: var(--t-text); }
1472
+ .log-level-toggle.log-level-error.is-on { color: var(--t-red); }
1473
+ .log-level-toggle.log-level-warning.is-on { color: var(--t-orange); }
1474
+
1475
+ .log-filter {
1476
+ flex: 1 1 auto;
1477
+ min-width: 4rem;
1478
+ max-width: 14rem;
1479
+ margin-left: .3rem;
1480
+ }
1481
+
1482
+ /* The scroll box, not the table, is the flex child. A table given `flex: 1` stretches its rows
1483
+ to fill the space instead of leaving the space empty, which drew a one-line log as one line a
1484
+ panel tall; and with nothing scrollable around it, a long log was simply unreachable past the
1485
+ height of the panel. */
1486
+ .log-table-scroll {
1487
+ flex: 1 1 auto;
1488
+ min-height: 0;
1489
+ overflow: auto;
1490
+ }
1491
+
1492
+ /* The header stays put while the messages scroll under it - a log is read by scrolling back,
1493
+ and a column heading that scrolls away takes the meaning of the columns with it. */
1494
+ .log-table thead th {
1495
+ position: sticky;
1496
+ top: 0;
1497
+ z-index: 1;
1498
+ }
1499
+
1500
+ /* The level column is one letter wide and coloured by what it says; the message takes the rest
1501
+ and wraps rather than being cut, because the end of an error is usually the part that names
1502
+ the cause. */
1503
+ .log-level-col { width: 2rem; }
1504
+
1505
+ .log-level-cell {
1506
+ text-align: center;
1507
+ font-family: var(--t-mono);
1508
+ color: var(--t-text-dim);
1509
+ }
1510
+
1511
+ .log-level-cell.log-level-error { color: var(--t-red); }
1512
+ .log-level-cell.log-level-warning { color: var(--t-orange); }
1513
+
1514
+ .log-message-cell {
1515
+ white-space: pre-wrap;
1516
+ word-break: break-word;
1517
+ }
1518
+
1519
+ .log-row-error td { color: var(--t-red); }
1520
+
1521
+ /* ---- Strategies dashboard ----------------------------------------------- */
1522
+
1523
+ /* The state reads twice: a dot that carries the colour across a long list, and the word beside
1524
+ it, because a colour cannot tell Starting from Started. */
1525
+ .strategy-state { white-space: nowrap; }
1526
+
1527
+ .strategy-dot {
1528
+ margin-right: .35rem;
1529
+ color: var(--t-text-dim);
1530
+ }
1531
+
1532
+ .strategy-state-started .strategy-dot { color: var(--t-green); }
1533
+ .strategy-state-starting .strategy-dot,
1534
+ .strategy-state-stopping .strategy-dot { color: var(--t-orange); }
1535
+
1536
+ /* A least width in characters, so the column does not jump as a run passes through Stopping on
1537
+ its way to Stopped, or Forming on its way to Running. Sized to the longest of those words; a
1538
+ floor rather than a cap, so a language that words them longer still gets its room. */
1539
+ .strategy-state-text {
1540
+ color: var(--t-text-dim);
1541
+ display: inline-block;
1542
+ min-width: 8ch;
1543
+ }
1544
+
1545
+ .strategy-actions,
1546
+ .strategy-position {
1547
+ white-space: nowrap;
1548
+ }
1549
+
1550
+ /* A least width in digits, so the flatten button beside the figure does not move when the
1551
+ position gains a minus sign or loses a digit. It is a floor, not a cap: a genuinely wider
1552
+ number still gets the room it needs. */
1553
+ .strategy-position-value {
1554
+ display: inline-block;
1555
+ min-width: 5ch;
1556
+ text-align: right;
1557
+ font-variant-numeric: tabular-nums;
1558
+ }
1559
+
1560
+ .strategy-actions .bt-icon-btn { margin-right: .1rem; }
1561
+
1562
+ /* Flattening sits with the number it acts on, quiet until the position is worth closing. */
1563
+ .strategy-flatten-btn { margin-left: .35rem; }
1564
+
1565
+ .strategy-online { text-align: center; color: var(--t-text-dim); }
1566
+ .strategy-online.is-on { color: var(--t-green); }
1567
+
1568
+ .strategy-mode { min-width: 7rem; }
1569
+
1570
+ /* The curve fills the cell it was measured for; the cell claims that width so a column of them
1571
+ lines up whether or not a given strategy has traded yet. */
1572
+ .strategy-spark { width: 140px; padding-top: 2px; padding-bottom: 2px; }
1573
+ .strategy-spark-canvas { display: block; }
1574
+
1575
+ /* An error is the one cell allowed to be long: it is read when something is wrong and truncating
1576
+ it hides the part that names the cause. */
1577
+ .strategy-error {
1578
+ color: var(--t-red);
1579
+ white-space: pre-wrap;
1580
+ word-break: break-word;
1581
+ }
1582
+
1583
+ .strategy-failed .strategy-dot { color: var(--t-red); }
1584
+
1585
+ /* ---- Option desk -------------------------------------------------------- */
1586
+
1587
+ /* A bar behind a figure rather than beside it: the number stays readable and the cell keeps its
1588
+ width, so a column of them can be compared down the strip. */
1589
+ .option-bar-cell {
1590
+ position: relative;
1591
+ display: block;
1592
+ text-align: right;
1593
+ }
1594
+
1595
+ .option-bar {
1596
+ position: absolute;
1597
+ inset: 0 auto 0 0;
1598
+ border-radius: 1px;
1599
+ opacity: .35;
1600
+ }
1601
+
1602
+ .option-bar-text { position: relative; }
1603
+
1604
+ /* Calls and puts are told apart by which side the bar is tinted; volatility is neither side's,
1605
+ so it takes the neutral one. */
1606
+ .option-bar-call { background: var(--t-green); }
1607
+ .option-bar-put { background: var(--t-red); }
1608
+ .option-bar-iv { background: var(--t-text-dim); }
1609
+
1610
+ .option-bid { color: var(--t-green); }
1611
+ .option-ask { color: var(--t-red); }
1612
+
1613
+ /* The strike is the spine of the chain: it is what the eye runs down, so it is the one column
1614
+ given weight, and the value beside it sits in the same block. */
1615
+ .option-strike {
1616
+ text-align: center;
1617
+ font-weight: 600;
1618
+ color: var(--t-text-bright);
1619
+ background: var(--t-header);
1620
+ }
1621
+
1622
+ .option-intrinsic {
1623
+ text-align: center;
1624
+ color: var(--t-text-dim);
1625
+ background: var(--t-header);
1626
+ }
1627
+
1628
+ /* Which half is in the money, shaded rather than outlined: the boundary between the two is the
1629
+ first thing a reader looks for and it moves with the underlying. */
1630
+ .option-itm-call td:first-child,
1631
+ .option-itm-put td:last-child { box-shadow: none; }
1632
+
1633
+ .option-itm-call .option-strike { border-left: 2px solid var(--t-green); }
1634
+ .option-itm-put .option-strike { border-right: 2px solid var(--t-red); }
1635
+
1636
+ /* ---- Fields the controls build themselves -------------------------------- */
1637
+
1638
+ /* A control that ships its own panel ships its own fields. These carry Bootstrap's class names
1639
+ because a host that has Bootstrap should keep its look, but a host that does not was getting
1640
+ the browser's default control: a white box with a native arrow, sitting on a dark panel. The
1641
+ rules below are what the package paints when nothing else has, and any real Bootstrap or host
1642
+ stylesheet loaded after this one still wins. */
1643
+ .terminal-table input.form-control,
1644
+ .terminal-table select.form-control,
1645
+ .panel-body input.form-control,
1646
+ .panel-body select.form-control {
1647
+ background: var(--t-bg);
1648
+ color: var(--t-text);
1649
+ border: 1px solid var(--t-border);
1650
+ border-radius: var(--t-radius);
1651
+ font-family: inherit;
1652
+ font-size: 11px;
1653
+ padding: 2px 6px;
1654
+ line-height: 1.4;
1655
+ }
1656
+
1657
+ .terminal-table select.form-control,
1658
+ .panel-body select.form-control {
1659
+ /* The native arrow is painted by the platform and cannot be themed, so it is replaced. */
1660
+ appearance: none;
1661
+ background-image: linear-gradient(45deg, transparent 50%, var(--t-text-dim) 50%),
1662
+ linear-gradient(135deg, var(--t-text-dim) 50%, transparent 50%);
1663
+ background-position: right 11px center, right 6px center;
1664
+ background-size: 5px 5px, 5px 5px;
1665
+ background-repeat: no-repeat;
1666
+ padding-right: 20px;
1667
+ }
1668
+
1669
+ .panel-body input.form-control::placeholder { color: var(--t-text-dim); }
1670
+
1671
+ .terminal-table input.form-control:focus,
1672
+ .terminal-table select.form-control:focus,
1673
+ .panel-body input.form-control:focus,
1674
+ .panel-body select.form-control:focus {
1675
+ outline: none;
1676
+ border-color: var(--t-accent);
1677
+ box-shadow: 0 0 0 2px var(--t-accent-glow-soft);
1678
+ }
1679
+
1680
+ /* The tables carry their own text colour rather than inheriting the page's. A panel is dropped
1681
+ into whatever document a host has, and one that never set a colour was rendering near-black
1682
+ rows on a near-black panel. */
1683
+ .terminal-table { color: var(--t-text); }
1684
+
1685
+ /* The scrollbars inside a panel, painted from the same tokens as everything else in it.
1686
+ A control makes its own scroll containers - the body, the table, the log's source tree, the
1687
+ watchlist pane - so a panel that brings its own background brings the bar over it too;
1688
+ without this a themed dark panel scrolls with the operating system's light grey.
1689
+
1690
+ Scoped to `.terminal-panel` on purpose: a host's page keeps whatever scrollbars it has, and
1691
+ only the boxes this package created are restyled.
1692
+
1693
+ Both spellings, because they are not interchangeable. `scrollbar-width` / `scrollbar-color`
1694
+ are the standard ones and are all Firefox reads; the `::-webkit-scrollbar` pseudo-elements
1695
+ are what WebKit and Chromium read, and they win there when both are present. */
1696
+ .terminal-panel,
1697
+ .terminal-panel * {
1698
+ scrollbar-width: thin;
1699
+ scrollbar-color: var(--t-border) transparent;
1700
+ }
1701
+
1702
+ .terminal-panel ::-webkit-scrollbar,
1703
+ .terminal-panel::-webkit-scrollbar {
1704
+ width: 10px;
1705
+ height: 10px;
1706
+ }
1707
+
1708
+ /* Transparent rather than a colour of its own: a scroll container sits on the panel, the header
1709
+ band or a grouped row, and a track painted one flat colour shows a seam against the other two. */
1710
+ .terminal-panel ::-webkit-scrollbar-track,
1711
+ .terminal-panel::-webkit-scrollbar-track {
1712
+ background: transparent;
1713
+ }
1714
+
1715
+ .terminal-panel ::-webkit-scrollbar-thumb,
1716
+ .terminal-panel::-webkit-scrollbar-thumb {
1717
+ background: var(--t-border);
1718
+ border-radius: var(--t-radius);
1719
+ /* An inset border keeps the thumb off the edge of the track, so it reads as a handle in a
1720
+ gutter rather than as a filled column. The border is the padding: it has to be
1721
+ transparent and clipped to the content box, or it paints as a second colour. */
1722
+ border: 2px solid transparent;
1723
+ background-clip: content-box;
1724
+ }
1725
+
1726
+ .terminal-panel ::-webkit-scrollbar-thumb:hover,
1727
+ .terminal-panel::-webkit-scrollbar-thumb:hover {
1728
+ background: var(--t-text-dim);
1729
+ background-clip: content-box;
1730
+ }
1731
+
1732
+ /* Where two bars meet. Left unpainted it is a white square in the corner of a dark panel. */
1733
+ .terminal-panel ::-webkit-scrollbar-corner,
1734
+ .terminal-panel::-webkit-scrollbar-corner {
1735
+ background: transparent;
1736
+ }