gloomberb 0.6.1 → 0.7.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.
Files changed (208) hide show
  1. package/package.json +1 -1
  2. package/patches/@opentui%2Fcore@0.1.90.patch +48 -24
  3. package/src/app.tsx +158 -57
  4. package/src/capabilities/factories.ts +3 -0
  5. package/src/cli/registry.test.ts +0 -1
  6. package/src/components/chart/chart-controller.ts +0 -1
  7. package/src/components/chart/chart-data.ts +1 -31
  8. package/src/components/chart/chart-renderer.ts +94 -12
  9. package/src/components/chart/chart-resolution.test.ts +0 -36
  10. package/src/components/chart/chart-resolution.ts +0 -27
  11. package/src/components/chart/chart.test.ts +60 -45
  12. package/src/components/chart/comparison-chart-data.test.ts +1 -5
  13. package/src/components/chart/comparison-chart-data.ts +1 -4
  14. package/src/components/chart/comparison-chart-renderer.test.ts +26 -0
  15. package/src/components/chart/comparison-chart-renderer.ts +13 -6
  16. package/src/components/chart/comparison-stock-chart.tsx +63 -39
  17. package/src/components/chart/price-axis-labels.tsx +114 -0
  18. package/src/components/chart/static-chart-surface.tsx +30 -4
  19. package/src/components/chart/stock-chart.test.ts +0 -16
  20. package/src/components/chart/stock-chart.tsx +69 -41
  21. package/src/components/chart/time-axis-label.tsx +120 -0
  22. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +0 -28
  23. package/src/components/command-bar/command-bar.test.tsx +182 -36
  24. package/src/components/command-bar/command-bar.tsx +943 -1016
  25. package/src/components/command-bar/command-registry.ts +0 -10
  26. package/src/components/command-bar/confirm-body.tsx +51 -0
  27. package/src/components/command-bar/helpers.ts +2 -5
  28. package/src/components/command-bar/list-model.ts +76 -0
  29. package/src/components/command-bar/theme-picker.tsx +254 -0
  30. package/src/components/command-bar/view-model.test.ts +4 -4
  31. package/src/components/command-bar/view-model.ts +1 -20
  32. package/src/components/command-bar/workflow-ops.ts +4 -0
  33. package/src/components/command-bar/workflow-types.ts +1 -2
  34. package/src/components/data-table-stack-view.test.tsx +39 -4
  35. package/src/components/data-table-stack-view.tsx +11 -104
  36. package/src/components/data-table-view.tsx +43 -79
  37. package/src/components/feed-data-table-stack-view.tsx +10 -17
  38. package/src/components/feed-stack-controller.ts +39 -0
  39. package/src/components/index.ts +5 -1
  40. package/src/components/layout/detached-pane-shell.tsx +29 -10
  41. package/src/components/layout/floating-pane.tsx +35 -4
  42. package/src/components/layout/header.tsx +12 -7
  43. package/src/components/layout/pane-content.test.tsx +64 -0
  44. package/src/components/layout/pane-content.tsx +2 -0
  45. package/src/components/layout/pane-cycle-order.test.ts +30 -0
  46. package/src/components/layout/pane-cycle-order.ts +32 -0
  47. package/src/components/layout/pane-footer.tsx +11 -4
  48. package/src/components/layout/pane-header.tsx +5 -5
  49. package/src/components/layout/pane-sizing.ts +9 -2
  50. package/src/components/layout/pane-title.ts +4 -1
  51. package/src/components/layout/pane.tsx +11 -5
  52. package/src/components/layout/shell.test.tsx +264 -22
  53. package/src/components/layout/shell.tsx +46 -8
  54. package/src/components/layout/status-bar.tsx +2 -0
  55. package/src/components/onboarding/onboarding-wizard.tsx +1 -2
  56. package/src/components/pane-settings-dialog.tsx +12 -4
  57. package/src/components/pane-template-wizard.test.tsx +3 -4
  58. package/src/components/speedometer-gauge.test.tsx +45 -0
  59. package/src/components/speedometer-gauge.tsx +562 -0
  60. package/src/components/table-view-shared.tsx +218 -0
  61. package/src/components/ticker-list-table-view.tsx +57 -73
  62. package/src/components/ticker-list-table.test.tsx +82 -1
  63. package/src/components/ticker-list-table.tsx +22 -9
  64. package/src/components/ui/confirm-dialog.tsx +66 -0
  65. package/src/components/ui/data-table.tsx +30 -19
  66. package/src/components/ui/frame.tsx +2 -0
  67. package/src/components/ui/index.ts +0 -1
  68. package/src/components/ui/page-stack-view.tsx +39 -10
  69. package/src/components/ui/table-layout.test.ts +42 -0
  70. package/src/components/ui/table-layout.ts +49 -4
  71. package/src/components/ui/tabs.tsx +34 -1
  72. package/src/components/ui/ui.test.tsx +168 -21
  73. package/src/core/state/app-state.ts +10 -17
  74. package/src/market-data/coordinator.test.ts +76 -1
  75. package/src/market-data/coordinator.ts +218 -49
  76. package/src/news/aggregator.test.ts +49 -1
  77. package/src/news/aggregator.ts +48 -3
  78. package/src/news/hooks.ts +6 -2
  79. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +46 -16
  80. package/src/plugins/builtin/ai/providers.ts +0 -17
  81. package/src/plugins/builtin/ai/screener-pane.test.tsx +3 -4
  82. package/src/plugins/builtin/ai/screener-pane.tsx +3 -5
  83. package/src/plugins/builtin/analytics/index.tsx +1 -18
  84. package/src/plugins/builtin/ask-ai.test.tsx +89 -2
  85. package/src/plugins/builtin/broker-manager/index.test.tsx +2 -2
  86. package/src/plugins/builtin/broker-manager/index.tsx +19 -36
  87. package/src/plugins/builtin/chat-controller.test.ts +188 -1
  88. package/src/plugins/builtin/chat-controller.ts +537 -220
  89. package/src/plugins/builtin/chat.test.tsx +431 -13
  90. package/src/plugins/builtin/chat.tsx +1134 -227
  91. package/src/plugins/builtin/cloud-tweets.tsx +1 -1
  92. package/src/plugins/builtin/comparison-chart.test.tsx +53 -2
  93. package/src/plugins/builtin/comparison-chart.tsx +11 -5
  94. package/src/plugins/builtin/correlation/index.tsx +1 -6
  95. package/src/plugins/builtin/debug.tsx +1 -3
  96. package/src/plugins/builtin/econ/index.tsx +1 -2
  97. package/src/plugins/builtin/fear-greed/fear-greed-data.test.ts +72 -0
  98. package/src/plugins/builtin/fear-greed/fear-greed-data.ts +366 -0
  99. package/src/plugins/builtin/fear-greed/index.tsx +587 -0
  100. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  101. package/src/plugins/builtin/help.test.tsx +2 -2
  102. package/src/plugins/builtin/help.tsx +2 -2
  103. package/src/plugins/builtin/holders/index.test.ts +13 -1
  104. package/src/plugins/builtin/holders/index.tsx +64 -29
  105. package/src/plugins/builtin/insider/index.tsx +22 -89
  106. package/src/plugins/builtin/market-movers/index.tsx +13 -19
  107. package/src/plugins/builtin/market-movers/screener.test.ts +39 -1
  108. package/src/plugins/builtin/market-movers/screener.ts +70 -17
  109. package/src/plugins/builtin/news-wire/breaking-pane.tsx +3 -2
  110. package/src/plugins/builtin/news-wire/index.tsx +41 -2
  111. package/src/plugins/builtin/news-wire/industry-pane.tsx +4 -18
  112. package/src/plugins/builtin/news-wire/news-detail-view.tsx +46 -4
  113. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +3 -2
  114. package/src/plugins/builtin/news-wire/news-table.tsx +11 -11
  115. package/src/plugins/builtin/news.tsx +4 -16
  116. package/src/plugins/builtin/notes.tsx +19 -55
  117. package/src/plugins/builtin/options.test.tsx +58 -2
  118. package/src/plugins/builtin/options.tsx +110 -65
  119. package/src/plugins/builtin/plugin-groups.ts +2 -0
  120. package/src/plugins/builtin/portfolio-list/cli/commands.ts +1 -1
  121. package/src/plugins/builtin/portfolio-list/pane.tsx +16 -39
  122. package/src/plugins/builtin/research/index.test.ts +94 -0
  123. package/src/plugins/builtin/research/index.tsx +164 -33
  124. package/src/plugins/builtin/sec-filing-display.tsx +84 -0
  125. package/src/plugins/builtin/sec.tsx +13 -66
  126. package/src/plugins/builtin/sectors/index.tsx +1 -1
  127. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +0 -1
  128. package/src/plugins/builtin/ticker-detail/index.tsx +0 -1
  129. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +0 -1
  130. package/src/plugins/builtin/ticker-detail/pane.tsx +28 -22
  131. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +3 -4
  132. package/src/plugins/builtin/ticker-detail.test.tsx +38 -0
  133. package/src/plugins/builtin/world-indices/index.tsx +1 -1
  134. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  135. package/src/plugins/ibkr/broker-adapter.ts +1 -3
  136. package/src/plugins/ibkr/gateway-service-native.ts +1 -5
  137. package/src/plugins/ibkr/gateway-service.ts +1 -1
  138. package/src/plugins/ibkr/index.test.tsx +0 -2
  139. package/src/plugins/ibkr/trade-tab.test.tsx +3 -4
  140. package/src/plugins/pane-manager.ts +0 -13
  141. package/src/plugins/plugin-runtime.test.tsx +40 -6
  142. package/src/plugins/plugin-runtime.tsx +20 -10
  143. package/src/plugins/prediction-markets/cli.ts +0 -1
  144. package/src/plugins/prediction-markets/controller-data.ts +0 -2
  145. package/src/plugins/prediction-markets/controller.ts +0 -15
  146. package/src/plugins/prediction-markets/detail/pane.tsx +1 -0
  147. package/src/plugins/prediction-markets/pane.test.tsx +3 -11
  148. package/src/plugins/prediction-markets/test-helpers.tsx +1 -1
  149. package/src/plugins/registry.ts +13 -42
  150. package/src/plugins/ticker-navigation.test.ts +66 -0
  151. package/src/plugins/ticker-navigation.ts +44 -0
  152. package/src/react/input.ts +2 -1
  153. package/src/renderers/electrobun/bun/index.ts +32 -11
  154. package/src/renderers/electrobun/bun/window-focus.ts +37 -0
  155. package/src/renderers/electrobun/shared/protocol.ts +7 -1
  156. package/src/renderers/electrobun/view/app-services.ts +88 -31
  157. package/src/renderers/electrobun/view/backend-rpc.ts +15 -0
  158. package/src/renderers/electrobun/view/data-table.tsx +138 -101
  159. package/src/renderers/electrobun/view/desktop-controls.tsx +42 -20
  160. package/src/renderers/electrobun/view/desktop-window-bridge.ts +12 -1
  161. package/src/renderers/electrobun/view/dialog-host.tsx +13 -1
  162. package/src/renderers/electrobun/view/input-host.tsx +74 -10
  163. package/src/renderers/electrobun/view/key-event.ts +1 -1
  164. package/src/renderers/electrobun/view/main.tsx +18 -1
  165. package/src/renderers/electrobun/view/styles.css +329 -0
  166. package/src/renderers/electrobun/view/toast-host.tsx +29 -3
  167. package/src/renderers/electrobun/view/ui-host.tsx +42 -0
  168. package/src/renderers/opentui/chart-surface.tsx +179 -0
  169. package/src/renderers/opentui/input-host.tsx +1 -1
  170. package/src/renderers/opentui/start.tsx +48 -30
  171. package/src/renderers/opentui/test-utils.tsx +17 -0
  172. package/src/renderers/opentui/ui-host.tsx +5 -2
  173. package/src/sources/gloomberb-cloud.test.ts +86 -1
  174. package/src/sources/gloomberb-cloud.ts +117 -21
  175. package/src/sources/provider-router.test.ts +122 -0
  176. package/src/sources/provider-router.ts +198 -15
  177. package/src/sources/yahoo-finance.test.ts +44 -0
  178. package/src/sources/yahoo-finance.ts +6 -0
  179. package/src/state/app-bootstrap.ts +20 -7
  180. package/src/state/app-context-selector.test.tsx +189 -4
  181. package/src/state/app-context.test.ts +65 -0
  182. package/src/state/app-context.tsx +48 -16
  183. package/src/state/use-inline-tickers.test.tsx +73 -0
  184. package/src/state/use-inline-tickers.ts +19 -10
  185. package/src/test-support/plugin-runtime.ts +3 -2
  186. package/src/theme/colors.test.ts +75 -0
  187. package/src/theme/colors.ts +76 -6
  188. package/src/theme/theme-context.tsx +26 -0
  189. package/src/theme/themes.test.ts +23 -1
  190. package/src/theme/themes.ts +115 -0
  191. package/src/types/broker.ts +1 -1
  192. package/src/types/capability-route-source.ts +1 -0
  193. package/src/types/data-provider.ts +15 -2
  194. package/src/types/desktop-window.ts +6 -0
  195. package/src/types/financials.ts +2 -0
  196. package/src/types/plugin.ts +1 -3
  197. package/src/ui/host.tsx +7 -0
  198. package/src/utils/api-client.test.ts +32 -0
  199. package/src/utils/api-client.ts +119 -18
  200. package/src/utils/back-navigation.ts +24 -1
  201. package/src/utils/double-escape-close.test.ts +33 -0
  202. package/src/utils/double-escape-close.ts +40 -0
  203. package/src/utils/throttled-fetch.test.ts +22 -0
  204. package/src/utils/throttled-fetch.ts +3 -1
  205. package/src/utils/ticker-search.ts +53 -9
  206. package/src/version.ts +1 -1
  207. package/src/plugins/builtin/news-wire/feed-pane.tsx +0 -21
  208. package/src/plugins/builtin/news-wire/top-pane.tsx +0 -21
