gloomberb 0.11.1 → 0.11.2

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 (233) hide show
  1. package/README.md +10 -9
  2. package/README.zh-CN.md +6 -6
  3. package/package.json +1 -1
  4. package/src/api-client/data.ts +7 -0
  5. package/src/api-client/index.test.ts +47 -0
  6. package/src/api-client/index.ts +61 -0
  7. package/src/api-client/paths.ts +16 -0
  8. package/src/api-client/types.ts +35 -0
  9. package/src/app/pane-runtime/index.tsx +4 -1
  10. package/src/app/pane-runtime/plugin-bindings.test.ts +117 -0
  11. package/src/app/pane-runtime/plugin-bindings.ts +25 -4
  12. package/src/app/pane-runtime/ticker-inspector-runtime.ts +25 -14
  13. package/src/app/runtime/desktop-deeplink.test.ts +16 -0
  14. package/src/app/runtime/desktop-deeplink.ts +66 -2
  15. package/src/app/runtime/desktop-menu.ts +3 -0
  16. package/src/app/runtime/plugin-bindings.ts +30 -28
  17. package/src/app.tsx +9 -2
  18. package/src/cli/commands/automation.test.ts +9 -0
  19. package/src/cli/commands/automation.ts +22 -73
  20. package/src/cli/index.ts +0 -2
  21. package/src/components/chart/composite/composite-chart.test.tsx +45 -2
  22. package/src/components/chart/composite/composite-chart.tsx +12 -9
  23. package/src/components/chart/composite/scene.ts +0 -21
  24. package/src/components/chart/composite/tools.ts +49 -0
  25. package/src/components/command-bar/commands/direct/index.ts +4 -0
  26. package/src/components/command-bar/commands/registry.ts +8 -1
  27. package/src/components/command-bar/layout-items/current-layout.ts +1 -1
  28. package/src/components/command-bar/pane-templates/workflow.ts +4 -0
  29. package/src/components/command-bar/routes/root/shortcut-feedback.ts +3 -0
  30. package/src/components/command-bar/routes/root/shortcuts.test.ts +19 -0
  31. package/src/components/command-bar/routes/root/shortcuts.ts +3 -1
  32. package/src/components/command-bar/surface/index.test.tsx +6 -6
  33. package/src/components/command-bar/surface/pane.test.tsx +66 -2
  34. package/src/components/command-bar/view-model.test.ts +2 -1
  35. package/src/components/command-bar/workflow/ops.test.ts +54 -0
  36. package/src/components/command-bar/workflow/ops.ts +1 -1
  37. package/src/components/layout/detached-pane-shell.tsx +74 -10
  38. package/src/components/layout/pane/title.ts +31 -3
  39. package/src/components/layout/shell/active-drag.ts +4 -11
  40. package/src/components/layout/shell/drag/index.ts +4 -20
  41. package/src/components/layout/shell/drag/runtime.ts +3 -4
  42. package/src/components/layout/shell/index.test.tsx +17 -3
  43. package/src/components/layout/shell/index.tsx +86 -30
  44. package/src/components/layout/shell/menu.test.ts +37 -1
  45. package/src/components/layout/shell/menu.ts +14 -0
  46. package/src/components/layout/shell/native/pointer-runtime.ts +22 -8
  47. package/src/components/layout/shell/pane/actions.ts +0 -5
  48. package/src/components/layout/shell/pane/management-shortcuts.ts +9 -4
  49. package/src/components/layout/shell/shortcuts.ts +6 -3
  50. package/src/components/layout/shell/ticker-link-menu.test.ts +82 -0
  51. package/src/components/layout/shell/ticker-link-menu.ts +78 -0
  52. package/src/components/layout/status-bar.test.tsx +62 -61
  53. package/src/components/layout/status-bar.tsx +53 -75
  54. package/src/core/state/app/layout-reducer.ts +36 -0
  55. package/src/core/state/app/layout.ts +10 -5
  56. package/src/core/state/app/state.ts +0 -13
  57. package/src/core/state/app/types.ts +6 -4
  58. package/src/data/config/chart-settings.ts +1 -1
  59. package/src/data/config/layout.ts +2 -0
  60. package/src/data/config/store/index.test.ts +27 -6
  61. package/src/data/config/store/normalize.ts +3 -5
  62. package/src/data/plugin-state-store.ts +11 -0
  63. package/src/data/sqlite/cache.test.ts +10 -0
  64. package/src/i18n/es.ts +3 -4
  65. package/src/i18n/ja.ts +3 -4
  66. package/src/i18n/ko.ts +3 -4
  67. package/src/i18n/zh-cn.ts +3 -4
  68. package/src/i18n/zh-tw.ts +3 -4
  69. package/src/layout-marketplace/api.test.ts +54 -0
  70. package/src/layout-marketplace/api.ts +65 -0
  71. package/src/layout-marketplace/gallery-desktop.test.tsx +262 -0
  72. package/src/layout-marketplace/gallery-desktop.tsx +375 -0
  73. package/src/layout-marketplace/gallery-terminal.tsx +376 -0
  74. package/src/layout-marketplace/gallery.test.tsx +84 -0
  75. package/src/layout-marketplace/gallery.tsx +323 -0
  76. package/src/layout-marketplace/geometry.test.ts +47 -0
  77. package/src/layout-marketplace/geometry.ts +80 -0
  78. package/src/layout-marketplace/mini-workspace.test.tsx +122 -0
  79. package/src/layout-marketplace/mini-workspace.tsx +428 -0
  80. package/src/layout-marketplace/model.test.ts +118 -0
  81. package/src/layout-marketplace/model.ts +171 -0
  82. package/src/layout-marketplace/name-dialog.tsx +60 -0
  83. package/src/layout-marketplace/pane-imagery.ts +85 -0
  84. package/src/layout-marketplace/pane.tsx +20 -0
  85. package/src/layout-marketplace/payload.test.ts +246 -0
  86. package/src/layout-marketplace/payload.ts +624 -0
  87. package/src/layout-marketplace/use-marketplace.ts +63 -0
  88. package/src/market-data/hooks.tsx +0 -41
  89. package/src/plugins/broker-sync/broker-sync.test.ts +29 -0
  90. package/src/plugins/broker-sync/normalize.ts +9 -2
  91. package/src/plugins/builtin/ai/index.tsx +18 -2
  92. package/src/plugins/builtin/ai/screener/pane.test.tsx +47 -4
  93. package/src/plugins/builtin/ai/screener/pane.tsx +35 -12
  94. package/src/plugins/builtin/alerts/alert-engine.test.ts +16 -1
  95. package/src/plugins/builtin/alerts/alert-engine.ts +4 -0
  96. package/src/plugins/builtin/alerts/index.tsx +4 -0
  97. package/src/plugins/builtin/alerts/pane.tsx +2 -8
  98. package/src/plugins/builtin/analytics/index.tsx +3 -0
  99. package/src/plugins/builtin/broker-manager/index.tsx +3 -0
  100. package/src/plugins/builtin/cds/client.test.ts +83 -0
  101. package/src/plugins/builtin/cds/client.ts +67 -0
  102. package/src/plugins/builtin/cds/index.tsx +41 -0
  103. package/src/plugins/builtin/cds/model.test.ts +232 -0
  104. package/src/plugins/builtin/cds/model.ts +401 -0
  105. package/src/plugins/builtin/cds/pane.test.tsx +132 -0
  106. package/src/plugins/builtin/cds/pane.tsx +358 -0
  107. package/src/plugins/builtin/chart-composer/chart-spec.ts +31 -0
  108. package/src/plugins/builtin/chart-composer/index.tsx +88 -2
  109. package/src/plugins/builtin/chart-composer/pane-share.test.ts +71 -0
  110. package/src/plugins/builtin/chart-composer/pane.tsx +81 -14
  111. package/src/plugins/builtin/chat/content/index.tsx +4 -1
  112. package/src/plugins/builtin/chat/controller/index.ts +8 -1
  113. package/src/plugins/builtin/chat/controller/notifications.ts +13 -3
  114. package/src/plugins/builtin/chat/controller/session-runtime.ts +14 -2
  115. package/src/plugins/builtin/chat/controller/utils.ts +4 -6
  116. package/src/plugins/builtin/chat/controller.test.ts +64 -14
  117. package/src/plugins/builtin/chat/message/desktop.test.tsx +88 -0
  118. package/src/plugins/builtin/chat/message/desktop.tsx +3 -1
  119. package/src/plugins/builtin/chat/message/profile-popover.test.tsx +1 -29
  120. package/src/plugins/builtin/chat/profile-popover.ts +53 -22
  121. package/src/plugins/builtin/chat/sidebar.test.tsx +81 -0
  122. package/src/plugins/builtin/chat/sidebar.tsx +63 -0
  123. package/src/plugins/builtin/chat/test-harness.tsx +29 -1
  124. package/src/plugins/builtin/cloud/auth-commands.ts +0 -4
  125. package/src/plugins/builtin/cloud/auth-model.ts +4 -1
  126. package/src/plugins/builtin/cloud/plugin.tsx +11 -1
  127. package/src/plugins/builtin/cloud-tweets/pane-share.test.ts +56 -0
  128. package/src/plugins/builtin/cloud-tweets/pane.tsx +20 -5
  129. package/src/plugins/builtin/cloud-tweets/registration.ts +44 -5
  130. package/src/plugins/builtin/composite-plugins.ts +3 -1
  131. package/src/plugins/builtin/connections/index.ts +3 -0
  132. package/src/plugins/builtin/econ/calendar-model.ts +0 -5
  133. package/src/plugins/builtin/fear-greed/charts.tsx +12 -8
  134. package/src/plugins/builtin/help/index.tsx +2 -2
  135. package/src/plugins/builtin/kelly-sizer/index.tsx +4 -1
  136. package/src/plugins/builtin/layout-manager/index.tsx +17 -4
  137. package/src/plugins/builtin/news/index.tsx +1 -0
  138. package/src/plugins/builtin/news/wire/feed-config.test.ts +15 -1
  139. package/src/plugins/builtin/news/wire/feed-config.ts +25 -5
  140. package/src/plugins/builtin/news/wire/index.tsx +2 -2
  141. package/src/plugins/builtin/options/index.tsx +1 -0
  142. package/src/plugins/builtin/options/view.test.tsx +25 -3
  143. package/src/plugins/builtin/options/view.tsx +1 -0
  144. package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +2 -0
  145. package/src/plugins/builtin/portfolio-list/account-metrics.ts +1 -1
  146. package/src/plugins/builtin/portfolio-list/cli/render.ts +5 -3
  147. package/src/plugins/builtin/portfolio-list/column-values.ts +15 -4
  148. package/src/plugins/builtin/portfolio-list/header.tsx +13 -3
  149. package/src/plugins/builtin/portfolio-list/index.tsx +8 -0
  150. package/src/plugins/builtin/portfolio-list/metrics.test.ts +31 -1
  151. package/src/plugins/builtin/portfolio-list/pane/index.tsx +10 -7
  152. package/src/plugins/builtin/portfolio-list/position-metrics.ts +25 -6
  153. package/src/plugins/builtin/portfolio-list/summary/index.tsx +21 -16
  154. package/src/plugins/builtin/portfolio-list/summary/totals.ts +20 -6
  155. package/src/plugins/builtin/research/equity-diagnostic-pane.test.tsx +33 -7
  156. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +109 -12
  157. package/src/plugins/builtin/research/index.tsx +4 -0
  158. package/src/plugins/builtin/shared/public-pane.test.ts +12 -0
  159. package/src/plugins/builtin/shared/public-pane.ts +9 -0
  160. package/src/plugins/builtin/shared/ticker-source.ts +32 -0
  161. package/src/plugins/builtin/shared/ticker-surface.test.ts +27 -0
  162. package/src/plugins/builtin/shared/ticker-surface.ts +17 -0
  163. package/src/plugins/builtin/ticker-detail/index.tsx +10 -0
  164. package/src/plugins/builtin/treasury-auctions/index.tsx +2 -0
  165. package/src/plugins/builtin/world-indices/index.tsx +2 -0
  166. package/src/plugins/catalog-browser.test.ts +10 -0
  167. package/src/plugins/catalog-browser.ts +3 -1
  168. package/src/plugins/gridlock-notification.ts +2 -1
  169. package/src/plugins/pane-manager/gridlock.ts +87 -3
  170. package/src/plugins/pane-manager.test.ts +30 -0
  171. package/src/plugins/pane-manager.ts +6 -1
  172. package/src/plugins/prediction-markets/chart.tsx +2 -2
  173. package/src/plugins/prediction-markets/controller/index.ts +5 -1
  174. package/src/plugins/prediction-markets/controller/keyboard.ts +10 -14
  175. package/src/plugins/prediction-markets/detail/overview.tsx +21 -0
  176. package/src/plugins/prediction-markets/detail/pane.tsx +43 -34
  177. package/src/plugins/prediction-markets/detail/trades.tsx +34 -9
  178. package/src/plugins/prediction-markets/detail.test.tsx +29 -2
  179. package/src/plugins/prediction-markets/index.tsx +8 -4
  180. package/src/plugins/prediction-markets/navigation.ts +0 -1
  181. package/src/plugins/prediction-markets/pane.test.tsx +77 -0
  182. package/src/plugins/prediction-markets/pane.tsx +4 -0
  183. package/src/plugins/prediction-markets/plugin.test.tsx +36 -0
  184. package/src/plugins/prediction-markets/test-helpers.tsx +9 -0
  185. package/src/plugins/registry/index.ts +2 -0
  186. package/src/plugins/ticker-navigation.test.ts +24 -0
  187. package/src/plugins/ticker-navigation.ts +40 -0
  188. package/src/remote/schema.ts +1 -1
  189. package/src/renderers/browser/cloud-transport.test.ts +36 -0
  190. package/src/renderers/browser/cloud-transport.ts +19 -0
  191. package/src/renderers/browser/config-host.ts +0 -4
  192. package/src/renderers/browser/deeplink-bridge.test.ts +41 -0
  193. package/src/renderers/browser/deeplink-bridge.ts +22 -0
  194. package/src/renderers/browser/main.tsx +3 -0
  195. package/src/renderers/cloudflare/worker.test.ts +8 -5
  196. package/src/renderers/cloudflare/worker.ts +6 -5
  197. package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
  198. package/src/renderers/electrobun/bun/desktop/initialization.ts +1 -0
  199. package/src/renderers/electrobun/bun/desktop/plugin-state.ts +7 -1
  200. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
  201. package/src/renderers/electrobun/view/host/native.ts +0 -5
  202. package/src/renderers/electrobun/view/remote/persistence.ts +5 -0
  203. package/src/renderers/electrobun/view/styles.css +107 -7
  204. package/src/renderers/electrobun/view/toast-host.test.tsx +78 -0
  205. package/src/renderers/electrobun/view/toast-host.tsx +69 -43
  206. package/src/renderers/electrobun/view/ui-host.tsx +4 -1
  207. package/src/renderers/share/layout-view.test.tsx +58 -0
  208. package/src/renderers/share/layout-view.tsx +133 -0
  209. package/src/renderers/share/main.tsx +54 -6
  210. package/src/renderers/share/styles.css +35 -0
  211. package/src/renderers/share/view.test.tsx +78 -0
  212. package/src/renderers/share/view.tsx +71 -0
  213. package/src/shares/api.test.ts +52 -3
  214. package/src/shares/api.ts +39 -15
  215. package/src/shares/location.test.ts +19 -0
  216. package/src/shares/location.ts +11 -0
  217. package/src/shares/pane.test.ts +110 -0
  218. package/src/shares/pane.ts +54 -0
  219. package/src/shares/payload.ts +74 -1
  220. package/src/state/app/bootstrap.ts +6 -7
  221. package/src/state/app/context/index.test.ts +41 -1
  222. package/src/state/app/context/index.test.tsx +34 -16
  223. package/src/state/selectors-ui.ts +0 -8
  224. package/src/sync/controller.test.ts +57 -1
  225. package/src/sync/controller.ts +22 -11
  226. package/src/sync/react.ts +13 -0
  227. package/src/theme/font-scale.ts +0 -11
  228. package/src/types/config.ts +35 -5
  229. package/src/types/desktop-menu.ts +1 -0
  230. package/src/types/plugin.ts +36 -0
  231. package/src/ui/context-menu.tsx +1 -1
  232. package/src/ui/toast.tsx +2 -0
  233. package/src/utils/back-navigation.ts +0 -4
