gloomberb 0.16.2 → 0.16.3

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 (124) hide show
  1. package/package.json +1 -1
  2. package/src/brokers/account-cache.ts +45 -0
  3. package/src/cli/command-options.ts +74 -0
  4. package/src/cli/commands/command-utils.ts +29 -0
  5. package/src/cli/commands/market.ts +170 -54
  6. package/src/cli/commands/overview.ts +62 -19
  7. package/src/cli/commands/ticker.ts +138 -43
  8. package/src/cli/desktop-pane-shot.ts +3 -0
  9. package/src/cli/help.ts +26 -6
  10. package/src/cli/history-rows.ts +187 -15
  11. package/src/cli/index.ts +12 -7
  12. package/src/cli/options.ts +5 -1
  13. package/src/cli/pane-functions/capabilities.ts +5 -0
  14. package/src/cli/pane-functions/dom.ts +69 -16
  15. package/src/cli/pane-functions/function-help.ts +4 -2
  16. package/src/cli/pane-functions/headless.ts +163 -31
  17. package/src/cli/pane-functions/index.ts +9 -2
  18. package/src/cli/pane-functions/report.ts +8 -1
  19. package/src/cli/pane-functions/resolver.ts +28 -2
  20. package/src/cli/pane-functions/screenshot.ts +4 -0
  21. package/src/cli/registry.ts +6 -3
  22. package/src/cli/report-tables.ts +460 -0
  23. package/src/cli/result.ts +13 -3
  24. package/src/components/data-table/export.ts +6 -51
  25. package/src/components/data-table/view.tsx +3 -1
  26. package/src/components/layout/shell/active-drag.ts +67 -35
  27. package/src/components/layout/shell/cursor-occlusion.ts +6 -5
  28. package/src/components/layout/shell/drag/index.ts +12 -7
  29. package/src/components/layout/shell/drag/live.ts +122 -0
  30. package/src/components/layout/shell/drag/overlays.tsx +179 -35
  31. package/src/components/layout/shell/drag/runtime.ts +62 -50
  32. package/src/components/layout/shell/drag/slide.ts +10 -0
  33. package/src/components/layout/shell/index.tsx +37 -51
  34. package/src/components/layout/shell/native/pointer-runtime.ts +13 -13
  35. package/src/components/layout/shell/native/surfaces.ts +30 -14
  36. package/src/components/layout/shell/pane/layers.tsx +419 -179
  37. package/src/components/layout/shell/terminal-pointer-runtime.ts +18 -18
  38. package/src/components/layout/shell/window-mode/overlays.tsx +12 -4
  39. package/src/data/json-persistence.ts +17 -3
  40. package/src/data/json-storage.ts +2 -0
  41. package/src/data/memory-resource-store.ts +46 -0
  42. package/src/market-data/market/freshness.ts +45 -0
  43. package/src/market-data/quotes/freshness.ts +5 -0
  44. package/src/plugins/builtin/analytics/pane-model.ts +10 -6
  45. package/src/plugins/builtin/attention/pane.tsx +1 -1
  46. package/src/plugins/builtin/catalysts/detail.tsx +1 -1
  47. package/src/plugins/builtin/company-attention/pane.tsx +1 -1
  48. package/src/plugins/builtin/holders/client.ts +3 -6
  49. package/src/plugins/builtin/holders/format.ts +31 -1
  50. package/src/plugins/builtin/holders/headless.ts +26 -5
  51. package/src/plugins/builtin/holders/pane.tsx +22 -13
  52. package/src/plugins/builtin/holders/report-header.ts +46 -5
  53. package/src/plugins/builtin/kelly-sizer/pane.tsx +12 -6
  54. package/src/plugins/builtin/kelly-sizer/portfolio.ts +25 -0
  55. package/src/plugins/builtin/members/pane.tsx +10 -0
  56. package/src/plugins/builtin/perps/pane.tsx +1 -1
  57. package/src/plugins/builtin/portfolio-list/account-metrics.ts +16 -1
  58. package/src/plugins/builtin/portfolio-list/allocation.ts +3 -0
  59. package/src/plugins/builtin/portfolio-list/cached-account.ts +18 -9
  60. package/src/plugins/builtin/portfolio-list/cli/render.ts +2 -0
  61. package/src/plugins/builtin/portfolio-list/headless.ts +2 -0
  62. package/src/plugins/builtin/portfolio-list/pane/index.tsx +7 -4
  63. package/src/plugins/builtin/portfolio-list/summary/index.tsx +22 -4
  64. package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +11 -1
  65. package/src/plugins/builtin/research/relative-valuation-model.ts +7 -3
  66. package/src/plugins/builtin/reverse-dcf/model.ts +2 -1
  67. package/src/plugins/builtin/shared/members-menu.tsx +260 -0
  68. package/src/plugins/builtin/short-volume/pane.tsx +1 -1
  69. package/src/plugins/builtin/social-mentions/pane.tsx +1 -1
  70. package/src/plugins/builtin/themes/pane.tsx +3 -0
  71. package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +4 -0
  72. package/src/plugins/builtin/ticker-detail/financials/model.ts +25 -2
  73. package/src/plugins/builtin/ticker-detail/headless.ts +20 -4
  74. package/src/plugins/builtin/ticker-detail/overview/model.ts +9 -4
  75. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +3 -2
  76. package/src/plugins/builtin/world-venue-map/basemap-layer.tsx +2 -0
  77. package/src/plugins/builtin/world-venue-map/basemap.ts +18 -0
  78. package/src/plugins/builtin/world-venue-map/country-labels.ts +78 -0
  79. package/src/plugins/builtin/world-venue-map/geo-svg.tsx +2 -1
  80. package/src/plugins/builtin/world-venue-map/index.ts +5 -5
  81. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +27 -14
  82. package/src/plugins/builtin/world-venue-map/layers.ts +68 -10
  83. package/src/plugins/builtin/world-venue-map/map.tsx +198 -62
  84. package/src/plugins/builtin/world-venue-map/model.ts +16 -0
  85. package/src/plugins/builtin/world-venue-map/natural-earth/index.chunk.ts +3 -0
  86. package/src/plugins/builtin/world-venue-map/natural-earth/labels.chunk.ts +244 -0
  87. package/src/plugins/builtin/world-venue-map/pane.tsx +16 -10
  88. package/src/renderers/browser/app-services.ts +3 -1
  89. package/src/renderers/dom/data-table/dom.ts +31 -0
  90. package/src/renderers/dom/data-table/index.tsx +5 -0
  91. package/src/renderers/dom/host/box.tsx +113 -17
  92. package/src/renderers/dom/host/drag-shield.ts +24 -0
  93. package/src/renderers/dom/host/live-frame.ts +54 -0
  94. package/src/renderers/dom/host/mouse.ts +8 -1
  95. package/src/renderers/dom/host/scroll-box.tsx +6 -5
  96. package/src/renderers/dom/host/tabs.tsx +3 -2
  97. package/src/renderers/dom/styles.css +13 -5
  98. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +7 -0
  99. package/src/renderers/electrobun/bun/desktop/backend.ts +2 -0
  100. package/src/renderers/electrobun/bun/desktop/initialization.ts +13 -0
  101. package/src/renderers/electrobun/shared/protocol.ts +10 -1
  102. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +3 -0
  103. package/src/renderers/electrobun/view/host/native.ts +0 -10
  104. package/src/renderers/electrobun/view/host/window-move.ts +28 -0
  105. package/src/renderers/electrobun/view/remote/persistence.ts +15 -2
  106. package/src/renderers/electrobun/view/ui-host.tsx +2 -5
  107. package/src/renderers/opentui/ui-host.tsx +7 -1
  108. package/src/sources/gloomberb-cloud/normalizers.ts +16 -20
  109. package/src/sources/provider-router/batches.ts +3 -3
  110. package/src/sources/provider-router/financial-routes.ts +2 -2
  111. package/src/sources/provider-router/financials.ts +70 -7
  112. package/src/sources/provider-router/primary.ts +2 -2
  113. package/src/state/pane-scroll-registry.tsx +67 -35
  114. package/src/test-support/margin-account.ts +108 -0
  115. package/src/time-series/resolution.ts +15 -0
  116. package/src/types/financials.ts +32 -3
  117. package/src/ui/host.tsx +12 -0
  118. package/src/ui/index.tsx +8 -3
  119. package/src/ui/scroll-box-axis.ts +26 -0
  120. package/src/utils/currency-units.ts +8 -3
  121. package/src/utils/depositary-receipt.ts +54 -10
  122. package/src/utils/display-number.ts +63 -0
  123. package/src/utils/fundamentals.ts +17 -1
  124. package/src/utils/price-history-integrity.ts +19 -6