@@ -14,7 +14,7 @@ import {
14
14
  type RefObject,
15
15
  } from "react";
16
16
  import { TextAttributes, type ScrollBoxRenderable } from "../../../ui/host";
17
- import { colors, hoverBg } from "../../../theme/colors";
17
+ import { useThemeColors } from "../../../theme/theme-context";
18
18
  import { useAppDispatch, usePaneInstance } from "../../../state/app-context";
19
19
  import { useRafCallback } from "../../../react/use-raf-callback";
20
20
  import { measurePerf } from "../../../utils/perf-marks";
@@ -24,6 +24,11 @@ import type {
24
24
  DataTableProps,
25
25
  DataTableSectionHeader,
26
26
  } from "../../../components/ui/data-table";
27
+ import {
28
+ buildTableGridTemplateColumns,
29
+ getTableWidth,
30
+ hasMeaningfulTableHorizontalOverflow,
31
+ } from "../../../components/ui/table-layout";
27
32
  import { WEB_CELL_HEIGHT, WEB_CELL_WIDTH } from "./input-host";
28
33
  import { useScrollbarActivity } from "./scrollbar-activity";
29
34
 
@@ -35,6 +40,14 @@ interface VirtualRow {
35
40
  }
36
41
 
37
42
  const TABLE_INLINE_PADDING_PX = 8;
