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/
# Gitea auth tokens belong in user-level ~/.npmrc or CI secrets, and are only needed
# when BYTELYST_PACKAGE_SOURCE=gitea.
# Gitea returns Docker-internal tarball URLs (172.17.0.1:3300); rewrite host to the public URL
replace-registry-host=always
link-workspace-packages=true
prefer-workspace-packages=true

View File

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

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

10
pnpm-lock.yaml generated
View File

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

View File

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

View File

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

View File

@ -193,21 +193,7 @@ function App() {
}}>
{/* Critical system alert banner */}
{hasCriticalEvents && (
<div style={{
position: 'fixed', top: 0, left: 0, right: 0, zIndex: 9999,
background: 'linear-gradient(90deg, var(--bl-danger-muted) 0%, var(--bl-danger) 50%, var(--bl-danger-muted) 100%)',
color: 'var(--primary-foreground)',
padding: '6px 20px',
textAlign: 'center',
fontSize: 11,
fontWeight: 900,
textTransform: 'uppercase',
letterSpacing: '0.1em',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 10,
}}>
<div className="critical-alert-banner">
<span></span>
<span>
SYSTEM ALERT: {recentCriticalEvents.length} CRITICAL ISSUES DETECTED GO TO SETTINGS ADMIN PANEL
@ -216,7 +202,7 @@ function App() {
</div>
)}
<div style={{ paddingTop: hasCriticalEvents ? 32 : 0 }}>
<div>
<AppShell />
</div>

View File

@ -15,11 +15,35 @@
}
.alerts-container {
max-height: 300px;
max-height: min(520px, 58vh);
overflow-y: auto;
padding-right: 8px;
}
.alert-empty-state {
display: grid;
min-height: 180px;
place-items: center;
gap: 6px;
border: 1px dashed var(--border-strong);
border-radius: 14px;
background: color-mix(in oklab, var(--muted) 54%, var(--card));
padding: 28px;
color: var(--muted-foreground);
text-align: center;
}
.alert-empty-state strong {
color: var(--foreground);
font-size: 15px;
}
.alert-empty-state span {
max-width: 360px;
font-size: 13px;
line-height: 1.6;
}
.alerts-container::-webkit-scrollbar {
width: 6px;
}

View File

