gloomberb 0.7.0 → 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 (125) hide show
  1. package/package.json +1 -1
  2. package/src/app.tsx +149 -51
  3. package/src/capabilities/factories.ts +3 -0
  4. package/src/components/chart/chart-renderer.ts +24 -12
  5. package/src/components/chart/chart.test.ts +43 -0
  6. package/src/components/chart/comparison-chart-renderer.test.ts +26 -0
  7. package/src/components/chart/comparison-chart-renderer.ts +13 -6
  8. package/src/components/chart/comparison-stock-chart.tsx +15 -12
  9. package/src/components/chart/price-axis-labels.tsx +114 -0
  10. package/src/components/chart/static-chart-surface.tsx +2 -0
  11. package/src/components/chart/stock-chart.tsx +31 -20
  12. package/src/components/chart/time-axis-label.tsx +78 -1
  13. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +0 -28
  14. package/src/components/command-bar/command-bar.test.tsx +114 -31
  15. package/src/components/command-bar/command-bar.tsx +587 -335
  16. package/src/components/command-bar/command-registry.ts +0 -10
  17. package/src/components/command-bar/helpers.ts +2 -5
  18. package/src/components/command-bar/list-model.ts +1 -2
  19. package/src/components/command-bar/theme-picker.tsx +254 -0
  20. package/src/components/command-bar/view-model.test.ts +4 -4
  21. package/src/components/command-bar/view-model.ts +1 -1
  22. package/src/components/command-bar/workflow-types.ts +1 -2
  23. package/src/components/layout/detached-pane-shell.tsx +29 -10
  24. package/src/components/layout/floating-pane.tsx +5 -2
  25. package/src/components/layout/header.tsx +2 -0
  26. package/src/components/layout/pane-content.test.tsx +64 -0
  27. package/src/components/layout/pane-content.tsx +2 -0
  28. package/src/components/layout/pane-sizing.ts +9 -2
  29. package/src/components/layout/pane-title.ts +4 -1
  30. package/src/components/layout/pane.tsx +11 -5
  31. package/src/components/layout/shell.test.tsx +198 -0
  32. package/src/components/layout/shell.tsx +33 -2
  33. package/src/components/layout/status-bar.tsx +2 -0
  34. package/src/components/speedometer-gauge.tsx +29 -25
  35. package/src/components/table-view-shared.tsx +63 -3
  36. package/src/components/ticker-list-table-view.tsx +16 -2
  37. package/src/components/ticker-list-table.test.tsx +82 -1
  38. package/src/components/ticker-list-table.tsx +22 -9
  39. package/src/components/ui/data-table.tsx +12 -9
  40. package/src/components/ui/frame.tsx +2 -0
  41. package/src/components/ui/table-layout.test.ts +27 -1
  42. package/src/components/ui/table-layout.ts +39 -1
  43. package/src/components/ui/tabs.tsx +34 -1
  44. package/src/components/ui/ui.test.tsx +68 -1
  45. package/src/core/state/app-state.ts +4 -14
  46. package/src/market-data/coordinator.test.ts +75 -0
  47. package/src/market-data/coordinator.ts +218 -48
  48. package/src/news/aggregator.test.ts +49 -1
  49. package/src/news/aggregator.ts +48 -0
  50. package/src/news/hooks.ts +6 -2
  51. package/src/plugins/builtin/ai/screener-pane.tsx +3 -2
  52. package/src/plugins/builtin/analytics/index.tsx +1 -18
  53. package/src/plugins/builtin/chat-controller.test.ts +83 -1
  54. package/src/plugins/builtin/chat-controller.ts +491 -243
  55. package/src/plugins/builtin/chat.test.tsx +425 -7
  56. package/src/plugins/builtin/chat.tsx +1109 -211
  57. package/src/plugins/builtin/cloud-tweets.tsx +1 -0
  58. package/src/plugins/builtin/fear-greed/index.tsx +148 -39
  59. package/src/plugins/builtin/holders/index.test.ts +13 -1
  60. package/src/plugins/builtin/holders/index.tsx +54 -14
  61. package/src/plugins/builtin/market-movers/index.tsx +12 -18
  62. package/src/plugins/builtin/market-movers/screener.test.ts +39 -1
  63. package/src/plugins/builtin/market-movers/screener.ts +70 -17
  64. package/src/plugins/builtin/news-wire/breaking-pane.tsx +3 -2
  65. package/src/plugins/builtin/news-wire/industry-pane.tsx +4 -18
  66. package/src/plugins/builtin/news-wire/news-detail-view.tsx +46 -4
  67. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +3 -2
  68. package/src/plugins/builtin/notes.tsx +1 -0
  69. package/src/plugins/builtin/options.tsx +2 -16
  70. package/src/plugins/builtin/portfolio-list/pane.tsx +16 -39
  71. package/src/plugins/builtin/research/index.test.ts +94 -0
  72. package/src/plugins/builtin/research/index.tsx +159 -12
  73. package/src/plugins/builtin/ticker-detail/pane.tsx +28 -22
  74. package/src/plugins/builtin/ticker-detail.test.tsx +38 -0
  75. package/src/plugins/plugin-runtime.test.tsx +34 -0
  76. package/src/plugins/plugin-runtime.tsx +7 -2
  77. package/src/plugins/prediction-markets/controller.ts +0 -15
  78. package/src/plugins/prediction-markets/detail/pane.tsx +1 -0
  79. package/src/plugins/registry.ts +3 -3
  80. package/src/plugins/ticker-navigation.test.ts +66 -0
  81. package/src/plugins/ticker-navigation.ts +44 -0
  82. package/src/react/input.ts +1 -1
  83. package/src/renderers/electrobun/bun/index.ts +32 -11
  84. package/src/renderers/electrobun/bun/window-focus.ts +37 -0
  85. package/src/renderers/electrobun/shared/protocol.ts +7 -1
  86. package/src/renderers/electrobun/view/app-services.ts +88 -31
  87. package/src/renderers/electrobun/view/backend-rpc.ts +15 -0
  88. package/src/renderers/electrobun/view/data-table.tsx +111 -96
  89. package/src/renderers/electrobun/view/desktop-controls.tsx +8 -0
  90. package/src/renderers/electrobun/view/desktop-window-bridge.ts +12 -1
  91. package/src/renderers/electrobun/view/dialog-host.tsx +2 -0
  92. package/src/renderers/electrobun/view/input-host.tsx +1 -1
  93. package/src/renderers/electrobun/view/main.tsx +18 -1
  94. package/src/renderers/electrobun/view/styles.css +6 -8
  95. package/src/renderers/electrobun/view/toast-host.tsx +29 -3
  96. package/src/renderers/electrobun/view/ui-host.tsx +42 -0
  97. package/src/renderers/opentui/input-host.tsx +1 -1
  98. package/src/sources/gloomberb-cloud.test.ts +86 -1
  99. package/src/sources/gloomberb-cloud.ts +117 -21
  100. package/src/sources/provider-router.test.ts +122 -0
  101. package/src/sources/provider-router.ts +198 -11
  102. package/src/sources/yahoo-finance.test.ts +44 -0
  103. package/src/sources/yahoo-finance.ts +6 -0
  104. package/src/state/app-bootstrap.ts +20 -7
  105. package/src/state/app-context-selector.test.tsx +189 -4
  106. package/src/state/app-context.test.ts +20 -0
  107. package/src/state/app-context.tsx +48 -16
  108. package/src/state/use-inline-tickers.ts +13 -10
  109. package/src/theme/colors.test.ts +75 -0
  110. package/src/theme/colors.ts +64 -3
  111. package/src/theme/theme-context.tsx +26 -0
  112. package/src/types/capability-route-source.ts +1 -0
  113. package/src/types/data-provider.ts +15 -1
  114. package/src/types/desktop-window.ts +6 -0
  115. package/src/types/financials.ts +2 -0
  116. package/src/types/plugin.ts +1 -1
  117. package/src/ui/host.tsx +6 -0
  118. package/src/utils/api-client.test.ts +32 -0
  119. package/src/utils/api-client.ts +119 -18
  120. package/src/utils/double-escape-close.test.ts +33 -0
  121. package/src/utils/double-escape-close.ts +40 -0
  122. package/src/utils/throttled-fetch.test.ts +22 -0
  123. package/src/utils/throttled-fetch.ts +3 -1
  124. package/src/utils/ticker-search.ts +53 -9
  125. package/src/version.ts +1 -1