43
+ const CSS_BG = "var(--gloom-bg)";
44
+ const CSS_PANEL = "var(--gloom-panel)";
45
+ const CSS_TEXT = "var(--gloom-text)";
46
+ const CSS_TEXT_DIM = "var(--gloom-text-dim)";
47
+ const CSS_TEXT_BRIGHT = "var(--gloom-text-bright)";
48
+ const CSS_SELECTED = "var(--gloom-selected)";
49
+ const CSS_SELECTED_TEXT = "var(--gloom-selected-text)";
50
+ const CSS_HOVER_BG = "var(--gloom-hover-bg)";
38
51
 
39
52
  function hasAttribute(attributes: unknown, flag: number): boolean {
40
53
  return typeof attributes === "number" && (attributes & flag) !== 0;
@@ -83,32 +96,6 @@ function clippedCellTextStyle(
83
96
  };
84
97
  }
85
98
 
86
- function columnMinCh(column: DataTableColumn): number {
87
- const width = Math.max(1, Math.floor(column.width));
88
- if ((column.flexGrow ?? 0) > 0) {
89
- return Math.max(8, Math.min(18, Math.floor(width * 0.35)));
90
- }
91
- if (column.align === "right") {
92
- return Math.max(3, Math.min(width, 8));
93
- }
94
- if (width >= 16) {
95
- return Math.max(8, Math.min(16, Math.floor(width * 0.35)));
96
- }
97
- return Math.max(1, Math.min(width, 8));
98
- }
99
-
100
- function columnFlexWeight(column: DataTableColumn): number {
101
- const flexGrow = column.flexGrow ?? 0;
102
- const baseWeight = Math.max(1, Math.floor(column.width));
103
- return flexGrow > 0 ? baseWeight * Math.max(1, flexGrow) : baseWeight;
104
- }
105
-
106
- function buildGridTemplateColumns(columns: readonly DataTableColumn[]): string {
107
- return columns
108
- .map((column) => `minmax(${columnMinCh(column)}ch, ${columnFlexWeight(column)}fr)`)
109
- .join(" ");
110
- }
111
-
112
99
  function toCellY(pixels: number): number {
113
100
  return Math.max(0, Math.round(pixels / WEB_CELL_HEIGHT));
114
101
  }
