gloomberb 0.6.1 → 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 (139) hide show
  1. package/package.json +1 -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 +48 -27
  14. package/src/components/chart/static-chart-surface.tsx +28 -4
  15. package/src/components/chart/stock-chart.test.ts +0 -16
  16. package/src/components/chart/stock-chart.tsx +38 -21
  17. package/src/components/chart/time-axis-label.tsx +43 -0
  18. package/src/components/command-bar/command-bar.test.tsx +71 -8
  19. package/src/components/command-bar/command-bar.tsx +388 -713
  20. package/src/components/command-bar/confirm-body.tsx +51 -0
  21. package/src/components/command-bar/list-model.ts +77 -0
  22. package/src/components/command-bar/view-model.ts +0 -19
  23. package/src/components/command-bar/workflow-ops.ts +4 -0
  24. package/src/components/data-table-stack-view.test.tsx +39 -4
  25. package/src/components/data-table-stack-view.tsx +11 -104
  26. package/src/components/data-table-view.tsx +43 -79
  27. package/src/components/feed-data-table-stack-view.tsx +10 -17
  28. package/src/components/feed-stack-controller.ts +39 -0
  29. package/src/components/index.ts +5 -1
  30. package/src/components/layout/floating-pane.tsx +30 -2
  31. package/src/components/layout/header.tsx +10 -7
  32. package/src/components/layout/pane-cycle-order.test.ts +30 -0
  33. package/src/components/layout/pane-cycle-order.ts +32 -0
  34. package/src/components/layout/pane-footer.tsx +11 -4
  35. package/src/components/layout/pane-header.tsx +5 -5
  36. package/src/components/layout/shell.test.tsx +66 -22
  37. package/src/components/layout/shell.tsx +13 -6
  38. package/src/components/onboarding/onboarding-wizard.tsx +1 -2
  39. package/src/components/pane-settings-dialog.tsx +12 -4
  40. package/src/components/pane-template-wizard.test.tsx +3 -4
  41. package/src/components/speedometer-gauge.test.tsx +45 -0
  42. package/src/components/speedometer-gauge.tsx +558 -0
  43. package/src/components/table-view-shared.tsx +158 -0
  44. package/src/components/ticker-list-table-view.tsx +42 -72
  45. package/src/components/ui/confirm-dialog.tsx +66 -0
  46. package/src/components/ui/data-table.tsx +18 -10
  47. package/src/components/ui/index.ts +0 -1
  48. package/src/components/ui/page-stack-view.tsx +39 -10
  49. package/src/components/ui/table-layout.test.ts +16 -0
  50. package/src/components/ui/table-layout.ts +10 -3
  51. package/src/components/ui/ui.test.tsx +100 -20
  52. package/src/core/state/app-state.ts +6 -3
  53. package/src/market-data/coordinator.test.ts +1 -1
  54. package/src/market-data/coordinator.ts +0 -1
  55. package/src/news/aggregator.ts +0 -3
  56. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +46 -16
  57. package/src/plugins/builtin/ai/providers.ts +0 -17
  58. package/src/plugins/builtin/ai/screener-pane.test.tsx +3 -4
  59. package/src/plugins/builtin/ai/screener-pane.tsx +0 -3
  60. package/src/plugins/builtin/ask-ai.test.tsx +89 -2
  61. package/src/plugins/builtin/broker-manager/index.test.tsx +2 -2
  62. package/src/plugins/builtin/broker-manager/index.tsx +19 -36
  63. package/src/plugins/builtin/chat-controller.test.ts +105 -0
  64. package/src/plugins/builtin/chat-controller.ts +75 -6
  65. package/src/plugins/builtin/chat.test.tsx +6 -6
  66. package/src/plugins/builtin/chat.tsx +29 -20
  67. package/src/plugins/builtin/cloud-tweets.tsx +0 -1
  68. package/src/plugins/builtin/comparison-chart.test.tsx +53 -2
  69. package/src/plugins/builtin/comparison-chart.tsx +11 -5
  70. package/src/plugins/builtin/correlation/index.tsx +1 -6
  71. package/src/plugins/builtin/debug.tsx +1 -3
  72. package/src/plugins/builtin/econ/index.tsx +1 -2
  73. package/src/plugins/builtin/fear-greed/fear-greed-data.test.ts +72 -0
  74. package/src/plugins/builtin/fear-greed/fear-greed-data.ts +366 -0
  75. package/src/plugins/builtin/fear-greed/index.tsx +478 -0
  76. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  77. package/src/plugins/builtin/help.test.tsx +2 -2
  78. package/src/plugins/builtin/help.tsx +2 -2
  79. package/src/plugins/builtin/holders/index.tsx +10 -15
  80. package/src/plugins/builtin/insider/index.tsx +22 -89
  81. package/src/plugins/builtin/market-movers/index.tsx +1 -1
  82. package/src/plugins/builtin/news-wire/index.tsx +41 -2
  83. package/src/plugins/builtin/news-wire/news-table.tsx +11 -11
  84. package/src/plugins/builtin/news.tsx +4 -16
  85. package/src/plugins/builtin/notes.tsx +18 -55
  86. package/src/plugins/builtin/options.test.tsx +58 -2
  87. package/src/plugins/builtin/options.tsx +108 -49
  88. package/src/plugins/builtin/plugin-groups.ts +2 -0
  89. package/src/plugins/builtin/portfolio-list/cli/commands.ts +1 -1
  90. package/src/plugins/builtin/research/index.tsx +5 -21
  91. package/src/plugins/builtin/sec-filing-display.tsx +84 -0
  92. package/src/plugins/builtin/sec.tsx +13 -66
  93. package/src/plugins/builtin/sectors/index.tsx +1 -1
  94. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +0 -1
  95. package/src/plugins/builtin/ticker-detail/index.tsx +0 -1
  96. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +0 -1
  97. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +3 -4
  98. package/src/plugins/builtin/world-indices/index.tsx +1 -1
  99. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  100. package/src/plugins/ibkr/broker-adapter.ts +1 -3
  101. package/src/plugins/ibkr/gateway-service-native.ts +1 -5
  102. package/src/plugins/ibkr/gateway-service.ts +1 -1
  103. package/src/plugins/ibkr/index.test.tsx +0 -2
  104. package/src/plugins/ibkr/trade-tab.test.tsx +3 -4
  105. package/src/plugins/pane-manager.ts +0 -13
  106. package/src/plugins/plugin-runtime.test.tsx +6 -6
  107. package/src/plugins/plugin-runtime.tsx +13 -8
  108. package/src/plugins/prediction-markets/cli.ts +0 -1
  109. package/src/plugins/prediction-markets/controller-data.ts +0 -2
  110. package/src/plugins/prediction-markets/pane.test.tsx +3 -11
  111. package/src/plugins/prediction-markets/test-helpers.tsx +1 -1
  112. package/src/plugins/registry.ts +10 -39
  113. package/src/react/input.ts +1 -0
  114. package/src/renderers/electrobun/view/data-table.tsx +28 -6
  115. package/src/renderers/electrobun/view/desktop-controls.tsx +34 -20
  116. package/src/renderers/electrobun/view/dialog-host.tsx +11 -1
  117. package/src/renderers/electrobun/view/input-host.tsx +74 -10
  118. package/src/renderers/electrobun/view/key-event.ts +1 -1
  119. package/src/renderers/electrobun/view/styles.css +331 -0
  120. package/src/renderers/opentui/chart-surface.tsx +179 -0
  121. package/src/renderers/opentui/start.tsx +48 -30
  122. package/src/renderers/opentui/test-utils.tsx +17 -0
  123. package/src/renderers/opentui/ui-host.tsx +5 -2
  124. package/src/sources/provider-router.ts +0 -4
  125. package/src/state/app-context.test.ts +45 -0
  126. package/src/state/use-inline-tickers.test.tsx +73 -0
  127. package/src/state/use-inline-tickers.ts +7 -1
  128. package/src/test-support/plugin-runtime.ts +3 -2
  129. package/src/theme/colors.ts +12 -3
  130. package/src/theme/themes.test.ts +23 -1
  131. package/src/theme/themes.ts +115 -0
  132. package/src/types/broker.ts +1 -1
  133. package/src/types/data-provider.ts +0 -1
  134. package/src/types/plugin.ts +0 -2
  135. package/src/ui/host.tsx +1 -0
  136. package/src/utils/back-navigation.ts +24 -1
  137. package/src/version.ts +1 -1
  138. package/src/plugins/builtin/news-wire/feed-pane.tsx +0 -21
  139. package/src/plugins/builtin/news-wire/top-pane.tsx +0 -21