@@ -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,7 +24,11 @@ import type {
24
24
  DataTableProps,
25
25
  DataTableSectionHeader,
26
26
  } from "../../../components/ui/data-table";
27
- import { getTableWidth, hasMeaningfulTableHorizontalOverflow } from "../../../components/ui/table-layout";
27
+ import {
28
+ buildTableGridTemplateColumns,
29
+ getTableWidth,
30
+ hasMeaningfulTableHorizontalOverflow,
31
+ } from "../../../components/ui/table-layout";
28
32
  import { WEB_CELL_HEIGHT, WEB_CELL_WIDTH } from "./input-host";
29
33
  import { useScrollbarActivity } from "./scrollbar-activity";
30
34
 
@@ -36,6 +40,14 @@ interface VirtualRow {
36
40
  }
37
41
 
38
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)";
39
51
 
40
52
  function hasAttribute(attributes: unknown, flag: number): boolean {
41
53
  return typeof attributes === "number" && (attributes & flag) !== 0;
@@ -84,32 +96,6 @@ function clippedCellTextStyle(
84
96
  };
85
97
  }
86
98
 
87
- function columnMinCh(column: DataTableColumn): number {
88
- const width = Math.max(1, Math.floor(column.width));
89
- if ((column.flexGrow ?? 0) > 0) {
90
- return Math.max(8, Math.min(18, Math.floor(width * 0.35)));
91
- }
92
- if (column.align === "right") {
93
- return Math.max(3, Math.min(width, 8));
94
- }
95
- if (width >= 16) {
96
- return Math.max(8, Math.min(16, Math.floor(width * 0.35)));
97
- }
98
- return Math.max(1, Math.min(width, 8));
99
- }
100
-
101
- function columnFlexWeight(column: DataTableColumn): number {
102
- const flexGrow = column.flexGrow ?? 0;
103
- const baseWeight = Math.max(1, Math.floor(column.width));
104
- return flexGrow > 0 ? baseWeight * Math.max(1, flexGrow) : baseWeight;
105
- }
106
-
107
- function buildGridTemplateColumns(columns: readonly DataTableColumn[]): string {
108
- return columns
109
- .map((column) => `minmax(${columnMinCh(column)}ch, ${columnFlexWeight(column)}fr)`)
110
- .join(" ");
111
- }
112
-
113
99
  function toCellY(pixels: number): number {
114
100
  return Math.max(0, Math.round(pixels / WEB_CELL_HEIGHT));
115
101
  }