@@ -264,6 +251,82 @@ function renderHeaderLabel<C extends DataTableColumn>(
264
251
  };
265
252
  }
266
253
 
254
+ function WebDataTableHeader<C extends DataTableColumn>({
255
+ columns,
256
+ focusPane,
257
+ gridTemplateColumns,
258
+ onHeaderClick,
259
+ sortColumnId,
260
+ sortDirection,
261
+ }: {
262
+ columns: C[];
263
+ focusPane: () => void;
264
+ gridTemplateColumns: string;
265
+ onHeaderClick: (columnId: string) => void;
266
+ sortColumnId: string | null;
267
+ sortDirection: "asc" | "desc";
268
+ }) {
269
+ useThemeColors();
270
+ return (
271
+ <div
272
+ data-gloom-role="data-table-header-row"
273
+ style={{
274
+ position: "sticky",
275
+ top: 0,
276
+ zIndex: 2,
277
+ display: "grid",
278
+ gridTemplateColumns,
279
+ columnGap: "1ch",
280
+ alignItems: "center",
281
+ width: "100%",
282
+ minWidth: 0,
283
+ height: WEB_CELL_HEIGHT,
284
+ paddingLeft: TABLE_INLINE_PADDING_PX,
285
+ paddingRight: TABLE_INLINE_PADDING_PX,
286
+ boxSizing: "border-box",
287
+ backgroundColor: CSS_PANEL,
288
+ }}
289
+ >
290
+ {columns.map((column) => {
291
+ const { isSorted, text } = renderHeaderLabel(
292
+ column,
293
+ sortColumnId,
294
+ sortDirection,
295
+ );
296
+ return (
297
+ <div
298
+ key={column.id}
299
+ data-gloom-role="data-table-header-cell"
300
+ data-gloom-interactive="true"
301
+ style={{
302
+ minWidth: 0,
303
+ height: WEB_CELL_HEIGHT,
304
+ overflow: "hidden",
305
+ backgroundColor: column.headerBackgroundColor ?? CSS_PANEL,
306
+ }}
307
+ onMouseDown={(event) => {
308
+ focusPane();
309
+ event.preventDefault();
310
+ onHeaderClick(column.id);
311
+ }}
312
+ >
313
+ <span
314
+ title={text}
315
+ style={clippedCellTextStyle(
316
+ column,
317
+ isSorted ? CSS_TEXT : column.headerColor ?? CSS_TEXT_DIM,
318
+ TextAttributes.BOLD,
319
+ )}
320
+ >
321
+ {text}
322
+ </span>
323
+ </div>
324
+ );
325
+ })}
326
+ </div>
327
+ );
328
+ }
329
+
267
330
  function WebDataTableRowInner<
268
331
  T,
269
332
  C extends DataTableColumn,
@@ -302,6 +365,7 @@ function WebDataTableRowInner<
302
365
  selected: boolean;
303
366
  setHoveredIdx: (index: number | null) => void;