@@ -97,6 +97,9 @@ export function clampWorldMapViewport(
97
97
  if (zoom <= 1) return DEFAULT_WORLD_MAP_VIEWPORT;
98
98
 
99
99
  const { availableWidth, effectiveHeight, latitudeSpan } = mapExtent(width, height, yUnitAspect);
100
+ // A plot with no area yet (a pane mid-layout) says nothing about what fits:
101
+ // keep the centre rather than resetting it to the middle of the world.
102
+ if (availableWidth <= 0 || effectiveHeight <= 0) return { ...viewport, zoom };
100
103
  const scale = fittedWorldMapScale(width, height, yUnitAspect) * zoom;
101
104
  const halfLongitude = availableWidth / (2 * scale);
102
105
  const halfLatitude = effectiveHeight / (2 * scale);
@@ -116,6 +119,19 @@ export function clampWorldMapViewport(
116
119
  };
117
120
  }
118
121
 
122
+ /** A viewport read back from saved pane state, or null when it is not one. */
123
+ export function savedWorldMapViewport(value: unknown, maxZoom = MAX_WORLD_MAP_ZOOM): WorldMapViewport | null {
124
+ if (!value || typeof value !== "object") return null;
125
+ const { zoom, centerLongitude, centerLatitude } = value as Partial<WorldMapViewport>;
126
+ if (![zoom, centerLongitude, centerLatitude].every((part) => typeof part === "number" && Number.isFinite(part))) return null;
127
+ if (zoom! <= 1) return DEFAULT_WORLD_MAP_VIEWPORT;
128
+ return {
129
+ zoom: Math.min(zoom!, maxZoom),
130
+ centerLongitude: clamp(centerLongitude!, -180, 180),
131
+ centerLatitude: clamp(centerLatitude!, MIN_LATITUDE, MAX_LATITUDE),
132
+ };
133
+ }
134
+
119
135
  export function zoomWorldMapViewport(
120
136
  viewport: WorldMapViewport,
121
137
  width: number,
@@ -4,6 +4,9 @@ import type { DetailTile } from "../basemap";
4
4
  /** @knipignore Loaded through the map's data specifier, which only view builds define (see ../basemap.ts). */
5
5
  export const loadWorld = () => import("./world-50m.chunk.js");
6
6
 
7
+ /** @knipignore Loaded through the map's data specifier, which only view builds define (see ../basemap.ts). */
8
+ export const loadLabels = () => import("./labels.chunk.js");
9
+
7
10
  export const DETAIL_TILES: readonly DetailTile[] = [
8
11
  { id: "w090s60", box: [-90.005, -60, -74.995, -44.995], load: () => import("./tiles/w090s60.chunk.js") },
9
12
  { id: "w075s60", box: [-75.005, -60, -59.995, -44.995], load: () => import("./tiles/w075s60.chunk.js") },
@@ -0,0 +1,244 @@
1
+ // Natural Earth 1:50m admin-0 country label points (public domain). Generated by scripts/generate-world-map-data.ts; do not edit.
2
+ /** Name, longitude, latitude, the web-map zoom it first shows at, rank: most prominent first. */
3
+ export const COUNTRY_LABELS: readonly (readonly [string, number, number, number, number])[] = [
4
+ ["Australia",134.05,-24.13,1.7,2],
5
+ ["Brazil",-49.56,-12.1,1.7,2],
6
+ ["Canada",-101.91,60.32,1.7,2],
7
+ ["Chile",-72.32,-38.15,1.7,2],
8
+ ["China",106.34,32.5,1.7,2],
9
+ ["Egypt",29.45,26.19,1.7,2],
10
+ ["France",2.55,46.7,1.7,2],
11
+ ["Germany",9.68,50.96,1.7,2],
12
+ ["India",79.36,22.69,1.7,2],
13
+ ["Indonesia",101.89,-0.95,1.7,2],
14
+ ["Japan",138.44,36.14,1.7,2],
15
+ ["Kenya",37.91,0.55,1.7,2],
16
+ ["Nigeria",7.5,9.44,1.7,2],
17
+ ["Russia",44.69,58.25,1.7,2],
18
+ ["Saudi Arabia",44.7,23.81,1.7,2],
19
+ ["South Africa",23.67,-29.71,1.7,2],
20
+ ["United Kingdom",-2.12,54.4,1.7,2],
21
+ ["United States",-97.48,39.54,1.7,2],
22
+ ["Greenland",-39.34,74.32,1.7,3],
23
+ ["Argentina",-64.17,-33.5,2,2],
24
+ ["Dem. Rep. Congo",23.46,-1.86,2,2],
25
+ ["Ethiopia",39.09,8.03,2,2],
26
+ ["Italy",11.08,44.73,2,2],
27
+ ["Mexico",-102.29,23.92,2,2],
28
+ ["New Zealand",172.79,-39.76,2,2],
29
+ ["Peru",-72.9,-12.98,2,2],
30
+ ["Spain",-3.46,40.09,2,2],
31
+ ["Turkey",34.51,39.35,2,2],
32
+ ["Vietnam",105.39,21.72,2,2],
33
+ ["Iceland",-18.67,64.78,2,3],
34
+ ["Sweden",19.02,65.86,2,3],
35
+ ["Iran",54.93,32.17,2.5,2],
36
+ ["Papua New Guinea",143.91,-5.7,2.5,2],
37
+ ["Philippines",122.47,11.2,2.5,2],
38
+ ["South Korea",128.13,36.38,2.5,2],
39
+ ["Algeria",2.81,27.4,2.5,3],
40
+ ["Côte d'Ivoire",-5.57,7.49,2.5,3],
41
+ ["Poland",19.49,51.99,2.5,3],
42
+ ["Sudan",29.26,16.33,2.5,3],
43
+ ["Venezuela",-64.6,7.18,2.5,3],
44
+ ["Zimbabwe",29.93,-18.91,2.5,3],
45
+ ["Costa Rica",-84.08,10.07,2.5,5],
46
+ ["Pakistan",68.55,29.33,2.7,2],
47
+ ["Cuba",-77.98,21.33,2.7,3],
48
+ ["Ghana",-1.04,7.72,2.7,3],
49
+ ["Greece",21.73,39.49,2.7,3],
50
+ ["Kazakhstan",68.69,49.05,2.7,3],
51
+ ["Madagascar",46.7,-18.63,2.7,3],
52
+ ["Morocco",-7.19,31.65,2.7,3],
53
+ ["Senegal",-14.78,15.14,2.7,3],
54
+ ["Thailand",101.07,15.46,2.7,3],
55
+ ["Ukraine",32.14,49.72,2.7,3],
56
+ ["Colombia",-73.17,3.37,3,2],
57
+ ["Portugal",-8.27,39.61,3,2],
58
+ ["Afghanistan",66.5,34.16,3,3],
59
+ ["Angola",17.98,-12.18,3,3],
60
+ ["Bangladesh",89.68,24.21,3,3],
61
+ ["Bolivia",-64.59,-16.67,3,3],
62
+ ["Burkina Faso",-1.36,12.67,3,3],
63
+ ["Cambodia",104.5,12.65,3,3],
64
+ ["Cameroon",12.47,4.59,3,3],
65
+ ["Chad",18.65,15.14,3,3],
66
+ ["Ecuador",-78.19,-1.26,3,3],
67
+ ["Finland",27.28,63.25,3,3],
68
+ ["Guatemala",-90.5,14.98,3,3],
69
+ ["Guinea",-10.02,10.62,3,3],
70
+ ["Iraq",43.26,33.09,3,3],
71
+ ["Ireland",-7.8,53.08,3,3],
72
+ ["Libya",18.01,26.64,3,3],
73
+ ["Malaysia",113.84,2.53,3,3],
74
+ ["Mali",-2.04,18.69,3,3],
75
+ ["Mauritania",-9.74,19.59,3,3],
76
+ ["Mongolia",104.15,46,3,3],
77
+ ["Mozambique",37.84,-13.94,3,3],
78
+ ["Myanmar",95.8,21.57,3,3],
79
+ ["Namibia",17.11,-20.58,3,3],
80
+ ["Nepal",83.64,28.3,3,3],
81
+ ["Niger",9.5,17.45,3,3],
82
+ ["North Korea",126.44,39.89,3,3],
83
+ ["Norway",9.68,61.36,3,3],
84
+ ["Romania",24.97,45.73,3,3],
85
+ ["Rwanda",30.1,-1.9,3,3],
86
+ ["S. Sudan",30.39,7.23,3,3],
87
+ ["Solomon Is.",159.17,-8.03,3,3],
88
+ ["Sri Lanka",80.7,7.58,3,3],
89
+ ["Syria",38.28,35.01,3,3],
90
+ ["Tanzania",34.96,-6.05,3,3],
91
+ ["Tunisia",9.01,33.69,3,3],
92
+ ["Uganda",32.95,1.97,3,3],
93
+ ["Uzbekistan",64.01,41.69,3,3],
94
+ ["Yemen",45.87,15.33,3,3],
95
+ ["Zambia",26.4,-14.66,3,3],
96
+ ["Austria",14.13,47.52,3,4],
97
+ ["Belarus",28.42,53.82,3,4],
98
+ ["Denmark",9.02,55.97,3,4],
99
+ ["Gabon",11.84,-0.44,3,4],
100
+ ["Israel",34.85,30.91,3,4],
101
+ ["Kyrgyzstan",74.53,41.67,3,4],
102
+ ["Paraguay",-60.15,-21.67,3,4],
103
+ ["Samoa",-172.44,-13.64,3,4],
104
+ ["Turkmenistan",58.68,39.86,3,4],
105
+ ["Uruguay",-55.97,-32.96,3,4],
106
+ ["Puerto Rico",-66.48,18.23,3,5],
107
+ ["Estonia",25.87,58.72,3,6],
108
+ ["Fiji",177.98,-17.83,3,6],
109
+ ["Guam",144.7,13.35,3,6],
110
+ ["Fr. Polynesia",-149.46,-17.63,3.5,4],
111
+ ["Belgium",4.8,50.79,4,2],
112
+ ["American Samoa",-170.75,-14.33,4,4],
113
+ ["Bahamas",-77.15,26.4,4,4],
114
+ ["Bahrain",50.55,26.06,4,4],
115
+ ["Botswana",24.18,-22.1,4,4],
116
+ ["Bulgaria",25.16,42.51,4,4],
117
+ ["Cabo Verde",-23.64,15.07,4,4],
118
+ ["Central African Rep.",20.91,6.99,4,4],
119
+ ["Congo",15.9,0.14,4,4],
120
+ ["Cook Is.",-159.79,-21.22,4,4],
121
+ ["Eq. Guinea",8.99,2.33,4,4],
122
+ ["Eritrea",38.29,15.79,4,4],
123
+ ["eSwatini",31.47,-26.53,4,4],
124
+ ["Guyana",-58.94,5.12,4,4],
125
+ ["Hong Kong",114.1,22.45,4,4],
126
+ ["Jamaica",-77.32,18.14,4,4],
127
+ ["Jordan",36.38,30.81,4,4],
128
+ ["Laos",102.53,19.43,4,4],
129
+ ["Liberia",-9.46,6.45,4,4],
130
+ ["Macao",113.56,22.13,4,4],
131
+ ["Niue",-169.86,-19.05,4,4],
132
+ ["Oman",57.34,22.12,4,4],
133
+ ["Panama",-80.35,8.72,4,4],
134
+ ["Sierra Leone",-11.76,8.62,4,4],
135
+ ["Suriname",-55.91,4.14,4,4],
136
+ ["Switzerland",7.46,46.72,4,4],
137
+ ["Tajikistan",72.59,38.2,4,4],
138
+ ["Tonga",-175.16,-21.21,4,4],
139
+ ["United Arab Emirates",54.55,23.47,4,4],
140
+ ["Vanuatu",166.91,-15.37,4,4],
141
+ ["Azerbaijan",47.21,40.4,4,5],
142
+ ["Benin",2.35,10.32,4,5],
143
+ ["Bhutan",90.04,27.54,4,5],
144
+ ["Czechia",15.38,49.88,4,5],
145
+ ["Djibouti",42.5,11.98,4,5],
146
+ ["Georgia",43.74,41.87,4,5],
147
+ ["Haiti",-72.22,19.26,4,5],
148
+ ["Hungary",19.45,47.09,4,5],
149
+ ["Latvia",25.46,57.07,4,5],
150
+ ["Lebanon",35.99,34.13,4,5],
151
+ ["Lithuania",24.09,55.1,4,5],
152
+ ["Maldives",73.51,4.17,4,5],
153
+ ["Malta",14.43,35.89,4,5],
154
+ ["Mauritius",57.57,-20.3,4,5],
155
+ ["Netherlands",5.61,52.42,4,5],
156
+ ["Nicaragua",-85.07,12.67,4,5],
157
+ ["Qatar",51.14,25.24,4,5],
158
+ ["Serbia",20.79,44.19,4,5],
159
+ ["Timor-Leste",125.85,-8.8,4,5],
160
+ ["Bermuda",-64.76,32.3,4,6],
161
+ ["Brunei",114.55,4.45,4,6],
162
+ ["Burundi",29.92,-3.33,4,6],
163
+ ["Comoros",43.32,-11.73,4,6],
164
+ ["Croatia",16.37,45.81,4,6],
165
+ ["Dominica",-61.34,15.46,4,6],
166
+ ["Faeroe Is.",-7.06,62.19,4,6],
167
+ ["Fr. S. Antarctic Lands",69.12,-49.3,4,6],
168
+ ["Grenada",-61.68,12.11,4,6],
169
+ ["Lesotho",28.25,-29.48,4,6],
170
+ ["Malawi",33.61,-13.39,4,6],
171
+ ["Singapore",103.82,1.37,4,6],
172
+ ["Slovakia",19.05,48.73,4,6],
173
+ ["Somalia",45.19,3.57,4,6],
174
+ ["Taiwan",120.87,23.65,4.5,3],
175
+ ["Ashmore and Cartier Is.",123.59,-12.43,4.5,5],
176
+ ["Barbados",-59.57,13.16,4.5,5],
177
+ ["Bosnia and Herz.",18.07,44.09,4.5,5],
178
+ ["Cyprus",33.08,34.91,4.5,5],
179
+ ["Dominican Rep.",-70.65,19.1,4.5,5],
180
+ ["Falkland Is.",-58.74,-51.61,4.5,5],
181
+ ["Heard I. and McDonald Is.",73.51,-53.1,4.5,5],
182
+ ["Honduras",-86.89,14.79,4.5,5],
183
+ ["Norfolk Island",167.95,-29.03,4.5,5],
184
+ ["Palestine",35.29,32.05,4.5,5],
185
+ ["Somaliland",46.73,9.44,4.5,5],
186
+ ["Trinidad and Tobago",-60.92,11,4.5,5],
187
+ ["New Caledonia",165.08,-21.06,4.6,3],
188
+ ["Wallis and Futuna Is.",-178.14,-14.29,4.7,4],
189
+ ["Pitcairn Is.",-128.32,-24.36,5,4],
190
+ ["S. Geo. and the Is.",-31.06,-55.68,5,4],
191
+ ["St. Pierre and Miquelon",-56.33,47.04,5,4],
192
+ ["Aruba",-69.97,12.52,5,5],
193
+ ["Br. Indian Ocean Ter.",71.35,-6.19,5,5],
194
+ ["Cayman Is.",-81.24,19.32,5,5],
195
+ ["Curaçao",-68.92,12.15,5,5],
196
+ ["Indian Ocean Ter.",105.67,-10.49,5,5],
197
+ ["Åland",19.87,60.16,5,6],
198
+ ["Albania",20.11,40.65,5,6],
199
+ ["Andorra",1.54,42.55,5,6],
200
+ ["Anguilla",-63.03,18.24,5,6],
201
+ ["Antigua and Barb.",-61.79,17.35,5,6],
202
+ ["Armenia",44.8,40.46,5,6],
203
+ ["Belize",-88.71,17.2,5,6],
204
+ ["British Virgin Is.",-64.64,18.43,5,6],
205
+ ["El Salvador",-88.89,13.69,5,6],
206
+ ["Gambia",-15,13.64,5,6],
207
+ ["Guernsey",-2.56,49.46,5,6],
208
+ ["Guinea-Bissau",-14.52,12.16,5,6],
209
+ ["Isle of Man",-4.53,54.22,5,6],
210
+ ["Jersey",-2.09,49.22,5,6],
211
+ ["Kiribati",-157.38,1.82,5,6],
212
+ ["Kosovo",20.86,42.59,5,6],
213
+ ["Kuwait",47.31,29.41,5,6],
214
+ ["Liechtenstein",9.56,47.11,5,6],
215
+ ["Marshall Is.",171.19,7.08,5,6],
216
+ ["Micronesia",158.23,6.89,5,6],
217
+ ["Moldova",28.49,47.43,5,6],
218
+ ["Monaco",7.4,43.74,5,6],
219
+ ["Montenegro",19.14,42.8,5,6],
220
+ ["Montserrat",-62.19,16.74,5,6],
221
+ ["N. Mariana Is.",145.73,15.19,5,6],
222
+ ["Nauru",166.93,-0.52,5,6],
223
+ ["North Macedonia",21.56,41.56,5,6],
224
+ ["Palau",134.58,7.52,5,6],
225
+ ["Saint Helena",-5.71,-15.95,5,6],
226
+ ["Saint Lucia",-60.98,13.89,5,6],
227
+ ["San Marino",12.44,43.93,5,6],
228
+ ["São Tomé and Principe",7.02,0.97,5,6],
229
+ ["Seychelles",55.48,-4.68,5,6],
230
+ ["Sint Maarten",-63.07,18.04,5,6],
231
+ ["Slovenia",14.92,46.06,5,6],
232
+ ["St-Martin",-63.05,18.08,5,6],
233
+ ["St. Kitts and Nevis",-62.76,17.34,5,6],
234
+ ["St. Vin. and Gren.",-61.34,13.09,5,6],
235
+ ["Togo",1.06,8.81,5,6],
236
+ ["Turks and Caicos Is.",-71.75,21.82,5,6],
237
+ ["Tuvalu",179.21,-8.51,5,6],
238
+ ["U.S. Virgin Is.",-64.78,17.75,5,6],
239
+ ["Vatican",12.45,41.9,5,6],
240
+ ["Luxembourg",6.08,49.73,5.7,6],
241
+ ["St-Barthélemy",-62.83,17.9,5.7,6],
242
+ ["N. Cyprus",33.69,35.22,6,6],
243
+ ["W. Sahara",-12.63,23.97,6,7],
244
+ ];
@@ -24,7 +24,7 @@ import { Box, TextAttributes, useUiCapabilities } from "../../../ui";
24
24
  import { loadWorldVenues } from "./client";
25
25
  import { WORLD_VENUE_MAP_PANE_ID } from "./ids";
26
26
  import { LayeredMapView } from "./layered-pane";
27
- import { LAYERS_SETTING_KEY, parseLayerTokens, resolveActiveLayers } from "./layers";
27
+ import { activeLayerTokens, isPlainMap, LAYERS_SETTING_KEY, resolveActiveLayers, VENUES_SETTING_KEY } from "./layers";
28
28
  import { WorldVenueMap } from "./map";
29
29
  import { filterWorldVenues, formatVenueLocalTime } from "./model";
30
30
  import { useGeoCatalog } from "./use-geo";
@@ -47,19 +47,22 @@ function venueColumns(width: number): VenueColumn[] {
47
47
  }
48
48
 
49
49
  /**
50
- * One map for anything with a place. Venues are the default layer; geo layers
51
- * come from the server's catalog, so a server without them leaves the venue
52
- * map exactly as it was.
50
+ * One map for anything with a place. A plain MAP shows venues under the
51
+ * server's default layers; geo layers come from the server's catalog, so a
52
+ * server without them leaves the venue map exactly as it was.
53
53
  */
54
54
  export function WorldVenueMapPane(props: PaneProps) {
55
55
  const [layerSetting] = usePaneSettingValue<unknown>(LAYERS_SETTING_KEY, null);
56
- const tokens = useMemo(() => parseLayerTokens(layerSetting), [layerSetting]);
56
+ const [venuesSetting] = usePaneSettingValue<boolean | undefined>(VENUES_SETTING_KEY, undefined);
57
57
  const { catalog, settled } = useGeoCatalog();
58
+ const settings = useMemo(() => ({ [LAYERS_SETTING_KEY]: layerSetting, [VENUES_SETTING_KEY]: venuesSetting }), [layerSetting, venuesSetting]);
59
+ const plain = isPlainMap(settings);
60
+ const tokens = useMemo(() => activeLayerTokens(settings, catalog?.layers), [catalog, settings]);
58
61
  const layers = useMemo(() => resolveActiveLayers(tokens, catalog?.layers), [catalog, tokens]);
59
- if (layers.length) return <LayeredMapView {...props} layers={layers} tokens={tokens} />;
60
- // A preset waits for the catalog rather than flashing the venue map first.
61
- if (tokens.length && !settled) {
62
- return <PaneStatusBody loading align="center" width={props.width} height={props.height} loadingLabel="Loading map layers..." />;
62
+ if (layers.length) return <LayeredMapView {...props} layers={layers} tokens={tokens} plain={plain} />;
63
+ // A preset or a plain map waits for the catalog rather than flashing the venue map first.
64
+ if ((plain || tokens.length) && !settled) {
65
+ return <PaneStatusBody loading align="center" width={props.width} height={props.height} loadingLabel="Loading map..." />;
63
66
  }
64
67
  return <VenueMapView {...props} />;
65
68
  }
@@ -119,6 +122,7 @@ function VenueMapView({ focused, width, height }: PaneProps) {
119
122
 
120
123
  const { nativePaneChrome } = useUiCapabilities();
121
124
  const [storedSidebarWidth, setStoredSidebarWidth] = usePluginPaneState<number | null>("sidebarWidth", null);
125
+ const [savedViewport, setSavedViewport] = usePluginPaneState<unknown>("map:viewport", null);
122
126
  const [draggedSidebarWidth, setDraggedSidebarWidth] = useState<number | null>(null);
123
127
  const horizontal = shouldShowPaneSidebar(data?.venues.length ?? 0, width, height);
124
128
  // Venue names need more room than a conversation list, so the width the pane
@@ -208,7 +212,7 @@ function VenueMapView({ focused, width, height }: PaneProps) {
208
212
  }
209
213
 
210
214
  const map = (
211
- <Box flexDirection="column" width={mapWidth} height={mapSectionHeight} overflow="hidden">
215
+ <Box key="map" flexDirection="column" width={mapWidth} height={mapSectionHeight} overflow="hidden">
212
216
  <SelectedVenueHeader venue={selectedVenue} checkedAt={data.checkedAt} now={now} width={mapWidth} />
213
217
  <WorldVenueMap
214
218
  venues={venues}
@@ -216,6 +220,8 @@ function VenueMapView({ focused, width, height }: PaneProps) {
216
220
  width={mapWidth}
217
221
  height={mapHeight}
218
222
  onSelect={(venue) => setSelectedMic(venue.mic)}
223
+ savedViewport={savedViewport}
224
+ onViewportSettled={setSavedViewport}
219
225
  />
220
226
  </Box>
221
227
  );
@@ -5,6 +5,7 @@ import { createAppRuntime } from "../../core/app-runtime";
5
5
  import { createGloomberbCloudCapabilities, createGloomberbCloudProvider } from "../../sources/gloomberb-cloud";
6
6
  import { AssetDataRouter } from "../../sources/provider-router";
7
7
  import { JsonPersistence } from "../../data/json-persistence";
8
+ import { isBrokerAccountSnapshotKey } from "../../brokers/account-cache";
8
9
  import { JsonTickerRepository } from "../../data/json-ticker-repository";
9
10
 
10
11
  export function createBrowserAppServices({ config, plugins, externalPlugins }: AppServicesFactoryOptions) {
@@ -15,7 +16,8 @@ export function createBrowserAppServices({ config, plugins, externalPlugins }: A
15
16
  );
16
17
  return createAppRuntime({
17
18
  config, plugins, externalPlugins, dataProvider,
18
- persistence: new JsonPersistence(localStorage),
19
+ // Broker account snapshots are kept with the positions, so a reload keeps the account's equity and cash.
20
+ persistence: new JsonPersistence(localStorage, { keepResource: isBrokerAccountSnapshotKey }),
19
21
  tickerRepository: new JsonTickerRepository(localStorage),
20
22
  configure({ pluginRegistry, newsService }) {
21
23
  dataProvider.attachRegistry(pluginRegistry);
@@ -143,6 +143,24 @@ export function useScrollBoxHandle(
143
143
  get scrollWidth() {
144
144
  return toCellX(elementRef.current?.scrollWidth ?? 0);
145
145
  },
146
+ // Whole rows are rounded, so the scroll keys read the ends in pixels.
147
+ get scrollTopPx() {
148
+ return options.headerOnly ? 0 : Math.max(0, elementRef.current?.scrollTop ?? 0);
149
+ },
150
+ get scrollHeightPx() {
151
+ const element = elementRef.current;
152
+ if (!element) return 0;
153
+ return options.headerOnly ? tableHeaderPx() : Math.max(0, element.scrollHeight - (options.viewportTopInsetPx ?? 0));
154
+ },
155
+ get viewportPx() {
156
+ const element = elementRef.current;
157
+ return {
158
+ width: Math.max(0, element?.clientWidth ?? 0),
159
+ height: options.headerOnly
160
+ ? tableHeaderPx()
161
+ : Math.max(0, (element?.clientHeight ?? 0) - (options.viewportTopInsetPx ?? 0)),
162
+ };
163
+ },
146
164
  get viewport() {
147
165
  const element = elementRef.current;
148
166
  if (options.headerOnly) {
@@ -161,6 +179,19 @@ export function useScrollBoxHandle(
161
179
  },
162
180
  horizontalScrollBar,
163
181
  verticalScrollBar,
182
+ scrollToPixels(target: number | { x?: number; y?: number }, y?: number) {
183
+ const element = elementRef.current;
184
+ if (!element || options.headerOnly) return;
185
+ if (typeof target === "number") {
186
+ element.scrollTop = Math.max(0, target);
187
+ if (typeof y === "number") element.scrollLeft = Math.max(0, y);
188
+ return;
189
+ }
190
+ element.scrollTo({
191
+ left: Math.max(0, target.x ?? element.scrollLeft),
192
+ top: Math.max(0, target.y ?? element.scrollTop),
193
+ });
194
+ },
164
195
  scrollTo(target: number | { x?: number; y?: number }, y?: number) {
165
196
  const element = elementRef.current;
166
197
  if (!element) return;
@@ -11,6 +11,7 @@ import {
11
11
  } from "react";
12
12
  import { EmptyState } from "../../../components/ui/status";
13
13
  import { usePaneInstance } from "../../../state/app/context";
14
+ import { useRegisterPaneScrollBox } from "../../../state/pane-scroll-registry";
14
15
  import { useRafCallback } from "../../../react/use-raf-callback";
15
16
  import { measurePerf } from "../../../utils/perf-marks";
16
17
  import type {
@@ -284,6 +285,10 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
284
285
  bodyVertical.bar,
285
286
  { viewportTopInsetPx: headerPx },
286
287
  );
288
+ // The terminal table body is a ScrollBox the pane scroll keys already reach;
289
+ // this is the same for the desktop one. A table with a cursor takes the keys
290
+ // first, so only a table without one is scrolled by them.
291
+ useRegisterPaneScrollBox(scrollRef, { enabled: true });
287
292
 
288
293
  useEffect(() => {
289
294
  headerHorizontal.bar.visible = false;