gloomberb 0.6.0 → 0.7.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 (156) hide show
  1. package/package.json +2 -1
  2. package/patches/@opentui%2Fcore@0.1.90.patch +48 -24
  3. package/src/app.tsx +9 -6
  4. package/src/cli/registry.test.ts +0 -1
  5. package/src/components/chart/chart-controller.ts +0 -1
  6. package/src/components/chart/chart-data.ts +1 -31
  7. package/src/components/chart/chart-renderer.ts +70 -0
  8. package/src/components/chart/chart-resolution.test.ts +0 -36
  9. package/src/components/chart/chart-resolution.ts +0 -27
  10. package/src/components/chart/chart.test.ts +17 -45
  11. package/src/components/chart/comparison-chart-data.test.ts +1 -5
  12. package/src/components/chart/comparison-chart-data.ts +1 -4
  13. package/src/components/chart/comparison-stock-chart.tsx +62 -77
  14. package/src/components/chart/native/kitty.test.ts +23 -0
  15. package/src/components/chart/native/surface-visibility.ts +48 -0
  16. package/src/components/chart/static-chart-surface.tsx +28 -4
  17. package/src/components/chart/stock-chart.test.ts +0 -16
  18. package/src/components/chart/stock-chart.tsx +52 -71
  19. package/src/components/chart/time-axis-label.tsx +43 -0
  20. package/src/components/command-bar/command-bar.test.tsx +71 -8
  21. package/src/components/command-bar/command-bar.tsx +388 -713
  22. package/src/components/command-bar/confirm-body.tsx +51 -0
  23. package/src/components/command-bar/list-model.ts +77 -0
  24. package/src/components/command-bar/view-model.ts +0 -19
  25. package/src/components/command-bar/workflow-ops.ts +4 -0
  26. package/src/components/data-table-stack-view.test.tsx +39 -4
  27. package/src/components/data-table-stack-view.tsx +11 -104
  28. package/src/components/data-table-view.tsx +43 -79
  29. package/src/components/feed-data-table-stack-view.tsx +10 -17
  30. package/src/components/feed-stack-controller.ts +39 -0
  31. package/src/components/index.ts +6 -2
  32. package/src/components/layout/floating-pane.tsx +30 -2
  33. package/src/components/layout/header.tsx +10 -7
  34. package/src/components/layout/pane-content.tsx +2 -0
  35. package/src/components/layout/pane-cycle-order.test.ts +30 -0
  36. package/src/components/layout/pane-cycle-order.ts +32 -0
  37. package/src/components/layout/pane-footer.tsx +26 -4
  38. package/src/components/layout/pane-header.tsx +5 -5
  39. package/src/components/layout/pane-keyboard-scroll.test.tsx +226 -0
  40. package/src/components/layout/shell.test.tsx +66 -22
  41. package/src/components/layout/shell.tsx +13 -6
  42. package/src/components/onboarding/onboarding-wizard.tsx +1 -2
  43. package/src/components/pane-settings-dialog.tsx +12 -4
  44. package/src/components/pane-template-wizard.test.tsx +3 -4
  45. package/src/components/speedometer-gauge.test.tsx +45 -0
  46. package/src/components/speedometer-gauge.tsx +558 -0
  47. package/src/components/table-view-shared.tsx +158 -0
  48. package/src/components/ticker-list-table-view.tsx +42 -72
  49. package/src/components/ui/confirm-dialog.tsx +66 -0
  50. package/src/components/ui/data-table.tsx +18 -10
  51. package/src/components/ui/index.ts +0 -1
  52. package/src/components/ui/page-stack-view.tsx +39 -10
  53. package/src/components/ui/remote-image.tsx +15 -40
  54. package/src/components/ui/table-layout.test.ts +16 -0
  55. package/src/components/ui/table-layout.ts +10 -3
  56. package/src/components/ui/ui.test.tsx +100 -20
  57. package/src/core/state/app-state.ts +6 -3
  58. package/src/market-data/coordinator.test.ts +1 -1
  59. package/src/market-data/coordinator.ts +0 -1
  60. package/src/news/aggregator.ts +0 -3
  61. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +46 -16
  62. package/src/plugins/builtin/ai/providers.ts +0 -17
  63. package/src/plugins/builtin/ai/screener-pane.test.tsx +3 -4
  64. package/src/plugins/builtin/ai/screener-pane.tsx +0 -3
  65. package/src/plugins/builtin/ask-ai.test.tsx +89 -2
  66. package/src/plugins/builtin/broker-manager/index.test.tsx +2 -2
  67. package/src/plugins/builtin/broker-manager/index.tsx +19 -36
  68. package/src/plugins/builtin/chat-controller.test.ts +105 -0
  69. package/src/plugins/builtin/chat-controller.ts +75 -6
  70. package/src/plugins/builtin/chat.test.tsx +6 -6
  71. package/src/plugins/builtin/chat.tsx +29 -20
  72. package/src/plugins/builtin/cloud-tweets.tsx +0 -1
  73. package/src/plugins/builtin/comparison-chart.test.tsx +53 -2
  74. package/src/plugins/builtin/comparison-chart.tsx +11 -5
  75. package/src/plugins/builtin/correlation/index.tsx +1 -6
  76. package/src/plugins/builtin/debug.tsx +1 -3
  77. package/src/plugins/builtin/econ/index.tsx +1 -2
  78. package/src/plugins/builtin/fear-greed/fear-greed-data.test.ts +72 -0
  79. package/src/plugins/builtin/fear-greed/fear-greed-data.ts +366 -0
  80. package/src/plugins/builtin/fear-greed/index.tsx +478 -0
  81. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  82. package/src/plugins/builtin/help.test.tsx +2 -2
  83. package/src/plugins/builtin/help.tsx +2 -2
  84. package/src/plugins/builtin/holders/index.tsx +10 -15
  85. package/src/plugins/builtin/insider/index.tsx +22 -89
  86. package/src/plugins/builtin/market-movers/index.tsx +1 -1
  87. package/src/plugins/builtin/news-wire/index.tsx +41 -2
  88. package/src/plugins/builtin/news-wire/news-table.tsx +11 -11
  89. package/src/plugins/builtin/news.tsx +4 -16
  90. package/src/plugins/builtin/notes.tsx +18 -55
  91. package/src/plugins/builtin/options.test.tsx +58 -2
  92. package/src/plugins/builtin/options.tsx +108 -49
  93. package/src/plugins/builtin/plugin-groups.ts +2 -0
  94. package/src/plugins/builtin/portfolio-list/cli/commands.ts +1 -1
  95. package/src/plugins/builtin/research/index.tsx +5 -21
  96. package/src/plugins/builtin/sec-filing-display.tsx +84 -0
  97. package/src/plugins/builtin/sec.tsx +13 -66
  98. package/src/plugins/builtin/sectors/index.tsx +1 -1
  99. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +0 -1
  100. package/src/plugins/builtin/ticker-detail/index.tsx +0 -1
  101. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +0 -1
  102. package/src/plugins/builtin/ticker-detail/pane.tsx +36 -28
  103. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +69 -4
  104. package/src/plugins/builtin/ticker-detail.test.tsx +102 -0
  105. package/src/plugins/builtin/world-indices/index.tsx +1 -1
  106. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  107. package/src/plugins/ibkr/broker-adapter.ts +1 -3
  108. package/src/plugins/ibkr/gateway-service-native.ts +1 -5
  109. package/src/plugins/ibkr/gateway-service.ts +1 -1
  110. package/src/plugins/ibkr/index.test.tsx +0 -2
  111. package/src/plugins/ibkr/trade-tab.test.tsx +56 -5
  112. package/src/plugins/ibkr/trade-tab.tsx +13 -11
  113. package/src/plugins/pane-manager.ts +0 -13
  114. package/src/plugins/plugin-runtime.test.tsx +6 -6
  115. package/src/plugins/plugin-runtime.tsx +13 -8
  116. package/src/plugins/prediction-markets/cli.ts +0 -1
  117. package/src/plugins/prediction-markets/controller-data.ts +0 -2
  118. package/src/plugins/prediction-markets/pane.test.tsx +3 -11
  119. package/src/plugins/prediction-markets/test-helpers.tsx +1 -1
  120. package/src/plugins/registry.ts +10 -39
  121. package/src/react/input.ts +11 -2
  122. package/src/renderers/electrobun/view/data-table.tsx +28 -6
  123. package/src/renderers/electrobun/view/desktop-controls.tsx +34 -20
  124. package/src/renderers/electrobun/view/dialog-host.tsx +11 -1
  125. package/src/renderers/electrobun/view/input-host.tsx +123 -12
  126. package/src/renderers/electrobun/view/key-event.ts +1 -1
  127. package/src/renderers/electrobun/view/styles.css +331 -0
  128. package/src/renderers/electrobun/view/ui-host.tsx +22 -3
  129. package/src/renderers/opentui/chart-surface.tsx +179 -0
  130. package/src/renderers/opentui/host.tsx +6 -0
  131. package/src/renderers/opentui/image-loader.test.ts +25 -0
  132. package/src/renderers/opentui/image-loader.ts +105 -0
  133. package/src/renderers/opentui/image-surface.tsx +266 -0
  134. package/src/renderers/opentui/input-host.tsx +45 -6
  135. package/src/renderers/opentui/start.tsx +48 -30
  136. package/src/renderers/opentui/test-utils.tsx +17 -0
  137. package/src/renderers/opentui/ui-host.tsx +7 -3
  138. package/src/sources/provider-router.ts +0 -4
  139. package/src/state/app-context.test.ts +45 -0
  140. package/src/state/app-context.tsx +4 -0
  141. package/src/state/pane-scroll-registry.tsx +254 -0
  142. package/src/state/use-inline-tickers.test.tsx +73 -0
  143. package/src/state/use-inline-tickers.ts +7 -1
  144. package/src/test-support/plugin-runtime.ts +3 -2
  145. package/src/theme/colors.ts +12 -3
  146. package/src/theme/themes.test.ts +23 -1
  147. package/src/theme/themes.ts +115 -0
  148. package/src/types/broker.ts +1 -1
  149. package/src/types/data-provider.ts +0 -1
  150. package/src/types/plugin.ts +0 -2
  151. package/src/ui/host.tsx +4 -0
  152. package/src/ui/index.tsx +9 -1
  153. package/src/utils/back-navigation.ts +24 -1
  154. package/src/version.ts +1 -1
  155. package/src/plugins/builtin/news-wire/feed-pane.tsx +0 -21
  156. package/src/plugins/builtin/news-wire/top-pane.tsx +0 -21