304
367
  }) {
368
+ useThemeColors();
305
369
  const sectionHeader: DataTableSectionHeader | null =
306
370
  renderSectionHeader?.(item, index) ?? null;
307
371
  const baseRowStyle: CSSProperties = {
@@ -329,7 +393,7 @@ function WebDataTableRowInner<
329
393
  data-gloom-role="data-table-section-header"
330
394
  style={{
331
395
  ...baseRowStyle,
332
- backgroundColor: sectionHeader.backgroundColor ?? colors.bg,
396
+ backgroundColor: sectionHeader.backgroundColor ?? CSS_BG,
333
397
  }}
334
398
  onMouseDown={(event) => {
335
399
  focusPane();
@@ -340,7 +404,7 @@ function WebDataTableRowInner<
340
404
  title={sectionHeader.text}
341
405
  style={{
342
406
  ...cellTextStyle(
343
- sectionHeader.color ?? colors.textBright,
407
+ sectionHeader.color ?? CSS_TEXT_BRIGHT,
344
408
  sectionHeader.attributes ?? TextAttributes.BOLD,
345
409
  ),
346
410
  gridColumn: "1 / -1",
@@ -357,10 +421,10 @@ function WebDataTableRowInner<
357
421
  const rowState = { selected, hovered };
358
422
  const rowBackgroundColor = getRowBackgroundColor?.(item, index, rowState);
359
423
  const rowBg = selected
360
- ? colors.selected
424
+ ? CSS_SELECTED
361
425
  : hovered
362
- ? hoverBg()
363
- : rowBackgroundColor ?? colors.bg;
426
+ ? CSS_HOVER_BG
427
+ : rowBackgroundColor ?? CSS_BG;
364
428
 
365
429
  return (
366
430
  <div
@@ -437,7 +501,7 @@ function WebDataTableRowInner<
437
501
  title={cell.text}
438
502
  style={clippedCellTextStyle(
439
503
  column,
440
- cell.color ?? (selected ? colors.selectedText : colors.text),
504
+ cell.color ?? (selected ? CSS_SELECTED_TEXT : CSS_TEXT),
441
505
  cell.attributes ?? TextAttributes.NONE,
442
506
  )}
443
507
  >
@@ -489,9 +553,11 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
489
553
  const headerVertical = useScrollbarState(false);
490
554
  const bodyHorizontal = useScrollbarState(showHorizontalScrollbar);
491
555
  const bodyVertical = useScrollbarState(true);
556
+ const [viewportWidth, setViewportWidth] = useState(0);
492
557
  const [scrollbarActive, markScrollbarActive] = useScrollbarActivity();
558
+ const tableWidth = useMemo(() => getTableWidth(columns), [columns]);
493
559
  const gridTemplateColumns = useMemo(
494
- () => buildGridTemplateColumns(columns),
560
+ () => buildTableGridTemplateColumns(columns),
495
561
  [columns],
496
562
  );
497
563
  const selectRow = useCallback((item: T, index: number) => {
@@ -538,6 +604,25 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
538
604
  const totalHeight = virtualize
539
605
  ? rowVirtualizer.getTotalSize()
540
606
  : WEB_CELL_HEIGHT + items.length * WEB_CELL_HEIGHT;
607
+ const horizontalScrollEnabled = showHorizontalScrollbar
608
+ && hasMeaningfulTableHorizontalOverflow(tableWidth, viewportWidth);
609
+ const scrollContentWidth = horizontalScrollEnabled
610
+ ? Math.max(1, tableWidth * WEB_CELL_WIDTH)
611
+ : "100%";
612
+ const measureViewportWidth = useCallback(() => {
613
+ const element = bodyElementRef.current;
614
+ const nextValue = element ? toCellX(element.clientWidth) : 0;
615
+ setViewportWidth((current) => current === nextValue ? current : nextValue);
616
+ }, []);
617
+
618
+ useEffect(() => {
619
+ measureViewportWidth();
620
+ const element = bodyElementRef.current;
621
+ if (!element || typeof ResizeObserver === "undefined") return;
622
+ const observer = new ResizeObserver(measureViewportWidth);
623
+ observer.observe(element);
624
+ return () => observer.disconnect();
625
+ }, [measureViewportWidth]);
541
626
 
542
627
  useEffect(() => {
543
628
  if (scrollToIndex == null || items.length === 0) return;
@@ -589,11 +674,11 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
589
674
 
590
675
  useEffect(() => {
591
676
  headerHorizontal.bar.visible = false;
592
- bodyHorizontal.bar.visible = showHorizontalScrollbar;
593
- if (!showHorizontalScrollbar) {
677
+ bodyHorizontal.bar.visible = horizontalScrollEnabled;
678
+ if (!horizontalScrollEnabled) {
594
679
  if (bodyElementRef.current) bodyElementRef.current.scrollLeft = 0;
595
680
  }
596
- }, [bodyHorizontal.bar, headerHorizontal.bar, showHorizontalScrollbar]);
681
+ }, [bodyHorizontal.bar, headerHorizontal.bar, horizontalScrollEnabled]);
597
682
 
598
683
  const rootStyle: CSSProperties = {
599
684
  display: "flex",
@@ -602,7 +687,7 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
602
687
  width: "100%",
603
688
  minWidth: 0,
604
689
  minHeight: 0,
605
- backgroundColor: colors.bg,
690
+ backgroundColor: CSS_BG,
606
691
  overflow: "hidden",
607
692
  };
608
693
  const bodyScrollerStyle: CSSProperties = {
@@ -610,9 +695,9 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
610
695
  flex: "1 1 0px",
611
696
  minWidth: 0,
612
697
  minHeight: 0,
613
- overflowX: showHorizontalScrollbar ? "auto" : "hidden",
698
+ overflowX: horizontalScrollEnabled ? "auto" : "hidden",
614
699
  overflowY: "auto",
615
- backgroundColor: colors.bg,
700
+ backgroundColor: CSS_BG,
616
701
  };
617
702
 
618
703
  return (
@@ -621,7 +706,7 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
621
706
  ref={bodyElementRef}
622
707
  data-gloom-role="data-table-body-scroll"
623
708
  data-gloom-scrollbar-x={
624
- showHorizontalScrollbar && bodyHorizontal.visible
709
+ horizontalScrollEnabled && bodyHorizontal.visible
625
710
  ? "visible"
626
711
  : "hidden"
627
712
  }
@@ -646,82 +731,34 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
646
731
  data-gloom-role="data-table-scroll-content"
647
732
  style={{
648
733
  position: "relative",
649
- width: "100%",
734
+ width: scrollContentWidth,
650
735
  height: items.length > 0 ? totalHeight : "100%",
651
736
  minHeight: WEB_CELL_HEIGHT,
652
737
  }}
653
738
  >
654
- <div
655
- data-gloom-role="data-table-header-row"
656
- style={{
657
- position: "sticky",
658
- top: 0,
659
- zIndex: 2,
660
- display: "grid",
661
- gridTemplateColumns,
662
- columnGap: "1ch",
663
- alignItems: "center",
664
- width: "100%",
665
- minWidth: 0,
666
- height: WEB_CELL_HEIGHT,
667
- paddingLeft: TABLE_INLINE_PADDING_PX,
668
- paddingRight: TABLE_INLINE_PADDING_PX,
669
- boxSizing: "border-box",
670
- backgroundColor: colors.panel,
671
- }}
672
- >
673
- {columns.map((column) => {
674
- const { isSorted, text } = renderHeaderLabel(
675
- column,
676
- sortColumnId,
677
- sortDirection,
678
- );
679
- return (
680
- <div
681
- key={column.id}
682
- data-gloom-role="data-table-header-cell"
683
- data-gloom-interactive="true"
684
- style={{
685
- minWidth: 0,
686
- height: WEB_CELL_HEIGHT,
687
- overflow: "hidden",
688
- backgroundColor: column.headerBackgroundColor ?? colors.panel,
689
- }}
690
- onMouseDown={(event) => {
691
- focusPane();
692
- event.preventDefault();
693
- onHeaderClick(column.id);
694
- }}
695
- >
696
- <span
697
- title={text}
698
- style={clippedCellTextStyle(
699
- column,
700
- isSorted ? colors.text : column.headerColor ?? colors.textDim,
701
- TextAttributes.BOLD,
702
- )}
703
- >
704
- {text}
705
- </span>
706
- </div>
707
- );
708
- })}
709
- </div>
739
+ <WebDataTableHeader
740
+ columns={columns}
741
+ focusPane={focusPane}
742
+ gridTemplateColumns={gridTemplateColumns}
743
+ onHeaderClick={onHeaderClick}
744
+ sortColumnId={sortColumnId}
745
+ sortDirection={sortDirection}
746
+ />
710
747
  {items.length === 0 ? (
711
748
  emptyContent ?? (
712
749
  <div
713
750
  style={{
714
751
  width: "100%",
715
752
  padding: `${WEB_CELL_HEIGHT}px ${WEB_CELL_WIDTH}px`,
716
- color: colors.textDim,
753
+ color: CSS_TEXT_DIM,
717
754
  lineHeight: "var(--cell-h)",
718
755
  }}
719
756
  >
720
- <div style={cellTextStyle(colors.textBright, TextAttributes.BOLD)}>
757
+ <div style={cellTextStyle(CSS_TEXT_BRIGHT, TextAttributes.BOLD)}>
721
758
  {emptyStateTitle}
722
759
  </div>
723
760
  {emptyStateHint ? (
724
- <div style={cellTextStyle(colors.textDim, TextAttributes.NONE)}>
761
+ <div style={cellTextStyle(CSS_TEXT_DIM, TextAttributes.NONE)}>
725
762
  {emptyStateHint}
726
763
  </div>
727
764
  ) : null}
@@ -4,6 +4,7 @@ import { Box, Input, ScrollBox, Text, Textarea, editableTextContextMenuItems, us
4
4
  import { TextAttributes, type InputRenderable, type ScrollBoxRenderable } from "../../../ui";
5
5
  import { useShortcut } from "../../../react/input";
6
6
  import { blendHex, colors, hoverBg } from "../../../theme/colors";
7
+ import { useThemeColors } from "../../../theme/theme-context";
7
8
  import { isDetailBackNavigationKey } from "../../../utils/back-navigation";
8
9
  import type { ButtonProps } from "../../../components/ui/button";
9
10
  import type { TextFieldProps } from "../../../components/ui/fields";
@@ -14,13 +15,27 @@ import type { PageStackViewProps } from "../../../components/ui/page-stack-view"
14
15
  import type { SegmentedControlProps } from "../../../components/ui/toggle";
15
16
 
16
17
  const CONTROL_RADIUS = 6;
17
- const PANEL_BORDER = blendHex(colors.border, colors.borderFocused, 0.18);
18
- const PANEL_FILL = blendHex(colors.panel, colors.bg, 0.22);
18
+
19
+ function panelBorder(): string {
20
+ return blendHex(colors.border, colors.borderFocused, 0.18);
21
+ }
22
+
23
+ function panelFill(): string {
24
+ return blendHex(colors.panel, colors.bg, 0.22);
25
+ }
26
+
27
+ function subtlePanelFill(): string {
28
+ return blendHex(colors.panel, colors.bg, 0.42);
29
+ }
30
+
31
+ function selectedPanelFill(): string {
32
+ return blendHex(colors.selected, colors.bg, 0.42);
33
+ }
19
34
 
20
35
  function controlBorderColor(focused = false, active = false): string {
21
36
  if (active) return colors.borderFocused;
22
37
  if (focused) return blendHex(colors.borderFocused, colors.textBright, 0.24);
23
- return PANEL_BORDER;
38
+ return panelBorder();
24
39
  }
25
40
 
26
41
  function controlShadow(active = false): string {
@@ -33,8 +48,8 @@ function buttonPalette(props: Pick<ButtonProps, "variant" | "active" | "disabled
33
48
  if (props.disabled) {
34
49
  return {
35
50
  fg: colors.textMuted,
36
- bg: "rgba(63, 72, 82, 0.35)",
37
- border: PANEL_BORDER,
51
+ bg: subtlePanelFill(),
52
+ border: panelBorder(),
38
53
  };
39
54
  }
40
55
  if (props.active) {
@@ -62,14 +77,14 @@ function buttonPalette(props: Pick<ButtonProps, "variant" | "active" | "disabled
62
77
  return {
63
78
  fg: colors.textDim,
64
79
  bg: "rgba(0, 0, 0, 0)",
65
- border: PANEL_BORDER,
80
+ border: panelBorder(),
66
81
  };
67
82
  case "secondary":
68
83
  default:
69
84
  return {
70
85
  fg: colors.text,
71
- bg: PANEL_FILL,
72
- border: PANEL_BORDER,
86
+ bg: panelFill(),
87
+ border: panelBorder(),
73
88
  };
74
89
  }
75
90
  }
@@ -83,6 +98,7 @@ export function WebButton({
83
98
  shortcut,
84
99
  width,
85
100
  }: ButtonProps) {
101
+ useThemeColors();
86
102
  const palette = buttonPalette({ variant, active, disabled });
87
103
 
88
104
  return (
@@ -142,6 +158,7 @@ export function WebTextField({
142
158
  placeholderColor = colors.textDim,
143
159
  onMouseDown,
144
160
  }: TextFieldProps) {
161
+ useThemeColors();
145
162
  const localInputRef = useRef<InputRenderable>(null);
146
163
  const resolvedInputRef = inputRef ?? localInputRef;
147
164
  const renderer = useRendererHost();
@@ -231,6 +248,7 @@ export function WebMessageComposer({
231
248
  keyBindings,
232
249
  wrapText = false,
233
250
  }: MessageComposerProps) {
251
+ useThemeColors();
234
252
  const borderColor = focused
235
253
  ? blendHex(colors.borderFocused, colors.textBright, 0.24)
236
254
  : colors.border;
@@ -248,7 +266,7 @@ export function WebMessageComposer({
248
266
  flexDirection="row"
249
267
  width={width}
250
268
  height={height}
251
- backgroundColor={PANEL_FILL}
269
+ backgroundColor={panelFill()}
252
270
  onMouseDown={requestFocus}
253
271
  data-gloom-role="desktop-message-composer"
254
272
  style={{
@@ -347,10 +365,11 @@ export function WebListView({
347
365
  scrollable = false,
348
366
  autoScrollToIndex = true,
349
367
  }: ListViewProps) {
368
+ useThemeColors();
350
369
  const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
351
370
  const scrollRef = useRef<ScrollBoxRenderable>(null);
352
371
  const baseBg = bgColor ?? colors.bg;
353
- const activeBg = selectedBgColor ?? "rgba(84, 201, 159, 0.12)";
372
+ const activeBg = selectedBgColor ?? selectedPanelFill();
354
373
  const rowHoverBg = hoverBgColor ?? hoverBg();
355
374
  const selectedItem = selectedIndex >= 0 ? items[selectedIndex] : undefined;
356
375
  const activeScrollIndex = scrollIndex ?? selectedIndex;
@@ -435,10 +454,10 @@ export function WebListView({
435
454
  backgroundColor: "transparent",
436
455
  }
437
456
  : {
438
- border: `1px solid ${PANEL_BORDER}`,
457
+ border: `1px solid ${panelBorder()}`,
439
458
  borderRadius: CONTROL_RADIUS,
440
459
  padding: 4,
441
- backgroundColor: "rgba(9, 12, 15, 0.22)",
460
+ backgroundColor: subtlePanelFill(),
442
461
  }}
443
462
  >
444
463
  <Box flexDirection="column" gap={rowGap}>
@@ -454,9 +473,9 @@ export function WebListView({
454
473
  {showSelectedDescription && selectedItem?.description && (
455
474
  <Box
456
475
  flexDirection="row"
457
- backgroundColor="rgba(9, 12, 15, 0.22)"
476
+ backgroundColor={subtlePanelFill()}
458
477
  style={{
459
- border: `1px solid ${PANEL_BORDER}`,
478
+ border: `1px solid ${panelBorder()}`,
460
479
  borderRadius: CONTROL_RADIUS,
461
480
  paddingInline: 10,
462
481
  }}
@@ -475,12 +494,13 @@ export function WebSegmentedControl({
475
494
  value,
476
495
  onChange,
477
496
  }: SegmentedControlProps) {
497
+ useThemeColors();
478
498
  return (
479
499
  <Box
480
500
  flexDirection="row"
481
- backgroundColor="rgba(8, 11, 14, 0.32)"
501
+ backgroundColor={panelFill()}
482
502
  style={{
483
- border: `1px solid ${PANEL_BORDER}`,
503
+ border: `1px solid ${panelBorder()}`,
484
504
  borderRadius: CONTROL_RADIUS,
485
505
  padding: 2,
486
506
  }}
@@ -524,6 +544,7 @@ export function WebDialogFrame({
524
544
  footer,
525
545
  showTitleDivider = true,
526
546
  }: DialogFrameProps) {
547
+ useThemeColors();
527
548
  return (
528
549
  <Box flexDirection="column" style={{ padding: 14 }}>
529
550
  <Box
@@ -531,7 +552,7 @@ export function WebDialogFrame({
531
552
  flexDirection="row"
532
553
  alignItems="center"
533
554
  style={{
534
- borderBottom: showTitleDivider ? `1px solid ${PANEL_BORDER}` : "none",
555
+ borderBottom: showTitleDivider ? `1px solid ${panelBorder()}` : "none",
535
556
  paddingBottom: showTitleDivider ? 8 : 0,
536
557
  marginBottom: showTitleDivider ? 10 : 14,
537
558
  }}
@@ -545,7 +566,7 @@ export function WebDialogFrame({
545
566
  <Box
546
567
  height={1}
547
568
  style={{
548
- borderTop: `1px solid ${PANEL_BORDER}`,
569
+ borderTop: `1px solid ${panelBorder()}`,
549
570
  paddingTop: 8,
550
571
  marginTop: 10,
551
572
  }}
@@ -569,6 +590,7 @@ export function WebPageStackView({
569
590
  backLabel = "Back",
570
591
  backHint,
571
592
  }: PageStackViewProps) {
593
+ useThemeColors();
572
594
  useShortcut((event) => {
573
595
  if (!focused || !detailOpen || !isDetailBackNavigationKey(event)) return;
574
596
  event.stopPropagation?.();
@@ -600,7 +622,7 @@ export function WebPageStackView({
600
622
  height={1}
601
623
  flexDirection="row"
602
624
  alignItems="center"
603
- backgroundColor={PANEL_FILL}
625
+ backgroundColor={panelFill()}
604
626
  onMouseDown={(event) => {
605
627
  event.preventDefault?.();
606
628
  event.stopPropagation?.();
@@ -608,7 +630,7 @@ export function WebPageStackView({
608
630
  }}
609
631
  data-gloom-interactive="true"
610
632
  style={{
611
- border: `1px solid ${PANEL_BORDER}`,
633
+ border: `1px solid ${panelBorder()}`,
612
634
  borderRadius: CONTROL_RADIUS,
613
635
  paddingInline: 8,
614
636
  cursor: "pointer",
@@ -2,9 +2,10 @@ import type { PaneRuntimeState } from "../../../core/state/app-state";
2
2
  import type {
3
3
  DesktopDockPreviewState,
4
4
  DesktopSharedStateSnapshot,
5
+ DesktopThemePreviewState,
5
6
  DesktopWindowBridge,
6
7
  } from "../../../types/desktop-window";
7
- import { backendRequest, getElectrobunBackendInitSnapshot, onDesktopDockPreview, onDesktopState } from "./backend-rpc";
8
+ import { backendRequest, getElectrobunBackendInitSnapshot, onDesktopDockPreview, onDesktopState, onDesktopThemePreview } from "./backend-rpc";
8
9
  import { detachedSnapshotKey, prepareDetachedSnapshot } from "./desktop-window-snapshot";
9
10
 
10
11
  export function createDesktopWindowBridge(kind: "main" | "detached", paneId?: string): DesktopWindowBridge {
@@ -21,6 +22,11 @@ export function createDesktopWindowBridge(kind: "main" | "detached", paneId?: st
21
22
  await backendRequest("desktop.syncMainState", { snapshot });
22
23
  }
23
24
  : undefined,
25
+ syncThemePreview: kind === "main"
26
+ ? async (preview: DesktopThemePreviewState) => {
27
+ await backendRequest("desktop.setThemePreview", { preview });
28
+ }
29
+ : undefined,
24
30
  replaceDetachedPaneState: kind === "detached"
25
31
  ? async (targetPaneId: string, paneState: PaneRuntimeState) => {
26
32
  await backendRequest("desktop.replaceDetachedPaneState", { paneId: targetPaneId, paneState });
@@ -55,5 +61,10 @@ export function createDesktopWindowBridge(kind: "main" | "detached", paneId?: st
55
61
  listener(message.preview);
56
62
  });
57
63
  },
64
+ subscribeThemePreview(listener: (preview: DesktopThemePreviewState) => void) {
65
+ return onDesktopThemePreview((message) => {
66
+ listener(message.preview);
67
+ });
68
+ },
58
69
  };
59
70
  }
@@ -2,6 +2,8 @@
2
2
  /** @jsxImportSource react */
3
3
  import { useCallback, useMemo, useState, type ReactNode } from "react";
4
4
  import { DialogHostProvider, type DialogApi } from "../../../ui/dialog";
5
+ import { blendHex, colors } from "../../../theme/colors";
6
+ import { useThemeColors } from "../../../theme/theme-context";
5
7
 
6
8
  interface DialogState {
7
9
  id: string;
@@ -12,7 +14,10 @@ interface DialogState {
12
14
  let nextDialogId = 1;
13
15
 
14
16
  export function WebDialogHostProvider({ children }: { children: ReactNode }) {
17
+ useThemeColors();
15
18
  const [dialogState, setDialogState] = useState<DialogState | null>(null);
19
+ const dialogBorder = blendHex(colors.border, colors.borderFocused, 0.18);
20
+ const dialogBg = blendHex(colors.panel, colors.bg, 0.12);
16
21
 
17
22
  const close = useCallback((value?: unknown) => {
18
23
  setDialogState((current) => {
@@ -46,7 +51,14 @@ export function WebDialogHostProvider({ children }: { children: ReactNode }) {
46
51
  if (event.target === event.currentTarget) close(undefined);
47
52
  }}
48
53
  >
49
- <div className="gloom-dialog">
54
+ <div
55
+ className="gloom-dialog"
56
+ style={{
57
+ borderColor: dialogBorder,
58
+ background: dialogBg,
59
+ color: colors.text,
60
+ }}
61
+ >
50
62
  {typeof dialogState.content === "function"
51
63
  ? dialogState.content({
52
64
  dialogId: dialogState.id,