Polish trading UI and add launch roadmap

This commit is contained in:
Saravana Achu Mac 2026-05-08 20:58:00 -07:00
parent 041e5e6f63
commit 1994821acf
35 changed files with 2653 additions and 665 deletions

7
.npmrc
View File

@ -1,5 +1,2 @@
@bytelyst:registry=https://gitea.bytelyst.com/api/packages/ByteLyst/npm/ link-workspace-packages=true
# Gitea auth tokens belong in user-level ~/.npmrc or CI secrets, and are only needed prefer-workspace-packages=true
# 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

View File

@ -2,7 +2,8 @@ const fs = require('node:fs');
const path = require('node:path'); const path = require('node:path');
const PACKAGE_SCOPE = '@bytelyst/'; 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 = [ const DEFAULT_COMMON_PLAT_ROOTS = [
path.resolve(__dirname, '..', 'learning_ai_common_plat'), path.resolve(__dirname, '..', 'learning_ai_common_plat'),
'/opt/bytelyst/learning_ai_common_plat', '/opt/bytelyst/learning_ai_common_plat',
@ -70,11 +71,6 @@ function resolveSpecifier(name) {
return packagePath ? `file:${packagePath}` : null; return packagePath ? `file:${packagePath}` : null;
} }
if (PACKAGE_SOURCE === 'gitea') {
const version = resolveRegistryVersion(name);
return version ?? null;
}
const vendorPath = pathIfPackageExists(VENDOR_PACKAGES_ROOT, name); const vendorPath = pathIfPackageExists(VENDOR_PACKAGES_ROOT, name);
if (vendorPath) { if (vendorPath) {
return `file:${vendorPath}`; return `file:${vendorPath}`;

View 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.

View File

@ -9,7 +9,6 @@
"audit:ui": "sh ./scripts/ui-drift-audit.sh", "audit:ui": "sh ./scripts/ui-drift-audit.sh",
"audit:ui:strict": "sh ./scripts/ui-drift-audit.sh strict", "audit:ui:strict": "sh ./scripts/ui-drift-audit.sh strict",
"install:common-plat": "BYTELYST_PACKAGE_SOURCE=common-plat pnpm install -r", "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", "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", "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", "smoke:release": "sh ./scripts/smoke-release.sh",
@ -22,10 +21,10 @@
"docker:down": "docker compose down" "docker:down": "docker compose down"
}, },
"dependencies": { "dependencies": {
"@bytelyst/kill-switch-client": "file:./vendor/bytelyst/kill-switch-client", "@bytelyst/kill-switch-client": "file:../learning_ai_common_plat/packages/kill-switch-client",
"@bytelyst/react-native-platform-sdk": "file:./vendor/bytelyst/react-native-platform-sdk", "@bytelyst/react-native-platform-sdk": "file:../learning_ai_common_plat/packages/react-native-platform-sdk",
"@bytelyst/react-auth": "file:./vendor/bytelyst/react-auth", "@bytelyst/react-auth": "file:../learning_ai_common_plat/packages/react-auth",
"@bytelyst/telemetry-client": "file:./vendor/bytelyst/telemetry-client" "@bytelyst/telemetry-client": "file:../learning_ai_common_plat/packages/telemetry-client"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5.9.3" "typescript": "^5.9.3"

10
pnpm-lock.yaml generated
View File

@ -4,7 +4,7 @@ settings:
autoInstallPeers: true autoInstallPeers: true
excludeLinksFromLockfile: false excludeLinksFromLockfile: false
pnpmfileChecksum: sha256-9nrEUcRW1dVQY29GEuBWlallHSWZBRpsHo3Y3rZLLdw= pnpmfileChecksum: sha256-6gxhEjw/htOhRcZhr2u1v7hYwxfTMaTz52IWFOJV6+k=
importers: importers:
@ -2694,6 +2694,7 @@ packages:
'@ungap/structured-clone@1.3.0': '@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} 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': '@unrs/resolver-binding-android-arm-eabi@1.11.1':
resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==}
@ -2845,6 +2846,7 @@ packages:
'@xmldom/xmldom@0.8.12': '@xmldom/xmldom@0.8.12':
resolution: {integrity: sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg==} resolution: {integrity: sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg==}
engines: {node: '>=10.0.0'} engines: {node: '>=10.0.0'}
deprecated: this version has critical issues, please update to the latest version
abort-controller@3.0.0: abort-controller@3.0.0:
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
@ -5198,6 +5200,7 @@ packages:
nats@1.4.12: nats@1.4.12:
resolution: {integrity: sha512-Jf4qesEF0Ay0D4AMw3OZnKMRTQm+6oZ5q8/m4gpy5bTmiDiK6wCXbZpzEslmezGpE93LV3RojNEG6dpK/mysLQ==} resolution: {integrity: sha512-Jf4qesEF0Ay0D4AMw3OZnKMRTQm+6oZ5q8/m4gpy5bTmiDiK6wCXbZpzEslmezGpE93LV3RojNEG6dpK/mysLQ==}
engines: {node: '>= 8.0.0'} engines: {node: '>= 8.0.0'}
deprecated: Package moved. Use @nats-io/transport-node from https://github.com/nats-io/nats.js
hasBin: true hasBin: true
natural-compare@1.4.0: natural-compare@1.4.0:
@ -5255,6 +5258,7 @@ packages:
nuid@1.1.6: nuid@1.1.6:
resolution: {integrity: sha512-Eb3CPCupYscP1/S1FQcO5nxtu6l/F3k0MQ69h7f5osnsemVk5pkc8/5AyalVT+NCfra9M71U8POqF6EZa6IHvg==} resolution: {integrity: sha512-Eb3CPCupYscP1/S1FQcO5nxtu6l/F3k0MQ69h7f5osnsemVk5pkc8/5AyalVT+NCfra9M71U8POqF6EZa6IHvg==}
engines: {node: '>= 8.16.0'} 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: nullthrows@1.1.1:
resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==} resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==}
@ -6470,10 +6474,12 @@ packages:
uuid@7.0.3: uuid@7.0.3:
resolution: {integrity: sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==} 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 hasBin: true
uuid@8.3.2: uuid@8.3.2:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} 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 hasBin: true
v8-compile-cache-lib@3.0.1: v8-compile-cache-lib@3.0.1:
@ -9317,7 +9323,9 @@ snapshots:
metro-runtime: 0.83.5 metro-runtime: 0.83.5
transitivePeerDependencies: transitivePeerDependencies:
- '@babel/core' - '@babel/core'
- bufferutil
- supports-color - supports-color
- utf-8-validate
'@react-native/normalize-colors@0.74.89': {} '@react-native/normalize-colors@0.74.89': {}

View File

@ -18,13 +18,13 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@bytelyst/api-client": "file:../vendor/bytelyst/api-client", "@bytelyst/api-client": "file:../../learning_ai_common_plat/packages/api-client",
"@bytelyst/design-tokens": "^0.1.5", "@bytelyst/design-tokens": "file:../../learning_ai_common_plat/packages/design-tokens",
"@bytelyst/errors": "file:../vendor/bytelyst/errors", "@bytelyst/errors": "file:../../learning_ai_common_plat/packages/errors",
"@bytelyst/kill-switch-client": "file:../vendor/bytelyst/kill-switch-client", "@bytelyst/kill-switch-client": "file:../../learning_ai_common_plat/packages/kill-switch-client",
"@bytelyst/react-auth": "file:../vendor/bytelyst/react-auth", "@bytelyst/react-auth": "file:../../learning_ai_common_plat/packages/react-auth",
"@bytelyst/telemetry-client": "file:../vendor/bytelyst/telemetry-client", "@bytelyst/telemetry-client": "file:../../learning_ai_common_plat/packages/telemetry-client",
"@bytelyst/ui": "^0.1.5", "@bytelyst/ui": "file:../../learning_ai_common_plat/packages/ui",
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",

View File