@@ -14,7 +14,6 @@ import {
14
14
  } from "./test-helpers";
15
15
  import { attachPredictionMarketsPersistence } from "./services/fetch";
16
16
  import {
17
- loadKalshiCatalog,
18
17
  normalizeKalshiMarket,
19
18
  } from "./services/kalshi-adapter";
20
19
  import { normalizePolymarketMarket } from "./services/polymarket-adapter";
@@ -379,7 +378,7 @@ describe("prediction markets pane interactions", () => {
379
378
  ).toBe("polymarket:pm-1");
380
379
  });
381
380
 
382
- test("supports detail outcome navigation and backspace return from the keyboard", async () => {
381
+ test("supports detail outcome navigation and escape return from the keyboard", async () => {
383
382
  attachPredictionMarketsPersistence(new MemoryPersistence());
384
383
 
385
384
  globalThis.fetch = (async (input: Request | string | URL) => {
@@ -512,20 +511,13 @@ describe("prediction markets pane interactions", () => {
512
511
  await flushFrames(testSetup);
513
512
 
514
513
  const escapeFrame = testSetup.captureCharFrame();
515
- expect(escapeFrame).toContain("\u2190 Back");
516
- expect(escapeFrame).toContain("Rule 2");
514
+ expect(escapeFrame).toContain("search markets");
515
+ expect(escapeFrame).not.toContain("Rule 2");
517
516
  expect(
518
517
  harnessStateRef.current?.paneState[TEST_PANE_ID]?.pluginState?.[
519
518
  "prediction-markets"
520
519
  ]?.selectedDetailMarketKey,
521
520
  ).toBe("kalshi:KXFED-27APR-T4.50");
522
-
523
- await emitKeypress(testSetup, { name: "backspace", sequence: "\u007f" });
524
- await flushFrames(testSetup);
525
-
526
- const frame = testSetup.captureCharFrame();
527
- expect(frame).toContain("search markets");
528
- expect(frame).not.toContain("Rule 2");
529
521
  });
530
522
 
531
523
  test("opens full-page detail from a row double click and preserves the browse selection", async () => {
@@ -1,5 +1,5 @@
1
1
  import { act, useMemo, useReducer, useRef } from "react";
2
- import type { ScrollBoxRenderable } from "#opentui/core";
2
+ import type { ScrollBoxRenderable } from "@opentui/core";
3
3
  import { PaneFooterBar, PaneFooterProvider } from "../../components/layout/pane-footer";
4
4
  import {
5
5
  AppContext,
@@ -32,9 +32,8 @@ import type {
32
32
  } from "../types/plugin";
33
33
  import type { ContextMenuContext, ContextMenuItem } from "../types/context-menu";
34
34
  import type { TickerRecord } from "../types/ticker";
35
- import { addPaneFloating, removePane } from "./pane-manager";
36
35
  import { EventBus, type PluginEvents } from "./event-bus";
37
- import { createPaneInstance, resolvePaneInstance } from "../types/config";
36
+ import { resolvePaneInstance } from "../types/config";
38
37
  import { createPluginPersistence } from "./plugin-persistence";
39
38
  import { debugLog } from "../utils/debug-log";
40
39
  import { PluginRenderProvider, type PluginRuntimeAccess } from "./plugin-runtime";
@@ -170,9 +169,18 @@ export class PluginRegistry implements PluginRuntimeAccess {
170
169
  openCommandBar = (query?: string) => {
171
170
  this.openCommandBarFn(query);
172
171
  };
172
+ openPaneSettings = (paneId?: string) => {
173
+ this.openPaneSettingsFn(paneId);
174
+ };
173
175
  openPluginCommandWorkflow = (commandId: string) => {
174
176
  this.openPluginCommandWorkflowFn(commandId);
175
177
  };
178
+ showPane = (paneId: string) => {
179
+ this.showPaneFn(paneId);
180
+ };
181
+ hidePane = (paneId: string) => {
182
+ this.hidePaneFn(paneId);
183
+ };
176
184
 
177
185
  getLayoutFn: (() => LayoutConfig) = () => ({ dockRoot: null, instances: [], floating: [], detached: [] });
178
186
  updateLayoutFn: ((layout: LayoutConfig) => void) = () => {};
@@ -528,41 +536,6 @@ export class PluginRegistry implements PluginRuntimeAccess {
528
536
  }
529
537
  }
530
538
 
531
- showWidget(paneId: string): void {
532
- try {
533
- const disabledPlugins = new Set(this.getConfigFn().disabledPlugins);
534
- for (const [pluginId, items] of this.pluginItems) {
535
- if (items.panes.includes(paneId) && disabledPlugins.has(pluginId)) {
536
- return;
537
- }
538
- }
539
- } catch {
540
- return;
541
- }
542
-
543
- const layout = this.getLayoutFn();
544
- const existingInstanceId = this.resolvePaneTarget(paneId);
545
- if (existingInstanceId && layout.floating.some((entry) => entry.instanceId === existingInstanceId)) {
546
- this.focusPaneFn(existingInstanceId);
547
- return;
548
- }
549
-
550
- const def = this.panesMap.get(paneId);
551
- const { width, height } = this.getTermSizeFn();
552
- const instance = existingInstanceId
553
- ? layout.instances.find((entry) => entry.instanceId === existingInstanceId)!
554
- : createPaneInstance(paneId, { instanceId: `${paneId}:main` });
555
- const nextLayout = addPaneFloating(layout, instance, width, height, def);
556
- this.updateLayoutFn(nextLayout);
557
- this.focusPaneFn(instance.instanceId);
558
- }
559
-
560
- hideWidget(paneId: string): void {
561
- const target = this.resolvePaneTarget(paneId);
562
- if (!target) return;
563
- this.updateLayoutFn(removePane(this.getLayoutFn(), target));
564
- }
565
-
566
539
  private createContext(pluginId: string): GloomPluginContext {
567
540
  const items = this.getOrCreatePluginItems(pluginId);
568
541
 
@@ -716,8 +689,6 @@ export class PluginRegistry implements PluginRuntimeAccess {
716
689
  },
717
690
  emit: (event, payload) => this.events.emit(event, payload),
718
691
 
719
- showWidget: (widgetId) => this.showWidget(widgetId),
720
- hideWidget: (widgetId) => this.hideWidget(widgetId),
721
692
  notify: (notification) => this.notifyFn(notification),
722
693
  };
723
694
  }
@@ -9,14 +9,23 @@ export interface KeyEventLike {
9
9
  alt: boolean;
10
10
  meta: boolean;
11
11
  super?: boolean;
12
+ targetEditable?: boolean;
13
+ readonly defaultPrevented?: boolean;
14
+ readonly propagationStopped?: boolean;
12
15
  preventDefault(): void;
13
16
  stopPropagation(): void;
14
17
  }
15
18
 
19
+ export interface ShortcutOptions {
20
+ enabled?: boolean;
21
+ scope?: string;
22
+ phase?: "normal" | "after";
23
+ }
24
+
16
25
  export interface InputHost {
17
26
  useShortcut(
18
27
  handler: (event: KeyEventLike) => void,
19
- options?: { enabled?: boolean; scope?: string },
28
+ options?: ShortcutOptions,
20
29
  ): void;
21
30
  useViewport(): { width: number; height: number };
22
31
  }
@@ -43,7 +52,7 @@ function useInputHost(): InputHost {
43
52
 
44
53
  export function useShortcut(
45
54
  handler: (event: KeyEventLike) => void,
46
- options?: { enabled?: boolean; scope?: string },
55
+ options?: ShortcutOptions,
47
56
  ): void {
48
57
  useInputHost().useShortcut(handler, options);
49
58
  }
@@ -24,6 +24,7 @@ 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
28
  import { WEB_CELL_HEIGHT, WEB_CELL_WIDTH } from "./input-host";
28
29
  import { useScrollbarActivity } from "./scrollbar-activity";
29
30
 
@@ -489,7 +490,9 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
489
490
  const headerVertical = useScrollbarState(false);
490
491
  const bodyHorizontal = useScrollbarState(showHorizontalScrollbar);
491
492
  const bodyVertical = useScrollbarState(true);
493
+ const [viewportWidth, setViewportWidth] = useState(0);
492
494
  const [scrollbarActive, markScrollbarActive] = useScrollbarActivity();
495
+ const tableWidth = useMemo(() => getTableWidth(columns), [columns]);
493
496
  const gridTemplateColumns = useMemo(
494
497
  () => buildGridTemplateColumns(columns),
495
498
  [columns],
@@ -538,6 +541,25 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
538
541
  const totalHeight = virtualize
539
542
  ? rowVirtualizer.getTotalSize()
540
543
  : WEB_CELL_HEIGHT + items.length * WEB_CELL_HEIGHT;
544
+ const horizontalScrollEnabled = showHorizontalScrollbar
545
+ && hasMeaningfulTableHorizontalOverflow(tableWidth, viewportWidth);
546
+ const scrollContentWidth = horizontalScrollEnabled
547
+ ? Math.max(1, tableWidth * WEB_CELL_WIDTH)
548
+ : "100%";
549
+ const measureViewportWidth = useCallback(() => {
550
+ const element = bodyElementRef.current;
551
+ const nextValue = element ? toCellX(element.clientWidth) : 0;
552
+ setViewportWidth((current) => current === nextValue ? current : nextValue);
553
+ }, []);
554
+
555
+ useEffect(() => {
556
+ measureViewportWidth();
557
+ const element = bodyElementRef.current;
558
+ if (!element || typeof ResizeObserver === "undefined") return;
559
+ const observer = new ResizeObserver(measureViewportWidth);
560
+ observer.observe(element);
561
+ return () => observer.disconnect();
562
+ }, [measureViewportWidth]);
541
563
 
542
564
  useEffect(() => {
543
565
  if (scrollToIndex == null || items.length === 0) return;
@@ -589,11 +611,11 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
589
611
 
590
612
  useEffect(() => {
591
613
  headerHorizontal.bar.visible = false;
592
- bodyHorizontal.bar.visible = showHorizontalScrollbar;
593
- if (!showHorizontalScrollbar) {
614
+ bodyHorizontal.bar.visible = horizontalScrollEnabled;
615
+ if (!horizontalScrollEnabled) {
594
616
  if (bodyElementRef.current) bodyElementRef.current.scrollLeft = 0;
595
617
  }
596
- }, [bodyHorizontal.bar, headerHorizontal.bar, showHorizontalScrollbar]);
618
+ }, [bodyHorizontal.bar, headerHorizontal.bar, horizontalScrollEnabled]);
597
619
 
598
620
  const rootStyle: CSSProperties = {
599
621
  display: "flex",
@@ -610,7 +632,7 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
610
632
  flex: "1 1 0px",
611
633
  minWidth: 0,
612
634
  minHeight: 0,
613
- overflowX: showHorizontalScrollbar ? "auto" : "hidden",
635
+ overflowX: horizontalScrollEnabled ? "auto" : "hidden",
614
636
  overflowY: "auto",
615
637
  backgroundColor: colors.bg,
616
638
  };
@@ -621,7 +643,7 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
621
643
  ref={bodyElementRef}
622
644
  data-gloom-role="data-table-body-scroll"
623
645
  data-gloom-scrollbar-x={
624
- showHorizontalScrollbar && bodyHorizontal.visible
646
+ horizontalScrollEnabled && bodyHorizontal.visible
625
647
  ? "visible"
626
648
  : "hidden"
627
649
  }
@@ -646,7 +668,7 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
646
668
  data-gloom-role="data-table-scroll-content"
647
669
  style={{
648
670
  position: "relative",
649
- width: "100%",
671
+ width: scrollContentWidth,
650
672
  height: items.length > 0 ? totalHeight : "100%",
651
673
  minHeight: WEB_CELL_HEIGHT,
652
674
  }}
@@ -14,13 +14,27 @@ import type { PageStackViewProps } from "../../../components/ui/page-stack-view"
14
14
  import type { SegmentedControlProps } from "../../../components/ui/toggle";
15
15
 
16
16
  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);
17
+
18
+ function panelBorder(): string {
19
+ return blendHex(colors.border, colors.borderFocused, 0.18);
20
+ }
21
+
22
+ function panelFill(): string {
23
+ return blendHex(colors.panel, colors.bg, 0.22);
24
+ }
25
+
26
+ function subtlePanelFill(): string {
27
+ return blendHex(colors.panel, colors.bg, 0.42);
28
+ }
29
+
30
+ function selectedPanelFill(): string {
31
+ return blendHex(colors.selected, colors.bg, 0.42);
32
+ }
19
33
 
20
34
  function controlBorderColor(focused = false, active = false): string {
21
35
  if (active) return colors.borderFocused;
22
36
  if (focused) return blendHex(colors.borderFocused, colors.textBright, 0.24);
23
- return PANEL_BORDER;
37
+ return panelBorder();
24
38
  }
25
39
 
26
40
  function controlShadow(active = false): string {
@@ -33,8 +47,8 @@ function buttonPalette(props: Pick<ButtonProps, "variant" | "active" | "disabled
33
47
  if (props.disabled) {
34
48
  return {
35
49
  fg: colors.textMuted,
36
- bg: "rgba(63, 72, 82, 0.35)",
37
- border: PANEL_BORDER,
50
+ bg: subtlePanelFill(),
51
+ border: panelBorder(),
38
52
  };
39
53
  }
40
54
  if (props.active) {
@@ -62,14 +76,14 @@ function buttonPalette(props: Pick<ButtonProps, "variant" | "active" | "disabled
62
76
  return {
63
77
  fg: colors.textDim,
64
78
  bg: "rgba(0, 0, 0, 0)",
65
- border: PANEL_BORDER,
79
+ border: panelBorder(),
66
80
  };
67
81
  case "secondary":
68
82
  default:
69
83
  return {
70
84
  fg: colors.text,
71
- bg: PANEL_FILL,
72
- border: PANEL_BORDER,
85
+ bg: panelFill(),
86
+ border: panelBorder(),
73
87
  };
74
88
  }
75
89
  }
@@ -248,7 +262,7 @@ export function WebMessageComposer({
248
262
  flexDirection="row"
249
263
  width={width}
250
264
  height={height}
251
- backgroundColor={PANEL_FILL}
265
+ backgroundColor={panelFill()}
252
266
  onMouseDown={requestFocus}
253
267
  data-gloom-role="desktop-message-composer"
254
268
  style={{
@@ -350,7 +364,7 @@ export function WebListView({
350
364
  const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
351
365
  const scrollRef = useRef<ScrollBoxRenderable>(null);
352
366
  const baseBg = bgColor ?? colors.bg;
353
- const activeBg = selectedBgColor ?? "rgba(84, 201, 159, 0.12)";
367
+ const activeBg = selectedBgColor ?? selectedPanelFill();
354
368
  const rowHoverBg = hoverBgColor ?? hoverBg();
355
369
  const selectedItem = selectedIndex >= 0 ? items[selectedIndex] : undefined;
356
370
  const activeScrollIndex = scrollIndex ?? selectedIndex;
@@ -435,10 +449,10 @@ export function WebListView({
435
449
  backgroundColor: "transparent",
436
450
  }
437
451
  : {
438
- border: `1px solid ${PANEL_BORDER}`,
452
+ border: `1px solid ${panelBorder()}`,
439
453
  borderRadius: CONTROL_RADIUS,
440
454
  padding: 4,
441
- backgroundColor: "rgba(9, 12, 15, 0.22)",
455
+ backgroundColor: subtlePanelFill(),
442
456
  }}
443
457
  >
444
458
  <Box flexDirection="column" gap={rowGap}>
@@ -454,9 +468,9 @@ export function WebListView({
454
468
  {showSelectedDescription && selectedItem?.description && (
455
469
  <Box
456
470
  flexDirection="row"
457
- backgroundColor="rgba(9, 12, 15, 0.22)"
471
+ backgroundColor={subtlePanelFill()}
458
472
  style={{
459
- border: `1px solid ${PANEL_BORDER}`,
473
+ border: `1px solid ${panelBorder()}`,
460
474
  borderRadius: CONTROL_RADIUS,
461
475
  paddingInline: 10,
462
476
  }}
@@ -478,9 +492,9 @@ export function WebSegmentedControl({
478
492
  return (
479
493
  <Box
480
494
  flexDirection="row"
481
- backgroundColor="rgba(8, 11, 14, 0.32)"
495
+ backgroundColor={panelFill()}
482
496
  style={{
483
- border: `1px solid ${PANEL_BORDER}`,
497
+ border: `1px solid ${panelBorder()}`,
484
498
  borderRadius: CONTROL_RADIUS,
485
499
  padding: 2,
486
500
  }}
@@ -531,7 +545,7 @@ export function WebDialogFrame({
531
545
  flexDirection="row"
532
546
  alignItems="center"
533
547
  style={{
534
- borderBottom: showTitleDivider ? `1px solid ${PANEL_BORDER}` : "none",
548
+ borderBottom: showTitleDivider ? `1px solid ${panelBorder()}` : "none",
535
549
  paddingBottom: showTitleDivider ? 8 : 0,
536
550
  marginBottom: showTitleDivider ? 10 : 14,
537
551
  }}
@@ -545,7 +559,7 @@ export function WebDialogFrame({
545
559
  <Box
546
560
  height={1}
547
561
  style={{
548
- borderTop: `1px solid ${PANEL_BORDER}`,
562
+ borderTop: `1px solid ${panelBorder()}`,
549
563
  paddingTop: 8,
550
564
  marginTop: 10,
551
565
  }}
@@ -600,7 +614,7 @@ export function WebPageStackView({
600
614
  height={1}
601
615
  flexDirection="row"
602
616
  alignItems="center"
603
- backgroundColor={PANEL_FILL}
617
+ backgroundColor={panelFill()}
604
618
  onMouseDown={(event) => {
605
619
  event.preventDefault?.();
606
620
  event.stopPropagation?.();
@@ -608,7 +622,7 @@ export function WebPageStackView({
608
622
  }}
609
623
  data-gloom-interactive="true"
610
624
  style={{
611
- border: `1px solid ${PANEL_BORDER}`,
625
+ border: `1px solid ${panelBorder()}`,
612
626
  borderRadius: CONTROL_RADIUS,
613
627
  paddingInline: 8,
614
628
  cursor: "pointer",
@@ -2,6 +2,7 @@
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";
5
6
 
6
7
  interface DialogState {
7
8
  id: string;
@@ -13,6 +14,8 @@ let nextDialogId = 1;
13
14
 
14
15
  export function WebDialogHostProvider({ children }: { children: ReactNode }) {
15
16
  const [dialogState, setDialogState] = useState<DialogState | null>(null);
17
+ const dialogBorder = blendHex(colors.border, colors.borderFocused, 0.18);
18
+ const dialogBg = blendHex(colors.panel, colors.bg, 0.12);
16
19
 
17
20
  const close = useCallback((value?: unknown) => {
18
21
  setDialogState((current) => {
@@ -46,7 +49,14 @@ export function WebDialogHostProvider({ children }: { children: ReactNode }) {
46
49
  if (event.target === event.currentTarget) close(undefined);
47
50
  }}
48
51
  >
49
- <div className="gloom-dialog">
52
+ <div
53
+ className="gloom-dialog"
54
+ style={{
55
+ borderColor: dialogBorder,
56
+ background: dialogBg,
57
+ color: colors.text,
58
+ }}
59
+ >
50
60
  {typeof dialogState.content === "function"
51
61
  ? dialogState.content({
52
62
  dialogId: dialogState.id,
@@ -1,14 +1,25 @@
1
1
  /// <reference lib="dom" />
2
2
  /** @jsxImportSource react */
3
- import { useEffect, useMemo, useSyncExternalStore, type ReactNode } from "react";
4
- import { InputHostProvider, type InputHost, type KeyEventLike } from "../../../react/input";
5
- import { hasWebCtrlModifier, normalizeWebKeyName, shouldConsumeWebAppKeyDown, webKeySequence } from "./key-event";
3
+ import { useEffect, useLayoutEffect, useMemo, useRef, useSyncExternalStore, type ReactNode } from "react";
4
+ import { InputHostProvider, type InputHost, type KeyEventLike, type ShortcutOptions } from "../../../react/input";
5
+ import {
6
+ isMouseBackNavigationButton,
7
+ MOUSE_BACK_NAVIGATION_EVENT_NAME,
8
+ } from "../../../utils/back-navigation";
9
+ import {
10
+ hasWebCtrlModifier,
11
+ isEditableKeyboardTarget,
12
+ normalizeWebKeyName,
13
+ shouldConsumeWebAppKeyDown,
14
+ webKeySequence,
15
+ } from "./key-event";
6
16
 
7
17
  export const WEB_CELL_WIDTH = 8;
8
18
  export const WEB_CELL_HEIGHT = 18;
9
19
 
10
20
  function toKeyEventLike(event: KeyboardEvent): KeyEventLike {
11
21
  const key = normalizeWebKeyName(event.key);
22
+ let propagationStopped = false;
12
23
  return {
13
24
  key,
14
25
  name: key,
@@ -18,11 +29,56 @@ function toKeyEventLike(event: KeyboardEvent): KeyEventLike {
18
29
  alt: event.altKey,
19
30
  meta: event.metaKey,
20
31
  super: event.metaKey,
32
+ targetEditable: isEditableKeyboardTarget(event.target),
33
+ get defaultPrevented() {
34
+ return event.defaultPrevented;
35
+ },
36
+ get propagationStopped() {
37
+ return propagationStopped;
38
+ },
39
+ preventDefault: () => event.preventDefault(),
40
+ stopPropagation: () => {
41
+ propagationStopped = true;
42
+ event.stopPropagation();
43
+ },
44
+ };
45
+ }
46
+
47
+ function toMouseBackKeyEventLike(event: MouseEvent): KeyEventLike {
48
+ let propagationStopped = false;
49
+ return {
50
+ key: MOUSE_BACK_NAVIGATION_EVENT_NAME,
51
+ name: MOUSE_BACK_NAVIGATION_EVENT_NAME,
52
+ sequence: "",
53
+ ctrl: event.ctrlKey,
54
+ shift: event.shiftKey,
55
+ alt: event.altKey,
56
+ meta: event.metaKey,
57
+ super: event.metaKey,
58
+ targetEditable: isEditableKeyboardTarget(event.target),
59
+ get defaultPrevented() {
60
+ return event.defaultPrevented;
61
+ },
62
+ get propagationStopped() {
63
+ return propagationStopped;
64
+ },
21
65
  preventDefault: () => event.preventDefault(),
22
- stopPropagation: () => event.stopPropagation(),
66
+ stopPropagation: () => {
67
+ propagationStopped = true;
68
+ event.stopPropagation();
69
+ },
23
70
  };
24
71
  }
25
72
 
73
+ interface ShortcutEntry {
74
+ handlerRef: { current: (event: KeyEventLike) => void };
75
+ enabledRef: { current: boolean };
76
+ phase: NonNullable<ShortcutOptions["phase"]>;
77
+ order: number;
78
+ }
79
+
80
+ let nextShortcutOrder = 1;
81
+
26
82
  function subscribeViewport(listener: () => void): () => void {
27
83
  window.addEventListener("resize", listener);
28
84
  return () => window.removeEventListener("resize", listener);
@@ -40,17 +96,72 @@ function getViewport() {
40
96
  }
41
97
 
42
98
  export function WebInputHostProvider({ children }: { children: ReactNode }) {
99
+ const shortcutsRef = useRef<ShortcutEntry[]>([]);
100
+
101
+ const dispatchShortcut = (shortcutEvent: KeyEventLike) => {
102
+ for (const phase of ["normal", "after"] as const) {
103
+ if (phase === "after" && (shortcutEvent.defaultPrevented || shortcutEvent.propagationStopped)) break;
104
+ for (const entry of shortcutsRef.current) {
105
+ if (entry.phase !== phase || !entry.enabledRef.current) continue;
106
+ entry.handlerRef.current(shortcutEvent);
107
+ if (shortcutEvent.propagationStopped) break;
108
+ }
109
+ if (shortcutEvent.propagationStopped) break;
110
+ }
111
+ };
112
+
113
+ useEffect(() => {
114
+ const onKeyDown = (event: KeyboardEvent) => {
115
+ const shortcutEvent = toKeyEventLike(event);
116
+ dispatchShortcut(shortcutEvent);
117
+ if (shouldConsumeWebAppKeyDown(event)) event.preventDefault();
118
+ };
119
+ window.addEventListener("keydown", onKeyDown);
120
+ return () => window.removeEventListener("keydown", onKeyDown);
121
+ }, []);
122
+
123
+ useEffect(() => {
124
+ const preventBrowserBack = (event: MouseEvent) => {
125
+ if (!isMouseBackNavigationButton(event.button)) return;
126
+ event.preventDefault();
127
+ event.stopPropagation();
128
+ };
129
+ const onMouseUp = (event: MouseEvent) => {
130
+ if (!isMouseBackNavigationButton(event.button)) return;
131
+ event.preventDefault();
132
+ event.stopPropagation();
133
+ dispatchShortcut(toMouseBackKeyEventLike(event));
134
+ };
135
+
136
+ window.addEventListener("mousedown", preventBrowserBack, true);
137
+ window.addEventListener("auxclick", preventBrowserBack, true);
138
+ window.addEventListener("mouseup", onMouseUp, true);
139
+ return () => {
140
+ window.removeEventListener("mousedown", preventBrowserBack, true);
141
+ window.removeEventListener("auxclick", preventBrowserBack, true);
142
+ window.removeEventListener("mouseup", onMouseUp, true);
143
+ };
144
+ }, []);
145
+
43
146
  const host = useMemo<InputHost>(() => ({
44
147
  useShortcut(handler, options) {
45
- useEffect(() => {
46
- if (options?.enabled === false) return;
47
- const onKeyDown = (event: KeyboardEvent) => {
48
- handler(toKeyEventLike(event));
49
- if (shouldConsumeWebAppKeyDown(event)) event.preventDefault();
148
+ const handlerRef = useRef(handler);
149
+ const enabledRef = useRef(options?.enabled !== false);
150
+ handlerRef.current = handler;
151
+ enabledRef.current = options?.enabled !== false;
152
+
153
+ useLayoutEffect(() => {
154
+ const entry: ShortcutEntry = {
155
+ handlerRef,
156
+ enabledRef,
157
+ phase: options?.phase ?? "normal",
158
+ order: nextShortcutOrder++,
159
+ };
160
+ shortcutsRef.current = [...shortcutsRef.current, entry].sort((a, b) => a.order - b.order);
161
+ return () => {
162
+ shortcutsRef.current = shortcutsRef.current.filter((current) => current !== entry);
50
163
  };
51
- window.addEventListener("keydown", onKeyDown);
52
- return () => window.removeEventListener("keydown", onKeyDown);
53
- }, [handler, options?.enabled, options?.scope]);
164
+ }, [options?.phase, options?.scope]);
54
165
  },
55
166
  useViewport() {
56
167
  return useSyncExternalStore(subscribeViewport, getViewport, getViewport);
@@ -38,7 +38,7 @@ function targetHasClosest(target: KeyboardTargetLike, selector: string): boolean
38
38
  }
39
39
  }
40
40
 
41
- function isEditableKeyboardTarget(target: EventTarget | null): boolean {
41
+ export function isEditableKeyboardTarget(target: EventTarget | null): boolean {
42
42
  const element = getKeyboardTarget(target);
43
43
  if (!element) return false;
44
44