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
|
@@ -96,6 +96,47 @@ function ChannelNotificationIcon({
|
|
|
96
96
|
);
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
function ProfileIcon({
|
|
100
|
+
color,
|
|
101
|
+
onMouseDown,
|
|
102
|
+
}: {
|
|
103
|
+
color: string;
|
|
104
|
+
onMouseDown?: (event: any) => void;
|
|
105
|
+
}) {
|
|
106
|
+
const { nativePaneChrome } = useUiCapabilities();
|
|
107
|
+
|
|
108
|
+
if (!nativePaneChrome) {
|
|
109
|
+
return (
|
|
110
|
+
<Text fg={color} selectable={false} onMouseDown={onMouseDown}>@</Text>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<Span
|
|
116
|
+
fg={color}
|
|
117
|
+
onMouseDown={onMouseDown}
|
|
118
|
+
style={{
|
|
119
|
+
color,
|
|
120
|
+
display: "inline-flex",
|
|
121
|
+
alignItems: "center",
|
|
122
|
+
justifyContent: "center",
|
|
123
|
+
width: 16,
|
|
124
|
+
height: 16,
|
|
125
|
+
}}
|
|
126
|
+
>
|
|
127
|
+
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" aria-hidden="true">
|
|
128
|
+
<circle cx="12" cy="8.5" r="3.4" stroke="currentColor" strokeWidth="1.8" />
|
|
129
|
+
<path
|
|
130
|
+
d="M5.5 19.5a6.5 6.5 0 0 1 13 0"
|
|
131
|
+
stroke="currentColor"
|
|
132
|
+
strokeWidth="1.8"
|
|
133
|
+
strokeLinecap="round"
|
|
134
|
+
/>
|
|
135
|
+
</svg>
|
|
136
|
+
</Span>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
99
140
|
export function ChannelSidebar({
|
|
100
141
|
channels,
|
|
101
142
|
channelStates,
|
|
@@ -109,9 +150,11 @@ export function ChannelSidebar({
|
|
|
109
150
|
canManageNotifications,
|
|
110
151
|
canCreateConversation,
|
|
111
152
|
directExpanded,
|
|
153
|
+
needsProfileSetup = false,
|
|
112
154
|
onSelect,
|
|
113
155
|
onFocusRequest,
|
|
114
156
|
onCreateConversation,
|
|
157
|
+
onOpenProfile,
|
|
115
158
|
onToggleNotifications,
|
|
116
159
|
onToggleDirectExpanded,
|
|
117
160
|
}: {
|
|
@@ -127,9 +170,11 @@ export function ChannelSidebar({
|
|
|
127
170
|
canManageNotifications: boolean;
|
|
128
171
|
canCreateConversation: boolean;
|
|
129
172
|
directExpanded: boolean;
|
|
173
|
+
needsProfileSetup?: boolean;
|
|
130
174
|
onSelect?: (channelId: string) => void;
|
|
131
175
|
onFocusRequest?: () => void;
|
|
132
176
|
onCreateConversation?: () => void;
|
|
177
|
+
onOpenProfile?: () => void;
|
|
133
178
|
onToggleNotifications?: (channelId: string, enabled: boolean) => void;
|
|
134
179
|
onToggleDirectExpanded?: () => void;
|
|
135
180
|
}) {
|
|
@@ -240,6 +285,24 @@ export function ChannelSidebar({
|
|
|
240
285
|
);
|
|
241
286
|
})}
|
|
242
287
|
<Box flexGrow={1} />
|
|
288
|
+
{needsProfileSetup && (
|
|
289
|
+
<PaneSidebarRow
|
|
290
|
+
active={false}
|
|
291
|
+
ariaLabel={t("Profile")}
|
|
292
|
+
onSelect={onOpenProfile}
|
|
293
|
+
>
|
|
294
|
+
{({ foregroundColor, onMouseDown }) => (
|
|
295
|
+
<>
|
|
296
|
+
<Text fg={foregroundColor} selectable={false} onMouseDown={onMouseDown}> </Text>
|
|
297
|
+
<ProfileIcon color={foregroundColor} onMouseDown={onMouseDown} />
|
|
298
|
+
<Text fg={foregroundColor} selectable={false} onMouseDown={onMouseDown}>
|
|
299
|
+
{` ${truncateChannelLabel(t("Profile"), Math.max(listWidth - 3, 1))}`}
|
|
300
|
+
</Text>
|
|
301
|
+
<Box flexGrow={1} onMouseDown={onMouseDown} />
|
|
302
|
+
</>
|
|
303
|
+
)}
|
|
304
|
+
</PaneSidebarRow>
|
|
305
|
+
)}
|
|
243
306
|
{loading && focused && (
|
|
244
307
|
<Box height={1} width={listWidth} flexDirection="row">
|
|
245
308
|
<Text fg={colors.textDim}>{` ${t("syncing")}`}</Text>
|
|
@@ -6,7 +6,7 @@ import { MemoryPluginPersistence } from "../../../test-support/plugin-persistenc
|
|
|
6
6
|
import { createTestPluginRuntime } from "../../../test-support/plugin-runtime";
|
|
7
7
|
import { createDefaultConfig } from "../../../types/config";
|
|
8
8
|
import { Box } from "../../../ui";
|
|
9
|
-
import { apiClient, type ChatChannel, type ChatMessage } from "../../../api-client";
|
|
9
|
+
import { apiClient, type AccountProfile, type ChatChannel, type ChatMessage } from "../../../api-client";
|
|
10
10
|
import { PluginRenderProvider, type PluginRuntimeAccess } from "../../runtime";
|
|
11
11
|
import { setSharedMarketDataForTests, setSharedRegistryForTests } from "../../registry";
|
|
12
12
|
import { ChatContent } from "./content";
|
|
@@ -67,6 +67,34 @@ export function installServerChannels(controller: ChatController, channels = TES
|
|
|
67
67
|
|
|
68
68
|
export { MemoryPluginPersistence as MemoryPersistence } from "../../../test-support/plugin-persistence";
|
|
69
69
|
|
|
70
|
+
export function makeAccountProfile(overrides: Partial<AccountProfile> = {}): AccountProfile {
|
|
71
|
+
return {
|
|
72
|
+
id: "u1",
|
|
73
|
+
email: "vince@example.com",
|
|
74
|
+
emailVerified: true,
|
|
75
|
+
plan: "pro",
|
|
76
|
+
username: "vince",
|
|
77
|
+
name: "Vince",
|
|
78
|
+
company: "Gloom",
|
|
79
|
+
title: null,
|
|
80
|
+
bio: "Made Gloomberb",
|
|
81
|
+
profilePublic: true,
|
|
82
|
+
publicEmail: null,
|
|
83
|
+
xAccount: null,
|
|
84
|
+
sharedPortfolioId: "broker:ibkr:coldstart",
|
|
85
|
+
acceptUnknownDms: false,
|
|
86
|
+
chatEmailNotificationsEnabled: true,
|
|
87
|
+
portfolioAnalytics: null,
|
|
88
|
+
syncEnabled: true,
|
|
89
|
+
weeklyRoundupEnabled: true,
|
|
90
|
+
positionAlertsEnabled: true,
|
|
91
|
+
lastSyncAt: null,
|
|
92
|
+
lastRoundupEmailAt: null,
|
|
93
|
+
updatedAt: "2026-07-21T22:03:59.832Z",
|
|
94
|
+
...overrides,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
70
98
|
export function makeMessage(index: number): ChatMessage {
|
|
71
99
|
return {
|
|
72
100
|
id: `m${index}`,
|
|
@@ -4,15 +4,19 @@ import { Button } from "../../../components";
|
|
|
4
4
|
import { usePluginAppActions } from "../../runtime";
|
|
5
5
|
import { colors, hoverBg } from "../../../theme/colors";
|
|
6
6
|
import { t } from "../../../i18n";
|
|
7
|
+
import { requestAuthDialog } from "./auth-dialog";
|
|
8
|
+
import type { AccountMode } from "./auth-model";
|
|
7
9
|
|
|
8
|
-
function
|
|
10
|
+
function openAuth(
|
|
9
11
|
openCommandBar: (query?: string) => void,
|
|
10
|
-
|
|
12
|
+
mode: AccountMode,
|
|
11
13
|
event?: { preventDefault?: () => void; stopPropagation?: () => void },
|
|
12
14
|
) {
|
|
13
15
|
event?.preventDefault?.();
|
|
14
16
|
event?.stopPropagation?.();
|
|
15
|
-
|
|
17
|
+
if (!requestAuthDialog({ mode })) {
|
|
18
|
+
openCommandBar(mode === "login" ? "Log In" : "Sign Up");
|
|
19
|
+
}
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
export function InlineAuthActions({ showSignup = true }: { showSignup?: boolean }) {
|
|
@@ -25,7 +29,7 @@ export function InlineAuthActions({ showSignup = true }: { showSignup?: boolean
|
|
|
25
29
|
backgroundColor={hoveredAction === "login" ? hoverBg() : undefined}
|
|
26
30
|
onMouseOver={() => setHoveredAction((current) => (current === "login" ? current : "login"))}
|
|
27
31
|
onMouseOut={() => setHoveredAction((current) => (current === "login" ? null : current))}
|
|
28
|
-
onMouseDown={(event: any) =>
|
|
32
|
+
onMouseDown={(event: any) => openAuth(openCommandBar, "login", event)}
|
|
29
33
|
>
|
|
30
34
|
<Text fg={hoveredAction === "login" ? colors.text : colors.textDim}>{` ${t("Log In")} `}</Text>
|
|
31
35
|
</Box>
|
|
@@ -36,7 +40,7 @@ export function InlineAuthActions({ showSignup = true }: { showSignup?: boolean
|
|
|
36
40
|
backgroundColor={hoveredAction === "signup" ? hoverBg() : undefined}
|
|
37
41
|
onMouseOver={() => setHoveredAction((current) => (current === "signup" ? current : "signup"))}
|
|
38
42
|
onMouseOut={() => setHoveredAction((current) => (current === "signup" ? null : current))}
|
|
39
|
-
onMouseDown={(event: any) =>
|
|
43
|
+
onMouseDown={(event: any) => openAuth(openCommandBar, "signup", event)}
|
|
40
44
|
>
|
|
41
45
|
<Text fg={hoveredAction === "signup" ? colors.text : colors.textDim}>{` ${t("Sign Up")} `}</Text>
|
|
42
46
|
</Box>
|
|
@@ -49,16 +53,19 @@ export function InlineAuthActions({ showSignup = true }: { showSignup?: boolean
|
|
|
49
53
|
export function CloudAuthNotice({
|
|
50
54
|
message,
|
|
51
55
|
showSignup = true,
|
|
56
|
+
needsVerification = false,
|
|
52
57
|
}: {
|
|
53
58
|
message: string;
|
|
54
59
|
showSignup?: boolean;
|
|
60
|
+
/** Forces the verification branch and uses `message` as its headline. */
|
|
61
|
+
needsVerification?: boolean;
|
|
55
62
|
}) {
|
|
56
63
|
const { openCommandBar } = usePluginAppActions();
|
|
57
64
|
|
|
58
|
-
if (/verification/i.test(message)) {
|
|
65
|
+
if (needsVerification || /verification/i.test(message)) {
|
|
59
66
|
return (
|
|
60
67
|
<Box flexDirection="column" padding={1} gap={1}>
|
|
61
|
-
<Text fg={colors.positive}>{t("Verify your email to use Cloud tweets.")}</Text>
|
|
68
|
+
<Text fg={colors.positive}>{needsVerification ? message : t("Verify your email to use Cloud tweets.")}</Text>
|
|
62
69
|
<Button label={t("Resend Verification Email")} variant="secondary" onPress={() => openCommandBar("Resend Verification Email")} />
|
|
63
70
|
</Box>
|
|
64
71
|
);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { GloomPluginContext } from "../../../types/plugin";
|
|
2
2
|
import { apiClient } from "../../../api-client";
|
|
3
3
|
import { chatController } from "../chat/controller";
|
|
4
|
+
import { requestAuthDialog } from "./auth-dialog";
|
|
4
5
|
import { requestDeviceSignInDialog } from "./device-signin-dialog";
|
|
5
6
|
|
|
6
7
|
export function registerCloudAuthCommands(ctx: GloomPluginContext): void {
|
|
@@ -10,7 +11,7 @@ export function registerCloudAuthCommands(ctx: GloomPluginContext): void {
|
|
|
10
11
|
description: "Sign in by scanning a QR code with the Gloom app",
|
|
11
12
|
keywords: ["login", "sign in", "qr", "scan", "device", "mobile", "phone", "app", "code"],
|
|
12
13
|
category: "config",
|
|
13
|
-
hidden: () =>
|
|
14
|
+
hidden: () => apiClient.isSignedIn(),
|
|
14
15
|
execute: () => {
|
|
15
16
|
const opened = requestDeviceSignInDialog({
|
|
16
17
|
onSignedIn: () => ctx.showPane("chat"),
|
|
@@ -27,24 +28,15 @@ export function registerCloudAuthCommands(ctx: GloomPluginContext): void {
|
|
|
27
28
|
description: "Log in to your Gloomberb account",
|
|
28
29
|
keywords: ["login", "sign in", "auth", "account"],
|
|
29
30
|
category: "config",
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (!values?.email || !values?.password) {
|
|
39
|
-
throw new Error("Email and password are required");
|
|
40
|
-
}
|
|
41
|
-
const user = await apiClient.signIn(values.email, values.password);
|
|
42
|
-
if (!user.emailVerified) {
|
|
43
|
-
await apiClient.sendVerification().catch(() => {});
|
|
31
|
+
hidden: () => apiClient.isSignedIn(),
|
|
32
|
+
execute: () => {
|
|
33
|
+
const opened = requestAuthDialog({
|
|
34
|
+
mode: "login",
|
|
35
|
+
onSignedIn: () => ctx.showPane("chat"),
|
|
36
|
+
});
|
|
37
|
+
if (!opened) {
|
|
38
|
+
ctx.notify({ body: "Sign-in is not available right now.", type: "error" });
|
|
44
39
|
}
|
|
45
|
-
chatController.clearSession();
|
|
46
|
-
await chatController.refreshSession();
|
|
47
|
-
ctx.showPane("chat");
|
|
48
40
|
},
|
|
49
41
|
});
|
|
50
42
|
|
|
@@ -54,33 +46,15 @@ export function registerCloudAuthCommands(ctx: GloomPluginContext): void {
|
|
|
54
46
|
description: "Create a Gloomberb account",
|
|
55
47
|
keywords: ["signup", "register", "create account"],
|
|
56
48
|
category: "config",
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
type: "
|
|
65
|
-
placeholder: "3-30 chars, starts with letter",
|
|
66
|
-
body: ["Choose a username (3-30 characters, starts with a letter, alphanumeric and underscore only)"],
|
|
67
|
-
},
|
|
68
|
-
{ key: "password", label: "Password", type: "password", placeholder: "Min 8 characters" },
|
|
69
|
-
{ key: "confirmPassword", label: "Confirm Password", type: "password", placeholder: "Re-enter password" },
|
|
70
|
-
{ key: "_validate", label: "Creating account...", type: "info", body: ["Registering with Gloomberb...", "Account created! Welcome to Gloomberb."] },
|
|
71
|
-
],
|
|
72
|
-
execute: async (values) => {
|
|
73
|
-
if (!values?.email || !values?.username || !values?.password) {
|
|
74
|
-
throw new Error("All fields are required");
|
|
75
|
-
}
|
|
76
|
-
if (values.password !== values.confirmPassword) {
|
|
77
|
-
throw new Error("Passwords do not match");
|
|
49
|
+
hidden: () => apiClient.isSignedIn(),
|
|
50
|
+
execute: () => {
|
|
51
|
+
const opened = requestAuthDialog({
|
|
52
|
+
mode: "signup",
|
|
53
|
+
onSignedIn: () => ctx.showPane("chat"),
|
|
54
|
+
});
|
|
55
|
+
if (!opened) {
|
|
56
|
+
ctx.notify({ body: "Sign-up is not available right now.", type: "error" });
|
|
78
57
|
}
|
|
79
|
-
await apiClient.signUp(values.email, values.username, values.username, values.password);
|
|
80
|
-
await apiClient.sendVerification();
|
|
81
|
-
chatController.clearSession();
|
|
82
|
-
await chatController.refreshSession();
|
|
83
|
-
ctx.showPane("chat");
|
|
84
58
|
},
|
|
85
59
|
});
|
|
86
60
|
|
|
@@ -92,7 +66,7 @@ export function registerCloudAuthCommands(ctx: GloomPluginContext): void {
|
|
|
92
66
|
category: "config",
|
|
93
67
|
hidden: () => {
|
|
94
68
|
const user = chatController.getSnapshot().user;
|
|
95
|
-
return !apiClient.
|
|
69
|
+
return !apiClient.isSignedIn() || !user || user.emailVerified;
|
|
96
70
|
},
|
|
97
71
|
execute: async () => {
|
|
98
72
|
await apiClient.sendVerification();
|
|
@@ -100,10 +74,6 @@ export function registerCloudAuthCommands(ctx: GloomPluginContext): void {
|
|
|
100
74
|
},
|
|
101
75
|
});
|
|
102
76
|
|
|
103
|
-
if (apiClient.getSessionToken()) {
|
|
104
|
-
void chatController.refreshSession().catch(() => {});
|
|
105
|
-
}
|
|
106
|
-
|
|
107
77
|
ctx.registerCommand({
|
|
108
78
|
id: "auth-logout",
|
|
109
79
|
label: "Logout",
|
|
@@ -111,7 +81,7 @@ export function registerCloudAuthCommands(ctx: GloomPluginContext): void {
|
|
|
111
81
|
keywords: ["logout", "sign out"],
|
|
112
82
|
category: "config",
|
|
113
83
|
execute: async () => {
|
|
114
|
-
if (!apiClient.
|
|
84
|
+
if (!apiClient.isSignedIn()) {
|
|
115
85
|
ctx.notify({ body: "Not logged in.", type: "error" });
|
|
116
86
|
return;
|
|
117
87
|
}
|
|
@@ -128,6 +98,6 @@ export function registerCloudAuthCommands(ctx: GloomPluginContext): void {
|
|
|
128
98
|
type: "info",
|
|
129
99
|
});
|
|
130
100
|
},
|
|
131
|
-
hidden: () => !apiClient.
|
|
101
|
+
hidden: () => !apiClient.isSignedIn(),
|
|
132
102
|
});
|
|
133
103
|
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared email/password auth dialog plus the request bridge that lets commands
|
|
3
|
+
* (which run outside the React tree) open it. `AuthDialogHost` is mounted by
|
|
4
|
+
* the shell for the life of the app and owns the actual dialog, mirroring the
|
|
5
|
+
* QR sign-in dialog host.
|
|
6
|
+
*/
|
|
7
|
+
import { useCallback, useState } from "react";
|
|
8
|
+
import { apiClient, type AuthUser } from "../../../api-client";
|
|
9
|
+
import { Button, Spinner, TextField } from "../../../components";
|
|
10
|
+
import { DialogFrame } from "../../../components/ui/frame";
|
|
11
|
+
import { t, tf } from "../../../i18n";
|
|
12
|
+
import { useAppLanguage } from "../../../i18n/react";
|
|
13
|
+
import { colors } from "../../../theme/colors";
|
|
14
|
+
import { Box, Text, TextAttributes } from "../../../ui";
|
|
15
|
+
import { useDialog, useDialogKeyboard, type PromptContext } from "../../../ui/dialog";
|
|
16
|
+
import { isPlainKey } from "../../../utils/keyboard";
|
|
17
|
+
import { useEffect, useRef } from "react";
|
|
18
|
+
import {
|
|
19
|
+
advanceAccountField,
|
|
20
|
+
classifyAccountError,
|
|
21
|
+
performEmailAuth,
|
|
22
|
+
validateAccountEmail,
|
|
23
|
+
validateAccountPassword,
|
|
24
|
+
type AccountMode,
|
|
25
|
+
type AccountSubmitError,
|
|
26
|
+
} from "./auth-model";
|
|
27
|
+
|
|
28
|
+
const FIELD_WIDTH = 42;
|
|
29
|
+
|
|
30
|
+
type AuthField = "email" | "password";
|
|
31
|
+
|
|
32
|
+
type ResetState = "idle" | "sending" | "sent";
|
|
33
|
+
|
|
34
|
+
export function AuthDialog({
|
|
35
|
+
initialMode,
|
|
36
|
+
resolve,
|
|
37
|
+
dismiss,
|
|
38
|
+
}: PromptContext<AuthUser | undefined> & { initialMode: AccountMode }) {
|
|
39
|
+
useAppLanguage();
|
|
40
|
+
const [mode, setMode] = useState<AccountMode>(initialMode);
|
|
41
|
+
const [email, setEmail] = useState("");
|
|
42
|
+
const [password, setPassword] = useState("");
|
|
43
|
+
const [activeField, setActiveField] = useState<AuthField>("email");
|
|
44
|
+
const [showPassword, setShowPassword] = useState(false);
|
|
45
|
+
const [submitting, setSubmitting] = useState(false);
|
|
46
|
+
const [validationError, setValidationError] = useState<string | null>(null);
|
|
47
|
+
const [submitError, setSubmitError] = useState<AccountSubmitError | null>(null);
|
|
48
|
+
const [resetState, setResetState] = useState<ResetState>("idle");
|
|
49
|
+
const attemptRef = useRef(0);
|
|
50
|
+
|
|
51
|
+
const clearErrors = useCallback(() => {
|
|
52
|
+
setValidationError(null);
|
|
53
|
+
setSubmitError(null);
|
|
54
|
+
}, []);
|
|
55
|
+
|
|
56
|
+
useEffect(() => () => {
|
|
57
|
+
// Unmounting abandons any in-flight attempt so a late response can't set state.
|
|
58
|
+
attemptRef.current += 1;
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
const switchMode = useCallback((nextMode: AccountMode) => {
|
|
62
|
+
attemptRef.current += 1;
|
|
63
|
+
setMode(nextMode);
|
|
64
|
+
setSubmitting(false);
|
|
65
|
+
setPassword("");
|
|
66
|
+
setResetState("idle");
|
|
67
|
+
clearErrors();
|
|
68
|
+
setActiveField(email.trim() ? "password" : "email");
|
|
69
|
+
}, [clearErrors, email]);
|
|
70
|
+
|
|
71
|
+
const requestReset = useCallback(() => {
|
|
72
|
+
if (submitting || resetState === "sending") return;
|
|
73
|
+
const trimmedEmail = email.trim();
|
|
74
|
+
const emailError = validateAccountEmail(trimmedEmail);
|
|
75
|
+
if (emailError) {
|
|
76
|
+
setActiveField("email");
|
|
77
|
+
setValidationError(emailError);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
setResetState("sending");
|
|
81
|
+
clearErrors();
|
|
82
|
+
void apiClient.requestPasswordReset(trimmedEmail)
|
|
83
|
+
.then(() => setResetState("sent"))
|
|
84
|
+
.catch(() => {
|
|
85
|
+
setResetState("idle");
|
|
86
|
+
setSubmitError({ message: t("Could not send the reset email."), kind: "retry" });
|
|
87
|
+
});
|
|
88
|
+
}, [clearErrors, email, resetState, submitting]);
|
|
89
|
+
|
|
90
|
+
const submit = useCallback(() => {
|
|
91
|
+
if (submitting) return;
|
|
92
|
+
const trimmedEmail = email.trim();
|
|
93
|
+
const emailError = validateAccountEmail(trimmedEmail);
|
|
94
|
+
if (emailError) {
|
|
95
|
+
setActiveField("email");
|
|
96
|
+
setValidationError(emailError);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const passwordError = validateAccountPassword(password, mode);
|
|
100
|
+
if (passwordError) {
|
|
101
|
+
setActiveField("password");
|
|
102
|
+
setValidationError(passwordError);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const attemptId = attemptRef.current + 1;
|
|
107
|
+
attemptRef.current = attemptId;
|
|
108
|
+
setSubmitting(true);
|
|
109
|
+
clearErrors();
|
|
110
|
+
void (async () => {
|
|
111
|
+
try {
|
|
112
|
+
const user = await performEmailAuth(mode, trimmedEmail, password);
|
|
113
|
+
if (attemptRef.current !== attemptId) return;
|
|
114
|
+
resolve(user);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (attemptRef.current !== attemptId) return;
|
|
117
|
+
setSubmitting(false);
|
|
118
|
+
setSubmitError(classifyAccountError(error, mode));
|
|
119
|
+
}
|
|
120
|
+
})();
|
|
121
|
+
}, [clearErrors, email, mode, password, resolve, submitting]);
|
|
122
|
+
|
|
123
|
+
const submitField = useCallback(() => {
|
|
124
|
+
const advance = advanceAccountField({
|
|
125
|
+
mode,
|
|
126
|
+
email: email.trim(),
|
|
127
|
+
password,
|
|
128
|
+
fieldIdx: activeField === "email" ? 0 : 1,
|
|
129
|
+
});
|
|
130
|
+
if (advance.action === "invalid") {
|
|
131
|
+
setValidationError(advance.message);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (advance.action === "next-field") {
|
|
135
|
+
setValidationError(null);
|
|
136
|
+
setActiveField("password");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
submit();
|
|
140
|
+
}, [activeField, email, mode, password, submit]);
|
|
141
|
+
|
|
142
|
+
useDialogKeyboard((event) => {
|
|
143
|
+
if (event.name === "escape") {
|
|
144
|
+
event.stopPropagation?.();
|
|
145
|
+
dismiss();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const forward = isPlainKey(event, "tab") || (!event.targetEditable && isPlainKey(event, "down", "j"));
|
|
149
|
+
const backward = !event.targetEditable && isPlainKey(event, "up", "k");
|
|
150
|
+
if (forward || backward) {
|
|
151
|
+
event.preventDefault?.();
|
|
152
|
+
event.stopPropagation?.();
|
|
153
|
+
setActiveField(forward ? "password" : "email");
|
|
154
|
+
}
|
|
155
|
+
}, { allowEditable: true });
|
|
156
|
+
|
|
157
|
+
const switchToLogin = submitError?.kind === "switch-to-login";
|
|
158
|
+
const error = validationError ?? submitError?.message ?? null;
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<DialogFrame title={mode === "login" ? t("Log in to Gloom Cloud") : t("Create your free Gloom Cloud account")}>
|
|
162
|
+
<Box flexDirection="column" gap={1}>
|
|
163
|
+
<TextField
|
|
164
|
+
label={t("Email")}
|
|
165
|
+
value={email}
|
|
166
|
+
placeholder="email@example.com"
|
|
167
|
+
focused={activeField === "email" && !submitting}
|
|
168
|
+
width={FIELD_WIDTH}
|
|
169
|
+
type="email"
|
|
170
|
+
autoComplete="email"
|
|
171
|
+
onMouseDown={() => setActiveField("email")}
|
|
172
|
+
onChange={(value) => {
|
|
173
|
+
setEmail(value);
|
|
174
|
+
setResetState("idle");
|
|
175
|
+
clearErrors();
|
|
176
|
+
}}
|
|
177
|
+
onSubmit={submitField}
|
|
178
|
+
/>
|
|
179
|
+
<Box flexDirection="column">
|
|
180
|
+
<Box height={1} width={FIELD_WIDTH} flexDirection="row" justifyContent="space-between">
|
|
181
|
+
<Text
|
|
182
|
+
fg={activeField === "password" ? colors.textBright : colors.textDim}
|
|
183
|
+
attributes={activeField === "password" ? TextAttributes.BOLD : 0}
|
|
184
|
+
>
|
|
185
|
+
{t("Password")}
|
|
186
|
+
</Text>
|
|
187
|
+
<Box onMouseDown={() => setShowPassword((current) => !current)}>
|
|
188
|
+
<Text fg={colors.textMuted}>{showPassword ? t("hide") : t("show")}</Text>
|
|
189
|
+
</Box>
|
|
190
|
+
</Box>
|
|
191
|
+
<TextField
|
|
192
|
+
value={password}
|
|
193
|
+
placeholder={mode === "signup" ? t("At least 8 characters") : t("Your password")}
|
|
194
|
+
focused={activeField === "password" && !submitting}
|
|
195
|
+
width={FIELD_WIDTH}
|
|
196
|
+
type={showPassword ? "text" : "password"}
|
|
197
|
+
autoComplete={mode === "signup" ? "new-password" : "current-password"}
|
|
198
|
+
onMouseDown={() => setActiveField("password")}
|
|
199
|
+
onChange={(value) => {
|
|
200
|
+
setPassword(value);
|
|
201
|
+
clearErrors();
|
|
202
|
+
}}
|
|
203
|
+
onSubmit={submitField}
|
|
204
|
+
/>
|
|
205
|
+
</Box>
|
|
206
|
+
<Box flexDirection="column" minHeight={2} width={FIELD_WIDTH}>
|
|
207
|
+
{submitting ? (
|
|
208
|
+
<Spinner label={mode === "signup" ? t("Creating your account...") : t("Signing you in...")} />
|
|
209
|
+
) : resetState === "sending" ? (
|
|
210
|
+
<Spinner label={t("Sending reset link...")} />
|
|
211
|
+
) : error ? (
|
|
212
|
+
<Text fg={colors.negative} wrapText>{error}</Text>
|
|
213
|
+
) : resetState === "sent" ? (
|
|
214
|
+
<Text fg={colors.positive} wrapText>
|
|
215
|
+
{tf("Reset link sent to {email}. Check your inbox.", { email: email.trim() })}
|
|
216
|
+
</Text>
|
|
217
|
+
) : mode === "signup" ? (
|
|
218
|
+
<Text fg={colors.textMuted} wrapText>{t("We'll email you a verification link.")}</Text>
|
|
219
|
+
) : null}
|
|
220
|
+
</Box>
|
|
221
|
+
<Box flexDirection="row" justifyContent="space-between" width={FIELD_WIDTH}>
|
|
222
|
+
<Button
|
|
223
|
+
label={switchToLogin || mode === "signup" ? t("Log in instead") : t("Sign up instead")}
|
|
224
|
+
variant="ghost"
|
|
225
|
+
disabled={submitting}
|
|
226
|
+
onPress={() => switchMode(mode === "login" ? "signup" : "login")}
|
|
227
|
+
/>
|
|
228
|
+
<Button
|
|
229
|
+
label={mode === "login" ? t("Log In") : t("Create Account")}
|
|
230
|
+
variant="primary"
|
|
231
|
+
disabled={submitting}
|
|
232
|
+
onPress={submit}
|
|
233
|
+
/>
|
|
234
|
+
</Box>
|
|
235
|
+
{mode === "login" && (
|
|
236
|
+
<Box flexDirection="row" width={FIELD_WIDTH}>
|
|
237
|
+
<Button
|
|
238
|
+
label={t("Forgot password?")}
|
|
239
|
+
variant="ghost"
|
|
240
|
+
disabled={submitting || resetState === "sending"}
|
|
241
|
+
onPress={requestReset}
|
|
242
|
+
/>
|
|
243
|
+
</Box>
|
|
244
|
+
)}
|
|
245
|
+
</Box>
|
|
246
|
+
</DialogFrame>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export interface AuthDialogRequest {
|
|
251
|
+
mode?: AccountMode;
|
|
252
|
+
onSignedIn?: (user: AuthUser) => void;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const authDialogListeners = new Set<(request: AuthDialogRequest) => void>();
|
|
256
|
+
|
|
257
|
+
/** Returns false when no dialog host is mounted, so callers can fall back. */
|
|
258
|
+
export function requestAuthDialog(request: AuthDialogRequest = {}): boolean {
|
|
259
|
+
if (authDialogListeners.size === 0) return false;
|
|
260
|
+
for (const listener of authDialogListeners) listener(request);
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function AuthDialogHost() {
|
|
265
|
+
const dialog = useDialog();
|
|
266
|
+
const openRef = useRef(false);
|
|
267
|
+
|
|
268
|
+
useEffect(() => {
|
|
269
|
+
const listener = (request: AuthDialogRequest) => {
|
|
270
|
+
if (openRef.current) return;
|
|
271
|
+
openRef.current = true;
|
|
272
|
+
void dialog
|
|
273
|
+
.prompt<AuthUser | undefined>({
|
|
274
|
+
closeOnClickOutside: false,
|
|
275
|
+
content: (context: unknown) => (
|
|
276
|
+
<AuthDialog
|
|
277
|
+
{...(context as PromptContext<AuthUser | undefined>)}
|
|
278
|
+
initialMode={request.mode ?? "login"}
|
|
279
|
+
/>
|
|
280
|
+
),
|
|
281
|
+
})
|
|
282
|
+
.then((user) => {
|
|
283
|
+
if (user) request.onSignedIn?.(user);
|
|
284
|
+
})
|
|
285
|
+
.finally(() => {
|
|
286
|
+
openRef.current = false;
|
|
287
|
+
});
|
|
288
|
+
};
|
|
289
|
+
authDialogListeners.add(listener);
|
|
290
|
+
return () => {
|
|
291
|
+
authDialogListeners.delete(listener);
|
|
292
|
+
};
|
|
293
|
+
}, [dialog]);
|
|
294
|
+
|
|
295
|
+
return null;
|
|
296
|
+
}
|