@@ -265,6 +251,82 @@ function renderHeaderLabel<C extends DataTableColumn>(
265
251
  };
266
252
  }
267
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
+
268
330
  function WebDataTableRowInner<
269
331
  T,
270
332
  C extends DataTableColumn,
@@ -303,6 +365,7 @@ function WebDataTableRowInner<
303
365
  selected: boolean;
304
366
  setHoveredIdx: (index: number | null) => void;
305
367
  }) {
368
+ useThemeColors();
306
369
  const sectionHeader: DataTableSectionHeader | null =
307
370
  renderSectionHeader?.(item, index) ?? null;
308
371
  const baseRowStyle: CSSProperties = {
@@ -330,7 +393,7 @@ function WebDataTableRowInner<
330
393
  data-gloom-role="data-table-section-header"
331
394
  style={{
332
395
  ...baseRowStyle,
333
- backgroundColor: sectionHeader.backgroundColor ?? colors.bg,
396
+ backgroundColor: sectionHeader.backgroundColor ?? CSS_BG,
334
397
  }}
335
398
  onMouseDown={(event) => {
336
399
  focusPane();
@@ -341,7 +404,7 @@ function WebDataTableRowInner<
341
404
  title={sectionHeader.text}
342
405
  style={{
343
406
  ...cellTextStyle(
344
- sectionHeader.color ?? colors.textBright,
407
+ sectionHeader.color ?? CSS_TEXT_BRIGHT,
345
408
  sectionHeader.attributes ?? TextAttributes.BOLD,
346
409
  ),
347
410
  gridColumn: "1 / -1",
@@ -358,10 +421,10 @@ function WebDataTableRowInner<
358
421
  const rowState = { selected, hovered };
359
422
  const rowBackgroundColor = getRowBackgroundColor?.(item, index, rowState);
360
423
  const rowBg = selected
361
- ? colors.selected
424
+ ? CSS_SELECTED
362
425
  : hovered
363
- ? hoverBg()
364
- : rowBackgroundColor ?? colors.bg;
426
+ ? CSS_HOVER_BG
427
+ : rowBackgroundColor ?? CSS_BG;
365
428
 
366
429
  return (
367
430
  <div
@@ -438,7 +501,7 @@ function WebDataTableRowInner<
438
501
  title={cell.text}
439
502
  style={clippedCellTextStyle(
440
503
  column,
441
- cell.color ?? (selected ? colors.selectedText : colors.text),
504
+ cell.color ?? (selected ? CSS_SELECTED_TEXT : CSS_TEXT),
442
505
  cell.attributes ?? TextAttributes.NONE,
443
506
  )}
444
507
  >
@@ -494,7 +557,7 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
494
557
  const [scrollbarActive, markScrollbarActive] = useScrollbarActivity();
495
558
  const tableWidth = useMemo(() => getTableWidth(columns), [columns]);
496
559
  const gridTemplateColumns = useMemo(
497
- () => buildGridTemplateColumns(columns),
560
+ () => buildTableGridTemplateColumns(columns),
498
561
  [columns],
499
562
  );
500
563
  const selectRow = useCallback((item: T, index: number) => {
@@ -624,7 +687,7 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
624
687
  width: "100%",
625
688
  minWidth: 0,
626
689
  minHeight: 0,
627
- backgroundColor: colors.bg,
690
+ backgroundColor: CSS_BG,
628
691
  overflow: "hidden",
629
692
  };
630
693
  const bodyScrollerStyle: CSSProperties = {
@@ -634,7 +697,7 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
634
697
  minHeight: 0,
635
698
  overflowX: horizontalScrollEnabled ? "auto" : "hidden",
636
699
  overflowY: "auto",
637
- backgroundColor: colors.bg,
700
+ backgroundColor: CSS_BG,
638
701
  };
639
702
 
640
703
  return (
@@ -673,77 +736,29 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
673
736
  minHeight: WEB_CELL_HEIGHT,
674
737
  }}
675
738
  >
676
- <div
677
- data-gloom-role="data-table-header-row"
678
- style={{
679
- position: "sticky",
680
- top: 0,
681
- zIndex: 2,
682
- display: "grid",
683
- gridTemplateColumns,
684
- columnGap: "1ch",
685
- alignItems: "center",
686
- width: "100%",
687
- minWidth: 0,
688
- height: WEB_CELL_HEIGHT,
689
- paddingLeft: TABLE_INLINE_PADDING_PX,
690
- paddingRight: TABLE_INLINE_PADDING_PX,
691
- boxSizing: "border-box",
692
- backgroundColor: colors.panel,
693
- }}
694
- >
695
- {columns.map((column) => {
696
- const { isSorted, text } = renderHeaderLabel(
697
- column,
698
- sortColumnId,
699
- sortDirection,
700
- );
701
- return (
702
- <div
703
- key={column.id}
704
- data-gloom-role="data-table-header-cell"
705
- data-gloom-interactive="true"
706
- style={{
707
- minWidth: 0,
708
- height: WEB_CELL_HEIGHT,
709
- overflow: "hidden",
710
- backgroundColor: column.headerBackgroundColor ?? colors.panel,
711
- }}
712
- onMouseDown={(event) => {
713
- focusPane();
714
- event.preventDefault();
715
- onHeaderClick(column.id);
716
- }}
717
- >
718
- <span
719
- title={text}
720
- style={clippedCellTextStyle(
721
- column,
722
- isSorted ? colors.text : column.headerColor ?? colors.textDim,
723
- TextAttributes.BOLD,
724
- )}
725
- >
726
- {text}
727
- </span>
728
- </div>
729
- );
730
- })}
731
- </div>
739
+ <WebDataTableHeader
740
+ columns={columns}
741
+ focusPane={focusPane}
742
+ gridTemplateColumns={gridTemplateColumns}
743
+ onHeaderClick={onHeaderClick}
744
+ sortColumnId={sortColumnId}
745
+ sortDirection={sortDirection}
746
+ />
732
747
  {items.length === 0 ? (
733
748
  emptyContent ?? (
734
749
  <div
735
750
  style={{
736
751
  width: "100%",
737
752
  padding: `${WEB_CELL_HEIGHT}px ${WEB_CELL_WIDTH}px`,
738
- color: colors.textDim,
753
+ color: CSS_TEXT_DIM,
739
754
  lineHeight: "var(--cell-h)",
740
755
  }}
741
756
  >
742
- <div style={cellTextStyle(colors.textBright, TextAttributes.BOLD)}>
757
+ <div style={cellTextStyle(CSS_TEXT_BRIGHT, TextAttributes.BOLD)}>
743
758
  {emptyStateTitle}
744
759
  </div>
745
760
  {emptyStateHint ? (
746
- <div style={cellTextStyle(colors.textDim, TextAttributes.NONE)}>
761
+ <div style={cellTextStyle(CSS_TEXT_DIM, TextAttributes.NONE)}>
747
762
  {emptyStateHint}
748
763
  </div>
749
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";
@@ -97,6 +98,7 @@ export function WebButton({
97
98
  shortcut,
98
99
  width,
99
100
  }: ButtonProps) {
101
+ useThemeColors();
100
102
  const palette = buttonPalette({ variant, active, disabled });
101
103
 
102
104
  return (
@@ -156,6 +158,7 @@ export function WebTextField({
156
158
  placeholderColor = colors.textDim,
157
159
  onMouseDown,
158
160
  }: TextFieldProps) {
161
+ useThemeColors();
159
162
  const localInputRef = useRef<InputRenderable>(null);
160
163
  const resolvedInputRef = inputRef ?? localInputRef;
161
164
  const renderer = useRendererHost();
@@ -245,6 +248,7 @@ export function WebMessageComposer({
245
248
  keyBindings,
246
249
  wrapText = false,
247
250
  }: MessageComposerProps) {
251
+ useThemeColors();
248
252
  const borderColor = focused
249
253
  ? blendHex(colors.borderFocused, colors.textBright, 0.24)
250
254
  : colors.border;
@@ -361,6 +365,7 @@ export function WebListView({
361
365
  scrollable = false,
362
366
  autoScrollToIndex = true,
363
367
  }: ListViewProps) {
368
+ useThemeColors();
364
369
  const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
365
370
  const scrollRef = useRef<ScrollBoxRenderable>(null);
366
371
  const baseBg = bgColor ?? colors.bg;
@@ -489,6 +494,7 @@ export function WebSegmentedControl({
489
494
  value,
490
495
  onChange,
491
496
  }: SegmentedControlProps) {
497
+ useThemeColors();
492
498
  return (
493
499
  <Box
494
500
  flexDirection="row"
@@ -538,6 +544,7 @@ export function WebDialogFrame({
538
544
  footer,
539
545
  showTitleDivider = true,
540
546
  }: DialogFrameProps) {
547
+ useThemeColors();
541
548
  return (
542
549
  <Box flexDirection="column" style={{ padding: 14 }}>
543
550
  <Box
@@ -583,6 +590,7 @@ export function WebPageStackView({
583
590
  backLabel = "Back",
584
591
  backHint,
585
592
  }: PageStackViewProps) {
593
+ useThemeColors();
586
594
  useShortcut((event) => {
587
595
  if (!focused || !detailOpen || !isDetailBackNavigationKey(event)) return;
588
596
  event.stopPropagation?.();
@@ -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
  }
@@ -3,6 +3,7 @@
3
3
  import { useCallback, useMemo, useState, type ReactNode } from "react";
4
4
  import { DialogHostProvider, type DialogApi } from "../../../ui/dialog";
5
5
  import { blendHex, colors } from "../../../theme/colors";
6
+ import { useThemeColors } from "../../../theme/theme-context";
6
7
 
7
8
  interface DialogState {
8
9
  id: string;
@@ -13,6 +14,7 @@ interface DialogState {
13
14
  let nextDialogId = 1;
14
15
 
15
16
  export function WebDialogHostProvider({ children }: { children: ReactNode }) {
17
+ useThemeColors();
16
18
  const [dialogState, setDialogState] = useState<DialogState | null>(null);
17
19
  const dialogBorder = blendHex(colors.border, colors.borderFocused, 0.18);
18
20
  const dialogBg = blendHex(colors.panel, colors.bg, 0.12);
@@ -99,7 +99,7 @@ export function WebInputHostProvider({ children }: { children: ReactNode }) {
99
99
  const shortcutsRef = useRef<ShortcutEntry[]>([]);
100
100
 
101
101
  const dispatchShortcut = (shortcutEvent: KeyEventLike) => {
102
- for (const phase of ["normal", "after"] as const) {
102
+ for (const phase of ["before", "normal", "after"] as const) {
103
103
  if (phase === "after" && (shortcutEvent.defaultPrevented || shortcutEvent.propagationStopped)) break;
104
104
  for (const entry of shortcutsRef.current) {
105
105
  if (entry.phase !== phase || !entry.enabledRef.current) continue;
@@ -6,7 +6,7 @@ import { UiHostProvider } from "../../../ui/host";
6
6
  import { debugLog } from "../../../utils/debug-log";
7
7
  import { measurePerfAsync } from "../../../utils/perf-marks";
8
8
  import { startMainThreadMonitor } from "../../../utils/main-thread-monitor";
9
- import { initElectrobunBackend } from "./backend-rpc";
9
+ import { backendRequest, initElectrobunBackend } from "./backend-rpc";
10
10
  import { installElectrobunAiHost } from "./ai-host";
11
11
  import { installElectrobunBrokerRemoteClient } from "./broker-remote-client";
12
12
  import { installElectrobunConfigStoreHost } from "./config-host";
@@ -45,9 +45,24 @@ debugLog.mirrorToConsole({
45
45
  });
46
46
  const stopMainThreadMonitor = startMainThreadMonitor("electrobun.web", { mirrorToConsole: true });
47
47
 
48
+ rootElement.tabIndex = -1;
48
49
  root.render(<div className="gloom-loading">Starting Gloomberb...</div>);
49
50
  bootLog.warn("diagnostic console mirror enabled", { sources: ELECTROBUN_WEB_CONSOLE_LOG_SOURCES });
50
51
 
52
+ function focusWebSurface(): void {
53
+ window.focus();
54
+ rootElement.focus({ preventScroll: true });
55
+ }
56
+
57
+ function requestStartupFocus(): void {
58
+ focusWebSurface();
59
+ requestAnimationFrame(() => {
60
+ void backendRequest("host.focusWindow")
61
+ .catch(() => null)
62
+ .then(() => focusWebSurface());
63
+ });
64
+ }
65
+
51
66
  async function boot() {
52
67
  bootLog.info("boot started");
53
68
  const backendInitPromise = initElectrobunBackend();
@@ -78,6 +93,7 @@ async function boot() {
78
93
  desktopWindowBridge={desktopWindowBridge}
79
94
  desktopApplicationMenuBridge={desktopApplicationMenuBridge}
80
95
  desktopSnapshot={desktopSnapshot}
96
+ desktopThemePreview={init.desktopThemePreview}
81
97
  />
82
98
  </WebDialogHostProvider>
83
99
  </WebToastHostProvider>
@@ -85,6 +101,7 @@ async function boot() {
85
101
  </UiHostProvider>,
86
102
  );
87
103
  });
104
+ requestStartupFocus();
88
105
  bootLog.info("root render scheduled", {
89
106
  layoutPanes: config.layout.instances.length,
90
107
  floatingPanes: config.layout.floating.length,
@@ -1,5 +1,6 @@
1
1
  @keyframes gloom-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
2
2
  html, body, #root { width: 100%; height: 100%; min-height: 100%; overflow: hidden; background: #000; }
3
+ #root:focus { outline: none; }
3
4
  body {
4
5
  --cell-w: 8px;
5
6
  --cell-h: 18px;
@@ -119,6 +120,8 @@ body.gloom-dragging {
119
120
  pointer-events: auto;
120
121
  }
121
122
  [data-gloom-role="status-bar"] {
123
+ flex: 0 0 var(--cell-h);
124
+ height: var(--cell-h);
122
125
  min-height: var(--cell-h);
123
126
  max-height: var(--cell-h);
124
127
  align-items: center;
@@ -129,6 +132,8 @@ body.gloom-dragging {
129
132
  line-height: var(--cell-h);
130
133
  }
131
134
  [data-gloom-role="pane-footer"] {
135
+ flex: 0 0 var(--cell-h);
136
+ height: var(--cell-h);
132
137
  min-height: var(--cell-h);
133
138
  max-height: var(--cell-h);
134
139
  align-items: center;
@@ -294,18 +299,11 @@ body.gloom-dragging {
294
299
  min-width: 260px;
295
300
  max-width: 420px;
296
301
  padding: 10px 12px;
297
- border: 1px solid #3a4148;
298
- background: #151b20;
299
- color: #d8dde3;
302
+ border: 1px solid;
300
303
  box-shadow: 0 12px 24px rgba(0,0,0,.28);
301
304
  }
302
- .gloom-toast-success { border-color: #54c99f; }
303
- .gloom-toast-error { border-color: #ff6b5f; }
304
305
  .gloom-toast-action {
305
306
  margin-top: 8px;
306
- color: #101417;
307
- background: #d8dde3;
308
- border: 0;
309
307
  padding: 4px 8px;
310
308
  border-radius: 4px;
311
309
  }
@@ -1,17 +1,42 @@
1
1
  /// <reference lib="dom" />
2
2
  /** @jsxImportSource react */
3
- import { useCallback, useMemo, useState, type ReactNode } from "react";
3
+ import { useCallback, useMemo, useState, type CSSProperties, type ReactNode } from "react";
4
+ import { colors } from "../../../theme/colors";
4
5
  import { ToastHostProvider, type ToastHost, type ToastOptions } from "../../../ui/toast";
5
6
 
7
+ type WebToastType = "info" | "success" | "error";
8
+
6
9
  interface ToastEntry {
7
10
  id: number;
8
11
  body: string;
9
- type: "info" | "success" | "error";
12
+ type: WebToastType;
10
13
  action?: ToastOptions["action"];
11
14
  }
12
15
 
13
16
  let nextToastId = 1;
14
17
 
18
+ function webToastBorderColor(type: WebToastType): string {
19
+ if (type === "success") return colors.positive;
20
+ if (type === "error") return colors.negative;
21
+ return colors.borderFocused;
22
+ }
23
+
24
+ function getToastStyle(type: WebToastType): CSSProperties {
25
+ return {
26
+ background: colors.panel,
27
+ borderColor: webToastBorderColor(type),
28
+ color: colors.text,
29
+ };
30
+ }
31
+
32
+ function getToastActionStyle(): CSSProperties {
33
+ return {
34
+ background: colors.selected,
35
+ border: `1px solid ${colors.borderFocused}`,
36
+ color: colors.selectedText,
37
+ };
38
+ }
39
+
15
40
  export function WebToastHostProvider({ children }: { children: ReactNode }) {
16
41
  const [toasts, setToasts] = useState<ToastEntry[]>([]);
17
42
 
@@ -33,11 +58,12 @@ export function WebToastHostProvider({ children }: { children: ReactNode }) {
33
58
  return (
34
59
  <div className="gloom-toast-viewport">
35
60
  {toasts.map((toast) => (
36
- <div key={toast.id} className={`gloom-toast gloom-toast-${toast.type}`}>
61
+ <div key={toast.id} className="gloom-toast" style={getToastStyle(toast.type)}>
37
62
  <div>{toast.body}</div>
38
63
  {toast.action && (
39
64
  <button
40
65
  className="gloom-toast-action"
66
+ style={getToastActionStyle()}
41
67
  onClick={() => {
42
68
  toast.action?.onClick();
43
69
  dismiss(toast.id);
@@ -940,6 +940,13 @@ const WebScrollBox = forwardRef<ScrollBoxRenderable, Record<string, unknown> & {
940
940
  const element = getElement();
941
941
  if (element) element.scrollTop = Math.max(0, value) * WEB_CELL_HEIGHT;
942
942
  },
943
+ get scrollTopPx() {
944
+ return Math.max(0, getElement()?.scrollTop ?? 0);
945
+ },
946
+ set scrollTopPx(value: number) {
947
+ const element = getElement();
948
+ if (element) element.scrollTop = Math.max(0, value);
949
+ },
943
950
  get scrollLeft() {
944
951
  return toCellX(getElement()?.scrollLeft ?? 0);
945
952
  },
@@ -947,12 +954,25 @@ const WebScrollBox = forwardRef<ScrollBoxRenderable, Record<string, unknown> & {
947
954
  const element = getElement();
948
955
  if (element) element.scrollLeft = Math.max(0, value) * WEB_CELL_WIDTH;
949
956
  },
957
+ get scrollLeftPx() {
958
+ return Math.max(0, getElement()?.scrollLeft ?? 0);
959
+ },
960
+ set scrollLeftPx(value: number) {
961
+ const element = getElement();
962
+ if (element) element.scrollLeft = Math.max(0, value);
963
+ },
950
964
  get scrollHeight() {
951
965
  return toCellY(getElement()?.scrollHeight ?? 0);
952
966
  },
953
967
  get scrollWidth() {
954
968
  return toCellX(getElement()?.scrollWidth ?? 0);
955
969
  },
970
+ get scrollHeightPx() {
971
+ return Math.max(0, getElement()?.scrollHeight ?? 0);
972
+ },
973
+ get scrollWidthPx() {
974
+ return Math.max(0, getElement()?.scrollWidth ?? 0);
975
+ },
956
976
  get viewport() {
957
977
  const element = getElement();
958
978
  return {
@@ -960,6 +980,13 @@ const WebScrollBox = forwardRef<ScrollBoxRenderable, Record<string, unknown> & {
960
980
  height: toCellY(element?.clientHeight ?? 0),
961
981
  };
962
982
  },
983
+ get viewportPx() {
984
+ const element = getElement();
985
+ return {
986
+ width: Math.max(0, element?.clientWidth ?? 0),
987
+ height: Math.max(0, element?.clientHeight ?? 0),
988
+ };
989
+ },
963
990
  getBoundingClientRect() {
964
991
  const rect = getElement()?.getBoundingClientRect();
965
992
  return {
@@ -986,6 +1013,21 @@ const WebScrollBox = forwardRef<ScrollBoxRenderable, Record<string, unknown> & {
986
1013
  top: Math.max(0, target.y ?? toCellY(element.scrollTop)) * WEB_CELL_HEIGHT,
987
1014
  });
988
1015
  },
1016
+ scrollToPixels(target: number | { x?: number; y?: number }, y?: number) {
1017
+ const element = getElement();
1018
+ if (!element) return;
1019
+ if (typeof target === "number") {
1020
+ element.scrollTop = Math.max(0, target);
1021
+ if (typeof y === "number") {
1022
+ element.scrollLeft = Math.max(0, y);
1023
+ }
1024
+ return;
1025
+ }
1026
+ element.scrollTo({
1027
+ left: Math.max(0, target.x ?? element.scrollLeft),
1028
+ top: Math.max(0, target.y ?? element.scrollTop),
1029
+ });
1030
+ },
989
1031
  }), [horizontalScrollBar, verticalScrollBar]);
990
1032
 
991
1033
  const scrollable = props.scrollX === true || props.scrollY === true;
@@ -15,7 +15,7 @@ export function OpenTuiInputHostProvider({ children }: { children: ReactNode })
15
15
  const shortcutsRef = useRef<ShortcutEntry[]>([]);
16
16
  const dispatchShortcut = (event: KeyEventLike) => {
17
17
  const shortcuts = shortcutsRef.current;
18
- for (const phase of ["normal", "after"] as const) {
18
+ for (const phase of ["before", "normal", "after"] as const) {
19
19
  if (phase === "after" && (event.defaultPrevented || event.propagationStopped)) return;
20
20
  for (const entry of shortcuts) {
21
21
  if (entry.phase !== phase || !entry.enabledRef.current) continue;