@ -16,8 +16,8 @@ const alertTypeIcons: { [key: string]: string } = {
info: ''
};
export const AlertFeed = ({ alerts }: AlertFeedProps) => {
const sortedAlerts = [...alerts].reverse().slice(0, 50);
export const AlertFeed = ({ alerts }: AlertFeedProps) => {
const sortedAlerts = [...alerts].reverse().slice(0, 50);
const getTimeAgo = (timestamp: number) => {
const diffMs = Date.now() - timestamp;
@ -29,13 +29,19 @@ export const AlertFeed = ({ alerts }: AlertFeedProps) => {
return `${Math.floor(diffHours / 24)}d ago`;
};
return (
<div className="alert-feed">
<h2>Recent Activity</h2>
<div className="alerts-container">
{sortedAlerts.map((alert, index) => (
<div key={index} className={`alert-item alert-${alert.type}`}>
<div className="alert-icon">{alertTypeIcons[alert.type]}</div>
return (
<div className="alert-feed">
<h2>Recent Activity</h2>
<div className="alerts-container">
{sortedAlerts.length === 0 && (
<div className="alert-empty-state">
<strong>No recent alerts</strong>
<span>Signals, warnings, and execution events will appear here as they arrive.</span>
</div>
)}
{sortedAlerts.map((alert, index) => (
<div key={index} className={`alert-item alert-${alert.type}`}>
<div className="alert-icon">{alertTypeIcons[alert.type]}</div>
<div className="alert-content">
<div className="alert-header">
<span className="alert-symbol">{alert.symbol}</span>

View File

@ -3,7 +3,7 @@ import { Link, Routes, Route, Navigate, useLocation } from 'react-router-dom';
import { Sidebar } from './Sidebar';
import { Header } from './Header';
import { RightPanel } from './RightPanel';
import { Button } from '../ui/button';
import { Button } from '../ui/Primitives';
import { getLegacySimpleRoute, getPlansRoute } from '../../views/tradePlansRoutes';
const HomeView = lazy(() => import('../../views/HomeView').then((mod) => ({ default: mod.HomeView })));
@ -20,20 +20,10 @@ function RouteFallback() {
return (
<section
aria-live="polite"
style={{
minHeight: 320,
borderRadius: 24,
border: '1px solid var(--border)',
background: 'var(--card)',
color: 'var(--foreground)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 14,
fontWeight: 700,
}}
className="route-fallback route-fallback-inline"
>
Loading workspace
<div className="route-fallback-spinner" aria-hidden="true" />
<span>Loading workspace</span>
</section>
);
}
@ -44,30 +34,18 @@ function NotFoundView() {
return (
<section
aria-labelledby="not-found-title"
style={{
minHeight: 420,
borderRadius: 24,
border: '1px solid var(--border)',
background: 'var(--hero-gradient)',
padding: '56px 32px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center',
color: 'var(--foreground)',
}}
className="route-empty-state"
>
<div style={{ fontSize: 12, fontWeight: 900, letterSpacing: '0.16em', color: 'var(--accent)', textTransform: 'uppercase' }}>
<div className="route-empty-eyebrow">
404
</div>
<h1 id="not-found-title" style={{ margin: '10px 0 8px', fontSize: 34, fontWeight: 900 }}>
<h1 id="not-found-title">
Route not found
</h1>
<p style={{ margin: 0, maxWidth: 520, color: 'var(--muted-foreground)', fontSize: 14, lineHeight: 1.6 }}>
<p>
No trading workspace exists at <code style={{ fontWeight: 800 }}>{location.pathname}</code>. The app is still running normally.
</p>
<Link to="/" style={{ marginTop: 24, textDecoration: 'none' }}>
<Link to="/" className="route-empty-action">
<Button>Return home</Button>
</Link>
</section>

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import { NavLink } from 'react-router-dom';
import {
Home, Briefcase, FlaskConical, Target, TrendingUp,
SlidersHorizontal, Star, Bell, Settings,
SlidersHorizontal, Star, Bell, Settings, ChartNoAxesCombined,
} from 'lucide-react';
import { useAppContext } from '../../context/AppContext';
import { getPlansRoute } from '../../views/tradePlansRoutes';
@ -27,21 +27,12 @@ export function Sidebar() {
return (
<aside className="trading-sidebar">
{/* Logo */}
<div className="trading-sidebar-logo" style={{
width: 40,
height: 40,
borderRadius: 10,
background: 'linear-gradient(135deg, var(--accent), color-mix(in oklab, var(--accent) 70%, var(--bl-info-strong) 30%))',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: 24,
flexShrink: 0,
boxShadow: '0 2px 8px color-mix(in oklab, var(--accent) 35%, transparent 65%)',
cursor: 'pointer',
fontSize: 20,
}}>
📈
<div className="trading-sidebar-logo" aria-label="ByteLyst trading">
<ChartNoAxesCombined size={22} />
<div className="trading-sidebar-brand">
<strong>ByteLyst</strong>
<span>Trading OS</span>
</div>
</div>
{/* Nav items */}
@ -51,32 +42,12 @@ export function Sidebar() {
key={to}
to={to}
end={end}
className="trading-sidebar-link"
style={({ isActive }) => ({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: '10px 0 8px',
gap: 3,
borderLeft: isActive ? '3px solid var(--accent)' : '3px solid transparent',
color: isActive ? 'var(--accent)' : 'var(--muted-foreground)',
textDecoration: 'none',
background: isActive ? 'var(--sidebar-active)' : 'transparent',
transition: 'all 0.15s',
})}
className={({ isActive }) => `trading-sidebar-link${isActive ? ' is-active' : ''}`}
>
{({ isActive }) => (
<>
<Icon size={20} strokeWidth={isActive ? 2.2 : 1.8} />
<span style={{
fontSize: 10,
fontWeight: isActive ? 700 : 500,
letterSpacing: '0.01em',
lineHeight: 1,
}}>
{label}
</span>
<span>{label}</span>
</>
)}
</NavLink>
@ -88,34 +59,9 @@ export function Sidebar() {
className="trading-sidebar-avatar"
title={`${user?.email ?? ''} — click to sign out`}
onClick={handleSignOut}
style={{
width: 36,
height: 36,
borderRadius: '50%',
background: 'var(--accent)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'var(--primary-foreground)',
fontSize: 12,
fontWeight: 700,
cursor: 'pointer',
position: 'relative',
userSelect: 'none',
flexShrink: 0,
}}
>
{initials}
<span style={{
position: 'absolute',
bottom: 1,
right: 1,
width: 9,
height: 9,
borderRadius: '50%',
background: 'var(--bl-success)',
border: '2px solid var(--card)',
}} />
<span className="trading-sidebar-avatar-status" />
</div>
</aside>
);

View File

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

View File

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

View File

@ -1,45 +1,30 @@
import type { ButtonHTMLAttributes, ReactNode } from 'react';
import { cn } from '../../lib/utils';
import type { ReactNode } from 'react';
import {
Button as ProductButton,
type ButtonProps as ProductButtonProps,
} from './Primitives';
type ButtonVariant = 'default' | 'outline' | 'ghost' | 'destructive';
type ButtonSize = 'sm' | 'md' | 'lg' | 'icon';
type LegacyButtonVariant = 'default' | 'outline' | 'ghost' | 'destructive' | 'subtle';
const variantClasses: Record<ButtonVariant, string> = {
default: 'bg-[var(--primary)] text-[var(--primary-foreground)] hover:brightness-95 shadow-sm',
outline: 'border border-[var(--border)] bg-[var(--card)] text-[var(--foreground)] hover:bg-[var(--accent-soft)]',
ghost: 'text-[var(--muted-foreground)] hover:bg-[var(--accent-soft)] hover:text-[var(--foreground)]',
destructive: 'bg-[var(--destructive)] text-white hover:brightness-95 shadow-sm',
};
const sizeClasses: Record<ButtonSize, string> = {
sm: 'h-9 px-3 text-xs',
md: 'h-10 px-4 text-sm',
lg: 'h-12 px-5 text-sm',
icon: 'h-10 w-10',
const variantMap: Record<LegacyButtonVariant, ProductButtonProps['variant']> = {
default: 'primary',
outline: 'outline',
ghost: 'ghost',
destructive: 'destructive',
subtle: 'subtle',
};
export function Button({
className,
variant = 'default',
size = 'md',
children,
...props
}: ButtonHTMLAttributes<HTMLButtonElement> & {
variant?: ButtonVariant;
size?: ButtonSize;
}: Omit<ProductButtonProps, 'variant'> & {
variant?: LegacyButtonVariant;
children: ReactNode;
}) {
return (
<button
className={cn(
'inline-flex items-center justify-center gap-2 rounded-xl font-semibold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)] disabled:cursor-not-allowed disabled:opacity-50',
variantClasses[variant],
sizeClasses[size],
className,
)}
{...props}
>
<ProductButton variant={variantMap[variant]} {...props}>
{children}
</button>
</ProductButton>
);
}

View File

@ -4,7 +4,7 @@ import { cn } from '../../lib/utils';
export function Card({ className, children, ...props }: HTMLAttributes<HTMLDivElement> & { children: ReactNode }) {
return (
<div
className={cn('rounded-3xl border border-[var(--border)] bg-[var(--card)] shadow-[var(--card-shadow)]', className)}
className={cn('rounded-[var(--bl-radius-card)] border border-[var(--border)] bg-[var(--card)] shadow-sm', className)}
{...props}
>
{children}
@ -14,7 +14,7 @@ export function Card({ className, children, ...props }: HTMLAttributes<HTMLDivEl
export function CardHeader({ className, children, ...props }: HTMLAttributes<HTMLDivElement> & { children: ReactNode }) {
return (
<div className={cn('flex items-start justify-between gap-4 p-6 pb-0', className)} {...props}>
<div className={cn('flex items-start justify-between gap-4 p-5 pb-0', className)} {...props}>
{children}
</div>
);
@ -22,7 +22,7 @@ export function CardHeader({ className, children, ...props }: HTMLAttributes<HTM
export function CardTitle({ className, children, ...props }: HTMLAttributes<HTMLHeadingElement> & { children: ReactNode }) {
return (
<h2 className={cn('text-lg font-bold tracking-tight text-[var(--foreground)]', className)} {...props}>
<h2 className={cn('m-0 text-base font-semibold tracking-tight text-[var(--foreground)]', className)} {...props}>
{children}
</h2>
);
@ -30,7 +30,7 @@ export function CardTitle({ className, children, ...props }: HTMLAttributes<HTML
export function CardDescription({ className, children, ...props }: HTMLAttributes<HTMLParagraphElement> & { children: ReactNode }) {
return (
<p className={cn('text-sm text-[var(--muted-foreground)]', className)} {...props}>
<p className={cn('m-0 text-sm leading-6 text-[var(--muted-foreground)]', className)} {...props}>
{children}
</p>
);
@ -38,7 +38,7 @@ export function CardDescription({ className, children, ...props }: HTMLAttribute
export function CardContent({ className, children, ...props }: HTMLAttributes<HTMLDivElement> & { children: ReactNode }) {
return (
<div className={cn('p-6', className)} {...props}>
<div className={cn('p-5', className)} {...props}>
{children}
</div>
);

View File

@ -13,16 +13,16 @@ export function PageHeader({
className?: string;
}) {
return (
<div className={cn('mb-6 flex items-start justify-between gap-4', className)}>
<div className={cn('mb-7 flex flex-col items-start justify-between gap-4 sm:flex-row', className)}>
<div className="min-w-0">
<h1 className="text-2xl font-bold tracking-tight text-[var(--foreground)] md:text-3xl">{title}</h1>
<h1 className="m-0 text-3xl font-semibold tracking-tight text-[var(--foreground)]">{title}</h1>
{description ? (
<p className="mt-2 max-w-3xl text-sm text-[var(--muted-foreground)] md:text-[15px]">
<p className="mt-3 max-w-3xl text-sm leading-6 text-[var(--muted-foreground)] md:text-[15px]">
{description}
</p>
) : null}
</div>
{action ? <div className="shrink-0">{action}</div> : null}
{action ? <div className="shrink-0 sm:pt-1">{action}</div> : null}
</div>
);
}

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 { Agentation } from 'agentation'
import './index.css'
import './App.css'
import App from './App.tsx'
import { AuthProvider } from './components/AuthContext';
import { ProductAccessibilityGate } from './components/ProductAccessibilityGate';

View File

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

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import { useAuth } from '../components/AuthContext';
import { EntryForm } from '../components/EntryForm';
import { Pencil, Trash2, Copy as CopyIcon, Search, Plus, Filter, LayoutGrid, Clock, ShieldCheck, Activity } from 'lucide-react';
import { Pencil, Trash2, Copy as CopyIcon, Plus, Filter, LayoutGrid, Clock, ShieldCheck, Activity, X } from 'lucide-react';
import type { BotState } from '../hooks/useWebSocket';
import { DEFAULT_BOT_STATE } from '../hooks/useWebSocket';
import { createManualEntry, deleteManualEntry, fetchManualEntries, type ManualEntryPayload } from '../lib/manualEntriesApi';
@ -99,7 +99,7 @@ const NAV_TABS = [
] as const;
const tabClass = (isActive: boolean) =>
`px-6 py-3 rounded-[1.5rem] text-[10px] font-black uppercase tracking-widest transition-all flex items-center gap-3 whitespace-nowrap ${isActive ? "bg-white text-black shadow-xl" : "text-gray-500 hover:text-gray-300"}`;
`min-h-9 rounded-lg px-3 text-xs font-semibold transition flex items-center gap-2 whitespace-nowrap ${isActive ? "border border-[var(--border-strong)] bg-[var(--accent-soft)] text-[var(--foreground)] shadow-sm" : "text-[var(--muted-foreground)] hover:bg-[var(--muted)] hover:text-[var(--foreground)]"}`;
export const EntriesTab = ({ botState = DEFAULT_BOT_STATE }: EntriesTabProps) => {
const { user } = useAuth();
@ -127,7 +127,7 @@ export const EntriesTab = ({ botState = DEFAULT_BOT_STATE }: EntriesTabProps) =>
}, [user]);
const handleDelete = async (id: string) => {
if (!confirm('⚠️ CRITICAL: Permanently delete this entry from neural watchlist?')) return;
if (!confirm('Permanently delete this watchlist entry?')) return;
await deleteManualEntry(id);
fetchEntries();
};
@ -140,121 +140,102 @@ export const EntriesTab = ({ botState = DEFAULT_BOT_STATE }: EntriesTabProps) =>
const entryCards = filteredEntries.map((entry) => {
const entryState = deriveEntryState(entry, botState);
return (
<div key={entry.stock_instance_id} className="group relative rounded-[2.5rem] p-1 transition-all duration-500 bg-white/[0.01] hover:bg-gradient-to-br hover:from-green-500/10 hover:to-transparent">
<div className="bg-[var(--card-elevated)] border border-white/5 rounded-[2.4rem] p-8 h-full flex flex-col transition-all group-hover:border-white/10 group-hover:shadow-2xl">
{/* Card Top */}
<div className="flex items-start justify-between mb-8">
<div className="space-y-1">
<h4 className="text-2xl font-black text-white tracking-tight group-hover:text-green-400 transition-colors uppercase">{entry.symbol}</h4>
<article key={entry.stock_instance_id} className="group flex h-full flex-col rounded-[var(--bl-radius-card)] border border-[var(--border)] bg-[var(--card)] p-5 shadow-sm transition hover:border-[var(--border-strong)] hover:shadow-[var(--card-shadow)]">
<div className="mb-5 flex items-start justify-between gap-4">
<div className="min-w-0 space-y-2">
<h4 className="m-0 truncate text-xl font-semibold tracking-tight text-[var(--foreground)]">{entry.symbol}</h4>
<div className="flex items-center gap-2">
<span className={`px-2 py-0.5 rounded text-[9px] font-black tracking-widest uppercase ${entry.is_real_trade ? 'bg-blue-500/20 text-blue-400' : 'bg-purple-500/20 text-purple-400'}`}>
{entry.is_real_trade ? 'REAL-UNIT' : 'PAPER-VOID'}
<span className={`rounded-full px-2.5 py-1 text-[11px] font-semibold ${entry.is_real_trade ? 'bg-blue-500/15 text-blue-300' : 'bg-violet-500/15 text-violet-300'}`}>
{entry.is_real_trade ? 'Real' : 'Paper'}
</span>
{entry.label && (
<span className="px-2 py-0.5 bg-white/5 rounded text-[9px] font-black text-gray-500 uppercase tracking-widest border border-white/5">
<span className="rounded-full border border-[var(--border)] bg-[var(--muted)] px-2.5 py-1 text-[11px] font-semibold text-[var(--muted-foreground)]">
{entry.label}
</span>
)}
</div>
</div>
<div className="flex gap-2 opacity-0 group-hover:opacity-100 transition-all translate-y-2 group-hover:translate-y-0">
<Button type="button" onClick={() => setEditingEntry(entry)} variant="ghost" className="p-3 bg-white/5 rounded-2xl text-gray-400 hover:text-white hover:bg-white/10 transition-all"><Pencil size={16} /></Button>
<Button type="button" onClick={() => handleClone(entry)} variant="ghost" className="p-3 bg-white/5 rounded-2xl text-gray-400 hover:text-green-400 hover:bg-green-500/10 transition-all"><CopyIcon size={16} /></Button>
<Button type="button" onClick={() => handleDelete(entry.stock_instance_id)} variant="ghost" className="p-3 bg-red-500/10 rounded-2xl text-red-500 hover:bg-red-500/20 transition-all"><Trash2 size={16} /></Button>
<div className="flex shrink-0 gap-1 opacity-100 transition md:opacity-0 md:group-hover:opacity-100">
<Button type="button" onClick={() => setEditingEntry(entry)} variant="ghost" size="icon" aria-label={`Edit ${entry.symbol}`}><Pencil size={15} /></Button>
<Button type="button" onClick={() => handleClone(entry)} variant="ghost" size="icon" aria-label={`Clone ${entry.symbol}`}><CopyIcon size={15} /></Button>
<Button type="button" onClick={() => handleDelete(entry.stock_instance_id)} variant="ghost" size="icon" aria-label={`Delete ${entry.symbol}`} className="text-[var(--destructive)] hover:bg-[var(--bl-danger-muted)]"><Trash2 size={15} /></Button>
</div>
</div>
{/* Metrics Grid */}
<div className="grid grid-cols-2 gap-4 mb-8">
<div className="bg-white/[0.02] p-5 rounded-3xl border border-white/5">
<span className="block text-[8px] text-gray-600 font-black uppercase tracking-[0.2em] mb-2">Entry Price</span>
<span className="text-xl font-black text-white font-mono">${entry.buy_price || '0.00'}</span>
<div className="mb-5 grid grid-cols-2 gap-3">
<div className="rounded-xl border border-[var(--border)] bg-[var(--muted)]/60 p-4">
<span className="mb-1 block text-[11px] font-semibold uppercase tracking-[0.08em] text-[var(--muted-foreground)]">Entry</span>
<span className="font-mono text-base font-semibold text-[var(--foreground)]">${entry.buy_price || '0.00'}</span>
</div>
<div className="bg-white/[0.02] p-5 rounded-3xl border border-white/5">
<span className="block text-[8px] text-gray-600 font-black uppercase tracking-[0.2em] mb-2">Target Exit</span>
<span className="text-xl font-black text-white font-mono">${entry.sell_price || '---'}</span>
<div className="rounded-xl border border-[var(--border)] bg-[var(--muted)]/60 p-4">
<span className="mb-1 block text-[11px] font-semibold uppercase tracking-[0.08em] text-[var(--muted-foreground)]">Target</span>
<span className="font-mono text-base font-semibold text-[var(--foreground)]">${entry.sell_price || '---'}</span>
</div>
<div className="bg-white/[0.02] p-4 rounded-3xl border border-white/5">
<span className="block text-[8px] text-gray-600 font-black uppercase tracking-[0.2em] mb-1">Quantity/Lot</span>
<span className="text-xs font-black text-gray-400 font-mono">{entry.quantity} UNITS</span>
<div className="rounded-xl border border-[var(--border)] bg-[var(--muted)]/60 p-4">
<span className="mb-1 block text-[11px] font-semibold uppercase tracking-[0.08em] text-[var(--muted-foreground)]">Quantity</span>
<span className="font-mono text-sm font-semibold text-[var(--foreground)]">{entry.quantity || '—'}</span>
</div>
<div className="bg-white/[0.02] p-4 rounded-3xl border border-white/5">
<span className="block text-[8px] text-gray-600 font-black uppercase tracking-[0.2em] mb-1">Status</span>
<span className={`text-[10px] font-black tracking-widest uppercase ${entry.active ? 'text-green-400 animate-pulse' : 'text-gray-600'}`}>
{entry.active ? 'SCANNING' : 'INACTIVE'}
<div className="rounded-xl border border-[var(--border)] bg-[var(--muted)]/60 p-4">
<span className="mb-1 block text-[11px] font-semibold uppercase tracking-[0.08em] text-[var(--muted-foreground)]">Status</span>
<span className={`text-sm font-semibold ${entry.active ? 'text-[var(--bl-success)]' : 'text-[var(--muted-foreground)]'}`}>
{entry.active ? 'Active' : 'Inactive'}
</span>
</div>
</div>
<div className="entry-state-row mb-6">
<span className="text-[9px] text-gray-500 font-black uppercase tracking-[0.2em]">Entry State</span>
<div className="mb-5 flex items-center justify-between gap-3 rounded-xl border border-[var(--border)] bg-[var(--card-elevated)] px-3 py-2">
<span className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[var(--muted-foreground)]">Entry state</span>
<span className={`entry-state-pill state-${entryState.toLowerCase().replace(/[^a-z]/g, '')}`}>
{entryState}
</span>
</div>
{/* Entry Notes/Context */}
<div className="flex-grow">
<span className="block text-[8px] text-gray-600 font-black uppercase tracking-[0.2em] mb-3">Neural Context</span>
<p className="text-[10px] font-bold text-gray-500 leading-relaxed uppercase italic">
{entry.notes || 'No manual notes injection provided for this asset cluster.'}
<span className="mb-2 block text-[11px] font-semibold uppercase tracking-[0.08em] text-[var(--muted-foreground)]">Notes</span>
<p className="m-0 text-sm leading-6 text-[var(--muted-foreground)]">
{entry.notes || 'No notes added yet.'}
</p>
</div>
{/* ID Context */}
<div className="mt-8 pt-6 border-t border-white/5 flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="w-6 h-6 rounded-full bg-white/5 flex items-center justify-center text-[10px] text-gray-600 font-black">
ID
</div>
<span className="text-[8px] text-gray-700 font-mono select-all truncate max-w-[150px]">{entry.stock_instance_id}</span>
</div>
<ShieldCheck size={14} className="text-gray-800" />
<div className="mt-5 flex items-center justify-between gap-3 border-t border-[var(--border)] pt-4">
<span className="truncate font-mono text-[11px] text-[var(--muted-foreground)]">{entry.stock_instance_id}</span>
<ShieldCheck size={15} className="shrink-0 text-[var(--muted-foreground)]" />
</div>
</div>
</div>
</article>
);
});
const navTabs = NAV_TABS;
return (
<div className="entries-tab space-y-10 animate-in fade-in duration-500">
<div className="entries-tab ux-page-stack animate-in fade-in duration-500">
{/* 1. HEADER & GLOBAL ACTIONS */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 px-4">
<div className="flex items-center gap-4">
<div className="w-14 h-14 bg-green-500/10 border border-green-500/20 rounded-2xl flex items-center justify-center text-green-400 shadow-2xl">
<Search size={30} />
</div>
<div>
<h2 className="text-3xl font-black text-white tracking-tighter uppercase">Watchlist & Entries</h2>
<p className="text-gray-500 text-[10px] font-black uppercase tracking-[0.2em] opacity-60">Neural Opportunity Cluster</p>
</div>
<div className="ux-section-header">
<div>
<h2 className="ux-section-title text-lg">Watchlist entries</h2>
<p className="ux-section-copy mt-1">Track manual symbols, paper setups, and real execution candidates.</p>
</div>
<Button
type="button"
onClick={() => { setIsAdding(!isAdding); setEditingEntry(null); }}
variant="ghost"
className={`flex items-center gap-3 px-8 py-4 rounded-[2rem] text-xs font-black uppercase tracking-widest transition-all ${isAdding ? 'bg-red-500/10 text-red-400 border border-red-500/20' : 'bg-white text-black shadow-2xl hover:scale-105'
}`}
variant={isAdding ? 'outline' : 'primary'}
className="flex items-center gap-2"
>
{isAdding ? <X size={18} /> : <Plus size={18} />}
{isAdding ? 'Cancel Entry' : 'Manual Entry Injection'}
{isAdding ? <X size={16} /> : <Plus size={16} />}
{isAdding ? 'Cancel' : 'Add entry'}
</Button>
</div>
{/* 2. FORM DRAWER (IF ADDING/EDITING) */}
{(isAdding || editingEntry) && (
<div className="bg-[var(--card-elevated)] border border-white/10 rounded-[3rem] p-10 shadow-2xl animate-in zoom-in-95 duration-300">
<div className="flex items-center justify-between mb-8">
<h3 className="text-lg font-black text-white uppercase tracking-tight flex items-center gap-3">
<Plus size={20} className="text-green-400" />
{editingEntry ? `Update Identity: ${editingEntry.symbol}` : 'New Opportunity Initialization'}
</h3>
<Button type="button" onClick={() => { setIsAdding(false); setEditingEntry(null); }} variant="ghost" className="text-gray-500 hover:text-white"><X size={20} /></Button>
<div className="ux-section animate-in zoom-in-95 duration-300">
<div className="ux-section-header">
<div>
<h3 className="ux-section-title">
{editingEntry ? `Edit ${editingEntry.symbol}` : 'New watchlist entry'}
</h3>
<p className="ux-section-copy mt-1">Save a symbol with target prices, quantity, and execution context.</p>
</div>
<Button type="button" onClick={() => { setIsAdding(false); setEditingEntry(null); }} variant="ghost" size="icon" aria-label="Close entry form"><X size={18} /></Button>
</div>
<div className="max-w-4xl">
<EntryForm
@ -265,8 +246,7 @@ export const EntriesTab = ({ botState = DEFAULT_BOT_STATE }: EntriesTabProps) =>
</div>
)}
{/* 3. NAVIGATION CLUSTERS */}
<div className="flex items-center p-1.5 bg-black/40 border border-white/5 rounded-[2rem] self-start shadow-2xl overflow-x-auto no-scrollbar max-w-full">
<div className="entries-tab-nav flex max-w-full items-center gap-1 overflow-x-auto rounded-xl border border-[var(--border)] bg-[var(--card)] p-1 shadow-sm no-scrollbar">
{navTabs.map((tab) => (
<Button
type="button"
@ -279,20 +259,18 @@ export const EntriesTab = ({ botState = DEFAULT_BOT_STATE }: EntriesTabProps) =>
</Button>
))}
</div>
{/* 4. CARDS GRID */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 gap-8">
<div className="grid grid-cols-1 gap-5 md:grid-cols-2 xl:grid-cols-3">
{entryCards}
{filteredEntries.length === 0 && (
<div className="col-span-full py-32 text-center bg-white/[0.02] border border-dashed border-white/5 rounded-[3rem]">
<LayoutGrid size={48} className="mx-auto mb-6 text-gray-800 opacity-40" />
<p className="text-sm font-black text-gray-700 uppercase tracking-[0.3em] opacity-40">Cluster Context Null</p>
<div className="ux-empty-state col-span-full">
<div>
<LayoutGrid size={34} className="mx-auto mb-4 text-[var(--muted-foreground)]" />
<strong>No entries in this view</strong>
<span>Try another status filter or add a new watchlist entry.</span>
</div>
</div>
)}
</div>
</div>
);
};
const X = ({ size }: { size: number }) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg>
);

View File

@ -16,8 +16,7 @@ import {
ChevronDown,
ChevronUp,
Lightbulb,
Dna,
Cpu,
Cpu,
Fingerprint,
Target,
DollarSign,
@ -328,108 +327,32 @@ export const MyStrategiesTab: React.FC<{ botState: any; alerts?: any[]; previewA
);
}
return (
<div style={{
maxWidth: '1400px',
margin: '0 auto',
padding: '0 20px 100px 20px',
animation: 'fadeIn 0.7s ease-out'
}}>
{/* Premium Header - Synchronized with Marketplace/Plans */}
<div style={{
display: 'flex',
flexDirection: 'column',
marginBottom: '60px',
padding: '60px 0',
borderBottom: '1px solid var(--bl-border-subtle)',
position: 'relative',
alignItems: 'flex-start'
}}>
<div style={{
position: 'absolute',
top: 0,
right: 0,
opacity: 0.03,
pointerEvents: 'none',
transform: 'translate(40px, -20px)'
}}>
<Dna size={320} strokeWidth={1} />
</div>
<div style={{
display: 'inline-flex',
alignItems: 'center',
gap: '12px',
color: 'var(--bl-success)',
fontSize: '11px',
fontWeight: 900,
textTransform: 'uppercase',
letterSpacing: '4px',
marginBottom: '24px'
}}>
DEPLOYED FLEET
<div style={{ width: '30px', height: '1px', background: 'var(--bl-success)', opacity: 0.2 }} />
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', width: '100%', flexWrap: 'wrap', gap: '32px' }}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
<h2 style={{
fontSize: '84px',
fontWeight: 950,
color: 'white',
letterSpacing: '-0.04em',
lineHeight: '0.9',
margin: 0,
textTransform: 'uppercase'
}}>
My<br />
<span style={{ color: 'var(--bl-success)' }}>Strategies</span>
</h2>
<p style={{ fontSize: '20px', color: 'var(--bl-text-quiet)', marginTop: '24px', maxWidth: '600px', fontWeight: 500, margin: '24px 0 0 0', textAlign: 'left' }}>
Monitor and manage your proprietary trading nodes.
</p>
</div>
<div style={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
<div style={{
background: 'var(--bl-success-muted)',
border: '1px solid var(--bl-success-muted)',
padding: '12px 24px',
borderRadius: '16px',
color: 'var(--bl-success)',
fontSize: '11px',
fontWeight: 900,
textTransform: 'uppercase',
letterSpacing: '1px'
}}>
{botState?.connected ? 'SYSTEMS ONLINE' : 'SYSTEMS DISCONNECTED'}
</div>
<Button
type="button"
onClick={() => setShowWizard(true)}
variant="secondary"
style={{
background: 'white',
border: 'none',
color: 'black',
padding: '14px 36px',
borderRadius: '16px',
cursor: 'pointer',
fontWeight: 900,
fontSize: '12px',
textTransform: 'uppercase',
letterSpacing: '1.5px',
transition: 'all 0.2s',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}
>
<Plus size={18} strokeWidth={3} /> NEW STRATEGY
</Button>
</div>
</div>
</div>
return (
<div className="strategy-workspace">
<div className="strategy-workspace-header">
<div>
<div className="strategy-workspace-eyebrow">
Strategy operations
</div>
<h2>My strategies</h2>
<p>
Monitor active profiles, review recent signals, and create new automated trading workflows.
</p>
</div>
<div className="strategy-workspace-actions">
<div className="strategy-connection-pill" data-connected={botState?.connected ? 'true' : 'false'}>
{botState?.connected ? 'Systems online' : 'Systems disconnected'}
</div>
<Button
type="button"
onClick={() => setShowWizard(true)}
variant="primary"
>
<Plus size={16} /> New strategy
</Button>
</div>
</div>
{/* Contextual Intelligence Row: Recent Activity + Symbol Volatility */}
{(() => {
@ -450,12 +373,11 @@ export const MyStrategiesTab: React.FC<{ botState: any; alerts?: any[]; previewA
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
{recentAlerts.map((alert, i) => {
const icons: Record<string, string> = { signal: '🚀', pulse: '⏰', error: '⚠️', info: '' };
const mins = Math.floor((Date.now() - alert.timestamp) / 60000);
const mins = Math.floor((Date.now() - alert.timestamp) / 60000);
const timeAgo = mins < 1 ? 'just now' : mins < 60 ? `${mins}m ago` : `${Math.floor(mins / 60)}h ago`;
return (
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: '10px', padding: '8px 12px', background: 'var(--bl-surface-overlay)', borderRadius: '10px' }}>
<span>{icons[alert.type] || ''}</span>
<Activity size={13} color="var(--bl-info)" />
<span style={{ fontSize: '12px', color: 'var(--bl-text-quiet)', fontWeight: 700, minWidth: '60px' }}>{alert.symbol}</span>
<span style={{ fontSize: '11px', color: 'var(--bl-text-quiet)', flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{alert.message}</span>
<span style={{ fontSize: '10px', color: 'var(--bl-text-tertiary)', fontWeight: 700, whiteSpace: 'nowrap' }}>{timeAgo}</span>
@ -469,7 +391,7 @@ export const MyStrategiesTab: React.FC<{ botState: any; alerts?: any[]; previewA
{/* Symbol-Specific Volatility */}
<div style={{ background: 'var(--bl-surface-highlight)', border: '1px solid var(--bl-border-subtle)', borderRadius: '24px', padding: '24px 28px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '16px' }}>
<span style={{ fontSize: '16px' }}>📊</span>
<TrendingUp size={15} color="var(--bl-info)" />
<span style={{ fontSize: '11px', fontWeight: 900, color: 'var(--bl-text-quiet)', textTransform: 'uppercase', letterSpacing: '2px' }}>Your Markets (24h)</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
@ -542,8 +464,8 @@ export const MyStrategiesTab: React.FC<{ botState: any; alerts?: any[]; previewA
textAlign: 'center'
}}>
<Activity size={60} color="var(--bl-text-quiet)" style={{ marginBottom: '24px' }} />
<h3 style={{ color: 'var(--bl-text-quiet)', fontWeight: 900, textTransform: 'uppercase', letterSpacing: '4px' }}>No Engines Deployed</h3>
<p style={{ color: 'var(--bl-text-quiet)', fontSize: '14px', marginTop: '12px', maxWidth: '300px' }}>Your fleet is currently dormant. Deploy your first engine to begin automated dominance.</p>
<h3 style={{ color: 'var(--bl-text-quiet)', fontWeight: 900, textTransform: 'uppercase', letterSpacing: '4px' }}>No strategies yet</h3>
<p style={{ color: 'var(--bl-text-quiet)', fontSize: '14px', marginTop: '12px', maxWidth: '300px' }}>Create your first strategy to start monitoring markets and testing automated execution.</p>
<Button
type="button"
onClick={() => setShowWizard(true)}

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { Star, Bell, BarChart2, Loader2 } from 'lucide-react';
import { ArrowRight, BarChart2, BarChart3, Bell, Loader2, Search, ShieldCheck, Sparkles, Star } from 'lucide-react';
import {
AreaChart, Area, Bar, ComposedChart, Line, LineChart, XAxis, YAxis, Tooltip,
ResponsiveContainer, CartesianGrid, ReferenceLine,
@ -796,44 +796,83 @@ function EmptyState({
const cryptoMode = suggestions.some(isCryptoLikeSymbol);
return (
<div style={{
display: 'flex', flexDirection: 'column', alignItems: 'center',
justifyContent: 'center', height: '60vh', gap: 16,
color: 'var(--muted-foreground)',
}}>
<div style={{ fontSize: 56 }}>📈</div>
<div style={{ fontSize: 20, fontWeight: 700, color: 'var(--foreground)' }}>
Search an asset to get started
</div>
<div style={{ fontSize: 14, textAlign: 'center', maxWidth: 360 }}>
Type a ticker symbol, crypto pair, or company name in the search bar above to view charts, financials, and news.
</div>
{cryptoMode && (
<div style={{ fontSize: 12, color: 'var(--muted-foreground)', fontWeight: 600 }}>
Suggested from your crypto bot configuration
<div className="home-command-center">
<section className="home-hero-panel" aria-labelledby="home-hero-title">
<div className="home-hero-copy">
<div className="home-hero-eyebrow">
<Sparkles size={15} />
Market workspace
</div>
<h1 id="home-hero-title">Start with a symbol. See the full trading picture.</h1>
<p>
Search or pick a configured asset to open charts, fundamentals, news, alerts, and execution context in one focused workspace.
</p>
<div className="home-hero-actions">
{suggestions.slice(0, 5).map(t => (
<Button
type="button"
key={t}
onClick={() => onSelect(t)}
variant={t === suggestions[0] ? 'default' : 'subtle'}
size="lg"
>
{t}
<ArrowRight size={15} />
</Button>
))}
</div>
{cryptoMode && (
<div className="home-hero-hint">
Suggested from your crypto bot configuration
</div>
)}
</div>
)}
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
{suggestions.map(t => (
<button
key={t}
onClick={() => onSelect(t)}
style={{
padding: '4px 12px',
background: 'var(--accent-soft)',
color: 'var(--primary)',
border: '1px solid var(--border)',
borderRadius: 20,
fontSize: 13,
fontWeight: 600,
cursor: 'pointer',
fontFamily: 'inherit',
}}
>
{t}
</button>
<div className="home-market-preview" aria-hidden="true">
<div className="home-preview-toolbar">
<span />
<span />
<span />
</div>
<div className="home-preview-chart">
<svg viewBox="0 0 420 180" role="img">
<path className="home-preview-area" d="M0 148 C42 136 62 92 102 104 C143 117 160 45 206 63 C244 78 260 124 302 92 C344 61 360 40 420 28 L420 180 L0 180 Z" />
<path className="home-preview-line" d="M0 148 C42 136 62 92 102 104 C143 117 160 45 206 63 C244 78 260 124 302 92 C344 61 360 40 420 28" />
</svg>
</div>
<div className="home-preview-stats">
<div>
<span>Readiness</span>
<strong>Live</strong>
</div>
<div>
<span>Risk</span>
<strong>Guarded</strong>
</div>
<div>
<span>Signals</span>
<strong>Synced</strong>
</div>
</div>
</div>
</section>
<section className="home-action-grid" aria-label="Primary workflows">
{[
{ icon: Search, title: 'Analyze', copy: 'Open a symbol workspace with chart, metrics, and news.' },
{ icon: ShieldCheck, title: 'Plan', copy: 'Build short-term setups with explicit sizing and exits.' },
{ icon: Bell, title: 'Monitor', copy: 'Track alerts, watchlist entries, and live status updates.' },
{ icon: BarChart3, title: 'Review', copy: 'Check positions, orders, and strategy performance.' },
].map(({ icon: Icon, title, copy }) => (
<article key={title} className="home-action-card">
<div className="home-action-icon">
<Icon size={18} />
</div>
<h2>{title}</h2>
<p>{copy}</p>
</article>
))}
</div>
</section>
</div>
);
}

View File

@ -13,12 +13,12 @@ export function MarketsView() {
const handleClone = (preset: StrategyPreset) => setClonedPreset(preset);
return (
<div>
<div className="ux-page-stack">
<PageHeader
title="Markets"
description="Scan live opportunities and reusable setups from the current market without leaving the workspace."
/>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 20, marginBottom: 24 }}>
<div className="markets-opportunity-grid">
<TopVolatile botState={botState} />
<AISetups botState={botState} />
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,6 +17,27 @@
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"@bytelyst/api-client": [
"../../learning_ai_common_plat/packages/api-client/dist/index.d.ts"
],
"@bytelyst/design-tokens": [
"../../learning_ai_common_plat/packages/design-tokens/dist/index.d.ts"
],
"@bytelyst/errors": [
"../../learning_ai_common_plat/packages/errors/dist/index.d.ts"
],
"@bytelyst/kill-switch-client": [
"../../learning_ai_common_plat/packages/kill-switch-client/dist/index.d.ts"
],
"@bytelyst/react-auth": [
"../../learning_ai_common_plat/packages/react-auth/dist/index.d.ts"
],
"@bytelyst/telemetry-client": [
"../../learning_ai_common_plat/packages/telemetry-client/dist/index.d.ts"
],
"@bytelyst/ui": [
"../../learning_ai_common_plat/packages/ui/dist/index.d.ts"
],
"monaco-editor": [
"../node_modules/.pnpm/monaco-editor@0.55.1/node_modules/monaco-editor/esm/vs/editor/editor.api"
],

View File

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