package/src/i18n/ko.ts CHANGED
@@ -11,6 +11,7 @@ export const ko: Record<string, string> = {
11
11
  "Cancel": "취소",
12
12
  "Close": "닫기",
13
13
  "Open": "열기",
14
+ "Linked to {source}": "{source}에 연결됨",
14
15
  "Working…": "처리 중…",
15
16
  "Searching…": "검색 중…",
16
17
  "Loading...": "불러오는 중...",
@@ -219,12 +220,12 @@ export const ko: Record<string, string> = {
219
220
  "Dock Pane": "패널 도킹",
220
221
  "Settings": "설정",
221
222
  "Copy Screenshot": "스크린샷 복사",
223
+ "Share Pane": "창 공유",
222
224
  "Pop Out": "별도 창으로 열기",
223
225
  "Move Window...": "창 이동...",
224
226
  "Resize Window...": "창 크기 조정...",
225
227
  "Move Window": "창 이동",
226
228
  "Resize Window": "창 크기 조정",
227
- "Gridlock All Windows": "모든 창 격자 정렬",
228
229
  "Change the current saved layout name": "현재 저장된 레이아웃 이름 변경",
229
230
  "Create a copy of the current layout": "현재 레이아웃의 복사본 만들기",
230
231
  "Create a fresh saved layout": "새로 저장된 레이아웃 만들기",
@@ -516,7 +517,6 @@ export const ko: Record<string, string> = {
516
517
  "Pop the focused pane out to a desktop window.": "포커스 패널을 데스크톱 창으로 분리합니다.",
517
518
  "Copy a screenshot of the focused pane.": "포커스 패널의 스크린샷을 복사합니다.",
518
519
  "Open layout actions.": "레이아웃 작업을 엽니다.",
519
- "Gridlock all windows.": "모든 창을 격자에 맞춥니다.",
520
520
  "Enter window move mode.": "창 이동 모드로 들어갑니다.",
521
521
  "Enter window resize mode.": "창 크기 조절 모드로 들어갑니다.",
522
522
  "Cancel an active pane drag.": "진행 중인 패널 드래그를 취소합니다.",
@@ -910,8 +910,7 @@ export const ko: Record<string, string> = {
910
910
  "v{version} available — run {command}": "v{version} 사용 가능 — {command} 실행",
911
911
  "v{version} available — starting download...": "v{version} 사용 가능 — 다운로드 시작 중...",
912
912
  "command bar": "명령 팔레트",
913
- "Snapped a window?": "창을 맞춰 배치했나요?",
914
- "Gridlock All": "모두 격자 정렬",
913
+ "Tidy Windows": "창 정리",
915
914
  "Dismiss": "닫기",
916
915
  "Minimize": "최소화",
917
916
  "Maximize": "최대화",
package/src/i18n/zh-cn.ts CHANGED
@@ -13,6 +13,7 @@ export const zhCN: Record<string, string> = {
13
13
  "Cancel": "取消",
14
14
  "Close": "关闭",
15
15
  "Open": "打开",
16
+ "Linked to {source}": "已链接到 {source}",
16
17
  "Working…": "处理中…",
17
18
  "Searching…": "搜索中…",
18
19
  "Loading...": "加载中...",
@@ -221,12 +222,12 @@ export const zhCN: Record<string, string> = {
221
222
  "Dock Pane": "停靠面板",
222
223
  "Settings": "设置",
223
224
  "Copy Screenshot": "复制截图",
225
+ "Share Pane": "分享窗格",
224
226
  "Pop Out": "弹出窗口",
225
227
  "Move Window...": "移动窗口...",
226
228
  "Resize Window...": "调整窗口大小...",
227
229
  "Move Window": "移动窗口",
228
230
  "Resize Window": "调整窗口大小",
229
- "Gridlock All Windows": "窗口网格对齐",
230
231
  "Change the current saved layout name": "修改当前已存布局的名称",
231
232
  "Create a copy of the current layout": "创建当前布局的副本",
232
233
  "Create a fresh saved layout": "创建一个新的已存布局",
@@ -518,7 +519,6 @@ export const zhCN: Record<string, string> = {
518
519
  "Pop the focused pane out to a desktop window.": "把聚焦面板弹出为桌面窗口。",
519
520
  "Copy a screenshot of the focused pane.": "复制聚焦面板的截图。",
520
521
  "Open layout actions.": "打开布局操作。",
521
- "Gridlock all windows.": "将所有窗口网格对齐。",
522
522
  "Enter window move mode.": "进入窗口移动模式。",
523
523
  "Enter window resize mode.": "进入窗口缩放模式。",
524
524
  "Cancel an active pane drag.": "取消正在进行的面板拖动。",
@@ -912,8 +912,7 @@ export const zhCN: Record<string, string> = {
912
912
  "v{version} available — run {command}": "v{version} 可用 — 执行 {command}",
913
913
  "v{version} available — starting download...": "v{version} 可用 — 开始下载...",
914
914
  "command bar": "命令栏",
915
- "Snapped a window?": "窗口已吸附?",
916
- "Gridlock All": "全部网格对齐",
915
+ "Tidy Windows": "整理窗口",
917
916
  "Dismiss": "忽略",
918
917
  "Minimize": "最小化",
919
918
  "Maximize": "最大化",
package/src/i18n/zh-tw.ts CHANGED
@@ -13,6 +13,7 @@ export const zhTW: Record<string, string> = {
13
13
  "Cancel": "取消",
14
14
  "Close": "關閉",
15
15
  "Open": "開啟",
16
+ "Linked to {source}": "已連結至 {source}",
16
17
  "Working…": "處理中…",
17
18
  "Searching…": "搜尋中…",
18
19
  "Loading...": "載入中...",
@@ -221,12 +222,12 @@ export const zhTW: Record<string, string> = {
221
222
  "Dock Pane": "停靠面板",
222
223
  "Settings": "設定",
223
224
  "Copy Screenshot": "複製截圖",
225
+ "Share Pane": "分享窗格",
224
226
  "Pop Out": "彈出視窗",
225
227
  "Move Window...": "移動視窗...",
226
228
  "Resize Window...": "調整視窗大小...",
227
229
  "Move Window": "移動視窗",
228
230
  "Resize Window": "調整視窗大小",
229
- "Gridlock All Windows": "視窗網格對齊",
230
231
  "Change the current saved layout name": "修改目前版面設定的名稱",
231
232
  "Create a copy of the current layout": "建立目前版面設定的副本",
232
233
  "Create a fresh saved layout": "建立新的版面設定",
@@ -518,7 +519,6 @@ export const zhTW: Record<string, string> = {
518
519
  "Pop the focused pane out to a desktop window.": "把聚焦面板彈出為桌面視窗。",
519
520
  "Copy a screenshot of the focused pane.": "複製聚焦面板的截圖。",
520
521
  "Open layout actions.": "開啟版面設定操作。",
521
- "Gridlock all windows.": "將所有視窗網格對齊。",
522
522
  "Enter window move mode.": "進入視窗移動模式。",
523
523
  "Enter window resize mode.": "進入視窗縮放模式。",
524
524
  "Cancel an active pane drag.": "取消正在進行的面板拖曳。",
@@ -912,8 +912,7 @@ export const zhTW: Record<string, string> = {
912
912
  "v{version} available — run {command}": "v{version} 可用 — 執行 {command}",
913
913
  "v{version} available — starting download...": "v{version} 可用 — 開始下載...",
914
914
  "command bar": "命令列",
915
- "Snapped a window?": "視窗已吸附?",
916
- "Gridlock All": "全部網格對齊",
915
+ "Tidy Windows": "整理視窗",
917
916
  "Dismiss": "忽略",
918
917
  "Minimize": "最小化",
919
918
  "Maximize": "最大化",
@@ -0,0 +1,54 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ getPublicMarketplaceLayout,
4
+ marketplaceLayoutIdFromSearch,
5
+ openLiveMarketplaceLayoutUrl,
6
+ parseMarketplaceLayoutId,
7
+ publicMarketplaceLayoutUrl,
8
+ } from "./api";
9
+
10
+ const id = "0123456789abcdef0123456789abcdef";
11
+ const entry = {
12
+ id,
13
+ name: "Social Desk",
14
+ schemaVersion: 2,
15
+ sourceConfigVersion: 21,
16
+ layout: {
17
+ dockRoot: { kind: "pane", instanceId: "p1" },
18
+ instances: [{
19
+ instanceId: "p1",
20
+ paneId: "twitter-feed",
21
+ params: { query: "$NVDA" },
22
+ }],
23
+ floating: [],
24
+ detached: [],
25
+ },
26
+ paneState: {},
27
+ author: { username: "analyst", displayName: "Analyst" },
28
+ publishedAt: "2026-08-27T00:00:00.000Z",
29
+ };
30
+
31
+ describe("public marketplace layout links", () => {
32
+ test("loads and validates an anonymous layout item", async () => {
33
+ const calls: Array<[string, RequestInit | undefined]> = [];
34
+ const loaded = await getPublicMarketplaceLayout(id, async (url, init) => {
35
+ calls.push([url, init]);
36
+ return new Response(JSON.stringify(entry), {
37
+ status: 200,
38
+ headers: { "content-type": "application/json" },
39
+ });
40
+ });
41
+
42
+ expect(loaded).toEqual(entry);
43
+ expect(new URL(calls[0]![0]).pathname).toBe(`/layouts/${id}`);
44
+ expect(calls[0]![1]?.credentials).toBe("include");
45
+ });
46
+
47
+ test("builds durable public and live-install URLs", () => {
48
+ expect(publicMarketplaceLayoutUrl(id, "https://term.gloom.sh")).toBe(`https://term.gloom.sh/l/${id}`);
49
+ expect(openLiveMarketplaceLayoutUrl(id, "https://term.gloom.sh")).toBe(`https://term.gloom.sh/?layout=${id}`);
50
+ expect(marketplaceLayoutIdFromSearch(`?layout=${id}`)).toBe(id);
51
+ expect(parseMarketplaceLayoutId(`/l/${id}`)).toBe(id);
52
+ expect(parseMarketplaceLayoutId("/l/not-an-id")).toBeNull();
53
+ });
54
+ });
@@ -0,0 +1,65 @@
1
+ import {
2
+ PUBLIC_SHARE_ORIGIN,
3
+ SHARE_API_ORIGIN,
4
+ } from "../shares/api";
5
+ import {
6
+ isMarketplaceLayoutId,
7
+ parseMarketplaceLayoutEntry,
8
+ type LayoutMarketplaceEntry,
9
+ } from "./payload";
10
+
11
+ type LayoutFetch = (input: string, init?: RequestInit) => Promise<Response>;
12
+
13
+ export async function getPublicMarketplaceLayout(
14
+ id: string,
15
+ fetchImpl: LayoutFetch = fetch,
16
+ ): Promise<LayoutMarketplaceEntry | null> {
17
+ if (!isMarketplaceLayoutId(id)) return null;
18
+ const response = await fetchImpl(
19
+ `${SHARE_API_ORIGIN}/layouts/${encodeURIComponent(id)}`,
20
+ { credentials: "include" },
21
+ );
22
+ if (response.status === 404) return null;
23
+ if (!response.ok) throw new Error("Could not load shared layout.");
24
+ let body: unknown;
25
+ try {
26
+ body = await response.json();
27
+ } catch {
28
+ throw new Error("The layout service returned an invalid response.");
29
+ }
30
+ return parseMarketplaceLayoutEntry(body);
31
+ }
32
+
33
+ export function publicMarketplaceLayoutUrl(
34
+ id: string,
35
+ origin = PUBLIC_SHARE_ORIGIN,
36
+ ): string {
37
+ if (!isMarketplaceLayoutId(id)) throw new Error("Invalid layout id.");
38
+ return new URL(`/l/${encodeURIComponent(id)}`, origin).toString();
39
+ }
40
+
41
+ export function openLiveMarketplaceLayoutUrl(
42
+ id: string,
43
+ origin = PUBLIC_SHARE_ORIGIN,
44
+ ): string {
45
+ if (!isMarketplaceLayoutId(id)) throw new Error("Invalid layout id.");
46
+ const url = new URL("/", origin);
47
+ url.searchParams.set("layout", id);
48
+ return url.toString();
49
+ }
50
+
51
+ export function marketplaceLayoutIdFromSearch(search: string): string | null {
52
+ const id = new URLSearchParams(search).get("layout")?.trim() ?? "";
53
+ return isMarketplaceLayoutId(id) ? id : null;
54
+ }
55
+
56
+ export function parseMarketplaceLayoutId(pathname: string): string | null {
57
+ const match = /^\/l\/([^/]+)\/?$/.exec(pathname);
58
+ if (!match) return null;
59
+ try {
60
+ const id = decodeURIComponent(match[1] ?? "");
61
+ return isMarketplaceLayoutId(id) ? id : null;
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
@@ -0,0 +1,262 @@
1
+ /** @jsxImportSource react */
2
+ import { Window } from "happy-dom";
3
+
4
+ const testWindow = new Window({ url: "http://localhost" });
5
+ const domGlobals = {
6
+ IS_REACT_ACT_ENVIRONMENT: true,
7
+ window: testWindow,
8
+ document: testWindow.document,
9
+ navigator: testWindow.navigator,
10
+ KeyboardEvent: testWindow.KeyboardEvent,
11
+ MouseEvent: testWindow.MouseEvent,
12
+ HTMLElement: testWindow.HTMLElement,
13
+ Node: testWindow.Node,
14
+ };
15
+
16
+ /** Bun shares one process across test files, so the DOM globals must not leak. */
17
+ const priorGlobals = Object.fromEntries(
18
+ Object.keys(domGlobals).map((key) => [key, (globalThis as Record<string, unknown>)[key]]),
19
+ );
20
+ Object.assign(globalThis, domGlobals);
21
+
22
+ import { afterAll, afterEach, expect, test } from "bun:test";
23
+ import { act } from "react";
24
+ import { createRoot } from "react-dom/client";
25
+ import { UiHostProvider, type RendererHost, type UiHost } from "../ui";
26
+ import { WebBox } from "../renderers/electrobun/view/host/box";
27
+ import { WebText, WebSpan } from "../renderers/electrobun/view/host/text";
28
+ import { WebScrollBox } from "../renderers/electrobun/view/host/scroll-box";
29
+ import { WebInput } from "../renderers/electrobun/view/host/input";
30
+ import { WebButton, WebTextField } from "../renderers/electrobun/view/desktop/controls";
31
+ import { cloneLayout, createDefaultConfig } from "../types/config";
32
+ import type { PaneDef } from "../types/plugin";
33
+ import { LayoutGalleryDesktop } from "./gallery-desktop";
34
+ import { buildOwnedEntries, type GalleryEntry } from "./model";
35
+ import type { LayoutGalleryController } from "./gallery";
36
+
37
+ const renderer: RendererHost = {
38
+ requestExit() {},
39
+ async openExternal() {},
40
+ async copyText() {},
41
+ async readText() { return ""; },
42
+ notify() {},
43
+ };
44
+
45
+ const ui = {
46
+ kind: "desktop-web",
47
+ capabilities: { cellWidthPx: 8, cellHeightPx: 18, fractionalViewport: true },
48
+ Box: WebBox,
49
+ Text: WebText,
50
+ Span: WebSpan,
51
+ ScrollBox: WebScrollBox,
52
+ Button: WebButton,
53
+ Input: WebInput,
54
+ TextField: WebTextField,
55
+ SpinnerMark: () => null,
56
+ } as unknown as UiHost;
57
+
58
+ afterAll(() => {
59
+ for (const [key, value] of Object.entries(priorGlobals)) {
60
+ if (value === undefined) delete (globalThis as Record<string, unknown>)[key];
61
+ else (globalThis as Record<string, unknown>)[key] = value;
62
+ }
63
+ });
64
+
65
+ function paneDef(id: string, name: string, icon: string): PaneDef {
66
+ return { id, name, icon, component: () => null, defaultPosition: "left" };
67
+ }
68
+
69
+ const panes = new Map<string, PaneDef>([
70
+ ["portfolio-list", paneDef("portfolio-list", "Portfolio", "P")],
71
+ ["ticker-research", paneDef("ticker-research", "Ticker Research", "T")],
72
+ ["chat", paneDef("chat", "Chat", "M")],
73
+ ]);
74
+
75
+ let root: ReturnType<typeof createRoot> | undefined;
76
+
77
+ afterEach(async () => {
78
+ if (root) {
79
+ await act(async () => root!.unmount());
80
+ root = undefined;
81
+ }
82
+ });
83
+
84
+ function communityEntry(): GalleryEntry {
85
+ return {
86
+ id: "community:abc",
87
+ marketplaceId: "0123456789abcdef0123456789abcdef",
88
+ kind: "community",
89
+ name: "Earnings War Room",
90
+ layout: cloneLayout(createDefaultConfig("/tmp/gloomberb-gallery-desktop-community").layout),
91
+ paneState: {},
92
+ index: null,
93
+ active: false,
94
+ author: "@analyst",
95
+ publishedAt: "2026-08-26T00:00:00.000Z",
96
+ };
97
+ }
98
+
99
+ function createController(overrides: Partial<LayoutGalleryController> = {}): {
100
+ controller: LayoutGalleryController;
101
+ activated: GalleryEntry[];
102
+ installed: GalleryEntry[];
103
+ selections: (string | null)[];
104
+ copied: GalleryEntry[];
105
+ } {
106
+ const config = createDefaultConfig("/tmp/gloomberb-gallery-desktop-test");
107
+ const owned = buildOwnedEntries([
108
+ { name: "Monitor", layout: cloneLayout(config.layout) },
109
+ { name: "Research Desk", layout: cloneLayout(config.layout) },
110
+ ], 1);
111
+ const activated: GalleryEntry[] = [];
112
+ const installed: GalleryEntry[] = [];
113
+ const selections: (string | null)[] = [];
114
+ const copied: GalleryEntry[] = [];
115
+ const community = overrides.community ?? [];
116
+ const controller: LayoutGalleryController = {
117
+ query: "",
118
+ setQuery: () => {},
119
+ owned,
120
+ community,
121
+ entries: [...(overrides.owned ?? owned), ...community],
122
+ selectedId: null,
123
+ select: (id) => selections.push(id),
124
+ detail: null,
125
+ openDetail: () => {},
126
+ closeDetail: () => {},
127
+ activate: (entry) => activated.push(entry),
128
+ install: (entry) => installed.push(entry),
129
+ discover: {
130
+ state: { status: "signed-out", items: [] },
131
+ refresh: () => {},
132
+ publish: async () => { throw new Error("unused"); },
133
+ },
134
+ signedIn: false,
135
+ requestSignIn: () => {},
136
+ publishCurrent: () => {},
137
+ copyLink: (entry) => copied.push(entry),
138
+ publishing: false,
139
+ newLayout: () => {},
140
+ renameLayout: () => {},
141
+ duplicateLayout: () => {},
142
+ deleteLayout: () => {},
143
+ canDelete: true,
144
+ close: () => {},
145
+ panes,
146
+ missingPaneIds: () => [],
147
+ ...overrides,
148
+ };
149
+ return { controller, activated, installed, selections, copied };
150
+ }
151
+
152
+ async function renderGallery(controller: LayoutGalleryController) {
153
+ const container = testWindow.document.createElement("div");
154
+ testWindow.document.body.appendChild(container);
155
+ root = createRoot(container as unknown as HTMLElement);
156
+ await act(async () => {
157
+ root!.render(
158
+ <UiHostProvider ui={ui} renderer={renderer}>
159
+ <LayoutGalleryDesktop controller={controller} />
160
+ </UiHostProvider>,
161
+ );
162
+ });
163
+ return container;
164
+ }
165
+
166
+ function rows(container: Element) {
167
+ return [...container.querySelectorAll('[data-gloom-role="layout-gallery-row"]')];
168
+ }
169
+
170
+ function pressButton(container: Element, label: string) {
171
+ const button = [...container.querySelectorAll('[data-gloom-role="desktop-button"]')]
172
+ .find((node) => node.textContent?.includes(label));
173
+ if (!button) throw new Error(`no button labelled ${label}`);
174
+ return act(async () => {
175
+ button.dispatchEvent(new testWindow.MouseEvent("mousedown", { bubbles: true, button: 0 }) as unknown as MouseEvent);
176
+ });
177
+ }
178
+
179
+ test("sidebar rows select the preview instead of activating the layout", async () => {
180
+ const { controller, activated, selections } = createController();
181
+ const container = await renderGallery(controller);
182
+
183
+ const text = container.textContent ?? "";
184
+ expect(text.indexOf("YOUR LAYOUTS")).toBeLessThan(text.indexOf("DISCOVER"));
185
+ expect(text).toContain("A Gloom account is required to browse community layouts.");
186
+
187
+ const sidebarRows = rows(container);
188
+ // Two owned layouts plus the account-gate row.
189
+ expect(sidebarRows.length).toBe(3);
190
+ expect(sidebarRows[0]!.getAttribute("role")).toBe("button");
191
+ expect(sidebarRows[0]!.getAttribute("tabindex")).toBe("0");
192
+ expect(sidebarRows[0]!.getAttribute("aria-label")).toContain("panes");
193
+
194
+ await act(async () => {
195
+ sidebarRows[0]!.dispatchEvent(new testWindow.MouseEvent("mousedown", { bubbles: true, button: 0 }) as unknown as MouseEvent);
196
+ });
197
+ await act(async () => {
198
+ sidebarRows[1]!.dispatchEvent(new testWindow.MouseEvent("mouseover", { bubbles: true }) as unknown as MouseEvent);
199
+ });
200
+ await act(async () => {
201
+ (sidebarRows[1] as unknown as HTMLElement).focus();
202
+ });
203
+
204
+ expect(selections).toEqual(["owned:0", "owned:1", "owned:1"]);
205
+ expect(activated).toEqual([]);
206
+ });
207
+
208
+ test("preview falls back to the layout in use and runs owned actions", async () => {
209
+ const renamed: GalleryEntry[] = [];
210
+ const { controller, activated } = createController({ renameLayout: (entry) => renamed.push(entry) });
211
+ const container = await renderGallery(controller);
212
+
213
+ const preview = container.querySelector('[data-gloom-role="layout-gallery-preview"]')!;
214
+ const previewText = preview.textContent ?? "";
215
+ // owned:1 is the active layout and there is no selection yet.
216
+ expect(previewText).toContain("Research Desk");
217
+ expect(preview.querySelector("svg")).not.toBeNull();
218
+
219
+ await pressButton(preview, "Rename");
220
+ expect(renamed.map((entry) => entry.name)).toEqual(["Research Desk"]);
221
+
222
+ await pressButton(preview, "Use Layout");
223
+ expect(activated.map((entry) => entry.name)).toEqual(["Research Desk"]);
224
+ });
225
+
226
+ test("a selected community layout installs as an independent copy", async () => {
227
+ const community = [communityEntry()];
228
+ const { controller, installed, copied } = createController({
229
+ community,
230
+ selectedId: "community:abc",
231
+ signedIn: true,
232
+ discover: {
233
+ state: { status: "ready", items: [] },
234
+ refresh: () => {},
235
+ publish: async () => { throw new Error("unused"); },
236
+ },
237
+ });
238
+ const container = await renderGallery(controller);
239
+
240
+ const preview = container.querySelector('[data-gloom-role="layout-gallery-preview"]')!;
241
+ const previewText = preview.textContent ?? "";
242
+ expect(previewText).toContain("Earnings War Room");
243
+ expect(previewText).toContain("@analyst");
244
+ expect(previewText).toContain("Adds an editable copy");
245
+ expect(previewText).not.toContain("Rename");
246
+
247
+ await pressButton(preview, "Copy Link");
248
+ expect(copied.map((entry) => entry.marketplaceId)).toEqual(["0123456789abcdef0123456789abcdef"]);
249
+
250
+ await pressButton(preview, "Add Layout");
251
+ expect(installed.map((entry) => entry.name)).toEqual(["Earnings War Room"]);
252
+ });
253
+
254
+ test("an empty gallery keeps a preview placeholder instead of a blank pane", async () => {
255
+ const { controller } = createController({ owned: [], entries: [] });
256
+ const container = await renderGallery(controller);
257
+
258
+ expect(rows(container).length).toBe(1); // account gate only
259
+ const empty = container.querySelector('[data-gloom-role="layout-gallery-preview-empty"]')!;
260
+ expect(empty.textContent).toContain("No layout selected.");
261
+ expect(container.querySelector('[data-gloom-role="layout-gallery-preview"]')).toBeNull();
262
+ });