@@ -2,7 +2,17 @@
2
2
  /** @jsxImportSource react */
3
3
  import { useEffect, useLayoutEffect, useMemo, useRef, useSyncExternalStore, type ReactNode } from "react";
4
4
  import { InputHostProvider, type InputHost, type KeyEventLike, type ShortcutOptions } from "../../../react/input";
5
- import { hasWebCtrlModifier, normalizeWebKeyName, shouldConsumeWebAppKeyDown, webKeySequence } from "./key-event";
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;
@@ -19,6 +29,33 @@ function toKeyEventLike(event: KeyboardEvent): KeyEventLike {
19
29
  alt: event.altKey,
20
30
  meta: event.metaKey,
21
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),
22
59
  get defaultPrevented() {
23
60
  return event.defaultPrevented;
24
61
  },
@@ -61,24 +98,51 @@ function getViewport() {
61
98
  export function WebInputHostProvider({ children }: { children: ReactNode }) {
62
99
  const shortcutsRef = useRef<ShortcutEntry[]>([]);
63
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
+
64
113
  useEffect(() => {
65
114
  const onKeyDown = (event: KeyboardEvent) => {
66
115
  const shortcutEvent = toKeyEventLike(event);
67
- for (const phase of ["normal", "after"] as const) {
68
- if (phase === "after" && (shortcutEvent.defaultPrevented || shortcutEvent.propagationStopped)) break;
69
- for (const entry of shortcutsRef.current) {
70
- if (entry.phase !== phase || !entry.enabledRef.current) continue;
71
- entry.handlerRef.current(shortcutEvent);
72
- if (shortcutEvent.propagationStopped) break;
73
- }
74
- if (shortcutEvent.propagationStopped) break;
75
- }
116
+ dispatchShortcut(shortcutEvent);
76
117
  if (shouldConsumeWebAppKeyDown(event)) event.preventDefault();
77
118
  };
78
119
  window.addEventListener("keydown", onKeyDown);
79
120
  return () => window.removeEventListener("keydown", onKeyDown);
80
121
  }, []);
81
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
+
82
146
  const host = useMemo<InputHost>(() => ({
83
147
  useShortcut(handler, options) {
84
148
  const handlerRef = useRef(handler);
@@ -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
 
@@ -0,0 +1,331 @@
1
+ @keyframes gloom-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
2
+ html, body, #root { width: 100%; height: 100%; min-height: 100%; overflow: hidden; background: #000; }
3
+ body {
4
+ --cell-w: 8px;
5
+ --cell-h: 18px;
6
+ margin: 0;
7
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
8
+ font-size: 12px;
9
+ line-height: var(--cell-h);
10
+ color: #d8dde3;
11
+ cursor: default;
12
+ user-select: none;
13
+ color-scheme: dark;
14
+ -webkit-font-smoothing: antialiased;
15
+ text-rendering: geometricPrecision;
16
+ }
17
+ * { box-sizing: border-box; }
18
+ #root > * { height: 100%; min-height: 100%; }
19
+ input, textarea, button { font: inherit; }
20
+ input, textarea { cursor: text; user-select: text; }
21
+ button, [data-gloom-interactive="true"] { cursor: pointer; }
22
+ [data-gloom-role="pane-window"] {
23
+ background-clip: padding-box;
24
+ isolation: isolate;
25
+ }
26
+ [data-gloom-role="detached-pane-window"] {
27
+ border: 0;
28
+ box-shadow: none;
29
+ background-clip: padding-box;
30
+ }
31
+ [data-gloom-role="app-header"][data-titlebar-overlay="true"],
32
+ .electrobun-webkit-app-region-drag {
33
+ cursor: default;
34
+ -webkit-app-region: drag;
35
+ app-region: drag;
36
+ -webkit-user-select: none;
37
+ user-select: none;
38
+ }
39
+ .electrobun-webkit-app-region-no-drag {
40
+ -webkit-app-region: no-drag;
41
+ app-region: no-drag;
42
+ }
43
+ [data-titlebar-overlay="true"] {
44
+ min-height: __TITLEBAR_OVERLAY_HEIGHT_PX__px;
45
+ max-height: __TITLEBAR_OVERLAY_HEIGHT_PX__px;
46
+ align-items: center;
47
+ }
48
+ [data-titlebar-overlay="true"] * {
49
+ cursor: inherit;
50
+ }
51
+ [data-titlebar-overlay="true"] button,
52
+ [data-titlebar-overlay="true"] input,
53
+ [data-titlebar-overlay="true"] textarea,
54
+ [data-titlebar-overlay="true"] a,
55
+ [data-titlebar-overlay="true"] .electrobun-webkit-app-region-no-drag,
56
+ [data-titlebar-overlay="true"] [data-gloom-interactive="true"] {
57
+ -webkit-app-region: no-drag;
58
+ app-region: no-drag;
59
+ }
60
+ [data-gloom-role="pane-window"][data-floating="true"] {
61
+ border: 0;
62
+ border-radius: 6px;
63
+ overflow: hidden;
64
+ box-shadow: 0 18px 38px rgba(0, 0, 0, .46);
65
+ }
66
+ [data-gloom-role="pane-window"][data-floating="true"]::after {
67
+ content: "";
68
+ position: absolute;
69
+ inset: 0;
70
+ border: 1px solid var(--pane-border-color, #3a4148);
71
+ border-radius: inherit;
72
+ pointer-events: none;
73
+ z-index: 100;
74
+ }
75
+ [data-gloom-role="pane-window"][data-floating="true"][data-focused="true"] {
76
+ box-shadow: 0 18px 40px rgba(0, 0, 0, .5);
77
+ }
78
+ [data-gloom-role="pane-window"][data-floating="false"] {
79
+ border: 0;
80
+ border-radius: 0;
81
+ }
82
+ [data-gloom-role="pane-window"][data-floating="false"][data-focused="true"] {
83
+ box-shadow: inset 0 0 0 1px var(--pane-border-color, #54c99f);
84
+ }
85
+ [data-gloom-role="pane-header"] {
86
+ align-items: center;
87
+ padding-inline: 7px;
88
+ cursor: grab;
89
+ -webkit-user-select: none;
90
+ user-select: none;
91
+ }
92
+ [data-gloom-role="pane-header"] * {
93
+ cursor: inherit;
94
+ }
95
+ [data-gloom-role="pane-action"],
96
+ [data-gloom-role="pane-close"] {
97
+ cursor: pointer;
98
+ }
99
+ body.gloom-dragging [data-gloom-role="pane-header"] {
100
+ cursor: grabbing;
101
+ }
102
+ body.gloom-dragging {
103
+ cursor: grabbing;
104
+ }
105
+ [data-gloom-role="pane-title"] {
106
+ overflow: hidden;
107
+ text-overflow: ellipsis;
108
+ }
109
+ [data-gloom-role="chat-message"]:hover [data-gloom-role="chat-message-row"][data-selected="false"] {
110
+ background-color: var(--chat-hover-bg);
111
+ }
112
+ [data-gloom-role="chat-message-reply-action"] {
113
+ opacity: 0;
114
+ pointer-events: none;
115
+ }
116
+ [data-gloom-role="chat-message"]:hover [data-gloom-role="chat-message-reply-action"],
117
+ [data-gloom-role="chat-message"][data-selected="true"] [data-gloom-role="chat-message-reply-action"] {
118
+ opacity: 1;
119
+ pointer-events: auto;
120
+ }
121
+ [data-gloom-role="status-bar"] {
122
+ min-height: var(--cell-h);
123
+ max-height: var(--cell-h);
124
+ align-items: center;
125
+ border-top: 1px solid color-mix(in srgb, #3a4148 72%, transparent);
126
+ font-size: 11.5px;
127
+ }
128
+ [data-gloom-role="status-bar"] span {
129
+ line-height: var(--cell-h);
130
+ }
131
+ [data-gloom-role="pane-footer"] {
132
+ min-height: var(--cell-h);
133
+ max-height: var(--cell-h);
134
+ align-items: center;
135
+ border-top: 1px solid color-mix(in srgb, var(--pane-footer-border-color, #3a4148) 72%, transparent);
136
+ font-size: 11.5px;
137
+ overflow: hidden;
138
+ }
139
+ [data-gloom-role="pane-footer"][data-empty="true"] {
140
+ border-top-color: transparent;
141
+ }
142
+ [data-gloom-role="pane-footer"] span,
143
+ [data-gloom-role="pane-hint"] span {
144
+ line-height: var(--cell-h);
145
+ }
146
+ [data-gloom-role="pane-hint"] {
147
+ cursor: pointer;
148
+ border-radius: 4px;
149
+ }
150
+ [data-gloom-role="pane-hint"]:hover {
151
+ background: rgba(255,255,255,.08);
152
+ }
153
+ [data-gloom-role="pane-action"],
154
+ [data-gloom-role="pane-close"] {
155
+ border-radius: 4px;
156
+ padding-inline: 2px;
157
+ }
158
+ [data-gloom-role="pane-action"]:hover,
159
+ [data-gloom-role="pane-close"]:hover {
160
+ background: rgba(255,255,255,.08);
161
+ }
162
+ [data-gloom-role="resize-handle"] {
163
+ cursor: nwse-resize;
164
+ }
165
+ [data-gloom-role="chart-surface"] {
166
+ cursor: crosshair;
167
+ }
168
+ [data-gloom-role="tab-list"] {
169
+ scrollbar-width: none;
170
+ -webkit-overflow-scrolling: touch;
171
+ }
172
+ [data-gloom-role="tab-list"]::-webkit-scrollbar {
173
+ width: 0;
174
+ height: 0;
175
+ display: none;
176
+ }
177
+ [data-gloom-role="tab-button"] {
178
+ all: unset;
179
+ box-sizing: border-box;
180
+ }
181
+ [data-gloom-role="tab-button"]:focus-visible {
182
+ outline: 1px solid var(--tab-hover-underline);
183
+ outline-offset: -1px;
184
+ }
185
+ [data-gloom-role="tab-close"] {
186
+ cursor: pointer;
187
+ }
188
+ [data-gloom-role="tab-close"]:hover {
189
+ background: rgba(255,255,255,.1);
190
+ }
191
+ [data-gloom-scrollbar-x],
192
+ [data-gloom-scrollbar-y] {
193
+ scrollbar-color: transparent transparent;
194
+ scrollbar-width: thin;
195
+ }
196
+ [data-gloom-scrollbar-active="true"] {
197
+ scrollbar-color: rgba(216,221,227,.26) transparent;
198
+ }
199
+ [data-gloom-scrollbar-x]::-webkit-scrollbar,
200
+ [data-gloom-scrollbar-y]::-webkit-scrollbar {
201
+ width: 8px;
202
+ height: 8px;
203
+ background: transparent;
204
+ }
205
+ [data-gloom-scrollbar-x]::-webkit-scrollbar-track,
206
+ [data-gloom-scrollbar-y]::-webkit-scrollbar-track {
207
+ background: transparent;
208
+ }
209
+ [data-gloom-scrollbar-x]::-webkit-scrollbar-thumb,
210
+ [data-gloom-scrollbar-y]::-webkit-scrollbar-thumb {
211
+ min-width: 24px;
212
+ min-height: 24px;
213
+ border: 2px solid transparent;
214
+ border-radius: 999px;
215
+ background: transparent;
216
+ background-clip: content-box;
217
+ }
218
+ [data-gloom-scrollbar-x]::-webkit-scrollbar-thumb:hover,
219
+ [data-gloom-scrollbar-y]::-webkit-scrollbar-thumb:hover {
220
+ background: transparent;
221
+ background-clip: content-box;
222
+ }
223
+ [data-gloom-scrollbar-active="true"]::-webkit-scrollbar-thumb,
224
+ [data-gloom-scrollbar-active="true"]::-webkit-scrollbar-thumb:hover {
225
+ background: rgba(216,221,227,.24);
226
+ background-clip: content-box;
227
+ }
228
+ [data-gloom-scrollbar-x]::-webkit-scrollbar-corner,
229
+ [data-gloom-scrollbar-y]::-webkit-scrollbar-corner {
230
+ background: transparent;
231
+ }
232
+ [data-gloom-scrollbar-x="hidden"]:not([data-gloom-scrollbar-y="visible"]),
233
+ [data-gloom-scrollbar-y="hidden"]:not([data-gloom-scrollbar-x="visible"]) {
234
+ scrollbar-width: none;
235
+ }
236
+ [data-gloom-scrollbar-x="hidden"]::-webkit-scrollbar:horizontal {
237
+ height: 0;
238
+ display: none;
239
+ }
240
+ [data-gloom-scrollbar-y="hidden"]::-webkit-scrollbar:vertical {
241
+ width: 0;
242
+ display: none;
243
+ }
244
+ [data-gloom-role="resize-handle"]::after {
245
+ content: "";
246
+ position: absolute;
247
+ right: 4px;
248
+ bottom: 4px;
249
+ width: 9px;
250
+ height: 9px;
251
+ border-right: 1px solid currentColor;
252
+ border-bottom: 1px solid currentColor;
253
+ opacity: .7;
254
+ }
255
+ [data-gloom-role="dock-divider"] {
256
+ background: transparent !important;
257
+ }
258
+ [data-gloom-role="dock-divider"]::before {
259
+ content: "";
260
+ position: absolute;
261
+ background: var(--divider-color);
262
+ opacity: .9;
263
+ }
264
+ [data-gloom-role="dock-divider"][data-axis="horizontal"] {
265
+ cursor: col-resize;
266
+ }
267
+ [data-gloom-role="dock-divider"][data-axis="horizontal"]::before {
268
+ top: 0;
269
+ bottom: 0;
270
+ left: 50%;
271
+ width: 1px;
272
+ }
273
+ [data-gloom-role="dock-divider"][data-axis="vertical"] {
274
+ cursor: row-resize;
275
+ }
276
+ [data-gloom-role="dock-divider"][data-axis="vertical"]::before {
277
+ left: 0;
278
+ right: 0;
279
+ top: 50%;
280
+ height: 1px;
281
+ }
282
+ .gloom-loading, .gloom-fatal { padding: 24px; color: #d8dde3; }
283
+ .gloom-fatal pre { white-space: pre-wrap; color: #ffb4a8; }
284
+ .gloom-toast-viewport {
285
+ position: fixed;
286
+ right: 16px;
287
+ bottom: 16px;
288
+ display: flex;
289
+ flex-direction: column;
290
+ gap: 8px;
291
+ z-index: 10000;
292
+ }
293
+ .gloom-toast {
294
+ min-width: 260px;
295
+ max-width: 420px;
296
+ padding: 10px 12px;
297
+ border: 1px solid #3a4148;
298
+ background: #151b20;
299
+ color: #d8dde3;
300
+ box-shadow: 0 12px 24px rgba(0,0,0,.28);
301
+ }
302
+ .gloom-toast-success { border-color: #54c99f; }
303
+ .gloom-toast-error { border-color: #ff6b5f; }
304
+ .gloom-toast-action {
305
+ margin-top: 8px;
306
+ color: #101417;
307
+ background: #d8dde3;
308
+ border: 0;
309
+ padding: 4px 8px;
310
+ border-radius: 4px;
311
+ }
312
+ .gloom-dialog-backdrop {
313
+ position: fixed;
314
+ inset: 0;
315
+ display: flex;
316
+ align-items: center;
317
+ justify-content: center;
318
+ background: rgba(0,0,0,.64);
319
+ z-index: 9999;
320
+ }
321
+ .gloom-dialog {
322
+ max-width: min(920px, calc(100vw - 48px));
323
+ max-height: calc(100vh - 48px);
324
+ overflow: auto;
325
+ border: 1px solid #3a4148;
326
+ border-radius: 6px;
327
+ background: #101417;
328
+ color: #d8dde3;
329
+ padding: 0;
330
+ box-shadow: 0 18px 48px rgba(0,0,0,.46), inset 0 1px 0 rgba(255,255,255,.05);
331
+ }
@@ -0,0 +1,179 @@
1
+ import { createElement, forwardRef, useCallback, useEffect, useMemo, useRef, useState, type ForwardedRef, type ReactNode } from "react";
2
+ import {
3
+ computeBitmapSize,
4
+ intersectCellRects,
5
+ type CellRect,
6
+ type NativeChartBitmap,
7
+ } from "../../components/chart/native/chart-rasterizer";
8
+ import { getCachedKittySupport, ensureKittySupport } from "../../components/chart/native/kitty-support";
9
+ import { getNativeSurfaceManager } from "../../components/chart/native/surface-manager";
10
+ import {
11
+ getRenderableCellRect,
12
+ resolveNativeSurfaceVisibleRect,
13
+ type NativeSurfaceRenderableNode,
14
+ } from "../../components/chart/native/surface-visibility";
15
+ import { useOptionalPaneInstanceId } from "../../state/app-context";
16
+ import { useNativeRenderer, type BoxRenderable, type ChartSurfaceProps } from "../../ui";
17
+
18
+ interface NativeRenderableNode extends BoxRenderable, NativeSurfaceRenderableNode {
19
+ x: number;
20
+ y: number;
21
+ width: number;
22
+ height: number;
23
+ parent: NativeRenderableNode | null;
24
+ onLifecyclePass: (() => void) | null;
25
+ }
26
+
27
+ interface SurfaceTarget {
28
+ rect: CellRect;
29
+ visibleRect: CellRect | null;
30
+ bitmapKey: string;
31
+ }
32
+
33
+ let nextChartSurfaceId = 1;
34
+
35
+ function assignRef(ref: ForwardedRef<unknown>, value: unknown) {
36
+ if (typeof ref === "function") {
37
+ ref(value);
38
+ return;
39
+ }
40
+ if (ref) {
41
+ (ref as { current: unknown }).current = value;
42
+ }
43
+ }
44
+
45
+ function sameRect(left: CellRect | null, right: CellRect | null): boolean {
46
+ if (left === right) return true;
47
+ if (!left || !right) return false;
48
+ return left.x === right.x
49
+ && left.y === right.y
50
+ && left.width === right.width
51
+ && left.height === right.height;
52
+ }
53
+
54
+ function sameTarget(left: SurfaceTarget | null, right: SurfaceTarget | null): boolean {
55
+ if (left === right) return true;
56
+ if (!left || !right) return false;
57
+ return left.bitmapKey === right.bitmapKey
58
+ && sameRect(left.rect, right.rect)
59
+ && sameRect(left.visibleRect, right.visibleRect);
60
+ }
61
+
62
+ function hashBitmap(bitmap: NativeChartBitmap): string {
63
+ let hash = 0x811c9dc5;
64
+ for (let index = 0; index < bitmap.pixels.length; index += 1) {
65
+ hash ^= bitmap.pixels[index]!;
66
+ hash = Math.imul(hash, 0x01000193);
67
+ }
68
+ return (hash >>> 0).toString(16);
69
+ }
70
+
71
+ function bitmapKey(bitmap: NativeChartBitmap): string {
72
+ return `${bitmap.width}x${bitmap.height}:${hashBitmap(bitmap)}`;
73
+ }
74
+
75
+ export const OpenTuiChartSurface = forwardRef<unknown, ChartSurfaceProps>(function OpenTuiChartSurface(
76
+ { children, bitmap, bitmaps, crosshair: _crosshair, ...props },
77
+ forwardedRef,
78
+ ) {
79
+ const renderer = useNativeRenderer();
80
+ const paneId = useOptionalPaneInstanceId();
81
+ const nativeSurfaceManager = useMemo(() => getNativeSurfaceManager(renderer), [renderer]);
82
+ const surfaceId = useRef(`opentui-chart:${nextChartSurfaceId++}`).current;
83
+ const renderableRef = useRef<NativeRenderableNode | null>(null);
84
+ const [kittySupport, setKittySupport] = useState<boolean | null>(() => getCachedKittySupport(renderer));
85
+ const [target, setTarget] = useState<SurfaceTarget | null>(null);
86
+ const nativeBitmap = (bitmaps?.[0] ?? bitmap ?? null) as NativeChartBitmap | null;
87
+ const nativeBitmapKey = useMemo(() => (nativeBitmap ? bitmapKey(nativeBitmap) : null), [nativeBitmap]);
88
+
89
+ const setRenderableRef = useCallback((node: unknown) => {
90
+ renderableRef.current = node as NativeRenderableNode | null;
91
+ assignRef(forwardedRef, node);
92
+ }, [forwardedRef]);
93
+
94
+ useEffect(() => {
95
+ let cancelled = false;
96
+ setKittySupport(getCachedKittySupport(renderer));
97
+ ensureKittySupport(renderer).then((supported) => {
98
+ if (!cancelled) setKittySupport(supported);
99
+ });
100
+ return () => {
101
+ cancelled = true;
102
+ };
103
+ }, [renderer]);
104
+
105
+ useEffect(() => {
106
+ const renderable = renderableRef.current;
107
+ if (!renderable || !nativeBitmap || !nativeBitmapKey || kittySupport !== true) {
108
+ setTarget(null);
109
+ return;
110
+ }
111
+
112
+ let mountTimer: Timer | null = null;
113
+ const previousLifecyclePass = renderable.onLifecyclePass;
114
+ const syncTarget = () => {
115
+ const rect = getRenderableCellRect(renderable);
116
+ if (!renderer.resolution || renderer.terminalWidth <= 0 || renderer.terminalHeight <= 0) {
117
+ setTarget((current) => (current === null ? current : null));
118
+ return;
119
+ }
120
+
121
+ const visibleRect = resolveNativeSurfaceVisibleRect(renderable, renderer.terminalWidth, renderer.terminalHeight);
122
+ const clippedVisibleRect = visibleRect ? intersectCellRects(rect, visibleRect) : null;
123
+ const expectedSize = computeBitmapSize(rect, renderer.resolution, renderer.terminalWidth, renderer.terminalHeight);
124
+ const nextTarget: SurfaceTarget = {
125
+ rect,
126
+ visibleRect: clippedVisibleRect,
127
+ bitmapKey: `${nativeBitmapKey}:${expectedSize.pixelWidth}x${expectedSize.pixelHeight}`,
128
+ };
129
+ setTarget((current) => (sameTarget(current, nextTarget) ? current : nextTarget));
130
+ };
131
+ const lifecyclePass = () => {
132
+ previousLifecyclePass?.();
133
+ syncTarget();
134
+ };
135
+
136
+ renderable.onLifecyclePass = lifecyclePass;
137
+ renderer.registerLifecyclePass(renderable);
138
+ syncTarget();
139
+ mountTimer = setTimeout(() => {
140
+ syncTarget();
141
+ renderer.requestRender();
142
+ }, 0);
143
+
144
+ return () => {
145
+ if (mountTimer) clearTimeout(mountTimer);
146
+ if (renderable.onLifecyclePass === lifecyclePass) {
147
+ renderable.onLifecyclePass = previousLifecyclePass;
148
+ }
149
+ renderer.unregisterLifecyclePass(renderable);
150
+ setTarget(null);
151
+ };
152
+ }, [kittySupport, nativeBitmap, nativeBitmapKey, renderer]);
153
+
154
+ useEffect(() => {
155
+ return () => {
156
+ nativeSurfaceManager.removeSurface(surfaceId);
157
+ };
158
+ }, [nativeSurfaceManager, surfaceId]);
159
+
160
+ useEffect(() => {
161
+ if (kittySupport !== true || !target?.visibleRect || !nativeBitmap) {
162
+ nativeSurfaceManager.removeSurface(surfaceId);
163
+ return;
164
+ }
165
+
166
+ nativeSurfaceManager.upsertSurface({
167
+ id: surfaceId,
168
+ paneId: paneId ?? "__global__",
169
+ rect: target.rect,
170
+ visibleRect: target.visibleRect,
171
+ bitmap: nativeBitmap,
172
+ bitmapKey: target.bitmapKey,
173
+ });
174
+ renderer.requestRender();
175
+ }, [kittySupport, nativeBitmap, nativeSurfaceManager, paneId, renderer, surfaceId, target]);
176
+
177
+ const showFallback = kittySupport !== true || !target || !nativeBitmap;
178
+ return createElement("box" as any, { ...props, ref: setRenderableRef }, showFallback ? children as ReactNode : null);
179
+ });