Polish trading UI and add launch roadmap
This commit is contained in:
parent
041e5e6f63
commit
1994821acf
7
.npmrc
7
.npmrc
@ -1,5 +1,2 @@
|
||||
@bytelyst:registry=https://gitea.bytelyst.com/api/packages/ByteLyst/npm/
|
||||
# Gitea auth tokens belong in user-level ~/.npmrc or CI secrets, and are only needed
|
||||
# when BYTELYST_PACKAGE_SOURCE=gitea.
|
||||
# Gitea returns Docker-internal tarball URLs (172.17.0.1:3300); rewrite host to the public URL
|
||||
replace-registry-host=always
|
||||
link-workspace-packages=true
|
||||
prefer-workspace-packages=true
|
||||
|
||||
@ -2,7 +2,8 @@ const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const PACKAGE_SCOPE = '@bytelyst/';
|
||||
const PACKAGE_SOURCE = process.env.BYTELYST_PACKAGE_SOURCE || 'common-plat';
|
||||
const requestedPackageSource = process.env.BYTELYST_PACKAGE_SOURCE || 'common-plat';
|
||||
const PACKAGE_SOURCE = requestedPackageSource === 'vendor' ? 'vendor' : 'common-plat';
|
||||
const DEFAULT_COMMON_PLAT_ROOTS = [
|
||||
path.resolve(__dirname, '..', 'learning_ai_common_plat'),
|
||||
'/opt/bytelyst/learning_ai_common_plat',
|
||||
@ -70,11 +71,6 @@ function resolveSpecifier(name) {
|
||||
return packagePath ? `file:${packagePath}` : null;
|
||||
}
|
||||
|
||||
if (PACKAGE_SOURCE === 'gitea') {
|
||||
const version = resolveRegistryVersion(name);
|
||||
return version ?? null;
|
||||
}
|
||||
|
||||
const vendorPath = pathIfPackageExists(VENDOR_PACKAGES_ROOT, name);
|
||||
if (vendorPath) {
|
||||
return `file:${vendorPath}`;
|
||||
|
||||
398
docs/LAUNCH_READY_UI_UX_ROADMAP.md
Normal file
398
docs/LAUNCH_READY_UI_UX_ROADMAP.md
Normal file
@ -0,0 +1,398 @@
|
||||
# Launch-Ready UI/UX Roadmap
|
||||
|
||||
Last updated: 2026-05-08
|
||||
|
||||
## Purpose
|
||||
|
||||
This roadmap defines the path to make the trading web product feel modern, coherent, professional, and launch-ready across the entire app. It covers the product frontend in `learning_ai_invt_trdg` and the shared UI/design-system foundation in `learning_ai_common_plat`.
|
||||
|
||||
The goal is not another styling pass. The goal is a stable product-quality experience with reusable platform components that can be applied to this product and future ByteLyst products.
|
||||
|
||||
## Product Standard
|
||||
|
||||
The target experience is a polished operational SaaS console for trading workflows:
|
||||
|
||||
- Calm, readable, premium, and practical.
|
||||
- Dense enough for portfolio/trading operations without feeling cramped.
|
||||
- Consistent component behavior across all routes.
|
||||
- Clear primary actions and obvious next steps.
|
||||
- Strong loading, empty, error, disabled, and success states.
|
||||
- Responsive across desktop, tablet, and phone without broken menus, overflow, or hidden content.
|
||||
- Accessible by keyboard and readable under normal contrast settings.
|
||||
|
||||
## Current State Summary
|
||||
|
||||
The app is functional, but launch readiness is blocked by inconsistent UI systems:
|
||||
|
||||
- The shared platform primitives exist, but many product screens still use raw page-specific styling.
|
||||
- `web/src/index.css` and `web/src/App.css` carry too much global/product-specific behavior.
|
||||
- Complex screens such as Trade Plans, Portfolio, Backtesting, Settings/Admin, Visual Builder, and Code Editor still contain legacy visual patterns.
|
||||
- Tables, cards, badges, timeline steps, forms, tabs, alerts, and empty states do not yet fully share one design language.
|
||||
- Some routes look modern after recent improvements, while deeper states still look like developer tooling.
|
||||
- Responsive behavior has improved, but the shell still needs a formal contract for desktop/tablet/phone.
|
||||
- Global overlays such as critical alerts and the assistant widget need collision rules.
|
||||
|
||||
## Guiding Principles
|
||||
|
||||
1. Centralize primitives in common platform.
|
||||
2. Compose product screens from approved primitives.
|
||||
3. Prefer quiet utility over decorative UI.
|
||||
4. Keep cards for bounded tools/items, not every page section.
|
||||
5. Use semantic colors only for meaning: success, warning, danger, info.
|
||||
6. Use monospace only for symbols, prices, IDs, and technical values.
|
||||
7. Avoid raw HTML controls in product screens unless wrapped by a shared primitive.
|
||||
8. Every user-facing state needs loading, empty, error, disabled, and success treatment.
|
||||
9. Every route must pass responsive, accessibility, and overflow checks before launch.
|
||||
|
||||
## Phase 0: Stop UI Drift
|
||||
|
||||
Objective: prevent new inconsistent UI while remediation is underway.
|
||||
|
||||
Scope:
|
||||
|
||||
- Expand `pnpm audit:ui` to flag:
|
||||
- raw `<button>`, `<input>`, `<select>`, `<textarea>` in production components
|
||||
- inline `style` usage outside approved exceptions
|
||||
- legacy dark classes such as `bg-black`, `bg-zinc`, `text-gray`, `border-white`
|
||||
- arbitrary `rounded-*`, `tracking-*`, and one-off uppercase styling in product components
|
||||
- direct `@bytelyst/ui` imports outside the product adapter
|
||||
- Quarantine legacy global styles in `App.css`.
|
||||
- Create a migration issue list from audit output.
|
||||
- Require new UI work to use common/product primitives.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- Audit runs locally and in CI.
|
||||
- New raw controls and legacy surface classes fail the audit.
|
||||
- Known legacy exceptions are documented and temporary.
|
||||
|
||||
## Phase 1: Platform Design System Foundation
|
||||
|
||||
Objective: make `learning_ai_common_plat` the real source of reusable UI quality.
|
||||
|
||||
Common platform components to standardize or add:
|
||||
|
||||
- `AppShell`: navigation, topbar, alert region, right panel slot, responsive contract.
|
||||
- `PageHeader`: title, subtitle, actions, breadcrumbs, compact mode.
|
||||
- `Section`: consistent section spacing, header, actions, description.
|
||||
- `Toolbar`: compact action groups.
|
||||
- `FilterBar`: search, selects, chips, refresh, and responsive wrapping.
|
||||
- `DataTable`: sticky header, density modes, row actions, loading, empty, error, horizontal scroll.
|
||||
- `EntityCard`: reusable card pattern for saved setups, watchlist entries, strategy profiles, and presets.
|
||||
- `FormSection`: sectioned form layout with labels, hints, errors, disabled/read-only states.
|
||||
- `FieldGrid`: responsive field layout.
|
||||
- `StatusBadge`: semantic states with consistent icon/color/label behavior.
|
||||
- `Timeline`: compact and detailed variants.
|
||||
- `AlertBanner`: inline page alerts and global critical alerts.
|
||||
- `Toast`: success/error/status feedback.
|
||||
- `ConfirmDialog`: destructive action confirmation.
|
||||
- `Skeleton`: route, card, table, and form skeleton variants.
|
||||
- `EmptyState`: title, body, action, icon, compact/full variants.
|
||||
|
||||
Token work:
|
||||
|
||||
- Finalize light theme tokens.
|
||||
- Normalize dark theme tokens after light theme is stable.
|
||||
- Define component-level radius, shadow, border, spacing, and typography tokens.
|
||||
- Add token documentation in Storybook.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- Common UI Storybook demonstrates every primitive with light and dark examples.
|
||||
- Product adapter maps common primitives to the trading app without duplicating logic.
|
||||
- At least 80 percent of product UI composition uses shared primitives or product adapters.
|
||||
|
||||
## Phase 2: Product Shell And Global UX
|
||||
|
||||
Objective: make the app frame stable and professional everywhere.
|
||||
|
||||
Scope:
|
||||
|
||||
- Replace product-local shell with platform `AppShell` or align it to the same contract.
|
||||
- Desktop: full left rail or compact left rail depending on width.
|
||||
- Tablet: compact left rail, no footer nav.
|
||||
- Phone: bottom nav only.
|
||||
- Right panel: desktop only, collapsible later if needed.
|
||||
- Critical alert: inline/sticky shell region, never covering form controls or cards.
|
||||
- Assistant widget: collision-safe offset and route-aware placement.
|
||||
- Route fallback: page-shaped skeleton instead of blank loading.
|
||||
- Global overflow policy: no route can widen the viewport accidentally.
|
||||
- Theme default: stable light mode; dark mode as a tested secondary mode.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- No route has horizontal page overflow at 390, 768, 1024, 1440 px.
|
||||
- Nav never appears as a broken full-page vertical menu.
|
||||
- Critical alerts and assistant controls do not cover primary actions.
|
||||
|
||||
## Phase 3: Core Trading Screen Rebuild
|
||||
|
||||
Objective: polish the highest-value product workflows first.
|
||||
|
||||
### 3.1 Trade Plans
|
||||
|
||||
Current issues:
|
||||
|
||||
- Complex form sections still feel dense.
|
||||
- Saved setup cards were improved but should become a first-class `EntityCard`.
|
||||
- Metadata and runtime state need better hierarchy.
|
||||
- Timeline should be a shared component.
|
||||
|
||||
Work:
|
||||
|
||||
- Convert create/edit flow into a guided plan builder.
|
||||
- Use `FormSection`, `FieldGrid`, `StatusBadge`, `Timeline`, and `EntityCard`.
|
||||
- Add saved setup filters: active, waiting, filled, closed, needs attention.
|
||||
- Add success toast after save/update/delete.
|
||||
- Add confirm dialog for delete.
|
||||
- Add compact card view and detail drawer for advanced runtime/event history.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- A new user can create or review a plan without reading dense operational text.
|
||||
- Saved setup cards fit comfortably on tablet and desktop.
|
||||
- Delete/edit/manage actions are clear and not visually dominant.
|
||||
|
||||
### 3.2 Portfolio
|
||||
|
||||
Current issues:
|
||||
|
||||
- `PositionsTab` is large, table-heavy, and still carries legacy dark utility styles.
|
||||
- Diagnostic/status messages can force overflow.
|
||||
- Mobile/tablet experience needs a card alternative.
|
||||
|
||||
Work:
|
||||
|
||||
- Rebuild positions and order activity with common `DataTable`.
|
||||
- Use compact `AlertBanner` for lifecycle/fallback warnings.
|
||||
- Create responsive card view below tablet width.
|
||||
- Replace raw profile filter with `SegmentedControl`.
|
||||
- Add table skeleton, empty, error, and retry states.
|
||||
- Add row-level action menus for plan management.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- No warning or trade ID can break layout.
|
||||
- Position/order data scans cleanly.
|
||||
- The page remains usable with many rows and long identifiers.
|
||||
|
||||
### 3.3 Research
|
||||
|
||||
Current issues:
|
||||
|
||||
- Strategies tab looks better, but builder/editor/backtest surfaces use different visual systems.
|
||||
- Backtest components are still dark/zinc styled.
|
||||
|
||||
Work:
|
||||
|
||||
- Normalize research tabs with shared `Tabs`.
|
||||
- Convert strategy cards to `EntityCard`.
|
||||
- Convert Visual Builder forms to `FormSection` and common controls.
|
||||
- Convert Code Editor shell to a polished tool panel.
|
||||
- Rebuild Backtest panels with common cards, tables, metric cards, charts, and alerts.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- Strategies, Visual Builder, Code Editor, Signals, and Backtesting feel like one product.
|
||||
- Backtest UI no longer looks like a separate app.
|
||||
|
||||
### 3.4 Screener
|
||||
|
||||
Current issues:
|
||||
|
||||
- Filter layout has improved but needs a final `FilterBar`.
|
||||
- Table states should use shared `DataTable`.
|
||||
|
||||
Work:
|
||||
|
||||
- Move filters to `FilterBar`.
|
||||
- Convert results to `DataTable`.
|
||||
- Add query count, active filter chips, reset filters.
|
||||
- Improve API error copy and retry.
|
||||
- Add row action affordance for opening chart/research.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- Filters are readable on tablet.
|
||||
- Result table has consistent empty/loading/error behavior.
|
||||
|
||||
### 3.5 Watchlist
|
||||
|
||||
Current issues:
|
||||
|
||||
- Empty state is improved, but entries should use reusable cards.
|
||||
- Add/edit flow should feel like a proper modal or side panel.
|
||||
|
||||
Work:
|
||||
|
||||
- Convert entry cards to `EntityCard`.
|
||||
- Move add/edit into a modal or drawer.
|
||||
- Add toasts for create/update/delete/clone.
|
||||
- Add confirmation for delete.
|
||||
- Add filter counts.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- Empty and populated states both look complete.
|
||||
- Entry management feels safe and predictable.
|
||||
|
||||
## Phase 4: Secondary And Admin Surfaces
|
||||
|
||||
Objective: remove remaining developer-tool styling from lower-frequency screens.
|
||||
|
||||
Scope:
|
||||
|
||||
- Settings account page.
|
||||
- Bot Config.
|
||||
- Admin Panel.
|
||||
- Alerts.
|
||||
- Markets and Marketplace.
|
||||
- Login, reset password, auth error states.
|
||||
- Right panel portfolio/news.
|
||||
- Chat assistant panel and floating button.
|
||||
|
||||
Work:
|
||||
|
||||
- Convert Settings/Admin/Config to `FormSection`, `DataTable`, `AlertBanner`, and `ConfirmDialog`.
|
||||
- Add real Alerts filters by severity/type/symbol.
|
||||
- Convert Markets/Marketplace to common cards and skeletons.
|
||||
- Improve right panel empty states and links.
|
||||
- Ensure login/reset flows use common auth UI from platform where possible.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- No screen looks like raw admin tooling.
|
||||
- Low-frequency screens are safe, clear, and consistent.
|
||||
|
||||
## Phase 5: Interaction And Feedback Quality
|
||||
|
||||
Objective: make the app feel trustworthy under real usage.
|
||||
|
||||
Add consistently:
|
||||
|
||||
- Toasts for create, update, delete, clone, save, refresh.
|
||||
- Confirmation dialogs for destructive actions.
|
||||
- Inline validation for all forms.
|
||||
- Disabled-state explanations.
|
||||
- Retry affordances for network errors.
|
||||
- Skeletons that match final layout.
|
||||
- Empty states with useful next action.
|
||||
- Success states that confirm what changed.
|
||||
- Optimistic or pending states where appropriate.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- A user never wonders whether an action worked.
|
||||
- Failed network/API states are recoverable.
|
||||
- Destructive actions are protected.
|
||||
|
||||
## Phase 6: Accessibility And Responsive QA
|
||||
|
||||
Objective: launch with confidence across device sizes and input modes.
|
||||
|
||||
Checks:
|
||||
|
||||
- Keyboard-only navigation.
|
||||
- Visible focus states.
|
||||
- ARIA labels for icon buttons.
|
||||
- Form labels and errors.
|
||||
- Color contrast for text, badges, and disabled states.
|
||||
- Reduced motion behavior.
|
||||
- Screen-reader-friendly empty/loading/error states.
|
||||
- No text clipping in buttons, cards, tabs, or chips.
|
||||
- No horizontal viewport overflow.
|
||||
|
||||
Viewport matrix:
|
||||
|
||||
- 390 px phone.
|
||||
- 768 px tablet.
|
||||
- 1024 px small desktop/tablet landscape.
|
||||
- 1440 px desktop.
|
||||
- 1728 px wide desktop.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- Every route passes visual QA at all target widths.
|
||||
- Every primary workflow works with keyboard navigation.
|
||||
|
||||
## Phase 7: Visual Regression And CI Gates
|
||||
|
||||
Objective: keep the app launch-ready after the cleanup.
|
||||
|
||||
Tests and automation:
|
||||
|
||||
- Playwright route screenshot suite.
|
||||
- Axe accessibility scan.
|
||||
- Horizontal overflow test.
|
||||
- Component smoke tests for common primitives.
|
||||
- Storybook snapshots for shared UI.
|
||||
- CI audit for raw controls, inline styles, and legacy classes.
|
||||
- Route-level tests for empty/loading/error states.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- CI blocks UI drift.
|
||||
- Screenshots make regressions obvious.
|
||||
- Design system changes can be safely adopted by other products.
|
||||
|
||||
## Suggested Implementation Sequence
|
||||
|
||||
### PR 1: Foundation And Guardrails
|
||||
|
||||
- Expand UI drift audit.
|
||||
- Introduce missing common primitives.
|
||||
- Move product shell toward common shell contract.
|
||||
- Add Storybook coverage for primitives.
|
||||
|
||||
### PR 2: Core Screen Rebuild
|
||||
|
||||
- Trade Plans.
|
||||
- Portfolio.
|
||||
- Watchlist.
|
||||
- Screener.
|
||||
|
||||
### PR 3: Research And Backtesting
|
||||
|
||||
- Research tab shell.
|
||||
- Visual Builder.
|
||||
- Code Editor.
|
||||
- Backtest configuration/results.
|
||||
|
||||
### PR 4: Settings, Admin, Markets, Alerts
|
||||
|
||||
- Settings and Bot Config.
|
||||
- Admin Panel.
|
||||
- Markets and Marketplace.
|
||||
- Alerts.
|
||||
- Right panel.
|
||||
|
||||
### PR 5: Launch QA
|
||||
|
||||
- Visual regression.
|
||||
- Accessibility pass.
|
||||
- Responsive pass.
|
||||
- Final polish and copy pass.
|
||||
|
||||
## Product Launch Readiness Checklist
|
||||
|
||||
- [ ] Shared primitives cover app shell, page header, sections, cards, forms, tables, tabs, badges, timelines, alerts, toasts, modals, skeletons, and empty states.
|
||||
- [ ] No production route uses raw controls outside approved wrappers.
|
||||
- [ ] No production route has accidental horizontal overflow.
|
||||
- [ ] Critical alerts never cover primary content.
|
||||
- [ ] Assistant widget never covers primary actions.
|
||||
- [ ] All destructive actions require confirmation.
|
||||
- [ ] All saves/deletes/updates produce feedback.
|
||||
- [ ] All pages have loading, empty, error, and success states.
|
||||
- [ ] All pages pass keyboard navigation checks.
|
||||
- [ ] All routes are visually checked at phone, tablet, desktop, and wide desktop widths.
|
||||
- [ ] UI screenshots are captured in CI.
|
||||
- [ ] Common platform UI can be reused by another product without trading-specific assumptions.
|
||||
|
||||
## Immediate Next Recommendation
|
||||
|
||||
Start with PR 1 and PR 2 together as the next focused milestone:
|
||||
|
||||
1. Harden the shared design system and audit gates.
|
||||
2. Rebuild Trade Plans and Portfolio using those primitives.
|
||||
|
||||
These two screens carry the most product credibility risk. Once they are polished, the rest of the app can converge around the same patterns faster and with less rework.
|
||||
@ -9,7 +9,6 @@
|
||||
"audit:ui": "sh ./scripts/ui-drift-audit.sh",
|
||||
"audit:ui:strict": "sh ./scripts/ui-drift-audit.sh strict",
|
||||
"install:common-plat": "BYTELYST_PACKAGE_SOURCE=common-plat pnpm install -r",
|
||||
"install:gitea": "BYTELYST_PACKAGE_SOURCE=gitea pnpm install -r",
|
||||
"install:vendor": "BYTELYST_PACKAGE_SOURCE=vendor pnpm install -r",
|
||||
"lint": "pnpm --filter @bytelyst/trading-backend lint && pnpm --filter @bytelyst/trading-web lint && pnpm --filter @bytelyst/trading-mobile lint",
|
||||
"smoke:release": "sh ./scripts/smoke-release.sh",
|
||||
@ -22,10 +21,10 @@
|
||||
"docker:down": "docker compose down"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bytelyst/kill-switch-client": "file:./vendor/bytelyst/kill-switch-client",
|
||||
"@bytelyst/react-native-platform-sdk": "file:./vendor/bytelyst/react-native-platform-sdk",
|
||||
"@bytelyst/react-auth": "file:./vendor/bytelyst/react-auth",
|
||||
"@bytelyst/telemetry-client": "file:./vendor/bytelyst/telemetry-client"
|
||||
"@bytelyst/kill-switch-client": "file:../learning_ai_common_plat/packages/kill-switch-client",
|
||||
"@bytelyst/react-native-platform-sdk": "file:../learning_ai_common_plat/packages/react-native-platform-sdk",
|
||||
"@bytelyst/react-auth": "file:../learning_ai_common_plat/packages/react-auth",
|
||||
"@bytelyst/telemetry-client": "file:../learning_ai_common_plat/packages/telemetry-client"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.9.3"
|
||||
|
||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@ -4,7 +4,7 @@ settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
pnpmfileChecksum: sha256-9nrEUcRW1dVQY29GEuBWlallHSWZBRpsHo3Y3rZLLdw=
|
||||
pnpmfileChecksum: sha256-6gxhEjw/htOhRcZhr2u1v7hYwxfTMaTz52IWFOJV6+k=
|
||||
|
||||
importers:
|
||||
|
||||
@ -2694,6 +2694,7 @@ packages:
|
||||
|
||||
'@ungap/structured-clone@1.3.0':
|
||||
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
|
||||
deprecated: Potential CWE-502 - Update to 1.3.1 or higher
|
||||
|
||||
'@unrs/resolver-binding-android-arm-eabi@1.11.1':
|
||||
resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==}
|
||||
@ -2845,6 +2846,7 @@ packages:
|
||||
'@xmldom/xmldom@0.8.12':
|
||||
resolution: {integrity: sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
deprecated: this version has critical issues, please update to the latest version
|
||||
|
||||
abort-controller@3.0.0:
|
||||
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
|
||||
@ -5198,6 +5200,7 @@ packages:
|
||||
nats@1.4.12:
|
||||
resolution: {integrity: sha512-Jf4qesEF0Ay0D4AMw3OZnKMRTQm+6oZ5q8/m4gpy5bTmiDiK6wCXbZpzEslmezGpE93LV3RojNEG6dpK/mysLQ==}
|
||||
engines: {node: '>= 8.0.0'}
|
||||
deprecated: Package moved. Use @nats-io/transport-node from https://github.com/nats-io/nats.js
|
||||
hasBin: true
|
||||
|
||||
natural-compare@1.4.0:
|
||||
@ -5255,6 +5258,7 @@ packages:
|
||||
nuid@1.1.6:
|
||||
resolution: {integrity: sha512-Eb3CPCupYscP1/S1FQcO5nxtu6l/F3k0MQ69h7f5osnsemVk5pkc8/5AyalVT+NCfra9M71U8POqF6EZa6IHvg==}
|
||||
engines: {node: '>= 8.16.0'}
|
||||
deprecated: 'Package deprecated. Use @nats-io/nuid instead: https://www.npmjs.com/package/@nats-io/nuid'
|
||||
|
||||
nullthrows@1.1.1:
|
||||
resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==}
|
||||
@ -6470,10 +6474,12 @@ packages:
|
||||
|
||||
uuid@7.0.3:
|
||||
resolution: {integrity: sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==}
|
||||
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
|
||||
hasBin: true
|
||||
|
||||
uuid@8.3.2:
|
||||
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
||||
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
|
||||
hasBin: true
|
||||
|
||||
v8-compile-cache-lib@3.0.1:
|
||||
@ -9317,7 +9323,9 @@ snapshots:
|
||||
metro-runtime: 0.83.5
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- bufferutil
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
'@react-native/normalize-colors@0.74.89': {}
|
||||
|
||||
|
||||
@ -18,13 +18,13 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bytelyst/api-client": "file:../vendor/bytelyst/api-client",
|
||||
"@bytelyst/design-tokens": "^0.1.5",
|
||||
"@bytelyst/errors": "file:../vendor/bytelyst/errors",
|
||||
"@bytelyst/kill-switch-client": "file:../vendor/bytelyst/kill-switch-client",
|
||||
"@bytelyst/react-auth": "file:../vendor/bytelyst/react-auth",
|
||||
"@bytelyst/telemetry-client": "file:../vendor/bytelyst/telemetry-client",
|
||||
"@bytelyst/ui": "^0.1.5",
|
||||
"@bytelyst/api-client": "file:../../learning_ai_common_plat/packages/api-client",
|
||||
"@bytelyst/design-tokens": "file:../../learning_ai_common_plat/packages/design-tokens",
|
||||
"@bytelyst/errors": "file:../../learning_ai_common_plat/packages/errors",
|
||||
"@bytelyst/kill-switch-client": "file:../../learning_ai_common_plat/packages/kill-switch-client",
|
||||
"@bytelyst/react-auth": "file:../../learning_ai_common_plat/packages/react-auth",
|
||||
"@bytelyst/telemetry-client": "file:../../learning_ai_common_plat/packages/telemetry-client",
|
||||
"@bytelyst/ui": "file:../../learning_ai_common_plat/packages/ui",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
:root {
|
||||
--bg-dark: var(--background);
|
||||
--bg-card: var(--card);
|
||||
--accent: var(--bl-success);
|
||||
--accent-down: var(--bl-danger);
|
||||
--text-main: var(--foreground);
|
||||
--text-dim: var(--muted-foreground);
|
||||
--border: var(--bl-border);
|
||||
--header-height: 120px;
|
||||
}
|
||||
|
||||
@ -402,13 +400,11 @@ body {
|
||||
|
||||
/* Tables (Premium Pro Style) */
|
||||
.table-container {
|
||||
background: linear-gradient(145deg, color-mix(in oklab, var(--bg-card) 86%, transparent), color-mix(in oklab, var(--bg-dark) 92%, transparent));
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 20px;
|
||||
border-radius: var(--bl-radius-card);
|
||||
overflow: hidden;
|
||||
margin-bottom: 32px;
|
||||
margin-bottom: 28px;
|
||||
box-shadow: var(--card-shadow);
|
||||
position: relative;
|
||||
}
|
||||
@ -420,7 +416,7 @@ body {
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, var(--bl-success-muted), transparent);
|
||||
background: linear-gradient(90deg, transparent, var(--accent-soft), transparent);
|
||||
}
|
||||
|
||||
.pro-table {
|
||||
@ -431,22 +427,24 @@ body {
|
||||
}
|
||||
|
||||
.pro-table th {
|
||||
padding: 20px 24px;
|
||||
background: color-mix(in oklab, var(--text-main) 2%, transparent);
|
||||
padding: 14px 16px;
|
||||
background: var(--muted);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 800;
|
||||
font-weight: 750;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-dim);
|
||||
letter-spacing: 0.15em;
|
||||
letter-spacing: 0.08em;
|
||||
border-bottom: 1px solid var(--border);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pro-table td {
|
||||
padding: 16px 24px;
|
||||
padding: 14px 16px;
|
||||
font-size: 0.85rem;
|
||||
color: color-mix(in oklab, var(--text-main) 80%, transparent);
|
||||
border-bottom: 1px solid color-mix(in oklab, var(--text-main) 3%, transparent);
|
||||
color: color-mix(in oklab, var(--text-main) 84%, transparent);
|
||||
border-bottom: 1px solid var(--border);
|
||||
transition: all 0.2s ease;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.pro-table tr {
|
||||
@ -454,8 +452,7 @@ body {
|
||||
}
|
||||
|
||||
.pro-table tbody tr:hover {
|
||||
background: color-mix(in oklab, var(--text-main) 2%, transparent);
|
||||
transform: scale(1.002);
|
||||
background: color-mix(in oklab, var(--accent) 8%, transparent);
|
||||
}
|
||||
|
||||
.pro-table tbody tr:hover td {
|
||||
@ -492,8 +489,9 @@ body {
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 40px !important;
|
||||
padding: 48px !important;
|
||||
color: var(--text-dim);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.side-buy {
|
||||
@ -515,10 +513,15 @@ body {
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 9px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--muted);
|
||||
font-size: 0.7rem;
|
||||
font-weight: bold;
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.status-tag.active {
|
||||
@ -554,16 +557,17 @@ body {
|
||||
}
|
||||
|
||||
.settings-group {
|
||||
background: var(--bg-card);
|
||||
background: var(--card);
|
||||
padding: 24px;
|
||||
border-radius: 12px;
|
||||
border-radius: var(--bl-radius-card);
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: var(--card-shadow);
|
||||
}
|
||||
|
||||
.settings-group h3 {
|
||||
margin-top: 0;
|
||||
font-size: 1.1rem;
|
||||
color: var(--accent);
|
||||
font-size: 1rem;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.control-card {
|
||||
@ -576,8 +580,8 @@ body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 1.1rem;
|
||||
font-weight: bold;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.dot {
|
||||
@ -598,7 +602,7 @@ body {
|
||||
background: var(--accent-down);
|
||||
color: var(--primary-foreground);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--bl-radius-control);
|
||||
font-weight: 800;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
|
||||
@ -193,21 +193,7 @@ function App() {
|
||||
}}>
|
||||
{/* Critical system alert banner */}
|
||||
{hasCriticalEvents && (
|
||||
<div style={{
|
||||
position: 'fixed', top: 0, left: 0, right: 0, zIndex: 9999,
|
||||
background: 'linear-gradient(90deg, var(--bl-danger-muted) 0%, var(--bl-danger) 50%, var(--bl-danger-muted) 100%)',
|
||||
color: 'var(--primary-foreground)',
|
||||
padding: '6px 20px',
|
||||
textAlign: 'center',
|
||||
fontSize: 11,
|
||||
fontWeight: 900,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 10,
|
||||
}}>
|
||||
<div className="critical-alert-banner">
|
||||
<span>⚠️</span>
|
||||
<span>
|
||||
SYSTEM ALERT: {recentCriticalEvents.length} CRITICAL ISSUES DETECTED — GO TO SETTINGS › ADMIN PANEL
|
||||
@ -216,7 +202,7 @@ function App() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ paddingTop: hasCriticalEvents ? 32 : 0 }}>
|
||||
<div>
|
||||
<AppShell />
|
||||
</div>
|
||||
|
||||
|
||||
@ -15,11 +15,35 @@
|
||||
}
|
||||
|
||||
.alerts-container {
|
||||
max-height: 300px;
|
||||
max-height: min(520px, 58vh);
|
||||
overflow-y: auto;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.alert-empty-state {
|
||||
display: grid;
|
||||
min-height: 180px;
|
||||
place-items: center;
|
||||
gap: 6px;
|
||||
border: 1px dashed var(--border-strong);
|
||||
border-radius: 14px;
|
||||
background: color-mix(in oklab, var(--muted) 54%, var(--card));
|
||||
padding: 28px;
|
||||
color: var(--muted-foreground);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.alert-empty-state strong {
|
||||
color: var(--foreground);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.alert-empty-state span {
|
||||
max-width: 360px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.alerts-container::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
@ -33,6 +33,12 @@ export const AlertFeed = ({ alerts }: AlertFeedProps) => {
|
||||
<div className="alert-feed">
|
||||
<h2>Recent Activity</h2>
|
||||
<div className="alerts-container">
|
||||
{sortedAlerts.length === 0 && (
|
||||
<div className="alert-empty-state">
|
||||
<strong>No recent alerts</strong>
|
||||
<span>Signals, warnings, and execution events will appear here as they arrive.</span>
|
||||
</div>
|
||||
)}
|
||||
{sortedAlerts.map((alert, index) => (
|
||||
<div key={index} className={`alert-item alert-${alert.type}`}>
|
||||
<div className="alert-icon">{alertTypeIcons[alert.type]}</div>
|
||||
|
||||
@ -3,7 +3,7 @@ import { Link, Routes, Route, Navigate, useLocation } from 'react-router-dom';
|
||||
import { Sidebar } from './Sidebar';
|
||||
import { Header } from './Header';
|
||||
import { RightPanel } from './RightPanel';
|
||||
import { Button } from '../ui/button';
|
||||
import { Button } from '../ui/Primitives';
|
||||
import { getLegacySimpleRoute, getPlansRoute } from '../../views/tradePlansRoutes';
|
||||
|
||||
const HomeView = lazy(() => import('../../views/HomeView').then((mod) => ({ default: mod.HomeView })));
|
||||
@ -20,20 +20,10 @@ function RouteFallback() {
|
||||
return (
|
||||
<section
|
||||
aria-live="polite"
|
||||
style={{
|
||||
minHeight: 320,
|
||||
borderRadius: 24,
|
||||
border: '1px solid var(--border)',
|
||||
background: 'var(--card)',
|
||||
color: 'var(--foreground)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: 14,
|
||||
fontWeight: 700,
|
||||
}}
|
||||
className="route-fallback route-fallback-inline"
|
||||
>
|
||||
Loading workspace…
|
||||
<div className="route-fallback-spinner" aria-hidden="true" />
|
||||
<span>Loading workspace…</span>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@ -44,30 +34,18 @@ function NotFoundView() {
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="not-found-title"
|
||||
style={{
|
||||
minHeight: 420,
|
||||
borderRadius: 24,
|
||||
border: '1px solid var(--border)',
|
||||
background: 'var(--hero-gradient)',
|
||||
padding: '56px 32px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
textAlign: 'center',
|
||||
color: 'var(--foreground)',
|
||||
}}
|
||||
className="route-empty-state"
|
||||
>
|
||||
<div style={{ fontSize: 12, fontWeight: 900, letterSpacing: '0.16em', color: 'var(--accent)', textTransform: 'uppercase' }}>
|
||||
<div className="route-empty-eyebrow">
|
||||
404
|
||||
</div>
|
||||
<h1 id="not-found-title" style={{ margin: '10px 0 8px', fontSize: 34, fontWeight: 900 }}>
|
||||
<h1 id="not-found-title">
|
||||
Route not found
|
||||
</h1>
|
||||
<p style={{ margin: 0, maxWidth: 520, color: 'var(--muted-foreground)', fontSize: 14, lineHeight: 1.6 }}>
|
||||
<p>
|
||||
No trading workspace exists at <code style={{ fontWeight: 800 }}>{location.pathname}</code>. The app is still running normally.
|
||||
</p>
|
||||
<Link to="/" style={{ marginTop: 24, textDecoration: 'none' }}>
|
||||
<Link to="/" className="route-empty-action">
|
||||
<Button>Return home</Button>
|
||||
</Link>
|
||||
</section>
|
||||
|
||||
@ -61,7 +61,7 @@ export function Header() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<header style={{
|
||||
<header className="trading-header" style={{
|
||||
height: 56,
|
||||
background: 'var(--header)',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
@ -74,7 +74,7 @@ export function Header() {
|
||||
backdropFilter: 'blur(18px)',
|
||||
}}>
|
||||
{/* Search bar */}
|
||||
<div style={{ position: 'relative', width: 300 }}>
|
||||
<div className="trading-header-search" style={{ position: 'relative', width: 300 }}>
|
||||
<Search
|
||||
size={15}
|
||||
style={{
|
||||
@ -112,7 +112,7 @@ export function Header() {
|
||||
<div style={{ flex: 1 }} />
|
||||
|
||||
{/* Market indices */}
|
||||
<div style={{ display: 'flex', gap: 28, alignItems: 'center' }}>
|
||||
<div className="trading-header-indices" style={{ display: 'flex', gap: 28, alignItems: 'center' }}>
|
||||
{indices.length === 0 ? (
|
||||
// Skeleton while loading
|
||||
['S&P 500','Dow Jones','Nasdaq'].map(label => (
|
||||
|
||||
@ -23,37 +23,29 @@ function PortfolioSummary() {
|
||||
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(n);
|
||||
|
||||
return (
|
||||
<div style={{ padding: '16px 16px 12px' }}>
|
||||
<div className="right-panel-section">
|
||||
{/* Header */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
||||
<span style={{ fontSize: 13, fontWeight: 700, color: 'var(--foreground)' }}>Portfolio</span>
|
||||
<div className="right-panel-header">
|
||||
<span>Portfolio</span>
|
||||
<Button variant="ghost" size="sm" className="h-8 px-2 text-xs">
|
||||
View All <ArrowRight size={12} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Total value */}
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<div style={{ fontSize: 20, fontWeight: 800, color: 'var(--foreground)', letterSpacing: '-0.5px' }}>
|
||||
<div className="right-panel-metric">
|
||||
<div className="right-panel-metric-value">
|
||||
{fmt$(totalValue)}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: 12, fontWeight: 600, marginTop: 2,
|
||||
color: pnlPos ? 'var(--bl-success)' : 'var(--bl-danger)',
|
||||
}}>
|
||||
<div className="right-panel-metric-change" data-positive={pnlPos ? 'true' : 'false'}>
|
||||
{pnlPos ? '+' : ''}{fmt$(totalPnl)} unrealized
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Column headers */}
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '2fr 1.2fr 1fr 1.2fr',
|
||||
gap: 4, paddingBottom: 6,
|
||||
borderBottom: '1px solid var(--border)', marginBottom: 4,
|
||||
}}>
|
||||
<div className="right-panel-table-head">
|
||||
{['Symbol','Price','Change','Value'].map(h => (
|
||||
<span key={h} style={{ fontSize: 10, color: 'var(--muted-foreground)', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.05em' }}>
|
||||
<span key={h}>
|
||||
{h}
|
||||
</span>
|
||||
))}
|
||||
@ -61,18 +53,11 @@ function PortfolioSummary() {
|
||||
|
||||
{/* Rows */}
|
||||
{positions.length === 0 ? (
|
||||
<div style={{
|
||||
border: '1px dashed var(--border)',
|
||||
borderRadius: 'var(--bl-radius-card)',
|
||||
background: 'var(--card-elevated)',
|
||||
color: 'var(--muted-foreground)',
|
||||
padding: '18px 14px',
|
||||
textAlign: 'center',
|
||||
}}>
|
||||
<div style={{ fontSize: 12, fontWeight: 700, color: 'var(--foreground)' }}>No open positions</div>
|
||||
<div style={{ fontSize: 12, lineHeight: 1.5, marginTop: 4 }}>
|
||||
<div className="right-panel-empty">
|
||||
<div>No open positions</div>
|
||||
<p>
|
||||
Filled Trade Plans and manual entries will appear here.
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
@ -80,13 +65,7 @@ function PortfolioSummary() {
|
||||
const pct = pos.unrealizedPnlPercent ?? 0;
|
||||
const pos_ = pct >= 0;
|
||||
return (
|
||||
<div key={pos.id} style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '2fr 1.2fr 1fr 1.2fr',
|
||||
gap: 4, padding: '8px 0',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
<div key={pos.id} className="right-panel-table-row">
|
||||
<span style={{ fontSize: 12, fontWeight: 700, color: 'var(--foreground)' }}>{pos.symbol}</span>
|
||||
<span style={{ fontSize: 12, color: 'var(--foreground)' }}>{pos.currentPrice?.toFixed(2) ?? '—'}</span>
|
||||
<span style={{ fontSize: 12, fontWeight: 600, color: pos_ ? 'var(--bl-success)' : 'var(--bl-danger)' }}>
|
||||
@ -122,15 +101,7 @@ function NewsCard({ article }: { article: NewsArticle }) {
|
||||
href={article.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
display: 'flex', gap: 10,
|
||||
padding: '10px 16px',
|
||||
textDecoration: 'none',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
transition: 'background 0.1s',
|
||||
}}
|
||||
onMouseEnter={e => (e.currentTarget.style.background = 'var(--muted)')}
|
||||
onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}
|
||||
className="right-panel-news-card"
|
||||
>
|
||||
{img && (
|
||||
<img
|
||||
@ -207,11 +178,8 @@ function NewsFeed() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{
|
||||
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||||
padding: '12px 16px 8px',
|
||||
}}>
|
||||
<span style={{ fontSize: 13, fontWeight: 700, color: 'var(--foreground)' }}>Latest News</span>
|
||||
<div className="right-panel-header right-panel-news-header">
|
||||
<span>Latest News</span>
|
||||
<Button variant="ghost" size="sm" className="h-8 px-2 text-xs">
|
||||
View All <ArrowRight size={12} />
|
||||
</Button>
|
||||
@ -220,25 +188,17 @@ function NewsFeed() {
|
||||
{loading && <NewsFeedSkeleton />}
|
||||
|
||||
{!loading && error && (
|
||||
<div style={{ fontSize: 12, color: 'var(--bl-danger)', padding: '12px 16px' }}>{error}</div>
|
||||
<div className="right-panel-error">{error}</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && news.length === 0 && (
|
||||
<div style={{
|
||||
border: '1px dashed var(--border)',
|
||||
borderRadius: 'var(--bl-radius-card)',
|
||||
background: 'var(--card-elevated)',
|
||||
color: 'var(--muted-foreground)',
|
||||
margin: '8px 16px 16px',
|
||||
padding: '18px 14px',
|
||||
textAlign: 'center',
|
||||
}}>
|
||||
<div style={{ fontSize: 12, fontWeight: 700, color: 'var(--foreground)' }}>
|
||||
<div className="right-panel-empty mx-4 mb-4 mt-2">
|
||||
<div>
|
||||
{activeSymbol ? `No news for ${activeSymbol}` : 'No symbol selected'}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, lineHeight: 1.5, marginTop: 4 }}>
|
||||
<p>
|
||||
{activeSymbol ? 'Try another ticker or check back after the next market update.' : 'Search a ticker to load market news.'}
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -251,15 +211,8 @@ function NewsFeed() {
|
||||
|
||||
export function RightPanel() {
|
||||
return (
|
||||
<aside className="dashboard-right-panel" style={{
|
||||
background: 'var(--card)',
|
||||
borderLeft: '1px solid var(--border)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflowY: 'auto',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<div style={{ borderBottom: '1px solid var(--border)' }}>
|
||||
<aside className="dashboard-right-panel right-panel">
|
||||
<div className="right-panel-block">
|
||||
<PortfolioSummary />
|
||||
</div>
|
||||
<NewsFeed />
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import {
|
||||
Home, Briefcase, FlaskConical, Target, TrendingUp,
|
||||
SlidersHorizontal, Star, Bell, Settings,
|
||||
SlidersHorizontal, Star, Bell, Settings, ChartNoAxesCombined,
|
||||
} from 'lucide-react';
|
||||
import { useAppContext } from '../../context/AppContext';
|
||||
import { getPlansRoute } from '../../views/tradePlansRoutes';
|
||||
@ -27,21 +27,12 @@ export function Sidebar() {
|
||||
return (
|
||||
<aside className="trading-sidebar">
|
||||
{/* Logo */}
|
||||
<div className="trading-sidebar-logo" style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 10,
|
||||
background: 'linear-gradient(135deg, var(--accent), color-mix(in oklab, var(--accent) 70%, var(--bl-info-strong) 30%))',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 24,
|
||||
flexShrink: 0,
|
||||
boxShadow: '0 2px 8px color-mix(in oklab, var(--accent) 35%, transparent 65%)',
|
||||
cursor: 'pointer',
|
||||
fontSize: 20,
|
||||
}}>
|
||||
📈
|
||||
<div className="trading-sidebar-logo" aria-label="ByteLyst trading">
|
||||
<ChartNoAxesCombined size={22} />
|
||||
<div className="trading-sidebar-brand">
|
||||
<strong>ByteLyst</strong>
|
||||
<span>Trading OS</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Nav items */}
|
||||
@ -51,32 +42,12 @@ export function Sidebar() {
|
||||
key={to}
|
||||
to={to}
|
||||
end={end}
|
||||
className="trading-sidebar-link"
|
||||
style={({ isActive }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '10px 0 8px',
|
||||
gap: 3,
|
||||
borderLeft: isActive ? '3px solid var(--accent)' : '3px solid transparent',
|
||||
color: isActive ? 'var(--accent)' : 'var(--muted-foreground)',
|
||||
textDecoration: 'none',
|
||||
background: isActive ? 'var(--sidebar-active)' : 'transparent',
|
||||
transition: 'all 0.15s',
|
||||
})}
|
||||
className={({ isActive }) => `trading-sidebar-link${isActive ? ' is-active' : ''}`}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<>
|
||||
<Icon size={20} strokeWidth={isActive ? 2.2 : 1.8} />
|
||||
<span style={{
|
||||
fontSize: 10,
|
||||
fontWeight: isActive ? 700 : 500,
|
||||
letterSpacing: '0.01em',
|
||||
lineHeight: 1,
|
||||
}}>
|
||||
{label}
|
||||
</span>
|
||||
<span>{label}</span>
|
||||
</>
|
||||
)}
|
||||
</NavLink>
|
||||
@ -88,34 +59,9 @@ export function Sidebar() {
|
||||
className="trading-sidebar-avatar"
|
||||
title={`${user?.email ?? ''} — click to sign out`}
|
||||
onClick={handleSignOut}
|
||||
style={{
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: '50%',
|
||||
background: 'var(--accent)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: 'var(--primary-foreground)',
|
||||
fontSize: 12,
|
||||
fontWeight: 700,
|
||||
cursor: 'pointer',
|
||||
position: 'relative',
|
||||
userSelect: 'none',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{initials}
|
||||
<span style={{
|
||||
position: 'absolute',
|
||||
bottom: 1,
|
||||
right: 1,
|
||||
width: 9,
|
||||
height: 9,
|
||||
borderRadius: '50%',
|
||||
background: 'var(--bl-success)',
|
||||
border: '2px solid var(--card)',
|
||||
}} />
|
||||
<span className="trading-sidebar-avatar-status" />
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
|
||||
@ -9,7 +9,7 @@ type ThemeContextValue = {
|
||||
toggleTheme: () => void;
|
||||
};
|
||||
|
||||
const STORAGE_KEY = 'trading-dashboard-theme';
|
||||
const STORAGE_KEY = 'trading-dashboard-theme-v2';
|
||||
|
||||
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
||||
|
||||
@ -46,7 +46,7 @@ function resolveInitialTheme(): Theme {
|
||||
if (typeof window === 'undefined') return 'light';
|
||||
const stored = readStoredTheme();
|
||||
if (stored) return stored;
|
||||
return window.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
return 'light';
|
||||
}
|
||||
|
||||
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
@ -2,6 +2,13 @@ import * as React from 'react';
|
||||
import {
|
||||
Badge as CommonBadge,
|
||||
Button as CommonButton,
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldTitle,
|
||||
Input as CommonInput,
|
||||
Select as CommonSelect,
|
||||
Textarea as CommonTextarea,
|
||||
@ -86,14 +93,24 @@ const buttonSizeClass: Record<ProductButtonSize, string> = {
|
||||
icon: 'h-10 w-10 px-0',
|
||||
};
|
||||
|
||||
const buttonVariantClass: Record<ProductButtonVariant, string> = {
|
||||
primary: 'product-button-primary',
|
||||
secondary: 'product-button-secondary',
|
||||
ghost: 'product-button-ghost',
|
||||
destructive: 'product-button-destructive',
|
||||
outline: 'product-button-outline',
|
||||
subtle: 'product-button-subtle',
|
||||
link: 'product-button-link',
|
||||
};
|
||||
|
||||
const fieldSizeClass: Record<ProductFieldSize, string> = {
|
||||
sm: 'min-h-9 px-3 py-2 text-xs leading-5',
|
||||
md: 'min-h-11 px-3.5 py-2.5 text-sm leading-6',
|
||||
};
|
||||
|
||||
const fieldVariantClass: Record<ProductFieldVariant, string> = {
|
||||
surface: 'rounded-[var(--bl-radius-control)] border-[var(--border)] bg-[var(--input)] text-[var(--foreground)] shadow-sm shadow-black/[0.02] focus-visible:border-[var(--ring)] focus-visible:ring-2 focus-visible:ring-[var(--ring-soft)]',
|
||||
muted: 'rounded-[var(--bl-radius-control)] border-[var(--border)] bg-[var(--muted)] text-[var(--foreground)] shadow-sm shadow-black/[0.02] focus-visible:border-[var(--ring)] focus-visible:ring-2 focus-visible:ring-[var(--ring-soft)]',
|
||||
surface: 'rounded-[var(--bl-radius-control)] border-[var(--bl-border)] bg-[var(--bl-input)] text-[var(--bl-text-primary)] shadow-sm shadow-black/[0.02] focus-visible:border-[var(--bl-focus-ring)] focus-visible:ring-2 focus-visible:ring-[var(--bl-focus-ring-muted)]',
|
||||
muted: 'rounded-[var(--bl-radius-control)] border-[var(--bl-border)] bg-[var(--bl-surface-muted)] text-[var(--bl-text-primary)] shadow-sm shadow-black/[0.02] focus-visible:border-[var(--bl-focus-ring)] focus-visible:ring-2 focus-visible:ring-[var(--bl-focus-ring-muted)]',
|
||||
};
|
||||
|
||||
const productStatusTone: Record<ProductStatus, ProductStatusTone> = {
|
||||
@ -146,7 +163,10 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
variant={buttonVariantFor(variant)}
|
||||
size={size === 'icon' ? 'sm' : size}
|
||||
className={cn(
|
||||
'product-button',
|
||||
`product-button-${size}`,
|
||||
buttonSizeClass[size],
|
||||
buttonVariantClass[variant],
|
||||
variant === 'link' && 'h-auto px-0 py-0 underline underline-offset-4 hover:bg-transparent',
|
||||
className,
|
||||
)}
|
||||
@ -237,6 +257,16 @@ export function Badge({ variant = 'neutral', ...props }: BadgeProps) {
|
||||
return <CommonBadge variant={badgeVariantFor(variant)} {...props} />;
|
||||
}
|
||||
|
||||
export {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldTitle,
|
||||
};
|
||||
|
||||
export function ProductStatusBadge({
|
||||
status,
|
||||
children,
|
||||
|
||||
@ -1,45 +1,30 @@
|
||||
import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
||||
import { cn } from '../../lib/utils';
|
||||
import type { ReactNode } from 'react';
|
||||
import {
|
||||
Button as ProductButton,
|
||||
type ButtonProps as ProductButtonProps,
|
||||
} from './Primitives';
|
||||
|
||||
type ButtonVariant = 'default' | 'outline' | 'ghost' | 'destructive';
|
||||
type ButtonSize = 'sm' | 'md' | 'lg' | 'icon';
|
||||
type LegacyButtonVariant = 'default' | 'outline' | 'ghost' | 'destructive' | 'subtle';
|
||||
|
||||
const variantClasses: Record<ButtonVariant, string> = {
|
||||
default: 'bg-[var(--primary)] text-[var(--primary-foreground)] hover:brightness-95 shadow-sm',
|
||||
outline: 'border border-[var(--border)] bg-[var(--card)] text-[var(--foreground)] hover:bg-[var(--accent-soft)]',
|
||||
ghost: 'text-[var(--muted-foreground)] hover:bg-[var(--accent-soft)] hover:text-[var(--foreground)]',
|
||||
destructive: 'bg-[var(--destructive)] text-white hover:brightness-95 shadow-sm',
|
||||
};
|
||||
|
||||
const sizeClasses: Record<ButtonSize, string> = {
|
||||
sm: 'h-9 px-3 text-xs',
|
||||
md: 'h-10 px-4 text-sm',
|
||||
lg: 'h-12 px-5 text-sm',
|
||||
icon: 'h-10 w-10',
|
||||
const variantMap: Record<LegacyButtonVariant, ProductButtonProps['variant']> = {
|
||||
default: 'primary',
|
||||
outline: 'outline',
|
||||
ghost: 'ghost',
|
||||
destructive: 'destructive',
|
||||
subtle: 'subtle',
|
||||
};
|
||||
|
||||
export function Button({
|
||||
className,
|
||||
variant = 'default',
|
||||
size = 'md',
|
||||
children,
|
||||
...props
|
||||
}: ButtonHTMLAttributes<HTMLButtonElement> & {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
}: Omit<ProductButtonProps, 'variant'> & {
|
||||
variant?: LegacyButtonVariant;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center gap-2 rounded-xl font-semibold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)] disabled:cursor-not-allowed disabled:opacity-50',
|
||||
variantClasses[variant],
|
||||
sizeClasses[size],
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProductButton variant={variantMap[variant]} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
</ProductButton>
|
||||
);
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import { cn } from '../../lib/utils';
|
||||
export function Card({ className, children, ...props }: HTMLAttributes<HTMLDivElement> & { children: ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
className={cn('rounded-3xl border border-[var(--border)] bg-[var(--card)] shadow-[var(--card-shadow)]', className)}
|
||||
className={cn('rounded-[var(--bl-radius-card)] border border-[var(--border)] bg-[var(--card)] shadow-sm', className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
@ -14,7 +14,7 @@ export function Card({ className, children, ...props }: HTMLAttributes<HTMLDivEl
|
||||
|
||||
export function CardHeader({ className, children, ...props }: HTMLAttributes<HTMLDivElement> & { children: ReactNode }) {
|
||||
return (
|
||||
<div className={cn('flex items-start justify-between gap-4 p-6 pb-0', className)} {...props}>
|
||||
<div className={cn('flex items-start justify-between gap-4 p-5 pb-0', className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
@ -22,7 +22,7 @@ export function CardHeader({ className, children, ...props }: HTMLAttributes<HTM
|
||||
|
||||
export function CardTitle({ className, children, ...props }: HTMLAttributes<HTMLHeadingElement> & { children: ReactNode }) {
|
||||
return (
|
||||
<h2 className={cn('text-lg font-bold tracking-tight text-[var(--foreground)]', className)} {...props}>
|
||||
<h2 className={cn('m-0 text-base font-semibold tracking-tight text-[var(--foreground)]', className)} {...props}>
|
||||
{children}
|
||||
</h2>
|
||||
);
|
||||
@ -30,7 +30,7 @@ export function CardTitle({ className, children, ...props }: HTMLAttributes<HTML
|
||||
|
||||
export function CardDescription({ className, children, ...props }: HTMLAttributes<HTMLParagraphElement> & { children: ReactNode }) {
|
||||
return (
|
||||
<p className={cn('text-sm text-[var(--muted-foreground)]', className)} {...props}>
|
||||
<p className={cn('m-0 text-sm leading-6 text-[var(--muted-foreground)]', className)} {...props}>
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
@ -38,7 +38,7 @@ export function CardDescription({ className, children, ...props }: HTMLAttribute
|
||||
|
||||
export function CardContent({ className, children, ...props }: HTMLAttributes<HTMLDivElement> & { children: ReactNode }) {
|
||||
return (
|
||||
<div className={cn('p-6', className)} {...props}>
|
||||
<div className={cn('p-5', className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -13,16 +13,16 @@ export function PageHeader({
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn('mb-6 flex items-start justify-between gap-4', className)}>
|
||||
<div className={cn('mb-7 flex flex-col items-start justify-between gap-4 sm:flex-row', className)}>
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-2xl font-bold tracking-tight text-[var(--foreground)] md:text-3xl">{title}</h1>
|
||||
<h1 className="m-0 text-3xl font-semibold tracking-tight text-[var(--foreground)]">{title}</h1>
|
||||
{description ? (
|
||||
<p className="mt-2 max-w-3xl text-sm text-[var(--muted-foreground)] md:text-[15px]">
|
||||
<p className="mt-3 max-w-3xl text-sm leading-6 text-[var(--muted-foreground)] md:text-[15px]">
|
||||
{description}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
{action ? <div className="shrink-0">{action}</div> : null}
|
||||
{action ? <div className="shrink-0 sm:pt-1">{action}</div> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
1777
web/src/index.css
1777
web/src/index.css
File diff suppressed because it is too large
Load Diff
@ -2,6 +2,7 @@ import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { Agentation } from 'agentation'
|
||||
import './index.css'
|
||||
import './App.css'
|
||||
import App from './App.tsx'
|
||||
import { AuthProvider } from './components/AuthContext';
|
||||
import { ProductAccessibilityGate } from './components/ProductAccessibilityGate';
|
||||
|
||||
@ -80,10 +80,10 @@ describe('EntriesTab master suite', () => {
|
||||
render(<EntriesTab botState={mockBotState} />);
|
||||
await waitFor(() => expect(screen.getByText('BTC/USDT')).toBeInTheDocument());
|
||||
|
||||
await user.click(screen.getByText(/Manual Entry Injection/i));
|
||||
expect(screen.getByText(/New Opportunity Initialization/i)).toBeInTheDocument();
|
||||
await user.click(screen.getByText(/Add entry/i));
|
||||
expect(screen.getByText(/New watchlist entry/i)).toBeInTheDocument();
|
||||
await user.click(screen.getByText('SubmitEntry'));
|
||||
expect(screen.queryByText(/New Opportunity Initialization/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/New watchlist entry/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('derives diverse entry states (LOCKED, BLOCKED, SUBMITTED, CONFIRMED, ORPHAN)', async () => {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useAuth } from '../components/AuthContext';
|
||||
import { EntryForm } from '../components/EntryForm';
|
||||
import { Pencil, Trash2, Copy as CopyIcon, Search, Plus, Filter, LayoutGrid, Clock, ShieldCheck, Activity } from 'lucide-react';
|
||||
import { Pencil, Trash2, Copy as CopyIcon, Plus, Filter, LayoutGrid, Clock, ShieldCheck, Activity, X } from 'lucide-react';
|
||||
import type { BotState } from '../hooks/useWebSocket';
|
||||
import { DEFAULT_BOT_STATE } from '../hooks/useWebSocket';
|
||||
import { createManualEntry, deleteManualEntry, fetchManualEntries, type ManualEntryPayload } from '../lib/manualEntriesApi';
|
||||
@ -99,7 +99,7 @@ const NAV_TABS = [
|
||||
] as const;
|
||||
|
||||
const tabClass = (isActive: boolean) =>
|
||||
`px-6 py-3 rounded-[1.5rem] text-[10px] font-black uppercase tracking-widest transition-all flex items-center gap-3 whitespace-nowrap ${isActive ? "bg-white text-black shadow-xl" : "text-gray-500 hover:text-gray-300"}`;
|
||||
`min-h-9 rounded-lg px-3 text-xs font-semibold transition flex items-center gap-2 whitespace-nowrap ${isActive ? "border border-[var(--border-strong)] bg-[var(--accent-soft)] text-[var(--foreground)] shadow-sm" : "text-[var(--muted-foreground)] hover:bg-[var(--muted)] hover:text-[var(--foreground)]"}`;
|
||||
|
||||
export const EntriesTab = ({ botState = DEFAULT_BOT_STATE }: EntriesTabProps) => {
|
||||
const { user } = useAuth();
|
||||
@ -127,7 +127,7 @@ export const EntriesTab = ({ botState = DEFAULT_BOT_STATE }: EntriesTabProps) =>
|
||||
}, [user]);
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!confirm('⚠️ CRITICAL: Permanently delete this entry from neural watchlist?')) return;
|
||||
if (!confirm('Permanently delete this watchlist entry?')) return;
|
||||
await deleteManualEntry(id);
|
||||
fetchEntries();
|
||||
};
|
||||
@ -140,121 +140,102 @@ export const EntriesTab = ({ botState = DEFAULT_BOT_STATE }: EntriesTabProps) =>
|
||||
const entryCards = filteredEntries.map((entry) => {
|
||||
const entryState = deriveEntryState(entry, botState);
|
||||
return (
|
||||
<div key={entry.stock_instance_id} className="group relative rounded-[2.5rem] p-1 transition-all duration-500 bg-white/[0.01] hover:bg-gradient-to-br hover:from-green-500/10 hover:to-transparent">
|
||||
<div className="bg-[var(--card-elevated)] border border-white/5 rounded-[2.4rem] p-8 h-full flex flex-col transition-all group-hover:border-white/10 group-hover:shadow-2xl">
|
||||
|
||||
{/* Card Top */}
|
||||
<div className="flex items-start justify-between mb-8">
|
||||
<div className="space-y-1">
|
||||
<h4 className="text-2xl font-black text-white tracking-tight group-hover:text-green-400 transition-colors uppercase">{entry.symbol}</h4>
|
||||
<article key={entry.stock_instance_id} className="group flex h-full flex-col rounded-[var(--bl-radius-card)] border border-[var(--border)] bg-[var(--card)] p-5 shadow-sm transition hover:border-[var(--border-strong)] hover:shadow-[var(--card-shadow)]">
|
||||
<div className="mb-5 flex items-start justify-between gap-4">
|
||||
<div className="min-w-0 space-y-2">
|
||||
<h4 className="m-0 truncate text-xl font-semibold tracking-tight text-[var(--foreground)]">{entry.symbol}</h4>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`px-2 py-0.5 rounded text-[9px] font-black tracking-widest uppercase ${entry.is_real_trade ? 'bg-blue-500/20 text-blue-400' : 'bg-purple-500/20 text-purple-400'}`}>
|
||||
{entry.is_real_trade ? 'REAL-UNIT' : 'PAPER-VOID'}
|
||||
<span className={`rounded-full px-2.5 py-1 text-[11px] font-semibold ${entry.is_real_trade ? 'bg-blue-500/15 text-blue-300' : 'bg-violet-500/15 text-violet-300'}`}>
|
||||
{entry.is_real_trade ? 'Real' : 'Paper'}
|
||||
</span>
|
||||
{entry.label && (
|
||||
<span className="px-2 py-0.5 bg-white/5 rounded text-[9px] font-black text-gray-500 uppercase tracking-widest border border-white/5">
|
||||
<span className="rounded-full border border-[var(--border)] bg-[var(--muted)] px-2.5 py-1 text-[11px] font-semibold text-[var(--muted-foreground)]">
|
||||
{entry.label}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 opacity-0 group-hover:opacity-100 transition-all translate-y-2 group-hover:translate-y-0">
|
||||
<Button type="button" onClick={() => setEditingEntry(entry)} variant="ghost" className="p-3 bg-white/5 rounded-2xl text-gray-400 hover:text-white hover:bg-white/10 transition-all"><Pencil size={16} /></Button>
|
||||
<Button type="button" onClick={() => handleClone(entry)} variant="ghost" className="p-3 bg-white/5 rounded-2xl text-gray-400 hover:text-green-400 hover:bg-green-500/10 transition-all"><CopyIcon size={16} /></Button>
|
||||
<Button type="button" onClick={() => handleDelete(entry.stock_instance_id)} variant="ghost" className="p-3 bg-red-500/10 rounded-2xl text-red-500 hover:bg-red-500/20 transition-all"><Trash2 size={16} /></Button>
|
||||
<div className="flex shrink-0 gap-1 opacity-100 transition md:opacity-0 md:group-hover:opacity-100">
|
||||
<Button type="button" onClick={() => setEditingEntry(entry)} variant="ghost" size="icon" aria-label={`Edit ${entry.symbol}`}><Pencil size={15} /></Button>
|
||||
<Button type="button" onClick={() => handleClone(entry)} variant="ghost" size="icon" aria-label={`Clone ${entry.symbol}`}><CopyIcon size={15} /></Button>
|
||||
<Button type="button" onClick={() => handleDelete(entry.stock_instance_id)} variant="ghost" size="icon" aria-label={`Delete ${entry.symbol}`} className="text-[var(--destructive)] hover:bg-[var(--bl-danger-muted)]"><Trash2 size={15} /></Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Metrics Grid */}
|
||||
<div className="grid grid-cols-2 gap-4 mb-8">
|
||||
<div className="bg-white/[0.02] p-5 rounded-3xl border border-white/5">
|
||||
<span className="block text-[8px] text-gray-600 font-black uppercase tracking-[0.2em] mb-2">Entry Price</span>
|
||||
<span className="text-xl font-black text-white font-mono">${entry.buy_price || '0.00'}</span>
|
||||
<div className="mb-5 grid grid-cols-2 gap-3">
|
||||
<div className="rounded-xl border border-[var(--border)] bg-[var(--muted)]/60 p-4">
|
||||
<span className="mb-1 block text-[11px] font-semibold uppercase tracking-[0.08em] text-[var(--muted-foreground)]">Entry</span>
|
||||
<span className="font-mono text-base font-semibold text-[var(--foreground)]">${entry.buy_price || '0.00'}</span>
|
||||
</div>
|
||||
<div className="bg-white/[0.02] p-5 rounded-3xl border border-white/5">
|
||||
<span className="block text-[8px] text-gray-600 font-black uppercase tracking-[0.2em] mb-2">Target Exit</span>
|
||||
<span className="text-xl font-black text-white font-mono">${entry.sell_price || '---'}</span>
|
||||
<div className="rounded-xl border border-[var(--border)] bg-[var(--muted)]/60 p-4">
|
||||
<span className="mb-1 block text-[11px] font-semibold uppercase tracking-[0.08em] text-[var(--muted-foreground)]">Target</span>
|
||||
<span className="font-mono text-base font-semibold text-[var(--foreground)]">${entry.sell_price || '---'}</span>
|
||||
</div>
|
||||
<div className="bg-white/[0.02] p-4 rounded-3xl border border-white/5">
|
||||
<span className="block text-[8px] text-gray-600 font-black uppercase tracking-[0.2em] mb-1">Quantity/Lot</span>
|
||||
<span className="text-xs font-black text-gray-400 font-mono">{entry.quantity} UNITS</span>
|
||||
<div className="rounded-xl border border-[var(--border)] bg-[var(--muted)]/60 p-4">
|
||||
<span className="mb-1 block text-[11px] font-semibold uppercase tracking-[0.08em] text-[var(--muted-foreground)]">Quantity</span>
|
||||
<span className="font-mono text-sm font-semibold text-[var(--foreground)]">{entry.quantity || '—'}</span>
|
||||
</div>
|
||||
<div className="bg-white/[0.02] p-4 rounded-3xl border border-white/5">
|
||||
<span className="block text-[8px] text-gray-600 font-black uppercase tracking-[0.2em] mb-1">Status</span>
|
||||
<span className={`text-[10px] font-black tracking-widest uppercase ${entry.active ? 'text-green-400 animate-pulse' : 'text-gray-600'}`}>
|
||||
{entry.active ? 'SCANNING' : 'INACTIVE'}
|
||||
<div className="rounded-xl border border-[var(--border)] bg-[var(--muted)]/60 p-4">
|
||||
<span className="mb-1 block text-[11px] font-semibold uppercase tracking-[0.08em] text-[var(--muted-foreground)]">Status</span>
|
||||
<span className={`text-sm font-semibold ${entry.active ? 'text-[var(--bl-success)]' : 'text-[var(--muted-foreground)]'}`}>
|
||||
{entry.active ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="entry-state-row mb-6">
|
||||
<span className="text-[9px] text-gray-500 font-black uppercase tracking-[0.2em]">Entry State</span>
|
||||
<div className="mb-5 flex items-center justify-between gap-3 rounded-xl border border-[var(--border)] bg-[var(--card-elevated)] px-3 py-2">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[var(--muted-foreground)]">Entry state</span>
|
||||
<span className={`entry-state-pill state-${entryState.toLowerCase().replace(/[^a-z]/g, '')}`}>
|
||||
{entryState}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Entry Notes/Context */}
|
||||
<div className="flex-grow">
|
||||
<span className="block text-[8px] text-gray-600 font-black uppercase tracking-[0.2em] mb-3">Neural Context</span>
|
||||
<p className="text-[10px] font-bold text-gray-500 leading-relaxed uppercase italic">
|
||||
{entry.notes || 'No manual notes injection provided for this asset cluster.'}
|
||||
<span className="mb-2 block text-[11px] font-semibold uppercase tracking-[0.08em] text-[var(--muted-foreground)]">Notes</span>
|
||||
<p className="m-0 text-sm leading-6 text-[var(--muted-foreground)]">
|
||||
{entry.notes || 'No notes added yet.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* ID Context */}
|
||||
<div className="mt-8 pt-6 border-t border-white/5 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-6 h-6 rounded-full bg-white/5 flex items-center justify-center text-[10px] text-gray-600 font-black">
|
||||
ID
|
||||
</div>
|
||||
<span className="text-[8px] text-gray-700 font-mono select-all truncate max-w-[150px]">{entry.stock_instance_id}</span>
|
||||
</div>
|
||||
<ShieldCheck size={14} className="text-gray-800" />
|
||||
<div className="mt-5 flex items-center justify-between gap-3 border-t border-[var(--border)] pt-4">
|
||||
<span className="truncate font-mono text-[11px] text-[var(--muted-foreground)]">{entry.stock_instance_id}</span>
|
||||
<ShieldCheck size={15} className="shrink-0 text-[var(--muted-foreground)]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
});
|
||||
|
||||
const navTabs = NAV_TABS;
|
||||
|
||||
return (
|
||||
<div className="entries-tab space-y-10 animate-in fade-in duration-500">
|
||||
<div className="entries-tab ux-page-stack animate-in fade-in duration-500">
|
||||
|
||||
{/* 1. HEADER & GLOBAL ACTIONS */}
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 px-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-14 h-14 bg-green-500/10 border border-green-500/20 rounded-2xl flex items-center justify-center text-green-400 shadow-2xl">
|
||||
<Search size={30} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-3xl font-black text-white tracking-tighter uppercase">Watchlist & Entries</h2>
|
||||
<p className="text-gray-500 text-[10px] font-black uppercase tracking-[0.2em] opacity-60">Neural Opportunity Cluster</p>
|
||||
</div>
|
||||
<div className="ux-section-header">
|
||||
<div>
|
||||
<h2 className="ux-section-title text-lg">Watchlist entries</h2>
|
||||
<p className="ux-section-copy mt-1">Track manual symbols, paper setups, and real execution candidates.</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => { setIsAdding(!isAdding); setEditingEntry(null); }}
|
||||
variant="ghost"
|
||||
className={`flex items-center gap-3 px-8 py-4 rounded-[2rem] text-xs font-black uppercase tracking-widest transition-all ${isAdding ? 'bg-red-500/10 text-red-400 border border-red-500/20' : 'bg-white text-black shadow-2xl hover:scale-105'
|
||||
}`}
|
||||
variant={isAdding ? 'outline' : 'primary'}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{isAdding ? <X size={18} /> : <Plus size={18} />}
|
||||
{isAdding ? 'Cancel Entry' : 'Manual Entry Injection'}
|
||||
{isAdding ? <X size={16} /> : <Plus size={16} />}
|
||||
{isAdding ? 'Cancel' : 'Add entry'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 2. FORM DRAWER (IF ADDING/EDITING) */}
|
||||
{(isAdding || editingEntry) && (
|
||||
<div className="bg-[var(--card-elevated)] border border-white/10 rounded-[3rem] p-10 shadow-2xl animate-in zoom-in-95 duration-300">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<h3 className="text-lg font-black text-white uppercase tracking-tight flex items-center gap-3">
|
||||
<Plus size={20} className="text-green-400" />
|
||||
{editingEntry ? `Update Identity: ${editingEntry.symbol}` : 'New Opportunity Initialization'}
|
||||
</h3>
|
||||
<Button type="button" onClick={() => { setIsAdding(false); setEditingEntry(null); }} variant="ghost" className="text-gray-500 hover:text-white"><X size={20} /></Button>
|
||||
<div className="ux-section animate-in zoom-in-95 duration-300">
|
||||
<div className="ux-section-header">
|
||||
<div>
|
||||
<h3 className="ux-section-title">
|
||||
{editingEntry ? `Edit ${editingEntry.symbol}` : 'New watchlist entry'}
|
||||
</h3>
|
||||
<p className="ux-section-copy mt-1">Save a symbol with target prices, quantity, and execution context.</p>
|
||||
</div>
|
||||
<Button type="button" onClick={() => { setIsAdding(false); setEditingEntry(null); }} variant="ghost" size="icon" aria-label="Close entry form"><X size={18} /></Button>
|
||||
</div>
|
||||
<div className="max-w-4xl">
|
||||
<EntryForm
|
||||
@ -265,8 +246,7 @@ export const EntriesTab = ({ botState = DEFAULT_BOT_STATE }: EntriesTabProps) =>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 3. NAVIGATION CLUSTERS */}
|
||||
<div className="flex items-center p-1.5 bg-black/40 border border-white/5 rounded-[2rem] self-start shadow-2xl overflow-x-auto no-scrollbar max-w-full">
|
||||
<div className="entries-tab-nav flex max-w-full items-center gap-1 overflow-x-auto rounded-xl border border-[var(--border)] bg-[var(--card)] p-1 shadow-sm no-scrollbar">
|
||||
{navTabs.map((tab) => (
|
||||
<Button
|
||||
type="button"
|
||||
@ -279,20 +259,18 @@ export const EntriesTab = ({ botState = DEFAULT_BOT_STATE }: EntriesTabProps) =>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
{/* 4. CARDS GRID */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 gap-8">
|
||||
<div className="grid grid-cols-1 gap-5 md:grid-cols-2 xl:grid-cols-3">
|
||||
{entryCards}
|
||||
{filteredEntries.length === 0 && (
|
||||
<div className="col-span-full py-32 text-center bg-white/[0.02] border border-dashed border-white/5 rounded-[3rem]">
|
||||
<LayoutGrid size={48} className="mx-auto mb-6 text-gray-800 opacity-40" />
|
||||
<p className="text-sm font-black text-gray-700 uppercase tracking-[0.3em] opacity-40">Cluster Context Null</p>
|
||||
<div className="ux-empty-state col-span-full">
|
||||
<div>
|
||||
<LayoutGrid size={34} className="mx-auto mb-4 text-[var(--muted-foreground)]" />
|
||||
<strong>No entries in this view</strong>
|
||||
<span>Try another status filter or add a new watchlist entry.</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const X = ({ size }: { size: number }) => (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg>
|
||||
);
|
||||
|
||||
@ -16,7 +16,6 @@ import {
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Lightbulb,
|
||||
Dna,
|
||||
Cpu,
|
||||
Fingerprint,
|
||||
Target,
|
||||
@ -329,105 +328,29 @@ export const MyStrategiesTab: React.FC<{ botState: any; alerts?: any[]; previewA
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
maxWidth: '1400px',
|
||||
margin: '0 auto',
|
||||
padding: '0 20px 100px 20px',
|
||||
animation: 'fadeIn 0.7s ease-out'
|
||||
}}>
|
||||
{/* Premium Header - Synchronized with Marketplace/Plans */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
marginBottom: '60px',
|
||||
padding: '60px 0',
|
||||
borderBottom: '1px solid var(--bl-border-subtle)',
|
||||
position: 'relative',
|
||||
alignItems: 'flex-start'
|
||||
}}>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
opacity: 0.03,
|
||||
pointerEvents: 'none',
|
||||
transform: 'translate(40px, -20px)'
|
||||
}}>
|
||||
<Dna size={320} strokeWidth={1} />
|
||||
<div className="strategy-workspace">
|
||||
<div className="strategy-workspace-header">
|
||||
<div>
|
||||
<div className="strategy-workspace-eyebrow">
|
||||
Strategy operations
|
||||
</div>
|
||||
<h2>My strategies</h2>
|
||||
<p>
|
||||
Monitor active profiles, review recent signals, and create new automated trading workflows.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
color: 'var(--bl-success)',
|
||||
fontSize: '11px',
|
||||
fontWeight: 900,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '4px',
|
||||
marginBottom: '24px'
|
||||
}}>
|
||||
DEPLOYED FLEET
|
||||
<div style={{ width: '30px', height: '1px', background: 'var(--bl-success)', opacity: 0.2 }} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', width: '100%', flexWrap: 'wrap', gap: '32px' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
<h2 style={{
|
||||
fontSize: '84px',
|
||||
fontWeight: 950,
|
||||
color: 'white',
|
||||
letterSpacing: '-0.04em',
|
||||
lineHeight: '0.9',
|
||||
margin: 0,
|
||||
textTransform: 'uppercase'
|
||||
}}>
|
||||
My<br />
|
||||
<span style={{ color: 'var(--bl-success)' }}>Strategies</span>
|
||||
</h2>
|
||||
<p style={{ fontSize: '20px', color: 'var(--bl-text-quiet)', marginTop: '24px', maxWidth: '600px', fontWeight: 500, margin: '24px 0 0 0', textAlign: 'left' }}>
|
||||
Monitor and manage your proprietary trading nodes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
|
||||
<div style={{
|
||||
background: 'var(--bl-success-muted)',
|
||||
border: '1px solid var(--bl-success-muted)',
|
||||
padding: '12px 24px',
|
||||
borderRadius: '16px',
|
||||
color: 'var(--bl-success)',
|
||||
fontSize: '11px',
|
||||
fontWeight: 900,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '1px'
|
||||
}}>
|
||||
{botState?.connected ? 'SYSTEMS ONLINE' : 'SYSTEMS DISCONNECTED'}
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => setShowWizard(true)}
|
||||
variant="secondary"
|
||||
style={{
|
||||
background: 'white',
|
||||
border: 'none',
|
||||
color: 'black',
|
||||
padding: '14px 36px',
|
||||
borderRadius: '16px',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 900,
|
||||
fontSize: '12px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '1.5px',
|
||||
transition: 'all 0.2s',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}
|
||||
>
|
||||
<Plus size={18} strokeWidth={3} /> NEW STRATEGY
|
||||
</Button>
|
||||
<div className="strategy-workspace-actions">
|
||||
<div className="strategy-connection-pill" data-connected={botState?.connected ? 'true' : 'false'}>
|
||||
{botState?.connected ? 'Systems online' : 'Systems disconnected'}
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => setShowWizard(true)}
|
||||
variant="primary"
|
||||
>
|
||||
<Plus size={16} /> New strategy
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -450,12 +373,11 @@ export const MyStrategiesTab: React.FC<{ botState: any; alerts?: any[]; previewA
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||||
{recentAlerts.map((alert, i) => {
|
||||
const icons: Record<string, string> = { signal: '🚀', pulse: '⏰', error: '⚠️', info: 'ℹ️' };
|
||||
const mins = Math.floor((Date.now() - alert.timestamp) / 60000);
|
||||
const timeAgo = mins < 1 ? 'just now' : mins < 60 ? `${mins}m ago` : `${Math.floor(mins / 60)}h ago`;
|
||||
return (
|
||||
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: '10px', padding: '8px 12px', background: 'var(--bl-surface-overlay)', borderRadius: '10px' }}>
|
||||
<span>{icons[alert.type] || 'ℹ️'}</span>
|
||||
<Activity size={13} color="var(--bl-info)" />
|
||||
<span style={{ fontSize: '12px', color: 'var(--bl-text-quiet)', fontWeight: 700, minWidth: '60px' }}>{alert.symbol}</span>
|
||||
<span style={{ fontSize: '11px', color: 'var(--bl-text-quiet)', flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{alert.message}</span>
|
||||
<span style={{ fontSize: '10px', color: 'var(--bl-text-tertiary)', fontWeight: 700, whiteSpace: 'nowrap' }}>{timeAgo}</span>
|
||||
@ -469,7 +391,7 @@ export const MyStrategiesTab: React.FC<{ botState: any; alerts?: any[]; previewA
|
||||
{/* Symbol-Specific Volatility */}
|
||||
<div style={{ background: 'var(--bl-surface-highlight)', border: '1px solid var(--bl-border-subtle)', borderRadius: '24px', padding: '24px 28px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '16px' }}>
|
||||
<span style={{ fontSize: '16px' }}>📊</span>
|
||||
<TrendingUp size={15} color="var(--bl-info)" />
|
||||
<span style={{ fontSize: '11px', fontWeight: 900, color: 'var(--bl-text-quiet)', textTransform: 'uppercase', letterSpacing: '2px' }}>Your Markets (24h)</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
@ -542,8 +464,8 @@ export const MyStrategiesTab: React.FC<{ botState: any; alerts?: any[]; previewA
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<Activity size={60} color="var(--bl-text-quiet)" style={{ marginBottom: '24px' }} />
|
||||
<h3 style={{ color: 'var(--bl-text-quiet)', fontWeight: 900, textTransform: 'uppercase', letterSpacing: '4px' }}>No Engines Deployed</h3>
|
||||
<p style={{ color: 'var(--bl-text-quiet)', fontSize: '14px', marginTop: '12px', maxWidth: '300px' }}>Your fleet is currently dormant. Deploy your first engine to begin automated dominance.</p>
|
||||
<h3 style={{ color: 'var(--bl-text-quiet)', fontWeight: 900, textTransform: 'uppercase', letterSpacing: '4px' }}>No strategies yet</h3>
|
||||
<p style={{ color: 'var(--bl-text-quiet)', fontSize: '14px', marginTop: '12px', maxWidth: '300px' }}>Create your first strategy to start monitoring markets and testing automated execution.</p>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => setShowWizard(true)}
|
||||
|
||||
@ -314,20 +314,21 @@ export const SettingsTab = ({ botState }: SettingsTabProps) => {
|
||||
|
||||
<style>{`
|
||||
.primary-button {
|
||||
background: var(--bl-success);
|
||||
color: black;
|
||||
background: var(--accent);
|
||||
color: var(--primary-foreground);
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
font-weight: bold;
|
||||
padding: 10px 16px;
|
||||
border-radius: 10px;
|
||||
font-weight: 750;
|
||||
cursor: pointer;
|
||||
box-shadow: var(--card-shadow);
|
||||
}
|
||||
.secondary-button {
|
||||
background: transparent;
|
||||
color: var(--bl-text-secondary);
|
||||
border: 1px solid var(--bl-text-secondary);
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
color: var(--muted-foreground);
|
||||
border: 1px solid var(--border);
|
||||
padding: 10px 16px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.form-grid {
|
||||
|
||||
@ -113,9 +113,9 @@ describe('dashboard tabs smoke coverage', () => {
|
||||
it('renders EntriesTab empty-state and primary controls', () => {
|
||||
const html = renderToStaticMarkup(React.createElement(EntriesTab));
|
||||
|
||||
expect(html).toContain('Watchlist & Entries');
|
||||
expect(html).toContain('Manual Entry Injection');
|
||||
expect(html).toContain('Cluster Context Null');
|
||||
expect(html).toContain('Watchlist entries');
|
||||
expect(html).toContain('Add entry');
|
||||
expect(html).toContain('No entries in this view');
|
||||
});
|
||||
|
||||
it('renders AdminTab strategy pipeline and ConfigTab loading shell', () => {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Star, Bell, BarChart2, Loader2 } from 'lucide-react';
|
||||
import { ArrowRight, BarChart2, BarChart3, Bell, Loader2, Search, ShieldCheck, Sparkles, Star } from 'lucide-react';
|
||||
import {
|
||||
AreaChart, Area, Bar, ComposedChart, Line, LineChart, XAxis, YAxis, Tooltip,
|
||||
ResponsiveContainer, CartesianGrid, ReferenceLine,
|
||||
@ -796,44 +796,83 @@ function EmptyState({
|
||||
const cryptoMode = suggestions.some(isCryptoLikeSymbol);
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center',
|
||||
justifyContent: 'center', height: '60vh', gap: 16,
|
||||
color: 'var(--muted-foreground)',
|
||||
}}>
|
||||
<div style={{ fontSize: 56 }}>📈</div>
|
||||
<div style={{ fontSize: 20, fontWeight: 700, color: 'var(--foreground)' }}>
|
||||
Search an asset to get started
|
||||
</div>
|
||||
<div style={{ fontSize: 14, textAlign: 'center', maxWidth: 360 }}>
|
||||
Type a ticker symbol, crypto pair, or company name in the search bar above to view charts, financials, and news.
|
||||
</div>
|
||||
{cryptoMode && (
|
||||
<div style={{ fontSize: 12, color: 'var(--muted-foreground)', fontWeight: 600 }}>
|
||||
Suggested from your crypto bot configuration
|
||||
<div className="home-command-center">
|
||||
<section className="home-hero-panel" aria-labelledby="home-hero-title">
|
||||
<div className="home-hero-copy">
|
||||
<div className="home-hero-eyebrow">
|
||||
<Sparkles size={15} />
|
||||
Market workspace
|
||||
</div>
|
||||
<h1 id="home-hero-title">Start with a symbol. See the full trading picture.</h1>
|
||||
<p>
|
||||
Search or pick a configured asset to open charts, fundamentals, news, alerts, and execution context in one focused workspace.
|
||||
</p>
|
||||
<div className="home-hero-actions">
|
||||
{suggestions.slice(0, 5).map(t => (
|
||||
<Button
|
||||
type="button"
|
||||
key={t}
|
||||
onClick={() => onSelect(t)}
|
||||
variant={t === suggestions[0] ? 'default' : 'subtle'}
|
||||
size="lg"
|
||||
>
|
||||
{t}
|
||||
<ArrowRight size={15} />
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
{cryptoMode && (
|
||||
<div className="home-hero-hint">
|
||||
Suggested from your crypto bot configuration
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
|
||||
{suggestions.map(t => (
|
||||
<button
|
||||
key={t}
|
||||
onClick={() => onSelect(t)}
|
||||
style={{
|
||||
padding: '4px 12px',
|
||||
background: 'var(--accent-soft)',
|
||||
color: 'var(--primary)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 20,
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
fontFamily: 'inherit',
|
||||
}}
|
||||
>
|
||||
{t}
|
||||
</button>
|
||||
|
||||
<div className="home-market-preview" aria-hidden="true">
|
||||
<div className="home-preview-toolbar">
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
</div>
|
||||
<div className="home-preview-chart">
|
||||
<svg viewBox="0 0 420 180" role="img">
|
||||
<path className="home-preview-area" d="M0 148 C42 136 62 92 102 104 C143 117 160 45 206 63 C244 78 260 124 302 92 C344 61 360 40 420 28 L420 180 L0 180 Z" />
|
||||
<path className="home-preview-line" d="M0 148 C42 136 62 92 102 104 C143 117 160 45 206 63 C244 78 260 124 302 92 C344 61 360 40 420 28" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="home-preview-stats">
|
||||
<div>
|
||||
<span>Readiness</span>
|
||||
<strong>Live</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Risk</span>
|
||||
<strong>Guarded</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Signals</span>
|
||||
<strong>Synced</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="home-action-grid" aria-label="Primary workflows">
|
||||
{[
|
||||
{ icon: Search, title: 'Analyze', copy: 'Open a symbol workspace with chart, metrics, and news.' },
|
||||
{ icon: ShieldCheck, title: 'Plan', copy: 'Build short-term setups with explicit sizing and exits.' },
|
||||
{ icon: Bell, title: 'Monitor', copy: 'Track alerts, watchlist entries, and live status updates.' },
|
||||
{ icon: BarChart3, title: 'Review', copy: 'Check positions, orders, and strategy performance.' },
|
||||
].map(({ icon: Icon, title, copy }) => (
|
||||
<article key={title} className="home-action-card">
|
||||
<div className="home-action-icon">
|
||||
<Icon size={18} />
|
||||
</div>
|
||||
<h2>{title}</h2>
|
||||
<p>{copy}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -13,12 +13,12 @@ export function MarketsView() {
|
||||
const handleClone = (preset: StrategyPreset) => setClonedPreset(preset);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="ux-page-stack">
|
||||
<PageHeader
|
||||
title="Markets"
|
||||
description="Scan live opportunities and reusable setups from the current market without leaving the workspace."
|
||||
/>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 20, marginBottom: 24 }}>
|
||||
<div className="markets-opportunity-grid">
|
||||
<TopVolatile botState={botState} />
|
||||
<AISetups botState={botState} />
|
||||
</div>
|
||||
|
||||
@ -16,20 +16,20 @@ export function PortfolioView() {
|
||||
const [tab, setTab] = useState<Tab>('Positions & Orders');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="ux-page-stack">
|
||||
<PageHeader
|
||||
title="Portfolio"
|
||||
description="Review live positions, order activity, and completed trades in one consistent operational view."
|
||||
/>
|
||||
|
||||
<div className="tab-strip" style={{ marginBottom: 20 }}>
|
||||
<div className="product-tabs">
|
||||
{TABS.map(t => (
|
||||
<Button
|
||||
key={t}
|
||||
onClick={() => setTab(t)}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="tab-button"
|
||||
className="product-tab"
|
||||
data-active={tab === t}
|
||||
>
|
||||
{t}
|
||||
|
||||
@ -7,6 +7,7 @@ import { VisualRuleBuilder, type VisualRule } from '../components/strategy/Visua
|
||||
import { createTradeProfile } from '../lib/profileApi';
|
||||
import { BacktestRunnerPanel } from '../backtest/components/BacktestRunnerPanel';
|
||||
import { PageHeader } from '../components/ui/page-header';
|
||||
import { Button } from '../components/ui/Primitives';
|
||||
import { Card, CardContent } from '../components/ui/card';
|
||||
|
||||
type ResearchTab = 'Strategies' | 'Visual Builder' | 'Code Editor' | 'Signals' | 'Backtesting';
|
||||
@ -35,13 +36,16 @@ function SubTab({
|
||||
label, active, onClick,
|
||||
}: { label: string; active: boolean; onClick: () => void }) {
|
||||
return (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className="tab-button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="product-tab"
|
||||
data-active={active}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@ -83,13 +87,13 @@ export function ResearchView() {
|
||||
const initialCapitalUsd = botState.settings?.totalCapital ?? 1000;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="ux-page-stack">
|
||||
<PageHeader
|
||||
title="Research"
|
||||
description="Design, test, and refine strategies before they go anywhere near live execution."
|
||||
/>
|
||||
|
||||
<div className="tab-strip">
|
||||
<div className="product-tabs">
|
||||
{tabs.map(t => (
|
||||
<SubTab key={t} label={t} active={tab === t} onClick={() => setTab(t)} />
|
||||
))}
|
||||
|
||||
@ -164,7 +164,7 @@ export function ScreenerView() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="ux-page-stack">
|
||||
<PageHeader
|
||||
title="Stock Screener"
|
||||
description="Filter liquid names by sector and market cap, then jump directly into charting and research."
|
||||
@ -176,10 +176,10 @@ export function ScreenerView() {
|
||||
}
|
||||
/>
|
||||
|
||||
<Card style={{ marginBottom: 16 }}>
|
||||
<CardContent style={{ padding: 16 }}>
|
||||
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<div style={{ position: 'relative', minWidth: 200, maxWidth: 280 }}>
|
||||
<Card className="screener-filter-card">
|
||||
<CardContent>
|
||||
<div className="screener-filter-row">
|
||||
<div className="screener-search-field">
|
||||
<Search size={14} style={{
|
||||
position: 'absolute', left: 10, top: '50%',
|
||||
transform: 'translateY(-50%)', color: 'var(--muted-foreground)',
|
||||
@ -200,7 +200,7 @@ export function ScreenerView() {
|
||||
options={CAP_OPTIONS.map((c, i) => ({ value: String(i), label: c.label }))}
|
||||
/>
|
||||
|
||||
<div style={{ display: 'flex', gap: 5, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<div className="screener-sector-row">
|
||||
<SlidersHorizontal size={13} color="var(--muted-foreground)" />
|
||||
{SECTORS.slice(0, 6).map(s => (
|
||||
<Button
|
||||
@ -243,26 +243,16 @@ export function ScreenerView() {
|
||||
</Card>
|
||||
|
||||
{error && !loading && (
|
||||
<div style={{
|
||||
padding: 12,
|
||||
background: 'color-mix(in oklab, var(--destructive) 10%, var(--card) 90%)',
|
||||
border: '1px solid color-mix(in oklab, var(--destructive) 45%, var(--border) 55%)',
|
||||
borderRadius: 8,
|
||||
fontSize: 13,
|
||||
color: 'var(--destructive)',
|
||||
marginBottom: 12,
|
||||
}}>
|
||||
<div className="ux-alert">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ background: 'var(--card)', borderRadius: 12, border: '1px solid var(--border)', overflowX: 'auto', overflowY: 'hidden' }}>
|
||||
<div style={{
|
||||
<div className="ux-data-grid">
|
||||
<div className="ux-data-grid-header" style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '100px 1fr 90px 90px 110px 80px 110px',
|
||||
padding: '10px 16px',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
background: 'var(--muted)',
|
||||
}}>
|
||||
{([
|
||||
['symbol', 'Symbol'],
|
||||
@ -276,11 +266,8 @@ export function ScreenerView() {
|
||||
<span
|
||||
key={key}
|
||||
onClick={() => handleSort(key)}
|
||||
style={{
|
||||
fontSize: 11, color: 'var(--muted-foreground)', fontWeight: 700,
|
||||
textTransform: 'uppercase', letterSpacing: '0.05em',
|
||||
cursor: 'pointer', userSelect: 'none',
|
||||
}}
|
||||
className="ux-data-grid-head"
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
{label}<SortIcon k={key} />
|
||||
</span>
|
||||
@ -289,25 +276,21 @@ export function ScreenerView() {
|
||||
|
||||
{loading && <ScreenerSkeletonRows />}
|
||||
|
||||
{!loading && filtered.map((row, i) => (
|
||||
{!loading && filtered.map((row) => (
|
||||
<div
|
||||
key={row.symbol}
|
||||
onClick={() => handleRowClick(row.symbol)}
|
||||
className="ux-data-grid-row"
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '100px 1fr 90px 90px 110px 80px 110px',
|
||||
padding: '11px 16px',
|
||||
borderBottom: i < filtered.length - 1 ? '1px solid var(--border)' : 'none',
|
||||
cursor: 'pointer',
|
||||
alignItems: 'center',
|
||||
transition: 'background 0.15s ease',
|
||||
}}
|
||||
onMouseEnter={e => (e.currentTarget.style.background = 'var(--muted)')}
|
||||
onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}
|
||||
>
|
||||
<span style={{ fontSize: 13, fontWeight: 700, color: 'var(--primary)' }}>{row.symbol}</span>
|
||||
<span style={{ fontSize: 12, color: 'var(--foreground)' }}>{row.companyName}</span>
|
||||
<span style={{ fontSize: 13, fontWeight: 600, color: 'var(--foreground)' }}>
|
||||
<span className="ux-data-grid-cell" style={{ fontWeight: 700, color: 'var(--primary)' }}>{row.symbol}</span>
|
||||
<span className="ux-data-grid-cell">{row.companyName}</span>
|
||||
<span className="ux-data-grid-cell" style={{ fontWeight: 600 }}>
|
||||
{row.price != null ? `$${row.price.toFixed(2)}` : '—'}
|
||||
</span>
|
||||
<span style={{
|
||||
@ -316,21 +299,24 @@ export function ScreenerView() {
|
||||
}}>
|
||||
{row.changesPercentage >= 0 ? '+' : ''}{row.changesPercentage?.toFixed(2)}%
|
||||
</span>
|
||||
<span style={{ fontSize: 12, color: 'var(--foreground)' }}>
|
||||
<span className="ux-data-grid-cell">
|
||||
{row.marketCap ? fmtCap(row.marketCap) : '—'}
|
||||
</span>
|
||||
<span style={{ fontSize: 12, color: 'var(--foreground)' }}>
|
||||
<span className="ux-data-grid-cell">
|
||||
{row.pe != null && row.pe > 0 ? row.pe.toFixed(1) : '—'}
|
||||
</span>
|
||||
<span style={{ fontSize: 12, color: 'var(--foreground)' }}>
|
||||
<span className="ux-data-grid-cell">
|
||||
{row.volume ? fmtCap(row.volume).replace('$', '') : '—'}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{!loading && filtered.length === 0 && !error && (
|
||||
<div style={{ padding: 32, textAlign: 'center', color: 'var(--muted-foreground)', fontSize: 13 }}>
|
||||
No results match your filters
|
||||
<div className="ux-empty-state" style={{ border: 0, borderRadius: 0 }}>
|
||||
<div>
|
||||
<strong>No results match your filters</strong>
|
||||
<span>Adjust the query, sector, or market cap filter.</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -35,13 +35,13 @@ export function SettingsView() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="ux-page-stack">
|
||||
<PageHeader
|
||||
title="Settings"
|
||||
description="Manage your account, credentials, and runtime configuration from one place."
|
||||
/>
|
||||
|
||||
<div className="tab-strip" style={{ marginBottom: 24 }}>
|
||||
<div className="product-tabs">
|
||||
{sections.map(s => (
|
||||
<Button
|
||||
key={s}
|
||||
@ -49,7 +49,7 @@ export function SettingsView() {
|
||||
onClick={() => handleSectionChange(s)}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="tab-button"
|
||||
className="product-tab"
|
||||
data-active={section === s}
|
||||
>
|
||||
{s}
|
||||
@ -57,17 +57,7 @@ export function SettingsView() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="settings-legacy-surface"
|
||||
style={{
|
||||
background: 'var(--card)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 24,
|
||||
padding: 24,
|
||||
boxShadow: 'var(--card-shadow)',
|
||||
color: 'var(--foreground)',
|
||||
}}
|
||||
>
|
||||
<div className="ux-surface settings-legacy-surface p-5" style={{ color: 'var(--foreground)' }}>
|
||||
{section === 'Account' && <SettingsTab botState={botState} />}
|
||||
{section === 'Bot Config' && isAdmin && <ConfigTab />}
|
||||
{section === 'Admin Panel' && isAdmin && <AdminTab botState={botState} socket={socket} />}
|
||||
|
||||
@ -965,7 +965,7 @@ export function SimpleView() {
|
||||
const saveButtonDisabled = submitting || loadingPrice || (draft.side === 'sell' && !selectedSellHolding);
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="trade-plans-page space-y-8">
|
||||
<PageHeader
|
||||
title="Trade Plans"
|
||||
description="Create and manage short-term trade plans, then convert filled positions into long-term holds when you want to stop automated exits."
|
||||
@ -1370,14 +1370,17 @@ export function SimpleView() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="block">
|
||||
<CardTitle className="uppercase">Saved setups</CardTitle>
|
||||
<CardDescription>Review armed setups, current runtime order state, and whether an executed setup is now showing in Portfolio as an open holding.</CardDescription>
|
||||
<Card className="saved-setups-panel">
|
||||
<CardHeader className="saved-setups-header">
|
||||
<div>
|
||||
<CardTitle>Saved setups</CardTitle>
|
||||
<CardDescription>Review armed setups, runtime order state, and portfolio handoff status.</CardDescription>
|
||||
</div>
|
||||
<span className="saved-setups-count">{savedSetups.length}</span>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="saved-setups-list">
|
||||
{savedSetups.length === 0 && (
|
||||
<div className="rounded-[1.5rem] border border-dashed border-[var(--border)] bg-[var(--card-elevated)] px-5 py-8 text-sm text-[var(--muted-foreground)]">
|
||||
No Trade Plans saved yet.
|
||||
@ -1413,32 +1416,32 @@ export function SimpleView() {
|
||||
ref={(node) => {
|
||||
setupCardRefs.current[entryId] = node;
|
||||
}}
|
||||
className={`rounded-[1.5rem] border bg-[var(--card-elevated)] p-5 transition ${
|
||||
className={`saved-setup-card rounded-[1.5rem] border bg-[var(--card-elevated)] p-5 transition ${
|
||||
focusedSetupId === entryId
|
||||
? 'border-[var(--primary)] ring-2 ring-[var(--primary)]/20'
|
||||
: 'border-[var(--border)]'
|
||||
}`}
|
||||
>
|
||||
<div className="mb-3 flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<h3 className="text-lg font-black uppercase text-[var(--foreground)]">{entry.symbol}</h3>
|
||||
<span className={`rounded-full px-3 py-1 text-[10px] font-black uppercase tracking-[0.2em] ${
|
||||
<div className="saved-setup-topline">
|
||||
<div className="saved-setup-title-block">
|
||||
<div className="saved-setup-asset-row">
|
||||
<h3>{entry.symbol}</h3>
|
||||
<span className={`saved-setup-side ${
|
||||
side === 'buy' ? 'bg-emerald-500/10 text-emerald-700 dark:text-emerald-300' : 'bg-violet-500/10 text-violet-700 dark:text-violet-300'
|
||||
}`}>
|
||||
{side}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-[var(--muted-foreground)]">{describeSavedSetup(entry)}</p>
|
||||
<p>{describeSavedSetup(entry)}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="saved-setup-actions">
|
||||
{canConvertToLongTerm ? (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => void handleConvertToLongTerm(entry)}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="uppercase tracking-[0.18em]"
|
||||
className="saved-setup-action"
|
||||
>
|
||||
Convert to long-term
|
||||
</Button>
|
||||
@ -1449,7 +1452,7 @@ export function SimpleView() {
|
||||
onClick={() => void handleResumeExitManagement(entry)}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="uppercase tracking-[0.18em]"
|
||||
className="saved-setup-action"
|
||||
>
|
||||
Resume exit management
|
||||
</Button>
|
||||
@ -1459,7 +1462,7 @@ export function SimpleView() {
|
||||
onClick={() => handleEdit(entry)}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className={isEditing ? 'border-emerald-500/30 text-emerald-700 dark:text-emerald-300' : 'uppercase tracking-[0.18em]'}
|
||||
className={isEditing ? 'saved-setup-action border-emerald-500/30 text-emerald-700 dark:text-emerald-300' : 'saved-setup-action'}
|
||||
>
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<Pencil size={14} />
|
||||
@ -1471,7 +1474,7 @@ export function SimpleView() {
|
||||
onClick={() => void handleDelete(entryId)}
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="uppercase tracking-[0.18em]"
|
||||
className="saved-setup-action"
|
||||
>
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<Trash2 size={14} />
|
||||
@ -1481,33 +1484,33 @@ export function SimpleView() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3 text-[11px] uppercase tracking-[0.2em] text-[var(--muted-foreground)]">
|
||||
<span className="rounded-full border border-[var(--border)] px-3 py-1">
|
||||
<div className="saved-setup-meta-grid">
|
||||
<span>
|
||||
{formatSetupStatus(entry.status)}
|
||||
</span>
|
||||
<span className="rounded-full border border-[var(--border)] px-3 py-1">
|
||||
<span>
|
||||
{formatHoldingMode(entry.holding_mode)}
|
||||
</span>
|
||||
<span className="rounded-full border border-[var(--border)] px-3 py-1">
|
||||
<span>
|
||||
{formatAutomationState(entry)}
|
||||
</span>
|
||||
{runtimeSnapshot ? (
|
||||
<span className={`rounded-full border px-3 py-1 ${statusToneClasses(runtimeSnapshot.tone)}`}>
|
||||
<span className={statusToneClasses(runtimeSnapshot.tone)}>
|
||||
{runtimeSnapshot.label}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="rounded-full border border-[var(--border)] px-3 py-1">
|
||||
<span>
|
||||
{String(entry.sizing_mode || '').trim().toLowerCase() === 'amount'
|
||||
? `Budget $${Number(entry.amount_usd || 0).toFixed(2)}`
|
||||
: `Qty ${entry.quantity || entry.filled_quantity || 0}`}
|
||||
</span>
|
||||
{entry.reference_price ? (
|
||||
<span className="rounded-full border border-[var(--border)] px-3 py-1">
|
||||
<span>
|
||||
Ref {Number(entry.reference_price).toFixed(4)}
|
||||
</span>
|
||||
) : null}
|
||||
{entry.entry_price ? (
|
||||
<span className="rounded-full border border-[var(--border)] px-3 py-1">
|
||||
<span>
|
||||
Entry {Number(entry.entry_price).toFixed(4)}
|
||||
</span>
|
||||
) : null}
|
||||
@ -1515,7 +1518,7 @@ export function SimpleView() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void copyIdentifier('trade', String(runtimeSnapshot?.tradeId || entry.linked_trade_id))}
|
||||
className="rounded-full border border-[var(--border)] px-3 py-1 transition hover:border-[var(--primary)] hover:text-[var(--foreground)]"
|
||||
className="saved-setup-id-chip"
|
||||
>
|
||||
{copiedKey === `trade:${String(runtimeSnapshot?.tradeId || entry.linked_trade_id)}`
|
||||
? 'Trade copied'
|
||||
@ -1526,7 +1529,7 @@ export function SimpleView() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void copyIdentifier('order', runtimeSnapshot.orderId)}
|
||||
className="rounded-full border border-[var(--border)] px-3 py-1 transition hover:border-[var(--primary)] hover:text-[var(--foreground)]"
|
||||
className="saved-setup-id-chip"
|
||||
>
|
||||
{copiedKey === `order:${runtimeSnapshot.orderId}`
|
||||
? 'Order copied'
|
||||
@ -1534,7 +1537,7 @@ export function SimpleView() {
|
||||
</button>
|
||||
) : null}
|
||||
{updatedAt ? (
|
||||
<span className="rounded-full border border-[var(--border)] px-3 py-1 normal-case tracking-normal">
|
||||
<span className="saved-setup-updated">
|
||||
Updated {updatedAt}
|
||||
</span>
|
||||
) : null}
|
||||
@ -1573,14 +1576,14 @@ export function SimpleView() {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mt-4 grid gap-2 md:grid-cols-5">
|
||||
<div className="saved-setup-timeline">
|
||||
{SIMPLE_TIMELINE_STEPS.map((step) => {
|
||||
const complete = isTimelineStepComplete(runtimeSnapshot?.stage, step);
|
||||
const isCurrent = runtimeSnapshot?.stage === step;
|
||||
return (
|
||||
<div
|
||||
key={step}
|
||||
className={`rounded-2xl border px-3 py-2 text-[11px] font-semibold ${
|
||||
className={`saved-setup-step ${
|
||||
isCurrent
|
||||
? 'border-[var(--primary)] bg-[var(--accent-soft)] text-[var(--primary)]'
|
||||
: complete
|
||||
@ -1594,7 +1597,7 @@ export function SimpleView() {
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 rounded-2xl border border-[var(--border)] bg-[var(--background)] px-4 py-3 text-sm text-[var(--muted-foreground)]">
|
||||
<div className="saved-setup-next-action">
|
||||
<span className="font-semibold text-[var(--foreground)]">Next action:</span>{' '}
|
||||
{nextActionText}
|
||||
</div>
|
||||
|
||||
@ -17,6 +17,27 @@
|
||||
"jsx": "react-jsx",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@bytelyst/api-client": [
|
||||
"../../learning_ai_common_plat/packages/api-client/dist/index.d.ts"
|
||||
],
|
||||
"@bytelyst/design-tokens": [
|
||||
"../../learning_ai_common_plat/packages/design-tokens/dist/index.d.ts"
|
||||
],
|
||||
"@bytelyst/errors": [
|
||||
"../../learning_ai_common_plat/packages/errors/dist/index.d.ts"
|
||||
],
|
||||
"@bytelyst/kill-switch-client": [
|
||||
"../../learning_ai_common_plat/packages/kill-switch-client/dist/index.d.ts"
|
||||
],
|
||||
"@bytelyst/react-auth": [
|
||||
"../../learning_ai_common_plat/packages/react-auth/dist/index.d.ts"
|
||||
],
|
||||
"@bytelyst/telemetry-client": [
|
||||
"../../learning_ai_common_plat/packages/telemetry-client/dist/index.d.ts"
|
||||
],
|
||||
"@bytelyst/ui": [
|
||||
"../../learning_ai_common_plat/packages/ui/dist/index.d.ts"
|
||||
],
|
||||
"monaco-editor": [
|
||||
"../node_modules/.pnpm/monaco-editor@0.55.1/node_modules/monaco-editor/esm/vs/editor/editor.api"
|
||||
],
|
||||
|
||||
@ -8,10 +8,19 @@ const monacoEditorPath = path.resolve(
|
||||
__dirname,
|
||||
'../node_modules/.pnpm/monaco-editor@0.55.1/node_modules/monaco-editor',
|
||||
);
|
||||
const commonUiSourcePath = '/opt/bytelyst/learning_ai_common_plat/packages/ui/src/index.ts';
|
||||
const workspaceRoot = path.resolve(__dirname, '..', '..');
|
||||
const commonPlatRoot = path.join(workspaceRoot, 'learning_ai_common_plat');
|
||||
|
||||
function commonPlatSourceEntry(pkg: string): string | null {
|
||||
const sourceEntry = path.join(commonPlatRoot, 'packages', pkg, 'src', 'index.ts');
|
||||
return fs.existsSync(sourceEntry) ? sourceEntry : null;
|
||||
}
|
||||
|
||||
// Resolve a @bytelyst/* package: prefer web/node_modules, fall back to vendor/
|
||||
function bytelystAlias(pkg: string): string {
|
||||
const sourceEntry = commonPlatSourceEntry(pkg);
|
||||
if (sourceEntry) return sourceEntry;
|
||||
|
||||
const nmPath = path.resolve(__dirname, 'node_modules/@bytelyst', pkg);
|
||||
const vendorPath = path.resolve(__dirname, '../vendor/bytelyst', pkg);
|
||||
if (fs.existsSync(nmPath)) return nmPath;
|
||||
@ -34,13 +43,12 @@ export default defineConfig({
|
||||
alias: [
|
||||
// Vendor packages that live only in vendor/ (not in web/node_modules/)
|
||||
{ find: '@bytelyst/api-client', replacement: bytelystAlias('api-client') },
|
||||
{ find: '@bytelyst/design-tokens', replacement: path.join(commonPlatRoot, 'packages', 'design-tokens') },
|
||||
{ find: '@bytelyst/errors', replacement: bytelystAlias('errors') },
|
||||
{
|
||||
find: '@bytelyst/ui',
|
||||
replacement: fs.existsSync(commonUiSourcePath)
|
||||
? commonUiSourcePath
|
||||
: path.resolve(__dirname, 'node_modules/@bytelyst/ui'),
|
||||
},
|
||||
{ find: '@bytelyst/kill-switch-client', replacement: bytelystAlias('kill-switch-client') },
|
||||
{ find: '@bytelyst/react-auth', replacement: bytelystAlias('react-auth') },
|
||||
{ find: '@bytelyst/telemetry-client', replacement: bytelystAlias('telemetry-client') },
|
||||
{ find: '@bytelyst/ui', replacement: bytelystAlias('ui') },
|
||||
// Monaco is an explicit web dependency, but this workspace often runs
|
||||
// against pnpm's root store without a web/node_modules symlink when the
|
||||
// private mobile registry is unavailable. Keep local worker imports
|
||||
|
||||
Loading…
Reference in New Issue
Block a user