@ -1,11 +1,9 @@
:root { :root {
--bg-dark: var(--background); --bg-dark: var(--background);
--bg-card: var(--card); --bg-card: var(--card);
--accent: var(--bl-success);
--accent-down: var(--bl-danger); --accent-down: var(--bl-danger);
--text-main: var(--foreground); --text-main: var(--foreground);
--text-dim: var(--muted-foreground); --text-dim: var(--muted-foreground);
--border: var(--bl-border);
--header-height: 120px; --header-height: 120px;
} }
@ -402,13 +400,11 @@ body {
/* Tables (Premium Pro Style) */ /* Tables (Premium Pro Style) */
.table-container { .table-container {
background: linear-gradient(145deg, color-mix(in oklab, var(--bg-card) 86%, transparent), color-mix(in oklab, var(--bg-dark) 92%, transparent)); background: var(--card);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 20px; border-radius: var(--bl-radius-card);
overflow: hidden; overflow: hidden;
margin-bottom: 32px; margin-bottom: 28px;
box-shadow: var(--card-shadow); box-shadow: var(--card-shadow);
position: relative; position: relative;
} }
@ -420,7 +416,7 @@ body {
left: 0; left: 0;
right: 0; right: 0;
height: 1px; height: 1px;
background: linear-gradient(90deg, transparent, var(--bl-success-muted), transparent); background: linear-gradient(90deg, transparent, var(--accent-soft), transparent);
} }
.pro-table { .pro-table {
@ -431,22 +427,24 @@ body {
} }
.pro-table th { .pro-table th {
padding: 20px 24px; padding: 14px 16px;
background: color-mix(in oklab, var(--text-main) 2%, transparent); background: var(--muted);
font-size: 0.7rem; font-size: 0.7rem;
font-weight: 800; font-weight: 750;
text-transform: uppercase; text-transform: uppercase;
color: var(--text-dim); color: var(--text-dim);
letter-spacing: 0.15em; letter-spacing: 0.08em;
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
white-space: nowrap;
} }
.pro-table td { .pro-table td {
padding: 16px 24px; padding: 14px 16px;
font-size: 0.85rem; font-size: 0.85rem;
color: color-mix(in oklab, var(--text-main) 80%, transparent); color: color-mix(in oklab, var(--text-main) 84%, transparent);
border-bottom: 1px solid color-mix(in oklab, var(--text-main) 3%, transparent); border-bottom: 1px solid var(--border);
transition: all 0.2s ease; transition: all 0.2s ease;
vertical-align: middle;
} }
.pro-table tr { .pro-table tr {
@ -454,8 +452,7 @@ body {
} }
.pro-table tbody tr:hover { .pro-table tbody tr:hover {
background: color-mix(in oklab, var(--text-main) 2%, transparent); background: color-mix(in oklab, var(--accent) 8%, transparent);
transform: scale(1.002);
} }
.pro-table tbody tr:hover td { .pro-table tbody tr:hover td {
@ -492,8 +489,9 @@ body {
} }
.empty { .empty {
padding: 40px !important; padding: 48px !important;
color: var(--text-dim); color: var(--text-dim);
text-align: center;
} }
.side-buy { .side-buy {
@ -515,10 +513,15 @@ body {
} }
.status-tag { .status-tag {
padding: 4px 8px; display: inline-flex;
border-radius: 4px; align-items: center;
gap: 6px;
padding: 4px 9px;
border-radius: 999px;
border: 1px solid var(--border);
background: var(--muted);
font-size: 0.7rem; font-size: 0.7rem;
font-weight: bold; font-weight: 750;
} }
.status-tag.active { .status-tag.active {
@ -554,16 +557,17 @@ body {
} }
.settings-group { .settings-group {
background: var(--bg-card); background: var(--card);
padding: 24px; padding: 24px;
border-radius: 12px; border-radius: var(--bl-radius-card);
border: 1px solid var(--border); border: 1px solid var(--border);
box-shadow: var(--card-shadow);
} }
.settings-group h3 { .settings-group h3 {
margin-top: 0; margin-top: 0;
font-size: 1.1rem; font-size: 1rem;
color: var(--accent); color: var(--foreground);
} }
.control-card { .control-card {
@ -576,8 +580,8 @@ body {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
font-size: 1.1rem; font-size: 0.95rem;
font-weight: bold; font-weight: 700;
} }
.dot { .dot {
@ -598,7 +602,7 @@ body {
background: var(--accent-down); background: var(--accent-down);
color: var(--primary-foreground); color: var(--primary-foreground);
border: none; border: none;
border-radius: 8px; border-radius: var(--bl-radius-control);
font-weight: 800; font-weight: 800;
cursor: not-allowed; cursor: not-allowed;
opacity: 0.5; opacity: 0.5;

View File

@ -193,21 +193,7 @@ function App() {
}}> }}>
{/* Critical system alert banner */} {/* Critical system alert banner */}
{hasCriticalEvents && ( {hasCriticalEvents && (
<div style={{ <div className="critical-alert-banner">
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,
}}>
<span></span> <span></span>
<span> <span>
SYSTEM ALERT: {recentCriticalEvents.length} CRITICAL ISSUES DETECTED GO TO SETTINGS ADMIN PANEL SYSTEM ALERT: {recentCriticalEvents.length} CRITICAL ISSUES DETECTED GO TO SETTINGS ADMIN PANEL
@ -216,7 +202,7 @@ function App() {
</div> </div>
)} )}
<div style={{ paddingTop: hasCriticalEvents ? 32 : 0 }}> <div>
<AppShell /> <AppShell />
</div> </div>

View File

@ -15,11 +15,35 @@
} }
.alerts-container { .alerts-container {
max-height: 300px; max-height: min(520px, 58vh);
overflow-y: auto; overflow-y: auto;
padding-right: 8px; 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 { .alerts-container::-webkit-scrollbar {
width: 6px; width: 6px;
} }

View File

@ -33,6 +33,12 @@ export const AlertFeed = ({ alerts }: AlertFeedProps) => {
<div className="alert-feed"> <div className="alert-feed">
<h2>Recent Activity</h2> <h2>Recent Activity</h2>
<div className="alerts-container"> <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) => ( {sortedAlerts.map((alert, index) => (
<div key={index} className={`alert-item alert-${alert.type}`}> <div key={index} className={`alert-item alert-${alert.type}`}>
<div className="alert-icon">{alertTypeIcons[alert.type]}</div> <div className="alert-icon">{alertTypeIcons[alert.type]}</div>

View File

@ -3,7 +3,7 @@ import { Link, Routes, Route, Navigate, useLocation } from 'react-router-dom';
import { Sidebar } from './Sidebar'; import { Sidebar } from './Sidebar';
import { Header } from './Header'; import { Header } from './Header';
import { RightPanel } from './RightPanel'; import { RightPanel } from './RightPanel';
import { Button } from '../ui/button'; import { Button } from '../ui/Primitives';
import { getLegacySimpleRoute, getPlansRoute } from '../../views/tradePlansRoutes'; import { getLegacySimpleRoute, getPlansRoute } from '../../views/tradePlansRoutes';
const HomeView = lazy(() => import('../../views/HomeView').then((mod) => ({ default: mod.HomeView }))); const HomeView = lazy(() => import('../../views/HomeView').then((mod) => ({ default: mod.HomeView })));
@ -20,20 +20,10 @@ function RouteFallback() {
return ( return (
<section <section
aria-live="polite" aria-live="polite"
style={{ className="route-fallback route-fallback-inline"
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,
}}
> >
Loading workspace <div className="route-fallback-spinner" aria-hidden="true" />
<span>Loading workspace</span>
</section> </section>
); );
} }
@ -44,30 +34,18 @@ function NotFoundView() {
return ( return (
<section <section
aria-labelledby="not-found-title" aria-labelledby="not-found-title"
style={{ className="route-empty-state"
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)',
}}
> >
<div style={{ fontSize: 12, fontWeight: 900, letterSpacing: '0.16em', color: 'var(--accent)', textTransform: 'uppercase' }}> <div className="route-empty-eyebrow">
404 404
</div> </div>
<h1 id="not-found-title" style={{ margin: '10px 0 8px', fontSize: 34, fontWeight: 900 }}> <h1 id="not-found-title">
Route not found Route not found
</h1> </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. No trading workspace exists at <code style={{ fontWeight: 800 }}>{location.pathname}</code>. The app is still running normally.
</p> </p>
<Link to="/" style={{ marginTop: 24, textDecoration: 'none' }}> <Link to="/" className="route-empty-action">
<Button>Return home</Button> <Button>Return home</Button>
</Link> </Link>
</section> </section>

View File

@ -61,7 +61,7 @@ export function Header() {
}, []); }, []);
return ( return (
<header style={{ <header className="trading-header" style={{
height: 56, height: 56,
background: 'var(--header)', background: 'var(--header)',
borderBottom: '1px solid var(--border)', borderBottom: '1px solid var(--border)',
@ -74,7 +74,7 @@ export function Header() {
backdropFilter: 'blur(18px)', backdropFilter: 'blur(18px)',
}}> }}>
{/* Search bar */} {/* Search bar */}
<div style={{ position: 'relative', width: 300 }}> <div className="trading-header-search" style={{ position: 'relative', width: 300 }}>
<Search <Search
size={15} size={15}
style={{ style={{
@ -112,7 +112,7 @@ export function Header() {
<div style={{ flex: 1 }} /> <div style={{ flex: 1 }} />
{/* Market indices */} {/* 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 ? ( {indices.length === 0 ? (
// Skeleton while loading // Skeleton while loading
['S&P 500','Dow Jones','Nasdaq'].map(label => ( ['S&P 500','Dow Jones','Nasdaq'].map(label => (

View File

@ -23,37 +23,29 @@ function PortfolioSummary() {
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(n); new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(n);
return ( return (
<div style={{ padding: '16px 16px 12px' }}> <div className="right-panel-section">
{/* Header */} {/* Header */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}> <div className="right-panel-header">
<span style={{ fontSize: 13, fontWeight: 700, color: 'var(--foreground)' }}>Portfolio</span> <span>Portfolio</span>
<Button variant="ghost" size="sm" className="h-8 px-2 text-xs"> <Button variant="ghost" size="sm" className="h-8 px-2 text-xs">
View All <ArrowRight size={12} /> View All <ArrowRight size={12} />
</Button> </Button>
</div> </div>
{/* Total value */} {/* Total value */}
<div style={{ marginBottom: 12 }}> <div className="right-panel-metric">
<div style={{ fontSize: 20, fontWeight: 800, color: 'var(--foreground)', letterSpacing: '-0.5px' }}> <div className="right-panel-metric-value">
{fmt$(totalValue)} {fmt$(totalValue)}
</div> </div>
<div style={{ <div className="right-panel-metric-change" data-positive={pnlPos ? 'true' : 'false'}>
fontSize: 12, fontWeight: 600, marginTop: 2,
color: pnlPos ? 'var(--bl-success)' : 'var(--bl-danger)',
}}>
{pnlPos ? '+' : ''}{fmt$(totalPnl)} unrealized {pnlPos ? '+' : ''}{fmt$(totalPnl)} unrealized
</div> </div>
</div> </div>
{/* Column headers */} {/* Column headers */}
<div style={{ <div className="right-panel-table-head">
display: 'grid',
gridTemplateColumns: '2fr 1.2fr 1fr 1.2fr',
gap: 4, paddingBottom: 6,
borderBottom: '1px solid var(--border)', marginBottom: 4,
}}>
{['Symbol','Price','Change','Value'].map(h => ( {['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} {h}
</span> </span>
))} ))}
@ -61,18 +53,11 @@ function PortfolioSummary() {
{/* Rows */} {/* Rows */}
{positions.length === 0 ? ( {positions.length === 0 ? (
<div style={{ <div className="right-panel-empty">
border: '1px dashed var(--border)', <div>No open positions</div>
borderRadius: 'var(--bl-radius-card)', <p>
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 }}>
Filled Trade Plans and manual entries will appear here. Filled Trade Plans and manual entries will appear here.
</div> </p>
</div> </div>
) : ( ) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
@ -80,13 +65,7 @@ function PortfolioSummary() {
const pct = pos.unrealizedPnlPercent ?? 0; const pct = pos.unrealizedPnlPercent ?? 0;
const pos_ = pct >= 0; const pos_ = pct >= 0;
return ( return (
<div key={pos.id} style={{ <div key={pos.id} className="right-panel-table-row">
display: 'grid',
gridTemplateColumns: '2fr 1.2fr 1fr 1.2fr',
gap: 4, padding: '8px 0',
borderBottom: '1px solid var(--border)',
alignItems: 'center',
}}>
<span style={{ fontSize: 12, fontWeight: 700, color: 'var(--foreground)' }}>{pos.symbol}</span> <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, color: 'var(--foreground)' }}>{pos.currentPrice?.toFixed(2) ?? '—'}</span>
<span style={{ fontSize: 12, fontWeight: 600, color: pos_ ? 'var(--bl-success)' : 'var(--bl-danger)' }}> <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} href={article.url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
style={{ className="right-panel-news-card"
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')}
> >
{img && ( {img && (
<img <img
@ -207,11 +178,8 @@ function NewsFeed() {
return ( return (
<div> <div>
<div style={{ <div className="right-panel-header right-panel-news-header">
display: 'flex', justifyContent: 'space-between', alignItems: 'center', <span>Latest News</span>
padding: '12px 16px 8px',
}}>
<span style={{ fontSize: 13, fontWeight: 700, color: 'var(--foreground)' }}>Latest News</span>
<Button variant="ghost" size="sm" className="h-8 px-2 text-xs"> <Button variant="ghost" size="sm" className="h-8 px-2 text-xs">
View All <ArrowRight size={12} /> View All <ArrowRight size={12} />
</Button> </Button>
@ -220,25 +188,17 @@ function NewsFeed() {
{loading && <NewsFeedSkeleton />} {loading && <NewsFeedSkeleton />}
{!loading && error && ( {!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 && ( {!loading && !error && news.length === 0 && (
<div style={{ <div className="right-panel-empty mx-4 mb-4 mt-2">
border: '1px dashed var(--border)', <div>
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)' }}>
{activeSymbol ? `No news for ${activeSymbol}` : 'No symbol selected'} {activeSymbol ? `No news for ${activeSymbol}` : 'No symbol selected'}
</div> </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.'} {activeSymbol ? 'Try another ticker or check back after the next market update.' : 'Search a ticker to load market news.'}
</div> </p>
</div> </div>
)} )}
@ -251,15 +211,8 @@ function NewsFeed() {
export function RightPanel() { export function RightPanel() {
return ( return (
<aside className="dashboard-right-panel" style={{ <aside className="dashboard-right-panel right-panel">
background: 'var(--card)', <div className="right-panel-block">
borderLeft: '1px solid var(--border)',
display: 'flex',
flexDirection: 'column',
overflowY: 'auto',
flexShrink: 0,
}}>
<div style={{ borderBottom: '1px solid var(--border)' }}>
<PortfolioSummary /> <PortfolioSummary />
</div> </div>
<NewsFeed /> <NewsFeed />

View File

@ -1,7 +1,7 @@
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import { import {
Home, Briefcase, FlaskConical, Target, TrendingUp, Home, Briefcase, FlaskConical, Target, TrendingUp,
SlidersHorizontal, Star, Bell, Settings, SlidersHorizontal, Star, Bell, Settings, ChartNoAxesCombined,
} from 'lucide-react'; } from 'lucide-react';
import { useAppContext } from '../../context/AppContext'; import { useAppContext } from '../../context/AppContext';
import { getPlansRoute } from '../../views/tradePlansRoutes'; import { getPlansRoute } from '../../views/tradePlansRoutes';
@ -27,21 +27,12 @@ export function Sidebar() {
return ( return (
<aside className="trading-sidebar"> <aside className="trading-sidebar">
{/* Logo */} {/* Logo */}
<div className="trading-sidebar-logo" style={{ <div className="trading-sidebar-logo" aria-label="ByteLyst trading">
width: 40, <ChartNoAxesCombined size={22} />
height: 40, <div className="trading-sidebar-brand">
borderRadius: 10, <strong>ByteLyst</strong>
background: 'linear-gradient(135deg, var(--accent), color-mix(in oklab, var(--accent) 70%, var(--bl-info-strong) 30%))', <span>Trading OS</span>
display: 'flex', </div>
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> </div>
{/* Nav items */} {/* Nav items */}
@ -51,32 +42,12 @@ export function Sidebar() {
key={to} key={to}
to={to} to={to}
end={end} end={end}
className="trading-sidebar-link" className={({ isActive }) => `trading-sidebar-link${isActive ? ' is-active' : ''}`}
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',
})}
> >
{({ isActive }) => ( {({ isActive }) => (
<> <>
<Icon size={20} strokeWidth={isActive ? 2.2 : 1.8} /> <Icon size={20} strokeWidth={isActive ? 2.2 : 1.8} />
<span style={{ <span>{label}</span>
fontSize: 10,
fontWeight: isActive ? 700 : 500,
letterSpacing: '0.01em',
lineHeight: 1,
}}>
{label}
</span>
</> </>
)} )}
</NavLink> </NavLink>
@ -88,34 +59,9 @@ export function Sidebar() {
className="trading-sidebar-avatar" className="trading-sidebar-avatar"
title={`${user?.email ?? ''} — click to sign out`} title={`${user?.email ?? ''} — click to sign out`}
onClick={handleSignOut} 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} {initials}
<span style={{ <span className="trading-sidebar-avatar-status" />
position: 'absolute',
bottom: 1,
right: 1,
width: 9,
height: 9,
borderRadius: '50%',
background: 'var(--bl-success)',
border: '2px solid var(--card)',
}} />
</div> </div>
</aside> </aside>
); );

View File

@ -9,7 +9,7 @@ type ThemeContextValue = {
toggleTheme: () => void; toggleTheme: () => void;
}; };
const STORAGE_KEY = 'trading-dashboard-theme'; const STORAGE_KEY = 'trading-dashboard-theme-v2';
const ThemeContext = createContext<ThemeContextValue | null>(null); const ThemeContext = createContext<ThemeContextValue | null>(null);
@ -46,7 +46,7 @@ function resolveInitialTheme(): Theme {
if (typeof window === 'undefined') return 'light'; if (typeof window === 'undefined') return 'light';
const stored = readStoredTheme(); const stored = readStoredTheme();
if (stored) return stored; if (stored) return stored;
return window.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; return 'light';
} }
export function ThemeProvider({ children }: { children: ReactNode }) { export function ThemeProvider({ children }: { children: ReactNode }) {

View File

@ -2,6 +2,13 @@ import * as React from 'react';
import { import {
Badge as CommonBadge, Badge as CommonBadge,
Button as CommonButton, Button as CommonButton,
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldTitle,
Input as CommonInput, Input as CommonInput,
Select as CommonSelect, Select as CommonSelect,
Textarea as CommonTextarea, Textarea as CommonTextarea,
@ -86,14 +93,24 @@ const buttonSizeClass: Record<ProductButtonSize, string> = {
icon: 'h-10 w-10 px-0', 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> = { const fieldSizeClass: Record<ProductFieldSize, string> = {
sm: 'min-h-9 px-3 py-2 text-xs leading-5', 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', md: 'min-h-11 px-3.5 py-2.5 text-sm leading-6',
}; };
const fieldVariantClass: Record<ProductFieldVariant, string> = { 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)]', 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(--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)]', 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> = { const productStatusTone: Record<ProductStatus, ProductStatusTone> = {
@ -146,7 +163,10 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
variant={buttonVariantFor(variant)} variant={buttonVariantFor(variant)}
size={size === 'icon' ? 'sm' : size} size={size === 'icon' ? 'sm' : size}
className={cn( className={cn(
'product-button',
`product-button-${size}`,
buttonSizeClass[size], buttonSizeClass[size],
buttonVariantClass[variant],
variant === 'link' && 'h-auto px-0 py-0 underline underline-offset-4 hover:bg-transparent', variant === 'link' && 'h-auto px-0 py-0 underline underline-offset-4 hover:bg-transparent',
className, className,
)} )}
@ -237,6 +257,16 @@ export function Badge({ variant = 'neutral', ...props }: BadgeProps) {
return <CommonBadge variant={badgeVariantFor(variant)} {...props} />; return <CommonBadge variant={badgeVariantFor(variant)} {...props} />;
} }
export {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldTitle,
};
export function ProductStatusBadge({ export function ProductStatusBadge({
status, status,
children, children,

View File

@ -1,45 +1,30 @@
import type { ButtonHTMLAttributes, ReactNode } from 'react'; import type { ReactNode } from 'react';
import { cn } from '../../lib/utils'; import {
Button as ProductButton,
type ButtonProps as ProductButtonProps,
} from './Primitives';
type ButtonVariant = 'default' | 'outline' | 'ghost' | 'destructive'; type LegacyButtonVariant = 'default' | 'outline' | 'ghost' | 'destructive' | 'subtle';
type ButtonSize = 'sm' | 'md' | 'lg' | 'icon';
const variantClasses: Record<ButtonVariant, string> = { const variantMap: Record<LegacyButtonVariant, ProductButtonProps['variant']> = {
default: 'bg-[var(--primary)] text-[var(--primary-foreground)] hover:brightness-95 shadow-sm', default: 'primary',
outline: 'border border-[var(--border)] bg-[var(--card)] text-[var(--foreground)] hover:bg-[var(--accent-soft)]', outline: 'outline',
ghost: 'text-[var(--muted-foreground)] hover:bg-[var(--accent-soft)] hover:text-[var(--foreground)]', ghost: 'ghost',
destructive: 'bg-[var(--destructive)] text-white hover:brightness-95 shadow-sm', destructive: 'destructive',
}; subtle: 'subtle',
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',
}; };
export function Button({ export function Button({
className,
variant = 'default', variant = 'default',
size = 'md',
children, children,
...props ...props
}: ButtonHTMLAttributes<HTMLButtonElement> & { }: Omit<ProductButtonProps, 'variant'> & {
variant?: ButtonVariant; variant?: LegacyButtonVariant;
size?: ButtonSize;
children: ReactNode; children: ReactNode;
}) { }) {
return ( return (
<button <ProductButton variant={variantMap[variant]} {...props}>
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}
>
{children} {children}
</button> </ProductButton>
); );
} }

View File

@ -4,7 +4,7 @@ import { cn } from '../../lib/utils';
export function Card({ className, children, ...props }: HTMLAttributes<HTMLDivElement> & { children: ReactNode }) { export function Card({ className, children, ...props }: HTMLAttributes<HTMLDivElement> & { children: ReactNode }) {
return ( return (
<div <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} {...props}
> >
{children} {children}
@ -14,7 +14,7 @@ export function Card({ className, children, ...props }: HTMLAttributes<HTMLDivEl
export function CardHeader({ className, children, ...props }: HTMLAttributes<HTMLDivElement> & { children: ReactNode }) { export function CardHeader({ className, children, ...props }: HTMLAttributes<HTMLDivElement> & { children: ReactNode }) {
return ( 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} {children}
</div> </div>
); );
@ -22,7 +22,7 @@ export function CardHeader({ className, children, ...props }: HTMLAttributes<HTM
export function CardTitle({ className, children, ...props }: HTMLAttributes<HTMLHeadingElement> & { children: ReactNode }) { export function CardTitle({ className, children, ...props }: HTMLAttributes<HTMLHeadingElement> & { children: ReactNode }) {
return ( 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} {children}
</h2> </h2>
); );
@ -30,7 +30,7 @@ export function CardTitle({ className, children, ...props }: HTMLAttributes<HTML
export function CardDescription({ className, children, ...props }: HTMLAttributes<HTMLParagraphElement> & { children: ReactNode }) { export function CardDescription({ className, children, ...props }: HTMLAttributes<HTMLParagraphElement> & { children: ReactNode }) {
return ( 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} {children}
</p> </p>
); );
@ -38,7 +38,7 @@ export function CardDescription({ className, children, ...props }: HTMLAttribute
export function CardContent({ className, children, ...props }: HTMLAttributes<HTMLDivElement> & { children: ReactNode }) { export function CardContent({ className, children, ...props }: HTMLAttributes<HTMLDivElement> & { children: ReactNode }) {
return ( return (
<div className={cn('p-6', className)} {...props}> <div className={cn('p-5', className)} {...props}>
{children} {children}
</div> </div>
); );

View File

@ -13,16 +13,16 @@ export function PageHeader({
className?: string; className?: string;
}) { }) {
return ( 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"> <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 ? ( {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} {description}
</p> </p>
) : null} ) : null}
</div> </div>
{action ? <div className="shrink-0">{action}</div> : null} {action ? <div className="shrink-0 sm:pt-1">{action}</div> : null}
</div> </div>
); );
} }

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,7 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import { Agentation } from 'agentation' import { Agentation } from 'agentation'
import './index.css' import './index.css'
import './App.css'
import App from './App.tsx' import App from './App.tsx'
import { AuthProvider } from './components/AuthContext'; import { AuthProvider } from './components/AuthContext';
import { ProductAccessibilityGate } from './components/ProductAccessibilityGate'; import { ProductAccessibilityGate } from './components/ProductAccessibilityGate';

View File

@ -80,10 +80,10 @@ describe('EntriesTab master suite', () => {
render(<EntriesTab botState={mockBotState} />); render(<EntriesTab botState={mockBotState} />);
await waitFor(() => expect(screen.getByText('BTC/USDT')).toBeInTheDocument()); await waitFor(() => expect(screen.getByText('BTC/USDT')).toBeInTheDocument());
await user.click(screen.getByText(/Manual Entry Injection/i)); await user.click(screen.getByText(/Add entry/i));
expect(screen.getByText(/New Opportunity Initialization/i)).toBeInTheDocument(); expect(screen.getByText(/New watchlist entry/i)).toBeInTheDocument();
await user.click(screen.getByText('SubmitEntry')); 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 () => { it('derives diverse entry states (LOCKED, BLOCKED, SUBMITTED, CONFIRMED, ORPHAN)', async () => {

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useAuth } from '../components/AuthContext'; import { useAuth } from '../components/AuthContext';
import { EntryForm } from '../components/EntryForm'; 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 type { BotState } from '../hooks/useWebSocket';
import { DEFAULT_BOT_STATE } from '../hooks/useWebSocket'; import { DEFAULT_BOT_STATE } from '../hooks/useWebSocket';
import { createManualEntry, deleteManualEntry, fetchManualEntries, type ManualEntryPayload } from '../lib/manualEntriesApi'; import { createManualEntry, deleteManualEntry, fetchManualEntries, type ManualEntryPayload } from '../lib/manualEntriesApi';
@ -99,7 +99,7 @@ const NAV_TABS = [
] as const; ] as const;
const tabClass = (isActive: boolean) => 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) => { export const EntriesTab = ({ botState = DEFAULT_BOT_STATE }: EntriesTabProps) => {
const { user } = useAuth(); const { user } = useAuth();
@ -127,7 +127,7 @@ export const EntriesTab = ({ botState = DEFAULT_BOT_STATE }: EntriesTabProps) =>
}, [user]); }, [user]);
const handleDelete = async (id: string) => { 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); await deleteManualEntry(id);
fetchEntries(); fetchEntries();
}; };
@ -140,121 +140,102 @@ export const EntriesTab = ({ botState = DEFAULT_BOT_STATE }: EntriesTabProps) =>
const entryCards = filteredEntries.map((entry) => { const entryCards = filteredEntries.map((entry) => {
const entryState = deriveEntryState(entry, botState); const entryState = deriveEntryState(entry, botState);
return ( 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"> <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="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"> <div className="mb-5 flex items-start justify-between gap-4">
<div className="min-w-0 space-y-2">
{/* Card Top */} <h4 className="m-0 truncate text-xl font-semibold tracking-tight text-[var(--foreground)]">{entry.symbol}</h4>
<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>
<div className="flex items-center gap-2"> <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'}`}> <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-UNIT' : 'PAPER-VOID'} {entry.is_real_trade ? 'Real' : 'Paper'}
</span> </span>
{entry.label && ( {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} {entry.label}
</span> </span>
)} )}
</div> </div>
</div> </div>
<div className="flex gap-2 opacity-0 group-hover:opacity-100 transition-all translate-y-2 group-hover:translate-y-0"> <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" 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={() => setEditingEntry(entry)} variant="ghost" size="icon" aria-label={`Edit ${entry.symbol}`}><Pencil size={15} /></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={() => 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" className="p-3 bg-red-500/10 rounded-2xl text-red-500 hover:bg-red-500/20 transition-all"><Trash2 size={16} /></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>
</div> </div>
{/* Metrics Grid */} <div className="mb-5 grid grid-cols-2 gap-3">
<div className="grid grid-cols-2 gap-4 mb-8"> <div className="rounded-xl border border-[var(--border)] bg-[var(--muted)]/60 p-4">
<div className="bg-white/[0.02] p-5 rounded-3xl border border-white/5"> <span className="mb-1 block text-[11px] font-semibold uppercase tracking-[0.08em] text-[var(--muted-foreground)]">Entry</span>
<span className="block text-[8px] text-gray-600 font-black uppercase tracking-[0.2em] mb-2">Entry Price</span> <span className="font-mono text-base font-semibold text-[var(--foreground)]">${entry.buy_price || '0.00'}</span>
<span className="text-xl font-black text-white font-mono">${entry.buy_price || '0.00'}</span>
</div> </div>
<div className="bg-white/[0.02] p-5 rounded-3xl border border-white/5"> <div className="rounded-xl border border-[var(--border)] bg-[var(--muted)]/60 p-4">
<span className="block text-[8px] text-gray-600 font-black uppercase tracking-[0.2em] mb-2">Target Exit</span> <span className="mb-1 block text-[11px] font-semibold uppercase tracking-[0.08em] text-[var(--muted-foreground)]">Target</span>
<span className="text-xl font-black text-white font-mono">${entry.sell_price || '---'}</span> <span className="font-mono text-base font-semibold text-[var(--foreground)]">${entry.sell_price || '---'}</span>
</div> </div>
<div className="bg-white/[0.02] p-4 rounded-3xl border border-white/5"> <div className="rounded-xl border border-[var(--border)] bg-[var(--muted)]/60 p-4">
<span className="block text-[8px] text-gray-600 font-black uppercase tracking-[0.2em] mb-1">Quantity/Lot</span> <span className="mb-1 block text-[11px] font-semibold uppercase tracking-[0.08em] text-[var(--muted-foreground)]">Quantity</span>
<span className="text-xs font-black text-gray-400 font-mono">{entry.quantity} UNITS</span> <span className="font-mono text-sm font-semibold text-[var(--foreground)]">{entry.quantity || '—'}</span>
</div> </div>
<div className="bg-white/[0.02] p-4 rounded-3xl border border-white/5"> <div className="rounded-xl border border-[var(--border)] bg-[var(--muted)]/60 p-4">
<span className="block text-[8px] text-gray-600 font-black uppercase tracking-[0.2em] mb-1">Status</span> <span className="mb-1 block text-[11px] font-semibold uppercase tracking-[0.08em] text-[var(--muted-foreground)]">Status</span>
<span className={`text-[10px] font-black tracking-widest uppercase ${entry.active ? 'text-green-400 animate-pulse' : 'text-gray-600'}`}> <span className={`text-sm font-semibold ${entry.active ? 'text-[var(--bl-success)]' : 'text-[var(--muted-foreground)]'}`}>
{entry.active ? 'SCANNING' : 'INACTIVE'} {entry.active ? 'Active' : 'Inactive'}
</span> </span>
</div> </div>
</div> </div>
<div className="entry-state-row mb-6"> <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-[9px] text-gray-500 font-black uppercase tracking-[0.2em]">Entry State</span> <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, '')}`}> <span className={`entry-state-pill state-${entryState.toLowerCase().replace(/[^a-z]/g, '')}`}>
{entryState} {entryState}
</span> </span>
</div> </div>
{/* Entry Notes/Context */}
<div className="flex-grow"> <div className="flex-grow">
<span className="block text-[8px] text-gray-600 font-black uppercase tracking-[0.2em] mb-3">Neural Context</span> <span className="mb-2 block text-[11px] font-semibold uppercase tracking-[0.08em] text-[var(--muted-foreground)]">Notes</span>
<p className="text-[10px] font-bold text-gray-500 leading-relaxed uppercase italic"> <p className="m-0 text-sm leading-6 text-[var(--muted-foreground)]">
{entry.notes || 'No manual notes injection provided for this asset cluster.'} {entry.notes || 'No notes added yet.'}
</p> </p>
</div> </div>
{/* ID Context */} <div className="mt-5 flex items-center justify-between gap-3 border-t border-[var(--border)] pt-4">
<div className="mt-8 pt-6 border-t border-white/5 flex items-center justify-between"> <span className="truncate font-mono text-[11px] text-[var(--muted-foreground)]">{entry.stock_instance_id}</span>
<div className="flex items-center gap-2"> <ShieldCheck size={15} className="shrink-0 text-[var(--muted-foreground)]" />
<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>
</div>
</div> </div>
</article>
); );
}); });
const navTabs = NAV_TABS; const navTabs = NAV_TABS;
return ( 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="ux-section-header">
<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> <div>
<h2 className="text-3xl font-black text-white tracking-tighter uppercase">Watchlist & Entries</h2> <h2 className="ux-section-title text-lg">Watchlist entries</h2>
<p className="text-gray-500 text-[10px] font-black uppercase tracking-[0.2em] opacity-60">Neural Opportunity Cluster</p> <p className="ux-section-copy mt-1">Track manual symbols, paper setups, and real execution candidates.</p>
</div> </div>
</div>
<Button <Button
type="button" type="button"
onClick={() => { setIsAdding(!isAdding); setEditingEntry(null); }} onClick={() => { setIsAdding(!isAdding); setEditingEntry(null); }}
variant="ghost" variant={isAdding ? 'outline' : 'primary'}
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' className="flex items-center gap-2"
}`}
> >
{isAdding ? <X size={18} /> : <Plus size={18} />} {isAdding ? <X size={16} /> : <Plus size={16} />}
{isAdding ? 'Cancel Entry' : 'Manual Entry Injection'} {isAdding ? 'Cancel' : 'Add entry'}
</Button> </Button>
</div> </div>
{/* 2. FORM DRAWER (IF ADDING/EDITING) */}
{(isAdding || editingEntry) && ( {(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="ux-section animate-in zoom-in-95 duration-300">
<div className="flex items-center justify-between mb-8"> <div className="ux-section-header">
<h3 className="text-lg font-black text-white uppercase tracking-tight flex items-center gap-3"> <div>
<Plus size={20} className="text-green-400" /> <h3 className="ux-section-title">
{editingEntry ? `Update Identity: ${editingEntry.symbol}` : 'New Opportunity Initialization'} {editingEntry ? `Edit ${editingEntry.symbol}` : 'New watchlist entry'}
</h3> </h3>
<Button type="button" onClick={() => { setIsAdding(false); setEditingEntry(null); }} variant="ghost" className="text-gray-500 hover:text-white"><X size={20} /></Button> <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>
<div className="max-w-4xl"> <div className="max-w-4xl">
<EntryForm <EntryForm
@ -265,8 +246,7 @@ export const EntriesTab = ({ botState = DEFAULT_BOT_STATE }: EntriesTabProps) =>
</div> </div>
)} )}
{/* 3. NAVIGATION CLUSTERS */} <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">
<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">
{navTabs.map((tab) => ( {navTabs.map((tab) => (
<Button <Button
type="button" type="button"
@ -279,20 +259,18 @@ export const EntriesTab = ({ botState = DEFAULT_BOT_STATE }: EntriesTabProps) =>
</Button> </Button>
))} ))}
</div> </div>
{/* 4. CARDS GRID */} <div className="grid grid-cols-1 gap-5 md:grid-cols-2 xl:grid-cols-3">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 gap-8">
{entryCards} {entryCards}
{filteredEntries.length === 0 && ( {filteredEntries.length === 0 && (
<div className="col-span-full py-32 text-center bg-white/[0.02] border border-dashed border-white/5 rounded-[3rem]"> <div className="ux-empty-state col-span-full">
<LayoutGrid size={48} className="mx-auto mb-6 text-gray-800 opacity-40" /> <div>
<p className="text-sm font-black text-gray-700 uppercase tracking-[0.3em] opacity-40">Cluster Context Null</p> <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> </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>
);

View File

@ -16,7 +16,6 @@ import {
ChevronDown, ChevronDown,
ChevronUp, ChevronUp,
Lightbulb, Lightbulb,
Dna,
Cpu, Cpu,
Fingerprint, Fingerprint,
Target, Target,
@ -329,107 +328,31 @@ export const MyStrategiesTab: React.FC<{ botState: any; alerts?: any[]; previewA
} }
return ( return (
<div style={{ <div className="strategy-workspace">
maxWidth: '1400px', <div className="strategy-workspace-header">
margin: '0 auto', <div>
padding: '0 20px 100px 20px', <div className="strategy-workspace-eyebrow">
animation: 'fadeIn 0.7s ease-out' Strategy operations
}}>
{/* 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> </div>
<h2>My strategies</h2>
<div style={{ <p>
display: 'inline-flex', Monitor active profiles, review recent signals, and create new automated trading workflows.
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> </p>
</div> </div>
<div style={{ display: 'flex', gap: '16px', alignItems: 'center' }}> <div className="strategy-workspace-actions">
<div style={{ <div className="strategy-connection-pill" data-connected={botState?.connected ? 'true' : 'false'}>
background: 'var(--bl-success-muted)', {botState?.connected ? 'Systems online' : 'Systems disconnected'}
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> </div>
<Button <Button
type="button" type="button"
onClick={() => setShowWizard(true)} onClick={() => setShowWizard(true)}
variant="secondary" variant="primary"
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 <Plus size={16} /> New strategy
</Button> </Button>
</div> </div>
</div> </div>
</div>
{/* Contextual Intelligence Row: Recent Activity + Symbol Volatility */} {/* Contextual Intelligence Row: Recent Activity + Symbol Volatility */}
{(() => { {(() => {
@ -450,12 +373,11 @@ export const MyStrategiesTab: React.FC<{ botState: any; alerts?: any[]; previewA
</div> </div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
{recentAlerts.map((alert, i) => { {recentAlerts.map((alert, i) => {
const icons: Record<string, string> = { signal: '🚀', pulse: '⏰', error: '⚠️', info: '' };
const mins = Math.floor((Date.now() - alert.timestamp) / 60000); 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`; const timeAgo = mins < 1 ? 'just now' : mins < 60 ? `${mins}m ago` : `${Math.floor(mins / 60)}h ago`;
return ( return (
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: '10px', padding: '8px 12px', background: 'var(--bl-surface-overlay)', borderRadius: '10px' }}> <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: '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: '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> <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 */} {/* Symbol-Specific Volatility */}
<div style={{ background: 'var(--bl-surface-highlight)', border: '1px solid var(--bl-border-subtle)', borderRadius: '24px', padding: '24px 28px' }}> <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' }}> <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> <span style={{ fontSize: '11px', fontWeight: 900, color: 'var(--bl-text-quiet)', textTransform: 'uppercase', letterSpacing: '2px' }}>Your Markets (24h)</span>
</div> </div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
@ -542,8 +464,8 @@ export const MyStrategiesTab: React.FC<{ botState: any; alerts?: any[]; previewA
textAlign: 'center' textAlign: 'center'
}}> }}>
<Activity size={60} color="var(--bl-text-quiet)" style={{ marginBottom: '24px' }} /> <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> <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' }}>Your fleet is currently dormant. Deploy your first engine to begin automated dominance.</p> <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 <Button
type="button" type="button"
onClick={() => setShowWizard(true)} onClick={() => setShowWizard(true)}

View File

@ -314,20 +314,21 @@ export const SettingsTab = ({ botState }: SettingsTabProps) => {
<style>{` <style>{`
.primary-button { .primary-button {
background: var(--bl-success); background: var(--accent);
color: black; color: var(--primary-foreground);
border: none; border: none;
padding: 8px 16px; padding: 10px 16px;
border-radius: 6px; border-radius: 10px;
font-weight: bold; font-weight: 750;
cursor: pointer; cursor: pointer;
box-shadow: var(--card-shadow);
} }
.secondary-button { .secondary-button {
background: transparent; background: transparent;
color: var(--bl-text-secondary); color: var(--muted-foreground);
border: 1px solid var(--bl-text-secondary); border: 1px solid var(--border);
padding: 8px 16px; padding: 10px 16px;
border-radius: 6px; border-radius: 10px;
cursor: pointer; cursor: pointer;
} }
.form-grid { .form-grid {

View File

@ -113,9 +113,9 @@ describe('dashboard tabs smoke coverage', () => {
it('renders EntriesTab empty-state and primary controls', () => { it('renders EntriesTab empty-state and primary controls', () => {
const html = renderToStaticMarkup(React.createElement(EntriesTab)); const html = renderToStaticMarkup(React.createElement(EntriesTab));
expect(html).toContain('Watchlist &amp; Entries'); expect(html).toContain('Watchlist entries');
expect(html).toContain('Manual Entry Injection'); expect(html).toContain('Add entry');
expect(html).toContain('Cluster Context Null'); expect(html).toContain('No entries in this view');
}); });
it('renders AdminTab strategy pipeline and ConfigTab loading shell', () => { it('renders AdminTab strategy pipeline and ConfigTab loading shell', () => {

View File

@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; 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 { import {
AreaChart, Area, Bar, ComposedChart, Line, LineChart, XAxis, YAxis, Tooltip, AreaChart, Area, Bar, ComposedChart, Line, LineChart, XAxis, YAxis, Tooltip,
ResponsiveContainer, CartesianGrid, ReferenceLine, ResponsiveContainer, CartesianGrid, ReferenceLine,
@ -796,44 +796,83 @@ function EmptyState({
const cryptoMode = suggestions.some(isCryptoLikeSymbol); const cryptoMode = suggestions.some(isCryptoLikeSymbol);
return ( return (
<div style={{ <div className="home-command-center">
display: 'flex', flexDirection: 'column', alignItems: 'center', <section className="home-hero-panel" aria-labelledby="home-hero-title">
justifyContent: 'center', height: '60vh', gap: 16, <div className="home-hero-copy">
color: 'var(--muted-foreground)', <div className="home-hero-eyebrow">
}}> <Sparkles size={15} />
<div style={{ fontSize: 56 }}>📈</div> Market workspace
<div style={{ fontSize: 20, fontWeight: 700, color: 'var(--foreground)' }}>
Search an asset to get started
</div> </div>
<div style={{ fontSize: 14, textAlign: 'center', maxWidth: 360 }}> <h1 id="home-hero-title">Start with a symbol. See the full trading picture.</h1>
Type a ticker symbol, crypto pair, or company name in the search bar above to view charts, financials, and news. <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> </div>
{cryptoMode && ( {cryptoMode && (
<div style={{ fontSize: 12, color: 'var(--muted-foreground)', fontWeight: 600 }}> <div className="home-hero-hint">
Suggested from your crypto bot configuration 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> </div>
<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>
))}
</section>
</div> </div>
); );
} }

View File

@ -13,12 +13,12 @@ export function MarketsView() {
const handleClone = (preset: StrategyPreset) => setClonedPreset(preset); const handleClone = (preset: StrategyPreset) => setClonedPreset(preset);
return ( return (
<div> <div className="ux-page-stack">
<PageHeader <PageHeader
title="Markets" title="Markets"
description="Scan live opportunities and reusable setups from the current market without leaving the workspace." 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} /> <TopVolatile botState={botState} />
<AISetups botState={botState} /> <AISetups botState={botState} />
</div> </div>

View File

@ -16,20 +16,20 @@ export function PortfolioView() {
const [tab, setTab] = useState<Tab>('Positions & Orders'); const [tab, setTab] = useState<Tab>('Positions & Orders');
return ( return (
<div> <div className="ux-page-stack">
<PageHeader <PageHeader
title="Portfolio" title="Portfolio"
description="Review live positions, order activity, and completed trades in one consistent operational view." 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 => ( {TABS.map(t => (
<Button <Button
key={t} key={t}
onClick={() => setTab(t)} onClick={() => setTab(t)}
variant="ghost" variant="ghost"
size="sm" size="sm"
className="tab-button" className="product-tab"
data-active={tab === t} data-active={tab === t}
> >
{t} {t}

View File

@ -7,6 +7,7 @@ import { VisualRuleBuilder, type VisualRule } from '../components/strategy/Visua
import { createTradeProfile } from '../lib/profileApi'; import { createTradeProfile } from '../lib/profileApi';
import { BacktestRunnerPanel } from '../backtest/components/BacktestRunnerPanel'; import { BacktestRunnerPanel } from '../backtest/components/BacktestRunnerPanel';
import { PageHeader } from '../components/ui/page-header'; import { PageHeader } from '../components/ui/page-header';
import { Button } from '../components/ui/Primitives';
import { Card, CardContent } from '../components/ui/card'; import { Card, CardContent } from '../components/ui/card';
type ResearchTab = 'Strategies' | 'Visual Builder' | 'Code Editor' | 'Signals' | 'Backtesting'; type ResearchTab = 'Strategies' | 'Visual Builder' | 'Code Editor' | 'Signals' | 'Backtesting';
@ -35,13 +36,16 @@ function SubTab({
label, active, onClick, label, active, onClick,
}: { label: string; active: boolean; onClick: () => void }) { }: { label: string; active: boolean; onClick: () => void }) {
return ( return (
<button <Button
type="button"
onClick={onClick} onClick={onClick}
className="tab-button" variant="ghost"
size="sm"
className="product-tab"
data-active={active} data-active={active}
> >
{label} {label}
</button> </Button>
); );
} }
@ -83,13 +87,13 @@ export function ResearchView() {
const initialCapitalUsd = botState.settings?.totalCapital ?? 1000; const initialCapitalUsd = botState.settings?.totalCapital ?? 1000;
return ( return (
<div> <div className="ux-page-stack">
<PageHeader <PageHeader
title="Research" title="Research"
description="Design, test, and refine strategies before they go anywhere near live execution." description="Design, test, and refine strategies before they go anywhere near live execution."
/> />
<div className="tab-strip"> <div className="product-tabs">
{tabs.map(t => ( {tabs.map(t => (
<SubTab key={t} label={t} active={tab === t} onClick={() => setTab(t)} /> <SubTab key={t} label={t} active={tab === t} onClick={() => setTab(t)} />
))} ))}

View File

@ -164,7 +164,7 @@ export function ScreenerView() {
}; };
return ( return (
<div> <div className="ux-page-stack">
<PageHeader <PageHeader
title="Stock Screener" title="Stock Screener"
description="Filter liquid names by sector and market cap, then jump directly into charting and research." 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 }}> <Card className="screener-filter-card">
<CardContent style={{ padding: 16 }}> <CardContent>
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}> <div className="screener-filter-row">
<div style={{ position: 'relative', minWidth: 200, maxWidth: 280 }}> <div className="screener-search-field">
<Search size={14} style={{ <Search size={14} style={{
position: 'absolute', left: 10, top: '50%', position: 'absolute', left: 10, top: '50%',
transform: 'translateY(-50%)', color: 'var(--muted-foreground)', 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 }))} 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)" /> <SlidersHorizontal size={13} color="var(--muted-foreground)" />
{SECTORS.slice(0, 6).map(s => ( {SECTORS.slice(0, 6).map(s => (
<Button <Button
@ -243,26 +243,16 @@ export function ScreenerView() {
</Card> </Card>
{error && !loading && ( {error && !loading && (
<div style={{ <div className="ux-alert">
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,
}}>
{error} {error}
</div> </div>
)} )}
<div style={{ background: 'var(--card)', borderRadius: 12, border: '1px solid var(--border)', overflowX: 'auto', overflowY: 'hidden' }}> <div className="ux-data-grid">
<div style={{ <div className="ux-data-grid-header" style={{
display: 'grid', display: 'grid',
gridTemplateColumns: '100px 1fr 90px 90px 110px 80px 110px', gridTemplateColumns: '100px 1fr 90px 90px 110px 80px 110px',
padding: '10px 16px', padding: '10px 16px',
borderBottom: '1px solid var(--border)',
background: 'var(--muted)',
}}> }}>
{([ {([
['symbol', 'Symbol'], ['symbol', 'Symbol'],
@ -276,11 +266,8 @@ export function ScreenerView() {
<span <span
key={key} key={key}
onClick={() => handleSort(key)} onClick={() => handleSort(key)}
style={{ className="ux-data-grid-head"
fontSize: 11, color: 'var(--muted-foreground)', fontWeight: 700, style={{ cursor: 'pointer' }}
textTransform: 'uppercase', letterSpacing: '0.05em',
cursor: 'pointer', userSelect: 'none',
}}
> >
{label}<SortIcon k={key} /> {label}<SortIcon k={key} />
</span> </span>
@ -289,25 +276,21 @@ export function ScreenerView() {
{loading && <ScreenerSkeletonRows />} {loading && <ScreenerSkeletonRows />}
{!loading && filtered.map((row, i) => ( {!loading && filtered.map((row) => (
<div <div
key={row.symbol} key={row.symbol}
onClick={() => handleRowClick(row.symbol)} onClick={() => handleRowClick(row.symbol)}
className="ux-data-grid-row"
style={{ style={{
display: 'grid', display: 'grid',
gridTemplateColumns: '100px 1fr 90px 90px 110px 80px 110px', gridTemplateColumns: '100px 1fr 90px 90px 110px 80px 110px',
padding: '11px 16px', padding: '11px 16px',
borderBottom: i < filtered.length - 1 ? '1px solid var(--border)' : 'none',
cursor: 'pointer',
alignItems: 'center', 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 className="ux-data-grid-cell" style={{ fontWeight: 700, color: 'var(--primary)' }}>{row.symbol}</span>
<span style={{ fontSize: 12, color: 'var(--foreground)' }}>{row.companyName}</span> <span className="ux-data-grid-cell">{row.companyName}</span>
<span style={{ fontSize: 13, fontWeight: 600, color: 'var(--foreground)' }}> <span className="ux-data-grid-cell" style={{ fontWeight: 600 }}>
{row.price != null ? `$${row.price.toFixed(2)}` : '—'} {row.price != null ? `$${row.price.toFixed(2)}` : '—'}
</span> </span>
<span style={{ <span style={{
@ -316,21 +299,24 @@ export function ScreenerView() {
}}> }}>
{row.changesPercentage >= 0 ? '+' : ''}{row.changesPercentage?.toFixed(2)}% {row.changesPercentage >= 0 ? '+' : ''}{row.changesPercentage?.toFixed(2)}%
</span> </span>
<span style={{ fontSize: 12, color: 'var(--foreground)' }}> <span className="ux-data-grid-cell">
{row.marketCap ? fmtCap(row.marketCap) : '—'} {row.marketCap ? fmtCap(row.marketCap) : '—'}
</span> </span>
<span style={{ fontSize: 12, color: 'var(--foreground)' }}> <span className="ux-data-grid-cell">
{row.pe != null && row.pe > 0 ? row.pe.toFixed(1) : '—'} {row.pe != null && row.pe > 0 ? row.pe.toFixed(1) : '—'}
</span> </span>
<span style={{ fontSize: 12, color: 'var(--foreground)' }}> <span className="ux-data-grid-cell">
{row.volume ? fmtCap(row.volume).replace('$', '') : '—'} {row.volume ? fmtCap(row.volume).replace('$', '') : '—'}
</span> </span>
</div> </div>
))} ))}
{!loading && filtered.length === 0 && !error && ( {!loading && filtered.length === 0 && !error && (
<div style={{ padding: 32, textAlign: 'center', color: 'var(--muted-foreground)', fontSize: 13 }}> <div className="ux-empty-state" style={{ border: 0, borderRadius: 0 }}>
No results match your filters <div>
<strong>No results match your filters</strong>
<span>Adjust the query, sector, or market cap filter.</span>
</div>
</div> </div>
)} )}
</div> </div>

View File

@ -35,13 +35,13 @@ export function SettingsView() {
}; };
return ( return (
<div> <div className="ux-page-stack">
<PageHeader <PageHeader
title="Settings" title="Settings"
description="Manage your account, credentials, and runtime configuration from one place." 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 => ( {sections.map(s => (
<Button <Button
key={s} key={s}
@ -49,7 +49,7 @@ export function SettingsView() {
onClick={() => handleSectionChange(s)} onClick={() => handleSectionChange(s)}
variant="ghost" variant="ghost"
size="sm" size="sm"
className="tab-button" className="product-tab"
data-active={section === s} data-active={section === s}
> >
{s} {s}
@ -57,17 +57,7 @@ export function SettingsView() {
))} ))}
</div> </div>
<div <div className="ux-surface settings-legacy-surface p-5" style={{ color: 'var(--foreground)' }}>
className="settings-legacy-surface"
style={{
background: 'var(--card)',
border: '1px solid var(--border)',
borderRadius: 24,
padding: 24,
boxShadow: 'var(--card-shadow)',
color: 'var(--foreground)',
}}
>
{section === 'Account' && <SettingsTab botState={botState} />} {section === 'Account' && <SettingsTab botState={botState} />}
{section === 'Bot Config' && isAdmin && <ConfigTab />} {section === 'Bot Config' && isAdmin && <ConfigTab />}
{section === 'Admin Panel' && isAdmin && <AdminTab botState={botState} socket={socket} />} {section === 'Admin Panel' && isAdmin && <AdminTab botState={botState} socket={socket} />}

View File

@ -965,7 +965,7 @@ export function SimpleView() {
const saveButtonDisabled = submitting || loadingPrice || (draft.side === 'sell' && !selectedSellHolding); const saveButtonDisabled = submitting || loadingPrice || (draft.side === 'sell' && !selectedSellHolding);
return ( return (
<div className="space-y-8"> <div className="trade-plans-page space-y-8">
<PageHeader <PageHeader
title="Trade Plans" 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." 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> </CardContent>
</Card> </Card>
<Card> <Card className="saved-setups-panel">
<CardHeader className="block"> <CardHeader className="saved-setups-header">
<CardTitle className="uppercase">Saved setups</CardTitle> <div>
<CardDescription>Review armed setups, current runtime order state, and whether an executed setup is now showing in Portfolio as an open holding.</CardDescription> <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> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-4"> <div className="saved-setups-list">
{savedSetups.length === 0 && ( {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)]"> <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. No Trade Plans saved yet.
@ -1413,32 +1416,32 @@ export function SimpleView() {
ref={(node) => { ref={(node) => {
setupCardRefs.current[entryId] = 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 focusedSetupId === entryId
? 'border-[var(--primary)] ring-2 ring-[var(--primary)]/20' ? 'border-[var(--primary)] ring-2 ring-[var(--primary)]/20'
: 'border-[var(--border)]' : 'border-[var(--border)]'
}`} }`}
> >
<div className="mb-3 flex items-start justify-between gap-4"> <div className="saved-setup-topline">
<div> <div className="saved-setup-title-block">
<div className="flex items-center gap-3"> <div className="saved-setup-asset-row">
<h3 className="text-lg font-black uppercase text-[var(--foreground)]">{entry.symbol}</h3> <h3>{entry.symbol}</h3>
<span className={`rounded-full px-3 py-1 text-[10px] font-black uppercase tracking-[0.2em] ${ <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 === 'buy' ? 'bg-emerald-500/10 text-emerald-700 dark:text-emerald-300' : 'bg-violet-500/10 text-violet-700 dark:text-violet-300'
}`}> }`}>
{side} {side}
</span> </span>
</div> </div>
<p className="mt-2 text-sm text-[var(--muted-foreground)]">{describeSavedSetup(entry)}</p> <p>{describeSavedSetup(entry)}</p>
</div> </div>
<div className="flex items-center gap-2"> <div className="saved-setup-actions">
{canConvertToLongTerm ? ( {canConvertToLongTerm ? (
<Button <Button
type="button" type="button"
onClick={() => void handleConvertToLongTerm(entry)} onClick={() => void handleConvertToLongTerm(entry)}
variant="outline" variant="outline"
size="sm" size="sm"
className="uppercase tracking-[0.18em]" className="saved-setup-action"
> >
Convert to long-term Convert to long-term
</Button> </Button>
@ -1449,7 +1452,7 @@ export function SimpleView() {
onClick={() => void handleResumeExitManagement(entry)} onClick={() => void handleResumeExitManagement(entry)}
variant="outline" variant="outline"
size="sm" size="sm"
className="uppercase tracking-[0.18em]" className="saved-setup-action"
> >
Resume exit management Resume exit management
</Button> </Button>
@ -1459,7 +1462,7 @@ export function SimpleView() {
onClick={() => handleEdit(entry)} onClick={() => handleEdit(entry)}
variant="outline" variant="outline"
size="sm" 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"> <span className="inline-flex items-center gap-2">
<Pencil size={14} /> <Pencil size={14} />
@ -1471,7 +1474,7 @@ export function SimpleView() {
onClick={() => void handleDelete(entryId)} onClick={() => void handleDelete(entryId)}
variant="destructive" variant="destructive"
size="sm" size="sm"
className="uppercase tracking-[0.18em]" className="saved-setup-action"
> >
<span className="inline-flex items-center gap-2"> <span className="inline-flex items-center gap-2">
<Trash2 size={14} /> <Trash2 size={14} />
@ -1481,33 +1484,33 @@ export function SimpleView() {
</div> </div>
</div> </div>
<div className="flex flex-wrap items-center gap-3 text-[11px] uppercase tracking-[0.2em] text-[var(--muted-foreground)]"> <div className="saved-setup-meta-grid">
<span className="rounded-full border border-[var(--border)] px-3 py-1"> <span>
{formatSetupStatus(entry.status)} {formatSetupStatus(entry.status)}
</span> </span>
<span className="rounded-full border border-[var(--border)] px-3 py-1"> <span>
{formatHoldingMode(entry.holding_mode)} {formatHoldingMode(entry.holding_mode)}
</span> </span>
<span className="rounded-full border border-[var(--border)] px-3 py-1"> <span>
{formatAutomationState(entry)} {formatAutomationState(entry)}
</span> </span>
{runtimeSnapshot ? ( {runtimeSnapshot ? (
<span className={`rounded-full border px-3 py-1 ${statusToneClasses(runtimeSnapshot.tone)}`}> <span className={statusToneClasses(runtimeSnapshot.tone)}>
{runtimeSnapshot.label} {runtimeSnapshot.label}
</span> </span>
) : null} ) : null}
<span className="rounded-full border border-[var(--border)] px-3 py-1"> <span>
{String(entry.sizing_mode || '').trim().toLowerCase() === 'amount' {String(entry.sizing_mode || '').trim().toLowerCase() === 'amount'
? `Budget $${Number(entry.amount_usd || 0).toFixed(2)}` ? `Budget $${Number(entry.amount_usd || 0).toFixed(2)}`
: `Qty ${entry.quantity || entry.filled_quantity || 0}`} : `Qty ${entry.quantity || entry.filled_quantity || 0}`}
</span> </span>
{entry.reference_price ? ( {entry.reference_price ? (
<span className="rounded-full border border-[var(--border)] px-3 py-1"> <span>
Ref {Number(entry.reference_price).toFixed(4)} Ref {Number(entry.reference_price).toFixed(4)}
</span> </span>
) : null} ) : null}
{entry.entry_price ? ( {entry.entry_price ? (
<span className="rounded-full border border-[var(--border)] px-3 py-1"> <span>
Entry {Number(entry.entry_price).toFixed(4)} Entry {Number(entry.entry_price).toFixed(4)}
</span> </span>
) : null} ) : null}
@ -1515,7 +1518,7 @@ export function SimpleView() {
<button <button
type="button" type="button"
onClick={() => void copyIdentifier('trade', String(runtimeSnapshot?.tradeId || entry.linked_trade_id))} 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)}` {copiedKey === `trade:${String(runtimeSnapshot?.tradeId || entry.linked_trade_id)}`
? 'Trade copied' ? 'Trade copied'
@ -1526,7 +1529,7 @@ export function SimpleView() {
<button <button
type="button" type="button"
onClick={() => void copyIdentifier('order', runtimeSnapshot.orderId)} 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}` {copiedKey === `order:${runtimeSnapshot.orderId}`
? 'Order copied' ? 'Order copied'
@ -1534,7 +1537,7 @@ export function SimpleView() {
</button> </button>
) : null} ) : null}
{updatedAt ? ( {updatedAt ? (
<span className="rounded-full border border-[var(--border)] px-3 py-1 normal-case tracking-normal"> <span className="saved-setup-updated">
Updated {updatedAt} Updated {updatedAt}
</span> </span>
) : null} ) : null}
@ -1573,14 +1576,14 @@ export function SimpleView() {
</div> </div>
) : null} ) : null}
<div className="mt-4 grid gap-2 md:grid-cols-5"> <div className="saved-setup-timeline">
{SIMPLE_TIMELINE_STEPS.map((step) => { {SIMPLE_TIMELINE_STEPS.map((step) => {
const complete = isTimelineStepComplete(runtimeSnapshot?.stage, step); const complete = isTimelineStepComplete(runtimeSnapshot?.stage, step);
const isCurrent = runtimeSnapshot?.stage === step; const isCurrent = runtimeSnapshot?.stage === step;
return ( return (
<div <div
key={step} key={step}
className={`rounded-2xl border px-3 py-2 text-[11px] font-semibold ${ className={`saved-setup-step ${
isCurrent isCurrent
? 'border-[var(--primary)] bg-[var(--accent-soft)] text-[var(--primary)]' ? 'border-[var(--primary)] bg-[var(--accent-soft)] text-[var(--primary)]'
: complete : complete
@ -1594,7 +1597,7 @@ export function SimpleView() {
})} })}
</div> </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>{' '} <span className="font-semibold text-[var(--foreground)]">Next action:</span>{' '}
{nextActionText} {nextActionText}
</div> </div>

View File

@ -17,6 +17,27 @@
"jsx": "react-jsx", "jsx": "react-jsx",
"baseUrl": ".", "baseUrl": ".",
"paths": { "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": [ "monaco-editor": [
"../node_modules/.pnpm/monaco-editor@0.55.1/node_modules/monaco-editor/esm/vs/editor/editor.api" "../node_modules/.pnpm/monaco-editor@0.55.1/node_modules/monaco-editor/esm/vs/editor/editor.api"
], ],

View File

@ -8,10 +8,19 @@ const monacoEditorPath = path.resolve(
__dirname, __dirname,
'../node_modules/.pnpm/monaco-editor@0.55.1/node_modules/monaco-editor', '../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/ // Resolve a @bytelyst/* package: prefer web/node_modules, fall back to vendor/
function bytelystAlias(pkg: string): string { function bytelystAlias(pkg: string): string {
const sourceEntry = commonPlatSourceEntry(pkg);
if (sourceEntry) return sourceEntry;
const nmPath = path.resolve(__dirname, 'node_modules/@bytelyst', pkg); const nmPath = path.resolve(__dirname, 'node_modules/@bytelyst', pkg);
const vendorPath = path.resolve(__dirname, '../vendor/bytelyst', pkg); const vendorPath = path.resolve(__dirname, '../vendor/bytelyst', pkg);
if (fs.existsSync(nmPath)) return nmPath; if (fs.existsSync(nmPath)) return nmPath;
@ -34,13 +43,12 @@ export default defineConfig({
alias: [ alias: [
// Vendor packages that live only in vendor/ (not in web/node_modules/) // Vendor packages that live only in vendor/ (not in web/node_modules/)
{ find: '@bytelyst/api-client', replacement: bytelystAlias('api-client') }, { 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/errors', replacement: bytelystAlias('errors') },
{ { find: '@bytelyst/kill-switch-client', replacement: bytelystAlias('kill-switch-client') },
find: '@bytelyst/ui', { find: '@bytelyst/react-auth', replacement: bytelystAlias('react-auth') },
replacement: fs.existsSync(commonUiSourcePath) { find: '@bytelyst/telemetry-client', replacement: bytelystAlias('telemetry-client') },
? commonUiSourcePath { find: '@bytelyst/ui', replacement: bytelystAlias('ui') },
: path.resolve(__dirname, 'node_modules/@bytelyst/ui'),
},
// Monaco is an explicit web dependency, but this workspace often runs // Monaco is an explicit web dependency, but this workspace often runs
// against pnpm's root store without a web/node_modules symlink when the // against pnpm's root store without a web/node_modules symlink when the
// private mobile registry is unavailable. Keep local worker imports // private mobile registry is unavailable. Keep local worker imports