gloomberb 0.18.1 → 0.18.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/package.json +1 -1
- package/src/api-client/commodity-balances.ts +6 -0
- package/src/api-client/company-kpis.ts +9 -0
- package/src/api-client/crypto-markets.ts +9 -0
- package/src/api-client/data.ts +6 -5
- package/src/api-client/equity-screener.ts +2 -0
- package/src/api-client/types.ts +5 -0
- package/src/app/pane-runtime/plugin-bindings.ts +3 -1
- package/src/app/pane-runtime/ticker-open-runtime.ts +17 -5
- package/src/app/runtime/plugin-bindings.ts +6 -2
- package/src/brokers/logos.ts +66 -0
- package/src/brokers/profile-form.ts +3 -0
- package/src/brokers/remote-broker-adapter.ts +13 -3
- package/src/capabilities/index.ts +1 -1
- package/src/capabilities/service-capabilities.ts +7 -1
- package/src/cli/base-converter.ts +60 -18
- package/src/cli/commands/market.ts +281 -154
- package/src/cli/commands/news-search.ts +35 -8
- package/src/cli/commands/system.ts +35 -1
- package/src/cli/commands/ticker.ts +100 -50
- package/src/cli/currency-hints.ts +27 -0
- package/src/cli/fx-pair.ts +41 -4
- package/src/cli/glossary.ts +57 -4
- package/src/cli/helpers.ts +36 -3
- package/src/cli/index.ts +4 -1
- package/src/cli/known-gaps.ts +37 -1
- package/src/cli/listing-arg.ts +37 -2
- package/src/cli/pane-functions/catalog.ts +3 -1
- package/src/cli/pane-functions/discovery.ts +15 -1
- package/src/cli/pane-functions/dom.ts +9 -4
- package/src/cli/pane-functions/freshness.ts +4 -4
- package/src/cli/pane-functions/function-help.ts +27 -3
- package/src/cli/pane-functions/headless.ts +6 -5
- package/src/cli/pane-functions/index.ts +3 -2
- package/src/cli/pane-functions/lock-note.ts +7 -4
- package/src/cli/pane-functions/resolver.ts +2 -1
- package/src/cli/quote-answer.ts +179 -0
- package/src/cli/series-search.ts +2 -1
- package/src/cli/types.ts +4 -0
- package/src/components/chart/composite/composite-chart.tsx +11 -2
- package/src/components/chart/composite/format.ts +13 -1
- package/src/components/chart/composite/level-labels.ts +30 -0
- package/src/components/chart/composite/levels.ts +17 -6
- package/src/components/chart/composite/panel-surface.tsx +58 -8
- package/src/components/command-bar/routes/ticker-search/actions.ts +6 -0
- package/src/components/index.ts +2 -0
- package/src/components/layout/pane/content.tsx +16 -14
- package/src/components/layout/pane/footer/index.tsx +22 -3
- package/src/components/layout/pane/footer/model.ts +99 -4
- package/src/components/layout/pane/header-tabs.tsx +15 -1
- package/src/components/onboarding/onboarding-frame.tsx +9 -11
- package/src/components/onboarding/use-onboarding-broker-form.ts +5 -2
- package/src/components/onboarding/wizard-model.ts +10 -3
- package/src/components/ui/button.tsx +11 -4
- package/src/components/ui/choice-dialog.tsx +8 -5
- package/src/components/ui/display.tsx +11 -6
- package/src/components/ui/fields.tsx +100 -20
- package/src/components/ui/icon.tsx +6 -1
- package/src/components/ui/list-row-lines.tsx +87 -0
- package/src/components/ui/list-view.tsx +14 -0
- package/src/components/ui/logo-mark.tsx +64 -0
- package/src/components/ui/number-step.ts +54 -0
- package/src/components/ui/select-button.tsx +5 -4
- package/src/components/ui/select-field.tsx +3 -3
- package/src/components/ui/status.tsx +17 -3
- package/src/components/ui/tabs.tsx +9 -1
- package/src/components/ui/toggle.tsx +17 -3
- package/src/core/state/app/instrument.ts +8 -3
- package/src/data/config/store/normalize.ts +4 -1
- package/src/data/plugin-cache.ts +32 -6
- package/src/docs/snapshot.json +5 -5
- package/src/i18n/es.ts +9 -3
- package/src/i18n/ja.ts +9 -3
- package/src/i18n/ko.ts +9 -3
- package/src/i18n/zh-cn.ts +10 -4
- package/src/i18n/zh-tw.ts +10 -4
- package/src/market-data/commodity-names.ts +80 -0
- package/src/market-data/coordinator/auxiliary.ts +3 -1
- package/src/market-data/coordinator/chart.ts +3 -1
- package/src/market-data/futures-board.ts +38 -1
- package/src/market-data/futures-contract.ts +91 -0
- package/src/market-data/fx-closes.ts +78 -1
- package/src/market-data/market/change-basis.ts +219 -0
- package/src/market-data/market/format.ts +28 -12
- package/src/market-data/market/freshness.ts +38 -0
- package/src/market-data/market/status.ts +55 -9
- package/src/market-data/market/year-to-date.ts +46 -0
- package/src/market-data/quotes/contributions.ts +12 -0
- package/src/market-data/quotes/delisting.ts +34 -0
- package/src/market-data/quotes/live-valuation.ts +23 -0
- package/src/market-data/quotes/resolution.ts +8 -2
- package/src/market-data/tax-equivalent.ts +10 -2
- package/src/notifications/app-notifier.ts +2 -2
- package/src/notifications/notification-log.ts +8 -6
- package/src/plugins/builtin/alerts/cli.ts +26 -18
- package/src/plugins/builtin/alerts/headless.ts +1 -1
- package/src/plugins/builtin/alerts/index.tsx +13 -4
- package/src/plugins/builtin/attention/trending.tsx +8 -6
- package/src/plugins/builtin/backtest/client.ts +5 -2
- package/src/plugins/builtin/backtest/window.ts +16 -3
- package/src/plugins/builtin/broker-manager/add-flow.tsx +40 -8
- package/src/plugins/builtin/broker-manager/detail.tsx +26 -1
- package/src/plugins/builtin/broker-manager/index.tsx +20 -9
- package/src/plugins/builtin/broker-manager/model.ts +5 -0
- package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -1
- package/src/plugins/builtin/broker-manager/table.ts +5 -1
- package/src/plugins/builtin/broker-trading/account-picker.tsx +91 -0
- package/src/plugins/builtin/broker-trading/chart-levels.ts +54 -0
- package/src/plugins/builtin/broker-trading/command.ts +103 -0
- package/src/plugins/builtin/broker-trading/controller.ts +688 -0
- package/src/plugins/builtin/broker-trading/dock.tsx +182 -0
- package/src/plugins/builtin/broker-trading/headless.ts +76 -0
- package/src/plugins/builtin/broker-trading/index.tsx +77 -0
- package/src/plugins/builtin/broker-trading/order-feed.ts +383 -0
- package/src/plugins/builtin/broker-trading/orders-data.ts +133 -0
- package/src/plugins/builtin/broker-trading/orders.tsx +437 -0
- package/src/plugins/builtin/broker-trading/overview-orders.tsx +121 -0
- package/src/plugins/builtin/broker-trading/pane-model.ts +207 -0
- package/src/plugins/builtin/broker-trading/pane.tsx +505 -0
- package/src/plugins/builtin/broker-trading/strategy.ts +153 -0
- package/src/plugins/builtin/broker-trading/ticket-parts.tsx +117 -0
- package/src/plugins/builtin/broker-trading/ticket.tsx +573 -0
- package/src/plugins/builtin/buildout/data-runtime.ts +1 -1
- package/src/plugins/builtin/buildout/format.ts +1 -0
- package/src/plugins/builtin/buildout/model/index.ts +16 -7
- package/src/plugins/builtin/buildout/pane/index.tsx +3 -2
- package/src/plugins/builtin/buildout/pane/status.tsx +3 -3
- package/src/plugins/builtin/chart-composer/cli-options.ts +19 -0
- package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
- package/src/plugins/builtin/chart-composer/headless-schema.ts +3 -3
- package/src/plugins/builtin/chart-composer/headless.ts +2 -1
- package/src/plugins/builtin/chart-composer/option-range.ts +39 -0
- package/src/plugins/builtin/chart-composer/pane.tsx +31 -11
- package/src/plugins/builtin/chart-composer/presets.ts +2 -2
- package/src/plugins/builtin/chart-composer/series-catalog.ts +4 -2
- package/src/plugins/builtin/chart-composer/shortcuts.ts +3 -3
- package/src/plugins/builtin/chart-composer/viewport-labels.ts +5 -2
- package/src/plugins/builtin/chat/index.tsx +1 -1
- package/src/plugins/builtin/chat/status-widget.tsx +1 -1
- package/src/plugins/builtin/chat/unread-inbox-pane.tsx +6 -1
- package/src/plugins/builtin/chat/unread-inbox.ts +1 -1
- package/src/plugins/builtin/commodity-balances/client.ts +6 -4
- package/src/plugins/builtin/commodity-balances/headless.ts +41 -7
- package/src/plugins/builtin/commodity-balances/index.tsx +19 -4
- package/src/plugins/builtin/commodity-balances/model.ts +22 -0
- package/src/plugins/builtin/commodity-balances/pane.tsx +36 -5
- package/src/plugins/builtin/company-kpis/client.ts +3 -2
- package/src/plugins/builtin/company-kpis/headless.ts +5 -7
- package/src/plugins/builtin/company-kpis/model.ts +85 -2
- package/src/plugins/builtin/company-kpis/pane.tsx +26 -12
- package/src/plugins/builtin/composite-plugins.ts +3 -2
- package/src/plugins/builtin/cot/headless.ts +1 -1
- package/src/plugins/builtin/cot/index.tsx +1 -1
- package/src/plugins/builtin/cot/model.ts +35 -12
- package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
- package/src/plugins/builtin/credit-boards/sovr-pane.tsx +2 -1
- package/src/plugins/builtin/credit-conditions/index.tsx +2 -1
- package/src/plugins/builtin/crypto-board/client.ts +33 -2
- package/src/plugins/builtin/crypto-board/headless.ts +5 -2
- package/src/plugins/builtin/crypto-board/model.ts +28 -8
- package/src/plugins/builtin/dividend-yield/headless.ts +30 -8
- package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
- package/src/plugins/builtin/earnings-calls/headless.ts +4 -4
- package/src/plugins/builtin/earnings-calls/lock-note.ts +12 -3
- package/src/plugins/builtin/econ/calendar-model.ts +35 -0
- package/src/plugins/builtin/econ/index.tsx +31 -4
- package/src/plugins/builtin/econ/print-watch.ts +279 -0
- package/src/plugins/builtin/equity-screener/headless.ts +20 -5
- package/src/plugins/builtin/equity-screener/model.ts +40 -2
- package/src/plugins/builtin/equity-screener/pane.tsx +24 -21
- package/src/plugins/builtin/estimate-revisions/headless.ts +3 -2
- package/src/plugins/builtin/estimate-revisions/lock-note.ts +29 -7
- package/src/plugins/builtin/exposure/client.ts +4 -1
- package/src/plugins/builtin/exposure/editor.tsx +3 -3
- package/src/plugins/builtin/exposure/evidence.ts +4 -1
- package/src/plugins/builtin/exposure/holdings.ts +3 -1
- package/src/plugins/builtin/exposure/pane.tsx +37 -16
- package/src/plugins/builtin/filing-events/index.tsx +9 -3
- package/src/plugins/builtin/futures/table.tsx +6 -3
- package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
- package/src/plugins/builtin/insider/base-values.ts +72 -0
- package/src/plugins/builtin/insider/headless.ts +29 -4
- package/src/plugins/builtin/jobs/headless.ts +2 -2
- package/src/plugins/builtin/jobs/lock-note.ts +14 -3
- package/src/plugins/builtin/market-halts/model.ts +31 -6
- package/src/plugins/builtin/market-halts/pane.tsx +3 -0
- package/src/plugins/builtin/market-valuation/pane.tsx +2 -1
- package/src/plugins/builtin/members/pane.tsx +8 -3
- package/src/plugins/builtin/news/wire/feed-config.ts +29 -12
- package/src/plugins/builtin/news/wire/index.tsx +7 -2
- package/src/plugins/builtin/news/wire/news/preset-pane.tsx +11 -3
- package/src/plugins/builtin/news/wire/rss/cli.ts +3 -2
- package/src/plugins/builtin/news/wire/rss/source.ts +13 -4
- package/src/plugins/builtin/options/footer.ts +25 -20
- package/src/plugins/builtin/options/live-quotes.ts +33 -18
- package/src/plugins/builtin/options/view.tsx +77 -22
- package/src/plugins/builtin/options-calculator/pane.tsx +4 -2
- package/src/plugins/builtin/options-scenario/pane.tsx +41 -6
- package/src/plugins/builtin/portfolio-list/account-metrics.ts +3 -0
- package/src/plugins/builtin/portfolio-list/cli/render.ts +34 -10
- package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
- package/src/plugins/builtin/portfolio-list/pane/data.ts +23 -1
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +33 -14
- package/src/plugins/builtin/portfolio-list/settings.ts +20 -2
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +12 -0
- package/src/plugins/builtin/portfolio-list/summary/totals.ts +24 -3
- package/src/plugins/builtin/primary-dealers/client.ts +137 -62
- package/src/plugins/builtin/primary-dealers/headless.ts +30 -5
- package/src/plugins/builtin/primary-dealers/index.tsx +3 -2
- package/src/plugins/builtin/primary-dealers/model.ts +179 -129
- package/src/plugins/builtin/primary-dealers/pane.tsx +135 -15
- package/src/plugins/builtin/research/analyst-model.ts +5 -1
- package/src/plugins/builtin/risk-factors/index.tsx +12 -3
- package/src/plugins/builtin/scanner/feed.ts +6 -1
- package/src/plugins/builtin/scanner/headless.ts +1 -1
- package/src/plugins/builtin/scanner/lock-note.ts +3 -2
- package/src/plugins/builtin/series-inventory.ts +1 -0
- package/src/plugins/builtin/shared/cloud-denied.ts +7 -6
- package/src/plugins/builtin/shared/cloud-upgrade.ts +10 -11
- package/src/plugins/builtin/shared/free-routes.ts +24 -0
- package/src/plugins/builtin/shared/headless-options.ts +11 -0
- package/src/plugins/builtin/shared/pro-lock-note.ts +89 -7
- package/src/plugins/builtin/shared/research-trading.tsx +62 -0
- package/src/plugins/builtin/social-mentions/headless.ts +1 -1
- package/src/plugins/builtin/social-mentions/lock-note.ts +1 -1
- package/src/plugins/builtin/themes/headless.ts +8 -2
- package/src/plugins/builtin/themes/model.ts +43 -10
- package/src/plugins/builtin/themes/pane.tsx +8 -6
- package/src/plugins/builtin/ticker-detail/overview/model.ts +44 -10
- package/src/plugins/builtin/ticker-detail/overview/option-contract.ts +64 -0
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +82 -21
- package/src/plugins/builtin/ticker-detail/pane.tsx +21 -14
- package/src/plugins/builtin/ticker-detail/quote-footer.ts +5 -1
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +37 -8
- package/src/plugins/builtin/time-sales/pane.tsx +2 -2
- package/src/plugins/builtin/trace-bonds/client.ts +14 -3
- package/src/plugins/builtin/trace-bonds/headless.ts +1 -1
- package/src/plugins/builtin/vol-surface/pane.tsx +2 -2
- package/src/plugins/builtin-editorial.ts +1 -1
- package/src/plugins/compat.ts +3 -0
- package/src/plugins/external-runtime.ts +5 -0
- package/src/plugins/loader.ts +6 -0
- package/src/plugins/plugin-icon.ts +61 -0
- package/src/plugins/registry/host-actions.ts +2 -1
- package/src/plugins/registry/index.ts +9 -1
- package/src/plugins/runtime/context.tsx +5 -0
- package/src/plugins/runtime/index.tsx +3 -0
- package/src/portfolio/column-values.ts +13 -4
- package/src/portfolio/cost-fx.ts +5 -28
- package/src/public/__snapshots__/public-api.test.ts.snap +5 -0
- package/src/public/broker.ts +8 -0
- package/src/public/market-data.ts +5 -0
- package/src/renderers/dom/desktop/control-styles.ts +7 -1
- package/src/renderers/dom/desktop/controls.tsx +39 -7
- package/src/renderers/dom/desktop/icons.tsx +14 -0
- package/src/renderers/dom/desktop/list-view.tsx +51 -19
- package/src/renderers/dom/desktop/menu.tsx +10 -1
- package/src/renderers/dom/desktop/select-field.tsx +8 -6
- package/src/renderers/dom/host/style.ts +6 -1
- package/src/renderers/dom/host/tabs.tsx +6 -0
- package/src/renderers/dom/styles.css +12 -5
- package/src/renderers/electrobun/bun/core-capabilities.ts +12 -0
- package/src/renderers/electrobun/bun/external-plugins.ts +3 -0
- package/src/renderers/electrobun/shared/protocol.ts +2 -0
- package/src/renderers/electrobun/view/broker-remote-client.ts +16 -0
- package/src/renderers/electrobun/view/external-plugins.ts +1 -0
- package/src/series/validate.ts +2 -2
- package/src/sources/gloomberb-cloud/index.ts +50 -17
- package/src/sources/gloomberb-cloud/normalizers.ts +4 -0
- package/src/sources/history-result.ts +3 -0
- package/src/sources/provider-errors.ts +3 -1
- package/src/sources/provider-router/history.ts +49 -6
- package/src/sources/provider-router/index.ts +2 -1
- package/src/state/app/activity.ts +13 -0
- package/src/sync/controller.ts +4 -0
- package/src/sync/core-contributors.ts +62 -11
- package/src/sync/local-only.ts +121 -0
- package/src/sync/react.ts +6 -0
- package/src/sync/types.ts +3 -0
- package/src/tickers/listing-fate.ts +27 -8
- package/src/tickers/search/index.ts +24 -3
- package/src/time-series/price-comparison.ts +35 -0
- package/src/time-series/resolve.ts +52 -11
- package/src/time-series/spec.ts +1 -1
- package/src/time-series/types.ts +5 -0
- package/src/types/broker.ts +56 -1
- package/src/types/config.ts +6 -0
- package/src/types/data-provider.ts +5 -1
- package/src/types/financials.ts +23 -0
- package/src/types/headless.ts +12 -2
- package/src/types/instrument.ts +4 -0
- package/src/types/plugin.ts +24 -6
- package/src/types/price-history.ts +6 -1
- package/src/types/trading.ts +67 -0
- package/src/ui/host.tsx +4 -0
- package/src/utils/base-currency.ts +108 -0
- package/src/utils/currency-names.ts +128 -0
- package/src/utils/currency-pair.ts +2 -1
- package/src/utils/currency-units.ts +32 -1
- package/src/utils/dom-screenshot.ts +22 -9
- package/src/utils/exchange-notation.generated.ts +360 -0
- package/src/utils/exchanges.ts +15 -322
- package/src/utils/format.ts +31 -1
- package/src/utils/instrument-identity.ts +38 -1
- package/src/utils/options.ts +46 -1
- package/src/utils/price-history.ts +4 -0
- package/src/utils/reported-money.ts +8 -4
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { Box, Input, Span, Text, TextAttributes, useUiCapabilities, useUiHost } from "../../ui";
|
|
2
|
-
import { useEffect, useRef, useState, type ComponentType, type RefObject } from "react";
|
|
2
|
+
import { useEffect, useLayoutEffect, useRef, useState, type ComponentType, type RefObject } from "react";
|
|
3
3
|
import { type InputRenderable } from "../../ui";
|
|
4
4
|
import { useThemeColors } from "../../theme/theme-context";
|
|
5
5
|
import { useRemoteUiNode } from "../../remote/semantic-tree";
|
|
6
6
|
import { remoteStringValue } from "../../remote/semantic-helpers";
|
|
7
7
|
import { truncateWithEllipsis } from "../../utils/text-wrap";
|
|
8
|
+
import { Button } from "./button";
|
|
9
|
+
import { stepNumber } from "./number-step";
|
|
8
10
|
|
|
9
11
|
export interface FieldLabelProps {
|
|
10
12
|
label: string;
|
|
@@ -35,6 +37,8 @@ export function FieldLabel({ label, active = false, width, maxWidth }: FieldLabe
|
|
|
35
37
|
|
|
36
38
|
export interface TextFieldProps {
|
|
37
39
|
label?: string;
|
|
40
|
+
/** Accessible name when the visible label is rendered separately. */
|
|
41
|
+
accessibleLabel?: string;
|
|
38
42
|
/**
|
|
39
43
|
* The form's current field, in a form that walks its fields with the
|
|
40
44
|
* keyboard. The terminal marks its label with "> " and indents the other
|
|
@@ -51,6 +55,7 @@ export interface TextFieldProps {
|
|
|
51
55
|
/** The most characters it takes; typing and pasting stop there. */
|
|
52
56
|
maxLength?: number;
|
|
53
57
|
focused?: boolean;
|
|
58
|
+
disabled?: boolean;
|
|
54
59
|
width?: number;
|
|
55
60
|
inputRef?: RefObject<InputRenderable | null>;
|
|
56
61
|
onChange?: (value: string) => void;
|
|
@@ -60,7 +65,8 @@ export interface TextFieldProps {
|
|
|
60
65
|
type?: "text" | "password" | "date" | "email";
|
|
61
66
|
/** DOM autofill hint, e.g. "email" or "current-password"; terminal hosts ignore it. */
|
|
62
67
|
autoComplete?: string;
|
|
63
|
-
|
|
68
|
+
/** Well is a quiet filled field with an underline only while focused. */
|
|
69
|
+
variant?: "default" | "plain" | "well";
|
|
64
70
|
/**
|
|
65
71
|
* `comfortable` is the form-page size on the DOM renderer: a taller field
|
|
66
72
|
* with a tight label gap, for dialogs a user fills in rather than dense pane
|
|
@@ -117,7 +123,7 @@ function InlineTextField({ label = "", labelWidth = 0, active, width, hint, onMo
|
|
|
117
123
|
<Box flexDirection="column" width={width}>
|
|
118
124
|
<Box height={1} flexDirection="row" alignItems="center" gap={1} onMouseDown={onMouseDown}>
|
|
119
125
|
<FieldLabel label={label} active={active} width={labelWidth} />
|
|
120
|
-
<TextFieldControl {...props} width={inputWidth} onMouseDown={onMouseDown} />
|
|
126
|
+
<TextFieldControl {...props} accessibleLabel={props.accessibleLabel ?? label} width={inputWidth} onMouseDown={onMouseDown} />
|
|
121
127
|
</Box>
|
|
122
128
|
{hint ? (
|
|
123
129
|
<Box paddingLeft={labelWidth + 1}>
|
|
@@ -130,11 +136,13 @@ function InlineTextField({ label = "", labelWidth = 0, active, width, hint, onMo
|
|
|
130
136
|
|
|
131
137
|
function TextFieldControl({
|
|
132
138
|
label,
|
|
139
|
+
accessibleLabel,
|
|
133
140
|
active,
|
|
134
141
|
value,
|
|
135
142
|
placeholder,
|
|
136
143
|
maxLength,
|
|
137
144
|
focused,
|
|
145
|
+
disabled = false,
|
|
138
146
|
width,
|
|
139
147
|
inputRef,
|
|
140
148
|
onChange,
|
|
@@ -152,20 +160,20 @@ function TextFieldControl({
|
|
|
152
160
|
onKeyDown,
|
|
153
161
|
}: TextFieldProps) {
|
|
154
162
|
const colors = useThemeColors();
|
|
155
|
-
backgroundColor ??= colors.bg;
|
|
163
|
+
backgroundColor ??= variant === "well" ? colors.panel : colors.bg;
|
|
156
164
|
textColor ??= colors.text;
|
|
157
165
|
placeholderColor ??= colors.textDim;
|
|
158
166
|
useRemoteUiNode({
|
|
159
167
|
role: "text-field",
|
|
160
|
-
label: label ?? placeholder,
|
|
168
|
+
label: accessibleLabel ?? label ?? placeholder,
|
|
161
169
|
actions: {
|
|
162
170
|
setValue: (input) => {
|
|
163
171
|
const nextValue = remoteStringValue(input);
|
|
164
|
-
onChange?.(nextValue);
|
|
172
|
+
if (!disabled) onChange?.(nextValue);
|
|
165
173
|
},
|
|
166
174
|
submit: (input) => {
|
|
167
175
|
const nextValue = remoteStringValue(input, value ?? "");
|
|
168
|
-
onSubmit?.(nextValue);
|
|
176
|
+
if (!disabled) onSubmit?.(nextValue);
|
|
169
177
|
},
|
|
170
178
|
blur: () => onBlur?.(value ?? ""),
|
|
171
179
|
},
|
|
@@ -176,10 +184,12 @@ function TextFieldControl({
|
|
|
176
184
|
return (
|
|
177
185
|
<HostTextField
|
|
178
186
|
label={label}
|
|
187
|
+
accessibleLabel={accessibleLabel}
|
|
179
188
|
value={value}
|
|
180
189
|
placeholder={placeholder}
|
|
181
190
|
maxLength={maxLength}
|
|
182
|
-
focused={focused}
|
|
191
|
+
focused={focused && !disabled}
|
|
192
|
+
disabled={disabled}
|
|
183
193
|
width={width}
|
|
184
194
|
inputRef={inputRef}
|
|
185
195
|
onChange={onChange}
|
|
@@ -233,6 +243,7 @@ function TextFieldControl({
|
|
|
233
243
|
</Box>
|
|
234
244
|
)}
|
|
235
245
|
<Box height={1} onMouseDown={() => {
|
|
246
|
+
if (disabled) return;
|
|
236
247
|
onMouseDown?.();
|
|
237
248
|
resolvedInputRef.current?.focus?.();
|
|
238
249
|
}}>
|
|
@@ -243,16 +254,20 @@ function TextFieldControl({
|
|
|
243
254
|
selectable={!isPassword}
|
|
244
255
|
placeholder={isPassword ? "" : placeholder}
|
|
245
256
|
{...(maxLength === undefined ? {} : { maxLength })}
|
|
246
|
-
focused={focused}
|
|
257
|
+
focused={focused && !disabled}
|
|
247
258
|
textColor={isPassword ? backgroundColor : textColor}
|
|
248
259
|
placeholderColor={placeholderColor}
|
|
249
260
|
backgroundColor={backgroundColor}
|
|
250
261
|
selectionBg={isPassword ? backgroundColor : undefined}
|
|
251
262
|
selectionFg={isPassword ? backgroundColor : undefined}
|
|
252
263
|
showCursor={!isPassword}
|
|
253
|
-
onKeyDown={onKeyDown
|
|
264
|
+
onKeyDown={(event: Parameters<NonNullable<TextFieldProps["onKeyDown"]>>[0]) => {
|
|
265
|
+
if (disabled) { event.preventDefault(); event.stopPropagation(); return; }
|
|
266
|
+
onKeyDown?.(event);
|
|
267
|
+
}}
|
|
254
268
|
onCursorChange={() => syncCursorOffset()}
|
|
255
269
|
onInput={(nextValue: string) => {
|
|
270
|
+
if (disabled) return;
|
|
256
271
|
currentValueRef.current = nextValue;
|
|
257
272
|
syncCursorOffset(nextValue);
|
|
258
273
|
onChange?.(nextValue);
|
|
@@ -262,12 +277,12 @@ function TextFieldControl({
|
|
|
262
277
|
// the form has since cleared and swapped out still blurs when the
|
|
263
278
|
// next one takes focus, and passing its old text on would put it
|
|
264
279
|
// back into the cleared draft.
|
|
265
|
-
if (nextValue === currentValueRef.current) return;
|
|
280
|
+
if (disabled || nextValue === currentValueRef.current) return;
|
|
266
281
|
currentValueRef.current = nextValue;
|
|
267
282
|
syncCursorOffset(nextValue);
|
|
268
283
|
onChange?.(nextValue);
|
|
269
284
|
}}
|
|
270
|
-
onSubmit={() => onSubmit?.(currentValueRef.current)}
|
|
285
|
+
onSubmit={() => { if (!disabled) onSubmit?.(currentValueRef.current); }}
|
|
271
286
|
onBlur={() => onBlur?.(currentValueRef.current)}
|
|
272
287
|
/>
|
|
273
288
|
{isPassword && (
|
|
@@ -321,6 +336,10 @@ function sanitizeNumberInput(value: string, allowDecimal: boolean, allowNegative
|
|
|
321
336
|
export interface NumberFieldProps extends Omit<TextFieldProps, "onChange" | "onSubmit" | "onBlur"> {
|
|
322
337
|
allowDecimal?: boolean;
|
|
323
338
|
allowNegative?: boolean;
|
|
339
|
+
/** Opt-in shared minus/plus, pointer wheel and Up/Down stepping. Not an exchange tick unless the caller supplies one. */
|
|
340
|
+
step?: number;
|
|
341
|
+
min?: number;
|
|
342
|
+
max?: number;
|
|
324
343
|
onChange?: (value: string) => void;
|
|
325
344
|
onSubmit?: (value: string) => void;
|
|
326
345
|
onBlur?: (value: string) => void;
|
|
@@ -332,14 +351,75 @@ export function NumberField({
|
|
|
332
351
|
onChange,
|
|
333
352
|
onSubmit,
|
|
334
353
|
onBlur,
|
|
354
|
+
step,
|
|
355
|
+
min,
|
|
356
|
+
max,
|
|
335
357
|
...props
|
|
336
358
|
}: NumberFieldProps) {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
359
|
+
const colors = useThemeColors();
|
|
360
|
+
const native = useUiCapabilities().nativePaneChrome;
|
|
361
|
+
const well = props.variant === "well";
|
|
362
|
+
const valueRef = useRef(props.value ?? "");
|
|
363
|
+
valueRef.current = props.value ?? "";
|
|
364
|
+
const localInput = useRef<InputRenderable | null>(null);
|
|
365
|
+
const inputRef = props.inputRef ?? localInput;
|
|
366
|
+
const stepping = step !== undefined;
|
|
367
|
+
const pendingCursor = useRef<number | undefined>(undefined);
|
|
368
|
+
useLayoutEffect(() => {
|
|
369
|
+
const cursor = pendingCursor.current, input = inputRef.current;
|
|
370
|
+
if (cursor === undefined || !input) return;
|
|
371
|
+
pendingCursor.current = undefined;
|
|
372
|
+
const offset = Math.min(cursor, (props.value ?? "").length);
|
|
373
|
+
if (input.setCursorOffset) input.setCursorOffset(offset);
|
|
374
|
+
else input.cursorOffset = offset;
|
|
375
|
+
}, [props.value, inputRef]);
|
|
376
|
+
const change = (value: string) => {
|
|
377
|
+
if (props.disabled) return;
|
|
378
|
+
valueRef.current = value;
|
|
379
|
+
onChange?.(value);
|
|
380
|
+
};
|
|
381
|
+
const next = (direction: 1 | -1) => step === undefined ? undefined : stepNumber(valueRef.current, direction, step, min ?? (allowNegative ? undefined : 0), max);
|
|
382
|
+
const advance = (direction: 1 | -1) => {
|
|
383
|
+
if (props.disabled) return;
|
|
384
|
+
const value = next(direction);
|
|
385
|
+
if (value === undefined) return;
|
|
386
|
+
// Restore after the controlled value commits, not before the DOM input updates.
|
|
387
|
+
pendingCursor.current = inputRef.current?.cursorOffset;
|
|
388
|
+
change(value);
|
|
389
|
+
};
|
|
390
|
+
const field = <TextField
|
|
391
|
+
{...props}
|
|
392
|
+
variant={stepping && well ? "plain" : props.variant}
|
|
393
|
+
backgroundColor={well ? colors.panel : props.backgroundColor}
|
|
394
|
+
label={stepping ? undefined : props.label}
|
|
395
|
+
accessibleLabel={props.accessibleLabel ?? props.label}
|
|
396
|
+
inputRef={inputRef}
|
|
397
|
+
width={stepping && props.width !== undefined ? Math.max(6, props.width - 8) : props.width}
|
|
398
|
+
onChange={(value) => change(sanitizeNumberInput(value, allowDecimal, allowNegative))}
|
|
399
|
+
onSubmit={(value) => { if (!props.disabled) onSubmit?.(sanitizeNumberInput(value, allowDecimal, allowNegative)); }}
|
|
400
|
+
onBlur={(value) => { if (!props.disabled) onBlur?.(sanitizeNumberInput(value, allowDecimal, allowNegative)); }}
|
|
401
|
+
onKeyDown={(event) => {
|
|
402
|
+
if (stepping && (event.name === "up" || event.name === "down")) {
|
|
403
|
+
event.preventDefault(); event.stopPropagation(); advance(event.name === "up" ? 1 : -1); return;
|
|
404
|
+
}
|
|
405
|
+
props.onKeyDown?.(event);
|
|
406
|
+
}}
|
|
407
|
+
/>;
|
|
408
|
+
if (!stepping) return field;
|
|
409
|
+
const name = props.accessibleLabel ?? props.label ?? "value";
|
|
410
|
+
return <Box flexDirection="column" width={props.width}>
|
|
411
|
+
{props.label ? <FieldLabel label={props.label} active={props.active} /> : null}
|
|
412
|
+
<Box flexDirection="row" alignItems="center" gap={1} backgroundColor={well ? colors.panel : undefined}
|
|
413
|
+
// An outline says the well takes typing even before it has focus.
|
|
414
|
+
style={native && well ? { borderRadius: "var(--gloom-radius-sm)", paddingLeft: 6, boxShadow: props.focused ? `inset 0 0 0 1px ${colors.borderFocused}, inset 0 -2px ${colors.borderFocused}` : `inset 0 0 0 1px ${colors.border}` } : undefined}
|
|
415
|
+
onMouseScroll={(event: { scroll?: { direction?: string }; preventDefault(): void; stopPropagation(): void }) => {
|
|
416
|
+
if (props.disabled || !["up", "down"].includes(event.scroll?.direction ?? "")) return;
|
|
417
|
+
event.preventDefault(); event.stopPropagation();
|
|
418
|
+
advance(event.scroll?.direction === "up" ? 1 : -1);
|
|
419
|
+
}}>
|
|
420
|
+
{field}
|
|
421
|
+
<Button label={`Decrease ${name}`} displayLabel="-" variant={well ? "plain" : undefined} width={3} compact disabled={props.disabled || next(-1) === undefined} onPress={() => { inputRef.current?.focus?.(); advance(-1); }} />
|
|
422
|
+
<Button label={`Increase ${name}`} displayLabel="+" variant={well ? "plain" : undefined} width={3} compact disabled={props.disabled || next(1) === undefined} onPress={() => { inputRef.current?.focus?.(); advance(1); }} />
|
|
423
|
+
</Box>
|
|
424
|
+
</Box>;
|
|
345
425
|
}
|
|
@@ -36,7 +36,9 @@ export type IconName =
|
|
|
36
36
|
| "ask"
|
|
37
37
|
| "bot"
|
|
38
38
|
| "team"
|
|
39
|
-
| "pro"
|
|
39
|
+
| "pro"
|
|
40
|
+
| "clock"
|
|
41
|
+
| "edit";
|
|
40
42
|
|
|
41
43
|
/** Terminal glyph for each icon; the desktop host draws SVG. */
|
|
42
44
|
export const ICON_GLYPHS: Record<IconName, string> = {
|
|
@@ -79,6 +81,9 @@ export const ICON_GLYPHS: Record<IconName, string> = {
|
|
|
79
81
|
bot: "⍠",
|
|
80
82
|
team: "◉",
|
|
81
83
|
pro: "◊",
|
|
84
|
+
// Time, for data that arrives late (a delayed quote).
|
|
85
|
+
clock: "◷",
|
|
86
|
+
edit: "✎",
|
|
82
87
|
};
|
|
83
88
|
|
|
84
89
|
export interface IconProps {
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
import { Box, Text, TextAttributes } from "../../ui";
|
|
3
|
+
import { useThemeColors } from "../../theme/theme-context";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Each line's height, in its own type: room for the font's ascent and descent,
|
|
7
|
+
* so a line's letters sit inside its box in every engine, without relying on
|
|
8
|
+
* `text-box` trimming (WebKit before 18.2 ignores it). Half of the space
|
|
9
|
+
* above and below the letters also separates the name from the line under it.
|
|
10
|
+
*/
|
|
11
|
+
const LINE_HEIGHT = 1.2;
|
|
12
|
+
|
|
13
|
+
/** Room above and below a line for accents and descenders that reach past it. */
|
|
14
|
+
const INK_ROOM = "0.3em";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The line clips for the ellipsis, and WebKit clips both ways whatever the
|
|
18
|
+
* other axis says, so the clip box reaches past the line by padding, and an
|
|
19
|
+
* equal negative margin keeps the line where it was.
|
|
20
|
+
*/
|
|
21
|
+
const LINE_STYLE = {
|
|
22
|
+
display: "block",
|
|
23
|
+
minWidth: 0,
|
|
24
|
+
maxWidth: "100%",
|
|
25
|
+
lineHeight: LINE_HEIGHT,
|
|
26
|
+
overflow: "hidden",
|
|
27
|
+
paddingBlock: INK_ROOM,
|
|
28
|
+
marginBlock: `calc(-1 * ${INK_ROOM})`,
|
|
29
|
+
textOverflow: "ellipsis",
|
|
30
|
+
whiteSpace: "nowrap",
|
|
31
|
+
} as CSSProperties;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The art beside the two lines (a `LogoMark` of size `lead`) is a square
|
|
35
|
+
* exactly as tall as the two lines, in their type, so the name and the line
|
|
36
|
+
* under it always sit within its height, the block centred on it.
|
|
37
|
+
*/
|
|
38
|
+
const LEADING_STYLE: CSSProperties = {
|
|
39
|
+
flexShrink: 0,
|
|
40
|
+
fontSize: "var(--gloom-type-ui)",
|
|
41
|
+
width: `${2 * LINE_HEIGHT}em`,
|
|
42
|
+
height: `${2 * LINE_HEIGHT}em`,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export interface ListRowLinesProps {
|
|
46
|
+
label: string;
|
|
47
|
+
/** The muted line under the label, such as how a broker connects. */
|
|
48
|
+
description?: string;
|
|
49
|
+
/** Art before the lines, such as a broker's logo. Decoration: hidden from assistive technology. */
|
|
50
|
+
leading?: ReactNode;
|
|
51
|
+
/** The label's colour; the theme's text colour by default. */
|
|
52
|
+
labelColor?: string;
|
|
53
|
+
/** The label in bold, as a selected row's. */
|
|
54
|
+
labelBold?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A desktop and web list row's name with one muted line under it, and its
|
|
59
|
+
* logo before them, the block centred in the row. Every two-line row with
|
|
60
|
+
* art uses it (`ListView`'s `rowDescriptions`, the first-run guide's list),
|
|
61
|
+
* so the lines sit on the logo the same way everywhere.
|
|
62
|
+
*/
|
|
63
|
+
export function ListRowLines({ label, description, leading, labelColor, labelBold = false }: ListRowLinesProps) {
|
|
64
|
+
const colors = useThemeColors();
|
|
65
|
+
return (
|
|
66
|
+
<Box
|
|
67
|
+
flexDirection="row"
|
|
68
|
+
alignItems="center"
|
|
69
|
+
minWidth={0}
|
|
70
|
+
flexShrink={1}
|
|
71
|
+
data-gloom-role="list-row-lines"
|
|
72
|
+
style={{ gap: "var(--gloom-space-sm)", fontSize: "var(--gloom-type-ui)" }}
|
|
73
|
+
>
|
|
74
|
+
{leading !== undefined && (
|
|
75
|
+
<Box aria-hidden="true" data-gloom-role="list-row-leading" style={LEADING_STYLE}>
|
|
76
|
+
{leading}
|
|
77
|
+
</Box>
|
|
78
|
+
)}
|
|
79
|
+
<Box flexDirection="column" minWidth={0} flexShrink={1}>
|
|
80
|
+
<Text fg={labelColor ?? colors.text} attributes={labelBold ? TextAttributes.BOLD : 0} style={LINE_STYLE}>
|
|
81
|
+
{label}
|
|
82
|
+
</Text>
|
|
83
|
+
{description ? <Text fg={colors.textMuted} style={LINE_STYLE}>{description}</Text> : null}
|
|
84
|
+
</Box>
|
|
85
|
+
</Box>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -22,6 +22,12 @@ export interface ListViewItem {
|
|
|
22
22
|
disabled?: boolean;
|
|
23
23
|
/** Tints the label, e.g. a status in its tone; the words carry the meaning. */
|
|
24
24
|
color?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Artwork before the label on the desktop and the web, such as a broker's
|
|
27
|
+
* logo (`LogoMark`). It is decoration: the terminal does not draw it, and
|
|
28
|
+
* automation and screen readers read only `label`.
|
|
29
|
+
*/
|
|
30
|
+
leading?: ReactNode;
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
export interface ListRowState {
|
|
@@ -39,6 +45,12 @@ export interface ListViewProps {
|
|
|
39
45
|
renderRow?: (item: ListViewItem, state: ListRowState, index: number) => ReactNode;
|
|
40
46
|
getRowBackgroundColor?: (item: ListViewItem, state: ListRowState, index: number) => string | undefined;
|
|
41
47
|
showSelectedDescription?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Desktop and web: every row sets its `description` on a second line under
|
|
50
|
+
* its label, and is two rows tall unless `rowHeight` says otherwise. The
|
|
51
|
+
* terminal keeps one line per row, with `detail` at its end.
|
|
52
|
+
*/
|
|
53
|
+
rowDescriptions?: boolean;
|
|
42
54
|
emptyMessage?: string;
|
|
43
55
|
bgColor?: string;
|
|
44
56
|
selectedBgColor?: string;
|
|
@@ -173,6 +185,7 @@ export function ListView({
|
|
|
173
185
|
renderRow,
|
|
174
186
|
getRowBackgroundColor,
|
|
175
187
|
showSelectedDescription = false,
|
|
188
|
+
rowDescriptions,
|
|
176
189
|
emptyMessage = "Nothing to show.",
|
|
177
190
|
bgColor,
|
|
178
191
|
selectedBgColor,
|
|
@@ -243,6 +256,7 @@ export function ListView({
|
|
|
243
256
|
renderRow={renderRow}
|
|
244
257
|
getRowBackgroundColor={getRowBackgroundColor}
|
|
245
258
|
showSelectedDescription={showSelectedDescription}
|
|
259
|
+
rowDescriptions={rowDescriptions}
|
|
246
260
|
emptyMessage={emptyMessage}
|
|
247
261
|
bgColor={bgColor}
|
|
248
262
|
selectedBgColor={selectedBgColor}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
import { Box, ImageSurface, Text, TextAttributes, useUiCapabilities } from "../../ui";
|
|
3
|
+
import { useThemeColors } from "../../theme/theme-context";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `row`: beside a label in one row (a table cell's `leading`, a list row).
|
|
7
|
+
* `lead`: the art of a two-line list row; it fills the square `ListRowLines`
|
|
8
|
+
* sizes to the two lines beside it.
|
|
9
|
+
*/
|
|
10
|
+
type LogoMarkSize = "row" | "lead";
|
|
11
|
+
|
|
12
|
+
const TILE: Record<LogoMarkSize, { side: string; radius: string; type: string }> = {
|
|
13
|
+
// A pixel above and below in its row, as a chip.
|
|
14
|
+
row: { side: "calc(var(--cell-h) - 2px)", radius: "var(--gloom-radius-sm)", type: "var(--gloom-type-caption)" },
|
|
15
|
+
lead: { side: "100%", radius: "var(--gloom-radius-md)", type: "var(--gloom-type-ui)" },
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Sized by its capital and baseline, so flex centring puts the letter in the
|
|
19
|
+
// middle of the tile whatever the font's ascent and descent (as chrome text).
|
|
20
|
+
const LETTER_STYLE: Record<LogoMarkSize, CSSProperties> = {
|
|
21
|
+
row: { fontSize: TILE.row.type, lineHeight: 1, textBox: "trim-both cap alphabetic" } as CSSProperties,
|
|
22
|
+
lead: { fontSize: TILE.lead.type, lineHeight: 1, textBox: "trim-both cap alphabetic" } as CSSProperties,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export interface LogoMarkProps {
|
|
26
|
+
/** The logo, an image URL; without one, or when it fails to load, the tile shows `name`'s initial. */
|
|
27
|
+
src?: string | null;
|
|
28
|
+
/** What the logo stands for, such as a broker. Only its initial is drawn; the label beside it names it. */
|
|
29
|
+
name: string;
|
|
30
|
+
size?: LogoMarkSize;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function initialOf(name: string): string {
|
|
34
|
+
const letter = [...name.trim()].find((character) => /[\p{L}\p{N}]/u.test(character));
|
|
35
|
+
return letter ? letter.toLocaleUpperCase() : "?";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A logo in a square tile, for lists of brokers and the accounts they hold, on
|
|
40
|
+
* the desktop and the web. Decoration: hidden from assistive technology, so
|
|
41
|
+
* the label beside it stays the name. The terminal draws nothing.
|
|
42
|
+
*/
|
|
43
|
+
export function LogoMark({ src, name, size = "row" }: LogoMarkProps) {
|
|
44
|
+
const desktop = useUiCapabilities().nativePaneChrome === true;
|
|
45
|
+
const colors = useThemeColors();
|
|
46
|
+
if (!desktop) return null;
|
|
47
|
+
const tile = TILE[size];
|
|
48
|
+
return (
|
|
49
|
+
<ImageSurface
|
|
50
|
+
src={src ?? undefined}
|
|
51
|
+
alt=""
|
|
52
|
+
objectFit="cover"
|
|
53
|
+
aria-hidden="true"
|
|
54
|
+
data-gloom-role="logo-mark"
|
|
55
|
+
style={{ width: tile.side, height: tile.side, flexShrink: 0, borderRadius: tile.radius }}
|
|
56
|
+
>
|
|
57
|
+
<Box width="100%" height="100%" alignItems="center" justifyContent="center" backgroundColor={colors.border}>
|
|
58
|
+
<Text fg={colors.textBright} attributes={TextAttributes.BOLD} style={LETTER_STYLE[size]}>
|
|
59
|
+
{initialOf(name)}
|
|
60
|
+
</Text>
|
|
61
|
+
</Box>
|
|
62
|
+
</ImageSurface>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Decimal stepping on a zero-based increment grid, without binary rounding drift. */
|
|
2
|
+
function decimal(value: string | number): { units: bigint; places: number } | undefined {
|
|
3
|
+
const text = String(value).trim();
|
|
4
|
+
const match = /^([+-]?)(\d*)(?:\.(\d*))?(?:e([+-]?\d+))?$/i.exec(text);
|
|
5
|
+
if (!match || !`${match[2]}${match[3] ?? ""}` || !Number.isFinite(Number(text))) return;
|
|
6
|
+
const exponent = Number(match[4] ?? 0);
|
|
7
|
+
const places = (match[3]?.length ?? 0) - exponent;
|
|
8
|
+
if (Math.abs(places) > 18 || text.length > 100) return;
|
|
9
|
+
const units = BigInt(`${match[1] === "-" ? "-" : ""}${match[2] || "0"}${match[3] ?? ""}`);
|
|
10
|
+
return places < 0 ? { units: units * 10n ** BigInt(-places), places: 0 } : { units, places };
|
|
11
|
+
}
|
|
12
|
+
function floorDivide(a: bigint, b: bigint): bigint { return a / b - (a < 0n && a % b !== 0n ? 1n : 0n); }
|
|
13
|
+
function ceilDivide(a: bigint, b: bigint): bigint { return -floorDivide(-a, b); }
|
|
14
|
+
function format(units: bigint, places: number): string {
|
|
15
|
+
const digits = (units < 0n ? -units : units).toString().padStart(places + 1, "0");
|
|
16
|
+
const text = places ? `${digits.slice(0, -places)}.${digits.slice(-places)}`.replace(/\.?0+$/, "") : digits;
|
|
17
|
+
return `${units < 0n ? "-" : ""}${text}`;
|
|
18
|
+
}
|
|
19
|
+
export function stepNumber(value: string, direction: 1 | -1, step: number, min?: number, max?: number): string | undefined {
|
|
20
|
+
const current = decimal(value === "" ? 0 : value), increment = decimal(step);
|
|
21
|
+
const lower = min === undefined ? undefined : decimal(min), upper = max === undefined ? undefined : decimal(max);
|
|
22
|
+
if (!current || !increment || increment.units <= 0n || min !== undefined && !lower || max !== undefined && !upper) return;
|
|
23
|
+
const places = Math.max(current.places, increment.places, lower?.places ?? 0, upper?.places ?? 0);
|
|
24
|
+
const scaled = (n: { units: bigint; places: number }) => n.units * 10n ** BigInt(places - n.places);
|
|
25
|
+
const at = scaled(current), by = scaled(increment);
|
|
26
|
+
let ticks = direction > 0 ? floorDivide(at, by) + 1n : ceilDivide(at, by) - 1n;
|
|
27
|
+
const low = lower && ceilDivide(scaled(lower), by), high = upper && floorDivide(scaled(upper), by);
|
|
28
|
+
if (low !== undefined && high !== undefined && low > high) return;
|
|
29
|
+
if (low !== undefined && ticks < low) ticks = low;
|
|
30
|
+
if (high !== undefined && ticks > high) ticks = high;
|
|
31
|
+
const next = ticks * by;
|
|
32
|
+
// A bound must not make a plus go down or a minus go up, except an empty field.
|
|
33
|
+
if (value !== "" && (direction > 0 ? next <= at : next >= at)) return;
|
|
34
|
+
const text = format(next, places), roundTrip = decimal(Number(text));
|
|
35
|
+
if (!roundTrip || roundTrip.places > places || scaled(roundTrip) !== next) return;
|
|
36
|
+
return text;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Smallest whole-number multiple, for brokers combining a step with whole quantities. */
|
|
40
|
+
export function wholeNumberStep(step: number): number | undefined {
|
|
41
|
+
const value = decimal(step);
|
|
42
|
+
if (!value || value.units <= 0n) return;
|
|
43
|
+
let a = value.units, b = 10n ** BigInt(value.places);
|
|
44
|
+
while (b) { const remainder = a % b; a = b; b = remainder; }
|
|
45
|
+
const result = Number(value.units / a);
|
|
46
|
+
return Number.isSafeInteger(result) ? result : undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function matchesNumberStep(value: number, step: number): boolean {
|
|
50
|
+
const n = decimal(value), s = decimal(step);
|
|
51
|
+
if (!n || !s || s.units <= 0n) return false;
|
|
52
|
+
const places = Math.max(n.places, s.places);
|
|
53
|
+
return n.units * 10n ** BigInt(places - n.places) % (s.units * 10n ** BigInt(places - s.places)) === 0n;
|
|
54
|
+
}
|
|
@@ -48,9 +48,10 @@ export interface SelectButtonProps<T extends string = string> {
|
|
|
48
48
|
* `inline`: the label and the value in a row, in a toolbar. `field`: a
|
|
49
49
|
* bordered field. `form`: a field in a form of text fields; on the desktop
|
|
50
50
|
* and the web its label sits above it, bold, as a TextField's does; the
|
|
51
|
-
* terminal draws it inline.
|
|
51
|
+
* terminal draws it inline. `well`: a field without its border, on a
|
|
52
|
+
* recessed fill, for a dense panel such as the order ticket.
|
|
52
53
|
*/
|
|
53
|
-
variant?: "inline" | "field" | "form";
|
|
54
|
+
variant?: "inline" | "field" | "form" | "well";
|
|
54
55
|
width?: number;
|
|
55
56
|
onFocus?: () => void;
|
|
56
57
|
/**
|
|
@@ -189,8 +190,8 @@ export function SelectButton<T extends string = string>({
|
|
|
189
190
|
}
|
|
190
191
|
|
|
191
192
|
// The terminal draws a form select as it draws an inline one.
|
|
192
|
-
if (variant === "field") {
|
|
193
|
-
return <Button label={showLabel ? `${label}: ${current?.short ?? current?.label ?? ""}` : current?.short ?? current?.label ?? label} width={width} active={emphasized} disabled={disabled} onPress={openDialog} stopPropagation />;
|
|
193
|
+
if (variant === "field" || variant === "well") {
|
|
194
|
+
return <Button label={showLabel ? `${label}: ${current?.short ?? current?.label ?? ""}` : current?.short ?? current?.label ?? label} width={width} variant={variant === "well" ? "quiet" : undefined} active={emphasized} disabled={disabled} onPress={openDialog} stopPropagation />;
|
|
194
195
|
}
|
|
195
196
|
|
|
196
197
|
return (
|
|
@@ -22,10 +22,10 @@ export interface SelectFieldProps {
|
|
|
22
22
|
width?: number | string;
|
|
23
23
|
height?: number;
|
|
24
24
|
/**
|
|
25
|
-
* `field` is the form control used in dialogs. `
|
|
26
|
-
*
|
|
25
|
+
* `field` is the framed form control used in dialogs. `well` uses a quiet
|
|
26
|
+
* filled surface; `inline` reads as text. All open the same Menu.
|
|
27
27
|
*/
|
|
28
|
-
variant?: "field" | "inline";
|
|
28
|
+
variant?: "field" | "inline" | "well";
|
|
29
29
|
includeUnsetOption?: boolean;
|
|
30
30
|
/**
|
|
31
31
|
* Return focus to the trigger when the menu closes (inside a dialog). A list
|
|
@@ -5,6 +5,7 @@ import { Box, Text, useUiCapabilities } from "../../ui";
|
|
|
5
5
|
import { formatRelativeAge } from "../../utils/datetime-format";
|
|
6
6
|
import { Spinner } from "./loading";
|
|
7
7
|
import { ButtonActionScope } from "./action-scope";
|
|
8
|
+
import { blendForContrast, higherContrast } from "../../theme/color-utils";
|
|
8
9
|
import { Button } from "./button";
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -233,14 +234,27 @@ function EmptyStateBody({ title, message, hint, actions, retry, status = "empty"
|
|
|
233
234
|
export interface NoticeProps {
|
|
234
235
|
children: ReactNode;
|
|
235
236
|
tone?: "muted" | "positive" | "warning" | "negative";
|
|
237
|
+
variant?: "inline" | "callout";
|
|
236
238
|
}
|
|
237
239
|
|
|
238
240
|
/** Inline feedback leaves existing content visible, including stale data after a refresh failure. */
|
|
239
|
-
export function Notice({ children, tone = "warning" }: NoticeProps) {
|
|
241
|
+
export function Notice({ children, tone = "warning", variant = "inline" }: NoticeProps) {
|
|
240
242
|
const colors = useThemeColors();
|
|
243
|
+
const native = useUiCapabilities().nativePaneChrome;
|
|
244
|
+
const color = tone === "muted" ? colors.textDim : colors[tone];
|
|
245
|
+
const status = tone === "negative" ? "error" : "notice";
|
|
246
|
+
// Inline feedback keeps its original single-text shape: a flex row here collapses inside content-sized parents.
|
|
247
|
+
if (variant !== "callout") {
|
|
248
|
+
return (
|
|
249
|
+
<Box data-gloom-status={status} data-gloom-ui="notice">
|
|
250
|
+
<Text fg={color} wrapText>{children}</Text>
|
|
251
|
+
</Box>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
const foreground = blendForContrast(color, colors.panel, higherContrast("#000000", "#ffffff", colors.panel), 4.5);
|
|
241
255
|
return (
|
|
242
|
-
<Box data-gloom-status={tone === "negative" ? "
|
|
243
|
-
<Text fg={
|
|
256
|
+
<Box flexDirection="row" gap={1} paddingX={1} paddingY={native ? 0.35 : 0} backgroundColor={colors.panel} border={["left"]} borderColor={color} data-gloom-status={status} data-gloom-ui="notice" role={native ? tone === "negative" ? "alert" : "status" : undefined} aria-atomic={native ? true : undefined}>
|
|
257
|
+
<Text fg={foreground}>!</Text><Box flexGrow={1} minWidth={0}><Text fg={foreground} wrapText>{children}</Text></Box>
|
|
244
258
|
</Box>
|
|
245
259
|
);
|
|
246
260
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useId, useMemo, useRef, useState, type ReactNode } from "react";
|
|
2
2
|
import { usePaneMenuItems, usePaneTabStripKeys } from "../layout/pane/footer/registration";
|
|
3
3
|
import type { ContextMenuItem } from "../../types/context-menu";
|
|
4
4
|
import { useShortcut } from "../../react/input";
|
|
@@ -22,6 +22,14 @@ type TabPointerEvent = {
|
|
|
22
22
|
interface TabItem {
|
|
23
23
|
label: string;
|
|
24
24
|
value: string;
|
|
25
|
+
/**
|
|
26
|
+
* Artwork before the label on the desktop and the web, such as the logo of
|
|
27
|
+
* the broker a portfolio's account is at (`LogoMark`). It is decoration: the
|
|
28
|
+
* terminal does not draw it, and automation and screen readers read only
|
|
29
|
+
* `label`. Keep the same element between renders (useMemo): a title-bar
|
|
30
|
+
* strip redraws when it changes.
|
|
31
|
+
*/
|
|
32
|
+
leading?: ReactNode;
|
|
25
33
|
disabled?: boolean;
|
|
26
34
|
/** Text color while inactive; used for group markers such as a team accent. */
|
|
27
35
|
fg?: string;
|