gloomberb 0.11.0 → 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.
- package/README.md +38 -15
- package/README.zh-CN.md +12 -12
- package/package.json +10 -3
- package/src/api-client/auth.ts +14 -10
- package/src/api-client/data.ts +54 -0
- package/src/api-client/index.test.ts +124 -0
- package/src/api-client/index.ts +139 -4
- package/src/api-client/paths.ts +89 -0
- package/src/api-client/request.ts +15 -2
- package/src/api-client/socket-health.test.ts +1 -0
- package/src/api-client/socket.ts +2 -1
- package/src/api-client/types.ts +163 -0
- package/src/app/pane-runtime/index.tsx +4 -1
- package/src/app/pane-runtime/plugin-bindings.test.ts +117 -0
- package/src/app/pane-runtime/plugin-bindings.ts +25 -4
- package/src/app/pane-runtime/ticker-inspector-runtime.ts +25 -14
- package/src/app/runtime/desktop-deeplink.test.ts +16 -0
- package/src/app/runtime/desktop-deeplink.ts +66 -2
- package/src/app/runtime/desktop-menu.ts +3 -0
- package/src/app/runtime/plugin-bindings.ts +30 -28
- package/src/app/runtime/update.ts +8 -6
- package/src/app.tsx +21 -7
- package/src/architecture/import-boundaries.test.ts +5 -1
- package/src/capabilities/factories.ts +4 -0
- package/src/cli/commands/automation.test.ts +9 -0
- package/src/cli/commands/automation.ts +22 -73
- package/src/cli/index.ts +10 -6
- package/src/cli/registry.ts +1 -1
- package/src/components/chart/composite/composite-chart.test.tsx +45 -2
- package/src/components/chart/composite/composite-chart.tsx +16 -12
- package/src/components/chart/composite/format.test.ts +34 -0
- package/src/components/chart/composite/format.ts +33 -10
- package/src/components/chart/composite/scene.ts +0 -21
- package/src/components/chart/composite/tools.ts +49 -0
- package/src/components/chart/core/axis-format.ts +13 -3
- package/src/components/chart/core/index.test.ts +7 -0
- package/src/components/chart/core/renderer.ts +3 -4
- package/src/components/command-bar/commands/direct/index.ts +4 -0
- package/src/components/command-bar/commands/registry.ts +8 -1
- package/src/components/command-bar/layout-items/current-layout.ts +1 -1
- package/src/components/command-bar/pane-templates/workflow.ts +4 -0
- package/src/components/command-bar/routes/root/shortcut-feedback.ts +3 -0
- package/src/components/command-bar/routes/root/shortcuts.test.ts +19 -0
- package/src/components/command-bar/routes/root/shortcuts.ts +3 -1
- package/src/components/command-bar/surface/index.test.tsx +6 -6
- package/src/components/command-bar/surface/index.tsx +4 -1
- package/src/components/command-bar/surface/pane.test.tsx +66 -2
- package/src/components/command-bar/view-model.test.ts +2 -1
- package/src/components/command-bar/workflow/ops.test.ts +54 -0
- package/src/components/command-bar/workflow/ops.ts +1 -1
- package/src/components/company-logo.test.ts +19 -0
- package/src/components/company-logo.tsx +60 -0
- package/src/components/feed-data-table/stack-view.tsx +7 -1
- package/src/components/index.ts +1 -0
- package/src/components/layout/detached-pane-shell.tsx +85 -15
- package/src/components/layout/header.tsx +5 -5
- package/src/components/layout/pane/title.ts +31 -3
- package/src/components/layout/shell/active-drag.ts +4 -11
- package/src/components/layout/shell/drag/index.ts +4 -20
- package/src/components/layout/shell/drag/runtime.ts +3 -4
- package/src/components/layout/shell/index.test.tsx +17 -3
- package/src/components/layout/shell/index.tsx +89 -31
- package/src/components/layout/shell/menu.test.ts +37 -1
- package/src/components/layout/shell/menu.ts +14 -0
- package/src/components/layout/shell/native/pointer-runtime.ts +22 -8
- package/src/components/layout/shell/pane/actions.ts +0 -5
- package/src/components/layout/shell/pane/management-shortcuts.ts +9 -4
- package/src/components/layout/shell/shortcuts.ts +6 -3
- package/src/components/layout/shell/ticker-link-menu.test.ts +82 -0
- package/src/components/layout/shell/ticker-link-menu.ts +78 -0
- package/src/components/layout/status-bar.test.tsx +62 -61
- package/src/components/layout/status-bar.tsx +53 -75
- package/src/components/onboarding/account-step/form-panel.tsx +4 -2
- package/src/components/onboarding/account-step/index.tsx +1 -1
- package/src/components/onboarding/account-step/signed-in-panel.tsx +1 -1
- package/src/components/onboarding/onboarding-wizard.test.tsx +44 -1
- package/src/components/onboarding/onboarding-wizard.tsx +13 -5
- package/src/components/onboarding/wizard-account.ts +4 -23
- package/src/components/table-view-shared.test.ts +11 -0
- package/src/components/table-view-shared.tsx +26 -0
- package/src/components/ui/data-table/index.tsx +19 -1
- package/src/components/ui/fields.tsx +5 -1
- package/src/core/state/app/layout-reducer.ts +36 -0
- package/src/core/state/app/layout.ts +10 -5
- package/src/core/state/app/state.ts +0 -13
- package/src/core/state/app/types.ts +6 -4
- package/src/data/config/chart-settings.ts +1 -1
- package/src/data/config/layout.ts +2 -0
- package/src/data/config/store/index.test.ts +27 -6
- package/src/data/config/store/normalize.ts +3 -5
- package/src/data/plugin-state-store.ts +11 -0
- package/src/data/sqlite/cache.test.ts +10 -0
- package/src/i18n/es.ts +13 -5
- package/src/i18n/ja.ts +13 -5
- package/src/i18n/ko.ts +13 -5
- package/src/i18n/zh-cn.ts +13 -5
- package/src/i18n/zh-tw.ts +13 -5
- package/src/layout-marketplace/api.test.ts +54 -0
- package/src/layout-marketplace/api.ts +65 -0
- package/src/layout-marketplace/gallery-desktop.test.tsx +262 -0
- package/src/layout-marketplace/gallery-desktop.tsx +375 -0
- package/src/layout-marketplace/gallery-terminal.tsx +376 -0
- package/src/layout-marketplace/gallery.test.tsx +84 -0
- package/src/layout-marketplace/gallery.tsx +323 -0
- package/src/layout-marketplace/geometry.test.ts +47 -0
- package/src/layout-marketplace/geometry.ts +80 -0
- package/src/layout-marketplace/mini-workspace.test.tsx +122 -0
- package/src/layout-marketplace/mini-workspace.tsx +428 -0
- package/src/layout-marketplace/model.test.ts +118 -0
- package/src/layout-marketplace/model.ts +171 -0
- package/src/layout-marketplace/name-dialog.tsx +60 -0
- package/src/layout-marketplace/pane-imagery.ts +85 -0
- package/src/layout-marketplace/pane.tsx +20 -0
- package/src/layout-marketplace/payload.test.ts +246 -0
- package/src/layout-marketplace/payload.ts +624 -0
- package/src/layout-marketplace/use-marketplace.ts +63 -0
- package/src/market-data/coordinator/index.test.ts +2 -2
- package/src/market-data/coordinator/index.ts +1 -1
- package/src/market-data/hooks.tsx +0 -41
- package/src/market-data/market/format.test.ts +7 -0
- package/src/market-data/market/freshness.ts +5 -2
- package/src/market-data/quotes/freshness.test.ts +16 -0
- package/src/news/aggregator.test.ts +92 -3
- package/src/news/aggregator.ts +98 -18
- package/src/news/hooks.test.tsx +2 -0
- package/src/news/hooks.ts +27 -1
- package/src/news/news-model.ts +5 -2
- package/src/news/types.ts +7 -0
- package/src/plugins/broker-sync/broker-sync.test.ts +58 -0
- package/src/plugins/broker-sync/normalize.ts +42 -5
- package/src/plugins/builtin/account-management/pane.tsx +5 -5
- package/src/plugins/builtin/ai/index.tsx +18 -2
- package/src/plugins/builtin/ai/screener/pane.test.tsx +47 -4
- package/src/plugins/builtin/ai/screener/pane.tsx +35 -12
- package/src/plugins/builtin/alerts/alert-engine.test.ts +53 -1
- package/src/plugins/builtin/alerts/alert-engine.ts +34 -1
- package/src/plugins/builtin/alerts/index.test.tsx +49 -6
- package/src/plugins/builtin/alerts/index.tsx +11 -6
- package/src/plugins/builtin/alerts/pane.tsx +51 -9
- package/src/plugins/builtin/alerts/quotes.ts +11 -3
- package/src/plugins/builtin/alerts/types.ts +1 -0
- package/src/plugins/builtin/analytics/index.tsx +11 -1
- package/src/plugins/builtin/analytics/pane-model.ts +6 -4
- package/src/plugins/builtin/broker-manager/index.tsx +3 -0
- package/src/plugins/builtin/buildout/model/index.ts +1 -1
- package/src/plugins/builtin/buildout/pane/index.tsx +17 -18
- package/src/plugins/builtin/cds/client.test.ts +83 -0
- package/src/plugins/builtin/cds/client.ts +67 -0
- package/src/plugins/builtin/cds/index.tsx +41 -0
- package/src/plugins/builtin/cds/model.test.ts +232 -0
- package/src/plugins/builtin/cds/model.ts +401 -0
- package/src/plugins/builtin/cds/pane.test.tsx +132 -0
- package/src/plugins/builtin/cds/pane.tsx +358 -0
- package/src/plugins/builtin/chart-composer/chart-spec.ts +31 -0
- package/src/plugins/builtin/chart-composer/index.tsx +88 -2
- package/src/plugins/builtin/chart-composer/pane-share.test.ts +71 -0
- package/src/plugins/builtin/chart-composer/pane.tsx +103 -15
- package/src/plugins/builtin/chat/content/index.tsx +4 -1
- package/src/plugins/builtin/chat/controller/channels.ts +7 -1
- package/src/plugins/builtin/chat/controller/index.ts +14 -9
- package/src/plugins/builtin/chat/controller/notifications.ts +13 -3
- package/src/plugins/builtin/chat/controller/persistence.ts +0 -3
- package/src/plugins/builtin/chat/controller/realtime.ts +3 -3
- package/src/plugins/builtin/chat/controller/send.ts +1 -3
- package/src/plugins/builtin/chat/controller/session-runtime.ts +18 -11
- package/src/plugins/builtin/chat/controller/state.ts +1 -3
- package/src/plugins/builtin/chat/controller/storage.ts +0 -2
- package/src/plugins/builtin/chat/controller/utils.ts +4 -6
- package/src/plugins/builtin/chat/controller/view.ts +2 -2
- package/src/plugins/builtin/chat/controller.test.ts +142 -14
- package/src/plugins/builtin/chat/message/desktop.test.tsx +88 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +3 -1
- package/src/plugins/builtin/chat/message/profile-popover.test.tsx +1 -29
- package/src/plugins/builtin/chat/profile-popover.ts +53 -22
- package/src/plugins/builtin/chat/sidebar.test.tsx +81 -0
- package/src/plugins/builtin/chat/sidebar.tsx +63 -0
- package/src/plugins/builtin/chat/test-harness.tsx +29 -1
- package/src/plugins/builtin/cloud/auth-actions.tsx +14 -7
- package/src/plugins/builtin/cloud/auth-commands.ts +21 -51
- package/src/plugins/builtin/cloud/auth-dialog.tsx +296 -0
- package/src/{components/onboarding/account-step/model.test.ts → plugins/builtin/cloud/auth-model.test.ts} +1 -1
- package/src/{components/onboarding/account-step/model.ts → plugins/builtin/cloud/auth-model.ts} +37 -3
- package/src/plugins/builtin/cloud/browser.tsx +9 -0
- package/src/plugins/builtin/cloud/buildout-module.tsx +23 -0
- package/src/plugins/builtin/cloud/device-signin.ts +2 -8
- package/src/plugins/builtin/cloud/index.tsx +2 -0
- package/src/plugins/builtin/cloud/plugin.tsx +14 -24
- package/src/plugins/builtin/cloud/upgrade-command.ts +13 -6
- package/src/plugins/builtin/cloud-tweets/pane-share.test.ts +56 -0
- package/src/plugins/builtin/cloud-tweets/pane.tsx +20 -5
- package/src/plugins/builtin/cloud-tweets/registration.ts +50 -6
- package/src/plugins/builtin/composite-plugins.ts +3 -1
- package/src/plugins/builtin/congress-trades/detail.tsx +43 -1
- package/src/plugins/builtin/congress-trades/model.test.ts +85 -0
- package/src/plugins/builtin/congress-trades/model.ts +69 -1
- package/src/plugins/builtin/congress-trades/pane.tsx +51 -2
- package/src/plugins/builtin/connections/index.ts +3 -0
- package/src/plugins/builtin/dividend-yield/client.ts +18 -0
- package/src/plugins/builtin/dividend-yield/pane.tsx +7 -6
- package/src/plugins/builtin/earnings/index.tsx +5 -3
- package/src/plugins/builtin/earnings/model.test.ts +19 -1
- package/src/plugins/builtin/earnings/model.ts +17 -15
- package/src/plugins/builtin/econ/calendar-model.ts +0 -5
- package/src/plugins/builtin/econ/index.test.tsx +6 -1
- package/src/plugins/builtin/fear-greed/charts.tsx +12 -8
- package/src/plugins/builtin/help/index.tsx +2 -2
- package/src/plugins/builtin/holders/thirteenf-match.ts +1 -2
- package/src/plugins/builtin/insider/index.tsx +27 -11
- package/src/plugins/builtin/kelly-sizer/index.tsx +4 -1
- package/src/plugins/builtin/layout-manager/index.tsx +17 -4
- package/src/plugins/builtin/news/index.tsx +11 -6
- package/src/plugins/builtin/news/wire/breaking/notifications.test.ts +2 -0
- package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +2 -0
- package/src/plugins/builtin/news/wire/breaking/pane.tsx +4 -1
- package/src/plugins/builtin/news/wire/feed-config.test.ts +15 -1
- package/src/plugins/builtin/news/wire/feed-config.ts +25 -5
- package/src/plugins/builtin/news/wire/index.tsx +3 -3
- package/src/plugins/builtin/news/wire/industry-pane.tsx +7 -2
- package/src/plugins/builtin/news/wire/news/footer.ts +34 -1
- package/src/plugins/builtin/news/wire/news/preset-pane.tsx +4 -1
- package/src/plugins/builtin/news/wire/news/table.tsx +8 -2
- package/src/plugins/builtin/news/wire/rss/source.test.ts +12 -0
- package/src/plugins/builtin/news/wire/rss/source.ts +1 -1
- package/src/plugins/builtin/notes/files.ts +80 -0
- package/src/plugins/builtin/notes/index.tsx +3 -0
- package/src/plugins/builtin/notes/sync.test.ts +49 -0
- package/src/plugins/builtin/notes/sync.ts +98 -0
- package/src/plugins/builtin/options/index.tsx +1 -0
- package/src/plugins/builtin/options/view.test.tsx +25 -3
- package/src/plugins/builtin/options/view.tsx +1 -0
- package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +22 -0
- package/src/plugins/builtin/portfolio-list/account-metrics.ts +12 -7
- package/src/plugins/builtin/portfolio-list/cli/render.ts +5 -3
- package/src/plugins/builtin/portfolio-list/column-values.ts +15 -4
- package/src/plugins/builtin/portfolio-list/header.tsx +13 -3
- package/src/plugins/builtin/portfolio-list/index.tsx +8 -0
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +31 -1
- package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +1 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +10 -7
- package/src/plugins/builtin/portfolio-list/pane/streaming.ts +6 -1
- package/src/plugins/builtin/portfolio-list/position-metrics.ts +25 -6
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +35 -21
- package/src/plugins/builtin/portfolio-list/summary/totals.ts +20 -6
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +5 -12
- package/src/plugins/builtin/research/equity-diagnostic-pane.test.tsx +329 -0
- package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +582 -0
- package/src/plugins/builtin/research/index.test.ts +14 -0
- package/src/plugins/builtin/research/index.tsx +29 -0
- package/src/plugins/builtin/sec/index.tsx +24 -9
- package/src/plugins/builtin/sectors/index.tsx +14 -0
- package/src/plugins/builtin/shared/cloud-upgrade.ts +18 -11
- package/src/plugins/builtin/shared/public-pane.test.ts +12 -0
- package/src/plugins/builtin/shared/public-pane.ts +9 -0
- package/src/plugins/builtin/shared/public-share.ts +23 -0
- package/src/plugins/builtin/shared/ticker-source.ts +32 -0
- package/src/plugins/builtin/shared/ticker-surface.test.ts +27 -0
- package/src/plugins/builtin/shared/ticker-surface.ts +17 -0
- package/src/plugins/builtin/substack/api/cache.ts +7 -3
- package/src/plugins/builtin/substack/api/loaders.ts +36 -12
- package/src/plugins/builtin/substack/api/types.ts +2 -0
- package/src/plugins/builtin/substack/pane-state.ts +1 -1
- package/src/plugins/builtin/substack/pane.tsx +55 -12
- package/src/plugins/builtin/thirteenf/api.test.ts +1 -1
- package/src/plugins/builtin/thirteenf/api.ts +40 -18
- package/src/plugins/builtin/thirteenf/data.ts +15 -7
- package/src/plugins/builtin/thirteenf/pane.tsx +45 -10
- package/src/plugins/builtin/ticker-detail/index.tsx +10 -0
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +12 -4
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +8 -2
- package/src/plugins/builtin/ticker-detail/settings.ts +12 -0
- package/src/plugins/builtin/treasury-auctions/index.tsx +2 -0
- package/src/plugins/builtin/world-indices/index.tsx +2 -0
- package/src/plugins/catalog-browser.test.ts +78 -0
- package/src/plugins/catalog-browser.ts +136 -0
- package/src/plugins/gridlock-notification.ts +2 -1
- package/src/plugins/ibkr/flex/index.test.ts +19 -0
- package/src/plugins/ibkr/flex/index.ts +1 -1
- package/src/plugins/pane-manager/gridlock.ts +87 -3
- package/src/plugins/pane-manager.test.ts +30 -0
- package/src/plugins/pane-manager.ts +6 -1
- package/src/plugins/prediction-markets/chart.tsx +2 -2
- package/src/plugins/prediction-markets/controller/catalog.ts +77 -2
- package/src/plugins/prediction-markets/controller/data.ts +7 -1
- package/src/plugins/prediction-markets/controller/index.ts +9 -2
- package/src/plugins/prediction-markets/controller/keyboard.ts +10 -14
- package/src/plugins/prediction-markets/detail/overview.tsx +21 -0
- package/src/plugins/prediction-markets/detail/pane.tsx +43 -34
- package/src/plugins/prediction-markets/detail/trades.tsx +34 -9
- package/src/plugins/prediction-markets/detail.test.tsx +29 -2
- package/src/plugins/prediction-markets/index.tsx +8 -4
- package/src/plugins/prediction-markets/navigation.ts +0 -1
- package/src/plugins/prediction-markets/pane.test.tsx +77 -0
- package/src/plugins/prediction-markets/pane.tsx +12 -0
- package/src/plugins/prediction-markets/plugin.test.tsx +36 -0
- package/src/plugins/prediction-markets/services/kalshi/adapter.ts +49 -9
- package/src/plugins/prediction-markets/services/polymarket/adapter.ts +31 -0
- package/src/plugins/prediction-markets/test-helpers.tsx +9 -0
- package/src/plugins/registry/index.ts +2 -0
- package/src/plugins/ticker-navigation.test.ts +24 -0
- package/src/plugins/ticker-navigation.ts +40 -0
- package/src/remote/schema.ts +1 -1
- package/src/renderers/browser/app-services.ts +61 -0
- package/src/renderers/browser/cloud-transport.test.ts +68 -0
- package/src/renderers/browser/cloud-transport.ts +48 -0
- package/src/renderers/browser/config-host.ts +58 -0
- package/src/renderers/browser/deeplink-bridge.test.ts +41 -0
- package/src/renderers/browser/deeplink-bridge.ts +22 -0
- package/src/renderers/browser/error-boundary.tsx +23 -0
- package/src/renderers/browser/main.tsx +64 -0
- package/src/renderers/browser/persistence.ts +74 -0
- package/src/renderers/browser/storage.test.ts +66 -0
- package/src/renderers/browser/storage.ts +50 -0
- package/src/renderers/browser/ticker-repository.ts +59 -0
- package/src/renderers/browser/ui-host.tsx +40 -0
- package/src/renderers/cloudflare/worker.test.ts +136 -0
- package/src/renderers/cloudflare/worker.ts +116 -0
- package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
- package/src/renderers/electrobun/bun/desktop/initialization.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/plugin-state.ts +7 -1
- package/src/renderers/electrobun/view/app-services.ts +14 -3
- package/src/renderers/electrobun/view/build-assets.ts +12 -10
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
- package/src/renderers/electrobun/view/data-table/index.tsx +10 -1
- package/src/renderers/electrobun/view/desktop/controls.tsx +2 -0
- package/src/renderers/electrobun/view/dom-ui-host.tsx +143 -0
- package/src/renderers/electrobun/view/host/box.test.tsx +33 -0
- package/src/renderers/electrobun/view/host/box.tsx +11 -6
- package/src/renderers/electrobun/view/host/chart-surface.tsx +7 -1
- package/src/renderers/electrobun/view/host/input.tsx +11 -7
- package/src/renderers/electrobun/view/host/native.ts +0 -5
- package/src/renderers/electrobun/view/main.tsx +2 -0
- package/src/renderers/electrobun/view/remote/persistence.ts +5 -0
- package/src/renderers/electrobun/view/styles.css +107 -7
- package/src/renderers/electrobun/view/toast-host.test.tsx +78 -0
- package/src/renderers/electrobun/view/toast-host.tsx +69 -43
- package/src/renderers/electrobun/view/ui-host.tsx +6 -137
- package/src/renderers/opentui/start.tsx +2 -0
- package/src/renderers/share/layout-view.test.tsx +58 -0
- package/src/renderers/share/layout-view.tsx +133 -0
- package/src/renderers/share/main.tsx +102 -0
- package/src/renderers/share/styles.css +54 -0
- package/src/renderers/share/view.test.tsx +100 -0
- package/src/renderers/share/view.tsx +150 -0
- package/src/shares/api.test.ts +105 -0
- package/src/shares/api.ts +127 -0
- package/src/shares/chart-snapshot.test.ts +34 -0
- package/src/shares/chart-snapshot.ts +28 -0
- package/src/shares/location.test.ts +19 -0
- package/src/shares/location.ts +11 -0
- package/src/shares/pane.test.ts +110 -0
- package/src/shares/pane.ts +54 -0
- package/src/shares/payload.ts +177 -0
- package/src/sources/gloomberb-cloud/index.test.ts +61 -1
- package/src/sources/gloomberb-cloud/index.ts +86 -14
- package/src/sources/gloomberb-cloud/normalizers.test.ts +34 -0
- package/src/sources/gloomberb-cloud/normalizers.ts +5 -5
- package/src/sources/provider-router/history.test.ts +2 -2
- package/src/sources/provider-router/news.test.ts +34 -0
- package/src/sources/provider-router/news.ts +15 -1
- package/src/sources/sec-edgar.test.ts +59 -0
- package/src/sources/sec-edgar.ts +63 -23
- package/src/sources/yahoo-finance/mappers.ts +3 -0
- package/src/sources/yahoo-finance/snapshots.ts +10 -2
- package/src/sources/yahoo-finance/symbols.test.ts +10 -0
- package/src/sources/yahoo-finance/symbols.ts +13 -8
- package/src/sources/yahoo-finance.test.ts +1 -1
- package/src/sources/yahoo-finance.ts +6 -2
- package/src/state/app/bootstrap.ts +6 -7
- package/src/state/app/context/index.test.ts +41 -1
- package/src/state/app/context/index.test.tsx +34 -16
- package/src/state/selectors-ui.ts +0 -8
- package/src/sync/baseline.ts +32 -0
- package/src/sync/controller.test.ts +124 -1
- package/src/sync/controller.ts +54 -12
- package/src/sync/core-contributors.test.ts +44 -0
- package/src/sync/core-contributors.ts +59 -10
- package/src/sync/react.ts +22 -2
- package/src/sync/types.ts +12 -0
- package/src/theme/font-scale.ts +0 -11
- package/src/tickers/search/index.test.ts +27 -0
- package/src/tickers/search/upsert.ts +10 -0
- package/src/types/capability-route-source.ts +2 -1
- package/src/types/config.ts +35 -5
- package/src/types/desktop-menu.ts +1 -0
- package/src/types/plugin.ts +36 -0
- package/src/ui/context-menu.tsx +1 -1
- package/src/ui/host.tsx +4 -0
- package/src/ui/toast.tsx +2 -0
- package/src/utils/back-navigation.ts +0 -4
- package/src/utils/price-history.ts +11 -10
- package/src/utils/sec.test.ts +6 -0
- package/src/utils/sec.ts +1 -1
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/** @jsxImportSource react */
|
|
2
|
+
import type { CSSProperties } from "react";
|
|
3
|
+
import type { DockLayoutNode, PaneInstanceConfig } from "../../types/config";
|
|
4
|
+
import type { LayoutMarketplaceEntry } from "../../layout-marketplace/payload";
|
|
5
|
+
|
|
6
|
+
const PANE_NAMES: Record<string, string> = {
|
|
7
|
+
"chart-composer": "Chart",
|
|
8
|
+
"credit-conditions": "Credit Spreads",
|
|
9
|
+
"earnings-calendar": "Earnings Calendar",
|
|
10
|
+
"fx-matrix": "FX Cross Rates",
|
|
11
|
+
"kelly-sizer": "Position Sizer",
|
|
12
|
+
"market-halts": "Market Halts",
|
|
13
|
+
"market-heatmap": "Market Heatmap",
|
|
14
|
+
"news-breaking": "Breaking News",
|
|
15
|
+
"news-feed": "News Feed",
|
|
16
|
+
"news-industry": "Sector News",
|
|
17
|
+
"news-top": "Top News",
|
|
18
|
+
"options-calculator": "Options Calculator",
|
|
19
|
+
"portfolio-list": "Portfolio",
|
|
20
|
+
"prediction-markets": "Prediction Markets",
|
|
21
|
+
"quick-notes": "Notes",
|
|
22
|
+
sec: "SEC",
|
|
23
|
+
"thirteenf-funds": "13F Funds",
|
|
24
|
+
"ticker-news": "Ticker News",
|
|
25
|
+
"ticker-research": "Ticker Research",
|
|
26
|
+
"twitter-feed": "X Feed",
|
|
27
|
+
"volatility-term-structure": "VIX Term Structure",
|
|
28
|
+
"world-indices": "World Indices",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function marketplacePaneName(paneId: string): string {
|
|
32
|
+
return PANE_NAMES[paneId] ?? paneId
|
|
33
|
+
.split("-")
|
|
34
|
+
.filter(Boolean)
|
|
35
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
36
|
+
.join(" ");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function paneSubtitle(pane: PaneInstanceConfig): string | null {
|
|
40
|
+
if (pane.binding?.kind === "fixed") return pane.binding.symbol;
|
|
41
|
+
const query = pane.params?.query;
|
|
42
|
+
return query?.trim() ? query.trim().slice(0, 80) : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function PaneCard({ pane }: { pane: PaneInstanceConfig | undefined }) {
|
|
46
|
+
if (!pane) return <div className="layout-pane missing">Unavailable pane</div>;
|
|
47
|
+
const subtitle = paneSubtitle(pane);
|
|
48
|
+
return (
|
|
49
|
+
<div className="layout-pane">
|
|
50
|
+
<strong>{pane.title?.trim() || marketplacePaneName(pane.paneId)}</strong>
|
|
51
|
+
{subtitle ? <span>{subtitle}</span> : null}
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function DockPreview({
|
|
57
|
+
node,
|
|
58
|
+
panes,
|
|
59
|
+
}: {
|
|
60
|
+
node: DockLayoutNode;
|
|
61
|
+
panes: ReadonlyMap<string, PaneInstanceConfig>;
|
|
62
|
+
}) {
|
|
63
|
+
if (node.kind === "pane") return <PaneCard pane={panes.get(node.instanceId)} />;
|
|
64
|
+
const firstStyle = { "--share-ratio": node.ratio } as CSSProperties;
|
|
65
|
+
const secondStyle = { "--share-ratio": 1 - node.ratio } as CSSProperties;
|
|
66
|
+
return (
|
|
67
|
+
<div className={`layout-split ${node.axis}`}>
|
|
68
|
+
<div className="layout-split-child" style={firstStyle}>
|
|
69
|
+
<DockPreview node={node.first} panes={panes} />
|
|
70
|
+
</div>
|
|
71
|
+
<div className="layout-split-child" style={secondStyle}>
|
|
72
|
+
<DockPreview node={node.second} panes={panes} />
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function formatDate(value: string): string {
|
|
79
|
+
const date = new Date(value);
|
|
80
|
+
return Number.isNaN(date.getTime())
|
|
81
|
+
? ""
|
|
82
|
+
: date.toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function LayoutShareView({
|
|
86
|
+
entry,
|
|
87
|
+
openLiveUrl,
|
|
88
|
+
}: {
|
|
89
|
+
entry: LayoutMarketplaceEntry;
|
|
90
|
+
openLiveUrl: string;
|
|
91
|
+
}) {
|
|
92
|
+
const panes = new Map(entry.layout.instances.map((pane) => [pane.instanceId, pane]));
|
|
93
|
+
const docked = new Set<string>();
|
|
94
|
+
const collect = (node: DockLayoutNode | null) => {
|
|
95
|
+
if (!node) return;
|
|
96
|
+
if (node.kind === "pane") {
|
|
97
|
+
docked.add(node.instanceId);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
collect(node.first);
|
|
101
|
+
collect(node.second);
|
|
102
|
+
};
|
|
103
|
+
collect(entry.layout.dockRoot);
|
|
104
|
+
const extra = entry.layout.instances.filter((pane) => !docked.has(pane.instanceId));
|
|
105
|
+
const author = entry.author.username ? `@${entry.author.username}` : entry.author.displayName;
|
|
106
|
+
const date = formatDate(entry.publishedAt);
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<main className="layout-share wide">
|
|
110
|
+
<header>
|
|
111
|
+
<p className="eyebrow">Shared via Gloomberb</p>
|
|
112
|
+
<h1>{entry.name}</h1>
|
|
113
|
+
<p className="layout-meta">{[author, date, `${entry.layout.instances.length} panes`].filter(Boolean).join(" · ")}</p>
|
|
114
|
+
</header>
|
|
115
|
+
|
|
116
|
+
<section className="layout-workspace" aria-label={`${entry.name} layout preview`}>
|
|
117
|
+
{entry.layout.dockRoot
|
|
118
|
+
? <DockPreview node={entry.layout.dockRoot} panes={panes} />
|
|
119
|
+
: <p>No docked panes</p>}
|
|
120
|
+
{extra.length > 0 ? (
|
|
121
|
+
<div className="layout-extra-panes">
|
|
122
|
+
{extra.map((pane) => <PaneCard key={pane.instanceId} pane={pane} />)}
|
|
123
|
+
</div>
|
|
124
|
+
) : null}
|
|
125
|
+
</section>
|
|
126
|
+
|
|
127
|
+
<div className="handoff">
|
|
128
|
+
<a className="cta" href={openLiveUrl}>Use this layout</a>
|
|
129
|
+
<p className="descriptor">Opens in Gloomberb as an independent, editable copy.</p>
|
|
130
|
+
</div>
|
|
131
|
+
</main>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/** @jsxImportSource react */
|
|
2
|
+
import "./styles.css";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import {
|
|
6
|
+
getPublicMarketplaceLayout,
|
|
7
|
+
openLiveMarketplaceLayoutUrl,
|
|
8
|
+
parseMarketplaceLayoutId,
|
|
9
|
+
} from "../../layout-marketplace/api";
|
|
10
|
+
import type { LayoutMarketplaceEntry } from "../../layout-marketplace/payload";
|
|
11
|
+
import {
|
|
12
|
+
deleteShare,
|
|
13
|
+
getShare,
|
|
14
|
+
openLiveShareUrl,
|
|
15
|
+
parseShareId,
|
|
16
|
+
type ShareRecord,
|
|
17
|
+
} from "../../shares/api";
|
|
18
|
+
import { LayoutShareView } from "./layout-view";
|
|
19
|
+
import { ShareView } from "./view";
|
|
20
|
+
|
|
21
|
+
function LayoutApp({ id }: { id: string }) {
|
|
22
|
+
const [state, setState] = useState<{
|
|
23
|
+
entry?: LayoutMarketplaceEntry;
|
|
24
|
+
error?: string;
|
|
25
|
+
}>({});
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const controller = new AbortController();
|
|
28
|
+
getPublicMarketplaceLayout(
|
|
29
|
+
id,
|
|
30
|
+
(url, init) => fetch(url, { ...init, signal: controller.signal }),
|
|
31
|
+
)
|
|
32
|
+
.then((entry) => setState(entry
|
|
33
|
+
? { entry }
|
|
34
|
+
: { error: "This shared layout is unavailable." }))
|
|
35
|
+
.catch(() => {
|
|
36
|
+
if (!controller.signal.aborted) setState({ error: "This shared layout could not be loaded." });
|
|
37
|
+
});
|
|
38
|
+
return () => controller.abort();
|
|
39
|
+
}, [id]);
|
|
40
|
+
|
|
41
|
+
return state.entry
|
|
42
|
+
? <LayoutShareView entry={state.entry} openLiveUrl={openLiveMarketplaceLayoutUrl(id)} />
|
|
43
|
+
: <main><h1>Gloomberb</h1><p>{state.error ?? "Loading shared layout..."}</p></main>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function ContentShareApp({ id }: { id: string }) {
|
|
47
|
+
const [state, setState] = useState<{
|
|
48
|
+
share?: ShareRecord;
|
|
49
|
+
error?: string;
|
|
50
|
+
deleting?: boolean;
|
|
51
|
+
deleted?: boolean;
|
|
52
|
+
}>({});
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
const controller = new AbortController();
|
|
55
|
+
getShare(id, (url, init) => fetch(url, { ...init, signal: controller.signal }))
|
|
56
|
+
.then((share) => setState(share ? { share } : { error: "This share is unavailable or has expired." }))
|
|
57
|
+
.catch(() => { if (!controller.signal.aborted) setState({ error: "This share could not be loaded." }); });
|
|
58
|
+
return () => controller.abort();
|
|
59
|
+
}, [id]);
|
|
60
|
+
const remove = async () => {
|
|
61
|
+
if (!state.share?.ownedByViewer || state.deleting) return;
|
|
62
|
+
setState((current) => ({ ...current, deleting: true, error: undefined }));
|
|
63
|
+
try {
|
|
64
|
+
await deleteShare(id);
|
|
65
|
+
setState({ deleted: true });
|
|
66
|
+
} catch (error) {
|
|
67
|
+
setState((current) => ({
|
|
68
|
+
...current,
|
|
69
|
+
deleting: false,
|
|
70
|
+
error: error instanceof Error ? error.message : "Could not delete share.",
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
if (state.deleted) return <main><h1>Share deleted</h1><p>This link is no longer available.</p></main>;
|
|
75
|
+
if (state.share) {
|
|
76
|
+
return (
|
|
77
|
+
<ShareView
|
|
78
|
+
share={state.share}
|
|
79
|
+
openLiveUrl={openLiveShareUrl(id)}
|
|
80
|
+
deleting={state.deleting === true}
|
|
81
|
+
deleteError={state.error}
|
|
82
|
+
onDelete={state.share.ownedByViewer ? remove : undefined}
|
|
83
|
+
/>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return <main><h1>Gloomberb</h1><p>{state.error ?? "Loading shared view..."}</p></main>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function SocialShareApp() {
|
|
90
|
+
const pathname = window.location.pathname;
|
|
91
|
+
const layoutId = parseMarketplaceLayoutId(pathname);
|
|
92
|
+
if (layoutId) return <LayoutApp id={layoutId} />;
|
|
93
|
+
if (pathname.startsWith("/l/")) return <main><h1>Gloomberb</h1><p>Invalid layout link.</p></main>;
|
|
94
|
+
const shareId = parseShareId(pathname);
|
|
95
|
+
return shareId
|
|
96
|
+
? <ContentShareApp id={shareId} />
|
|
97
|
+
: <main><h1>Gloomberb</h1><p>Invalid share link.</p></main>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const root = document.getElementById("root");
|
|
101
|
+
if (!root) throw new Error("Missing root element");
|
|
102
|
+
createRoot(root).render(<SocialShareApp />);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
html { color-scheme: dark; background: #272a34; color: #f2f2f2; font: 14px/1.55 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
2
|
+
body { margin: 0; }
|
|
3
|
+
main { width: min(760px, calc(100% - 40px)); margin: 0 auto; padding: 64px 0; }
|
|
4
|
+
main.wide { width: min(1180px, calc(100% - 40px)); }
|
|
5
|
+
h1 { margin: 0 0 28px; color: #f1b94a; font-size: 24px; }
|
|
6
|
+
a { display: inline-block; margin-top: 24px; color: #f1b94a; }
|
|
7
|
+
.article-text { white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
8
|
+
.table-wrap { overflow: auto; border: 1px solid #4a4e5c; }
|
|
9
|
+
table { width: 100%; border-collapse: collapse; }
|
|
10
|
+
th, td { padding: 8px 10px; border-bottom: 1px solid #3a3e49; text-align: left; white-space: nowrap; }
|
|
11
|
+
th { color: #f1b94a; background: #30333d; }
|
|
12
|
+
.chart { width: 100%; height: auto; border: 1px solid #4a4e5c; background: #20232b; }
|
|
13
|
+
.series { fill: none; stroke: #f1b94a; stroke-width: 2; vector-effect: non-scaling-stroke; }
|
|
14
|
+
.series-1 { stroke: #62b8f6; } .series-2 { stroke: #7fd18a; } .series-3 { stroke: #ef7f7f; } .series-4 { stroke: #b697ef; } .series-5 { stroke: #e29b63; }
|
|
15
|
+
.legend { display: flex; flex-wrap: wrap; gap: 10px 24px; padding: 0; list-style: none; color: #b8bbc5; }
|
|
16
|
+
.owner-actions { display: flex; align-items: center; gap: 12px; margin-top: 32px; }
|
|
17
|
+
.owner-actions button { border: 1px solid #6d7180; border-radius: 4px; padding: 6px 10px; background: transparent; color: #f2f2f2; font: inherit; cursor: pointer; }
|
|
18
|
+
.owner-actions button:disabled { cursor: default; opacity: 0.55; }
|
|
19
|
+
.owner-actions [role="alert"] { color: #ef7f7f; }
|
|
20
|
+
:focus-visible { outline: 2px solid #f1b94a; outline-offset: 2px; }
|
|
21
|
+
.eyebrow { margin: 0 0 10px; color: #b8bbc5; font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; }
|
|
22
|
+
main.pane h1 { margin-bottom: 14px; overflow-wrap: anywhere; }
|
|
23
|
+
.pane-description { margin: 0; max-width: 62ch; color: #d5d7de; overflow-wrap: anywhere; }
|
|
24
|
+
.pane-facts { margin: 28px 0 0; border: 1px solid #4a4e5c; background: #20232b; }
|
|
25
|
+
.pane-facts > div { display: flex; gap: 16px; padding: 8px 12px; border-bottom: 1px solid #3a3e49; }
|
|
26
|
+
.pane-facts > div:last-child { border-bottom: none; }
|
|
27
|
+
.pane-facts dt { flex: 0 0 clamp(110px, 26%, 200px); color: #b8bbc5; }
|
|
28
|
+
.pane-facts dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
|
|
29
|
+
.handoff { margin-top: 32px; }
|
|
30
|
+
.cta { margin: 0; border: 1px solid #f1b94a; border-radius: 4px; padding: 10px 16px; color: #f1b94a; text-decoration: none; }
|
|
31
|
+
.cta:hover { background: #f1b94a; color: #20232b; }
|
|
32
|
+
.descriptor { margin: 14px 0 0; max-width: 58ch; color: #b8bbc5; }
|
|
33
|
+
.layout-share h1 { margin-bottom: 10px; overflow-wrap: anywhere; }
|
|
34
|
+
.layout-meta { margin: 0; color: #b8bbc5; }
|
|
35
|
+
.layout-workspace { position: relative; display: flex; height: 440px; margin-top: 28px; padding: 10px; border: 1px solid #4a4e5c; background: #181b22; overflow: hidden; }
|
|
36
|
+
.layout-split { display: flex; flex: 1 1 0; min-width: 0; min-height: 0; gap: 8px; }
|
|
37
|
+
.layout-split.horizontal { flex-direction: row; }
|
|
38
|
+
.layout-split.vertical { flex-direction: column; }
|
|
39
|
+
.layout-split-child { display: flex; flex-grow: var(--share-ratio); flex-basis: 0; min-width: 0; min-height: 0; }
|
|
40
|
+
.layout-pane { display: flex; flex: 1 1 0; min-width: 0; min-height: 42px; flex-direction: column; justify-content: center; gap: 4px; padding: 10px 12px; border: 1px solid #3a4759; background: #20232b; overflow: hidden; }
|
|
41
|
+
.layout-pane strong { color: #f1b94a; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
42
|
+
.layout-pane span { color: #b8bbc5; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
43
|
+
.layout-pane.missing { color: #b8bbc5; }
|
|
44
|
+
.layout-extra-panes { position: absolute; right: 20px; bottom: 20px; display: grid; width: min(360px, 42%); gap: 8px; padding: 8px; border: 1px solid #52657d; background: #181b22; }
|
|
45
|
+
@media (max-width: 640px) {
|
|
46
|
+
main { padding: 40px 0; }
|
|
47
|
+
.pane-facts > div { flex-direction: column; gap: 2px; }
|
|
48
|
+
.pane-facts dt { flex: none; }
|
|
49
|
+
.cta { display: block; text-align: center; }
|
|
50
|
+
.layout-workspace { height: 360px; padding: 6px; }
|
|
51
|
+
.layout-split { gap: 4px; }
|
|
52
|
+
.layout-pane { padding: 7px; font-size: 12px; }
|
|
53
|
+
.layout-extra-panes { right: 10px; bottom: 10px; width: 52%; }
|
|
54
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
3
|
+
import { ShareView } from "./view";
|
|
4
|
+
|
|
5
|
+
test("share rendering escapes content and protects external links", () => {
|
|
6
|
+
const html = renderToStaticMarkup(<ShareView share={{
|
|
7
|
+
kind: "article",
|
|
8
|
+
data: {
|
|
9
|
+
title: "<img src=x onerror=alert(1)>",
|
|
10
|
+
text: "<script>alert(1)</script>",
|
|
11
|
+
sourceUrl: "https://example.com/story",
|
|
12
|
+
},
|
|
13
|
+
createdAt: "2026-08-21T00:00:00Z",
|
|
14
|
+
expiresAt: "2026-09-20T00:00:00Z",
|
|
15
|
+
ownedByViewer: true,
|
|
16
|
+
}} onDelete={() => {}} />);
|
|
17
|
+
expect(html).not.toContain("<script>");
|
|
18
|
+
expect(html).toContain("<script>");
|
|
19
|
+
expect(html).toContain('target="_blank"');
|
|
20
|
+
expect(html).toContain('rel="noopener noreferrer"');
|
|
21
|
+
expect(html).toContain("Delete share");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("portable pane share renders sanitized pane configuration", () => {
|
|
25
|
+
const html = renderToStaticMarkup(<ShareView
|
|
26
|
+
share={{
|
|
27
|
+
kind: "pane",
|
|
28
|
+
data: {
|
|
29
|
+
version: 2,
|
|
30
|
+
title: "Fed Markets",
|
|
31
|
+
description: "Prediction markets filtered to the Fed.",
|
|
32
|
+
layout: {
|
|
33
|
+
schemaVersion: 2,
|
|
34
|
+
sourceConfigVersion: 13,
|
|
35
|
+
layout: {
|
|
36
|
+
dockRoot: null,
|
|
37
|
+
instances: [{
|
|
38
|
+
instanceId: "p1",
|
|
39
|
+
paneId: "prediction-markets",
|
|
40
|
+
binding: { kind: "none" },
|
|
41
|
+
params: { query: "fed", scope: "polymarket" },
|
|
42
|
+
}],
|
|
43
|
+
floating: [{ instanceId: "p1", x: 0, y: 0, width: 100, height: 30 }],
|
|
44
|
+
detached: [],
|
|
45
|
+
},
|
|
46
|
+
paneState: {},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
createdAt: "2026-08-25T00:00:00Z",
|
|
50
|
+
expiresAt: "2026-09-24T00:00:00Z",
|
|
51
|
+
ownedByViewer: false,
|
|
52
|
+
}}
|
|
53
|
+
openLiveUrl="https://api.gloom.sh/shares/abc/open"
|
|
54
|
+
/>);
|
|
55
|
+
expect(html).toContain("Fed Markets");
|
|
56
|
+
expect(html).toContain("Prediction Markets");
|
|
57
|
+
expect(html).toContain("Query");
|
|
58
|
+
expect(html).toContain("fed");
|
|
59
|
+
expect(html).toContain("polymarket");
|
|
60
|
+
expect(html).toContain("Explore this pane live");
|
|
61
|
+
expect(html).not.toContain("p1");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("pane share renders the handoff copy, tracked CTA and printable facts only", () => {
|
|
65
|
+
const html = renderToStaticMarkup(<ShareView
|
|
66
|
+
share={{
|
|
67
|
+
kind: "pane",
|
|
68
|
+
data: {
|
|
69
|
+
version: 1,
|
|
70
|
+
templateId: "watchlist",
|
|
71
|
+
title: "Momentum <script>alert(1)</script>",
|
|
72
|
+
description: "Movers I watch daily.",
|
|
73
|
+
data: {
|
|
74
|
+
symbols: ["AAPL", "MSFT"],
|
|
75
|
+
lookbackDays: 90,
|
|
76
|
+
blank: " ",
|
|
77
|
+
nestedConfig: { hidden: true },
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
createdAt: "2026-08-25T00:00:00Z",
|
|
81
|
+
expiresAt: "2026-09-24T00:00:00Z",
|
|
82
|
+
ownedByViewer: true,
|
|
83
|
+
}}
|
|
84
|
+
openLiveUrl="https://api.gloom.sh/shares/abc/open"
|
|
85
|
+
onDelete={() => {}}
|
|
86
|
+
/>);
|
|
87
|
+
expect(html).toContain("Shared via Gloomberb");
|
|
88
|
+
expect(html).toContain("A free, open-source finance terminal for market data, charts, and research.");
|
|
89
|
+
expect(html).toContain('href="https://api.gloom.sh/shares/abc/open"');
|
|
90
|
+
expect(html).toContain("Explore this pane live");
|
|
91
|
+
expect(html).toContain("Movers I watch daily.");
|
|
92
|
+
expect(html).toContain("Symbols");
|
|
93
|
+
expect(html).toContain("AAPL, MSFT");
|
|
94
|
+
expect(html).toContain("Lookback Days");
|
|
95
|
+
expect(html).not.toContain("[object Object]");
|
|
96
|
+
expect(html).not.toContain("Nested Config");
|
|
97
|
+
expect(html).not.toContain("Blank");
|
|
98
|
+
expect(html).not.toContain("<script>");
|
|
99
|
+
expect(html).toContain("Delete share");
|
|
100
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/** @jsxImportSource react */
|
|
2
|
+
import type { ShareRecord } from "../../shares/api";
|
|
3
|
+
|
|
4
|
+
const FACT_LIMIT = 8;
|
|
5
|
+
const FACT_MAX_LENGTH = 120;
|
|
6
|
+
|
|
7
|
+
function factValue(value: unknown): string | null {
|
|
8
|
+
if (typeof value === "string") return value.trim() ? value.trim().slice(0, FACT_MAX_LENGTH) : null;
|
|
9
|
+
if (typeof value === "number") return Number.isFinite(value) ? String(value) : null;
|
|
10
|
+
if (typeof value === "boolean") return value ? "Yes" : "No";
|
|
11
|
+
if (Array.isArray(value)) {
|
|
12
|
+
const items = value.filter((item) => typeof item === "string" || typeof item === "number");
|
|
13
|
+
return items.length ? items.join(", ").slice(0, FACT_MAX_LENGTH) : null;
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function factLabel(key: string): string {
|
|
19
|
+
const spaced = key.replace(/[_-]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").trim();
|
|
20
|
+
return spaced ? spaced.charAt(0).toUpperCase() + spaced.slice(1) : key;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function paneFacts(data: Record<string, unknown>): Array<{ key: string; label: string; value: string }> {
|
|
24
|
+
const facts: Array<{ key: string; label: string; value: string }> = [];
|
|
25
|
+
for (const [key, raw] of Object.entries(data)) {
|
|
26
|
+
const value = factValue(raw);
|
|
27
|
+
if (value !== null) facts.push({ key, label: factLabel(key), value });
|
|
28
|
+
if (facts.length === FACT_LIMIT) break;
|
|
29
|
+
}
|
|
30
|
+
return facts;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function SourceLink({ url }: { url?: string }) {
|
|
34
|
+
return url ? <a href={url} target="_blank" rel="noopener noreferrer">View source</a> : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function OwnerActions({
|
|
38
|
+
deleting,
|
|
39
|
+
error,
|
|
40
|
+
onDelete,
|
|
41
|
+
}: {
|
|
42
|
+
deleting: boolean;
|
|
43
|
+
error?: string;
|
|
44
|
+
onDelete?: () => void;
|
|
45
|
+
}) {
|
|
46
|
+
if (!onDelete) return null;
|
|
47
|
+
return (
|
|
48
|
+
<div className="owner-actions">
|
|
49
|
+
<button type="button" disabled={deleting} onClick={onDelete}>
|
|
50
|
+
{deleting ? "Deleting..." : "Delete share"}
|
|
51
|
+
</button>
|
|
52
|
+
{error ? <span role="alert">{error}</span> : null}
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function ShareView({
|
|
58
|
+
share,
|
|
59
|
+
openLiveUrl,
|
|
60
|
+
deleting = false,
|
|
61
|
+
deleteError,
|
|
62
|
+
onDelete,
|
|
63
|
+
}: {
|
|
64
|
+
share: ShareRecord;
|
|
65
|
+
/** Platform-tracked `/shares/:id/open` URL. */
|
|
66
|
+
openLiveUrl?: string;
|
|
67
|
+
deleting?: boolean;
|
|
68
|
+
deleteError?: string;
|
|
69
|
+
onDelete?: () => void;
|
|
70
|
+
}) {
|
|
71
|
+
const ownerActions = (
|
|
72
|
+
<OwnerActions deleting={deleting} error={deleteError} onDelete={onDelete} />
|
|
73
|
+
);
|
|
74
|
+
if (share.kind === "pane") {
|
|
75
|
+
const instance = share.data.version === 2
|
|
76
|
+
? share.data.layout.layout.instances[0]
|
|
77
|
+
: null;
|
|
78
|
+
const facts = share.data.version === 1
|
|
79
|
+
? paneFacts(share.data.data)
|
|
80
|
+
: paneFacts({
|
|
81
|
+
paneType: instance
|
|
82
|
+
? factLabel(instance.paneId).replace(/\b\w/g, (character) => character.toUpperCase())
|
|
83
|
+
: undefined,
|
|
84
|
+
...(instance?.binding?.kind === "fixed" ? { symbol: instance.binding.symbol } : {}),
|
|
85
|
+
...(instance?.params ?? {}),
|
|
86
|
+
...(instance?.settings ?? {}),
|
|
87
|
+
});
|
|
88
|
+
return (
|
|
89
|
+
<main className="pane">
|
|
90
|
+
<header>
|
|
91
|
+
<p className="eyebrow">Shared via Gloomberb</p>
|
|
92
|
+
<h1>{share.data.title}</h1>
|
|
93
|
+
{share.data.description ? <p className="pane-description">{share.data.description}</p> : null}
|
|
94
|
+
</header>
|
|
95
|
+
{facts.length ? (
|
|
96
|
+
<dl className="pane-facts">
|
|
97
|
+
{facts.map((fact) => (
|
|
98
|
+
<div key={fact.key}>
|
|
99
|
+
<dt>{fact.label}</dt>
|
|
100
|
+
<dd>{fact.value}</dd>
|
|
101
|
+
</div>
|
|
102
|
+
))}
|
|
103
|
+
</dl>
|
|
104
|
+
) : null}
|
|
105
|
+
<div className="handoff">
|
|
106
|
+
{openLiveUrl ? <a className="cta" href={openLiveUrl}>Explore this pane live</a> : null}
|
|
107
|
+
<p className="descriptor">A free, open-source finance terminal for market data, charts, and research.</p>
|
|
108
|
+
</div>
|
|
109
|
+
{ownerActions}
|
|
110
|
+
</main>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
if (share.kind === "article") {
|
|
114
|
+
return <main><h1>{share.data.title}</h1><p className="article-text">{share.data.text}</p><SourceLink url={share.data.sourceUrl} />{ownerActions}</main>;
|
|
115
|
+
}
|
|
116
|
+
if (share.kind === "table") {
|
|
117
|
+
return (
|
|
118
|
+
<main className="wide">
|
|
119
|
+
<h1>{share.data.title}</h1>
|
|
120
|
+
<div className="table-wrap"><table><thead><tr>{share.data.columns.map((column) => <th key={column.key}>{column.label}</th>)}</tr></thead>
|
|
121
|
+
<tbody>{share.data.rows.map((row, index) => <tr key={index}>{share.data.columns.map((column) => <td key={column.key}>{String(row[column.key] ?? "")}</td>)}</tr>)}</tbody>
|
|
122
|
+
</table></div>
|
|
123
|
+
<SourceLink url={share.data.sourceUrl} />
|
|
124
|
+
{ownerActions}
|
|
125
|
+
</main>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
const values = share.data.series.flatMap((series) => series.points.map((point) => point.y));
|
|
129
|
+
const min = Math.min(...values);
|
|
130
|
+
const max = Math.max(...values);
|
|
131
|
+
const span = max - min || 1;
|
|
132
|
+
return (
|
|
133
|
+
<main className="wide">
|
|
134
|
+
<h1>{share.data.title}</h1>
|
|
135
|
+
<svg className="chart" viewBox="0 0 1000 420" role="img" aria-label={share.data.title}>
|
|
136
|
+
{share.data.series.map((series, seriesIndex) => {
|
|
137
|
+
const points = series.points.map((point, index) => {
|
|
138
|
+
const x = series.points.length <= 1 ? 500 : 20 + (index / (series.points.length - 1)) * 960;
|
|
139
|
+
const y = 400 - ((point.y - min) / span) * 380;
|
|
140
|
+
return `${x},${y}`;
|
|
141
|
+
}).join(" ");
|
|
142
|
+
return <polyline key={series.name} points={points} className={`series series-${seriesIndex % 6}`} />;
|
|
143
|
+
})}
|
|
144
|
+
</svg>
|
|
145
|
+
<ul className="legend">{share.data.series.map((series) => <li key={series.name}>{series.name}</li>)}</ul>
|
|
146
|
+
<SourceLink url={share.data.sourceUrl} />
|
|
147
|
+
{ownerActions}
|
|
148
|
+
</main>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { createShare, deleteShare, getShare, openLiveShareUrl, publicShareUrl, SHARE_API_ORIGIN } from "./api";
|
|
3
|
+
import { parseSharePayload } from "./payload";
|
|
4
|
+
|
|
5
|
+
const article = { kind: "article", data: { title: "AAPL", text: "Research", sourceUrl: "https://example.com/a" } } as const;
|
|
6
|
+
const portablePane = {
|
|
7
|
+
kind: "pane",
|
|
8
|
+
data: {
|
|
9
|
+
version: 2,
|
|
10
|
+
title: "AAPL Price",
|
|
11
|
+
layout: {
|
|
12
|
+
schemaVersion: 2,
|
|
13
|
+
sourceConfigVersion: 13,
|
|
14
|
+
layout: {
|
|
15
|
+
dockRoot: null,
|
|
16
|
+
instances: [{ instanceId: "p1", paneId: "graph-price", binding: { kind: "fixed", symbol: "AAPL" } }],
|
|
17
|
+
floating: [{ instanceId: "p1", x: 0, y: 0, width: 100, height: 30 }],
|
|
18
|
+
detached: [],
|
|
19
|
+
},
|
|
20
|
+
paneState: {},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
} as const;
|
|
24
|
+
const shareId = "0123456789abcdef0123456789abcdef";
|
|
25
|
+
|
|
26
|
+
describe("share API client", () => {
|
|
27
|
+
test("validates strict payloads and http(s)-only source URLs", () => {
|
|
28
|
+
expect(parseSharePayload(article)).toEqual(article);
|
|
29
|
+
expect(parseSharePayload({ ...article, data: { ...article.data, sourceUrl: "javascript:alert(1)" } })).toBeNull();
|
|
30
|
+
expect(parseSharePayload({ kind: "table", data: { title: "x", columns: [], rows: [] } })).toBeNull();
|
|
31
|
+
expect(parseSharePayload({
|
|
32
|
+
kind: "pane",
|
|
33
|
+
data: { version: 1, templateId: "graph-price-pane", title: "AAPL Price", data: { symbol: "AAPL" } },
|
|
34
|
+
})?.kind).toBe("pane");
|
|
35
|
+
expect(parseSharePayload({
|
|
36
|
+
kind: "pane",
|
|
37
|
+
data: { version: 1, templateId: "../private", title: "Private", data: {} },
|
|
38
|
+
})).toBeNull();
|
|
39
|
+
expect(parseSharePayload(portablePane)).toEqual(portablePane);
|
|
40
|
+
expect(parseSharePayload({
|
|
41
|
+
...portablePane,
|
|
42
|
+
data: {
|
|
43
|
+
...portablePane.data,
|
|
44
|
+
layout: {
|
|
45
|
+
...portablePane.data.layout,
|
|
46
|
+
layout: {
|
|
47
|
+
...portablePane.data.layout.layout,
|
|
48
|
+
instances: [],
|
|
49
|
+
floating: [],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
})).toBeNull();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("creates and deletes through api.gloom.sh with credentials", async () => {
|
|
57
|
+
const calls: Array<[string, RequestInit | undefined]> = [];
|
|
58
|
+
const fetchImpl = (async (url: string | URL | Request, init?: RequestInit) => {
|
|
59
|
+
calls.push([String(url), init]);
|
|
60
|
+
return init?.method === "DELETE"
|
|
61
|
+
? new Response(null, { status: 204 })
|
|
62
|
+
: Response.json({ id: shareId, expiresAt: "2026-09-20T00:00:00.000Z" });
|
|
63
|
+
}) as typeof fetch;
|
|
64
|
+
const created = await createShare(article, fetchImpl);
|
|
65
|
+
await deleteShare(created.id, fetchImpl);
|
|
66
|
+
expect(calls[0]).toEqual([`${SHARE_API_ORIGIN}/shares`, expect.objectContaining({ method: "POST", credentials: "include" })]);
|
|
67
|
+
expect(calls[1]).toEqual([`${SHARE_API_ORIGIN}/shares/${shareId}`, expect.objectContaining({ method: "DELETE", credentials: "include" })]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("explains when a Cloud account still needs email verification", async () => {
|
|
71
|
+
const fetchImpl = (async () => Response.json(
|
|
72
|
+
{ error: "Email verification required" },
|
|
73
|
+
{ status: 403 },
|
|
74
|
+
)) as typeof fetch;
|
|
75
|
+
await expect(createShare(article, fetchImpl)).rejects.toThrow(
|
|
76
|
+
"Verify your Gloom Cloud email to share.",
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("loads public shares with optional owner credentials and builds current-origin URLs", async () => {
|
|
81
|
+
let init: RequestInit | undefined;
|
|
82
|
+
const urls: string[] = [];
|
|
83
|
+
const fetchImpl = (async (url: string | URL | Request, options?: RequestInit) => {
|
|
84
|
+
urls.push(String(url));
|
|
85
|
+
init = options;
|
|
86
|
+
return Response.json({
|
|
87
|
+
...article,
|
|
88
|
+
createdAt: "2026-08-21T00:00:00Z",
|
|
89
|
+
expiresAt: "2026-09-20T00:00:00Z",
|
|
90
|
+
ownedByViewer: true,
|
|
91
|
+
});
|
|
92
|
+
}) as typeof fetch;
|
|
93
|
+
const share = await getShare(shareId, fetchImpl);
|
|
94
|
+
await getShare(shareId, fetchImpl, { trackView: false });
|
|
95
|
+
expect(share?.kind).toBe("article");
|
|
96
|
+
expect(share?.ownedByViewer).toBe(true);
|
|
97
|
+
expect(init?.credentials).toBe("include");
|
|
98
|
+
expect(urls).toEqual([
|
|
99
|
+
`${SHARE_API_ORIGIN}/shares/${shareId}`,
|
|
100
|
+
`${SHARE_API_ORIGIN}/shares/${shareId}?purpose=open`,
|
|
101
|
+
]);
|
|
102
|
+
expect(publicShareUrl(shareId)).toBe(`https://term.gloom.sh/s/${shareId}`);
|
|
103
|
+
expect(openLiveShareUrl(shareId)).toBe(`https://term.gloom.sh/api/shares/${shareId}/open`);
|
|
104
|
+
});
|
|
105
|
+
});
|