Polish trading UI and add launch roadmap
This commit is contained in:
parent
041e5e6f63
commit
1994821acf
7
.npmrc
7
.npmrc
@ -1,5 +1,2 @@
|
|||||||
@bytelyst:registry=https://gitea.bytelyst.com/api/packages/ByteLyst/npm/
|
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
|
|
||||||
|
|||||||
@ -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}`;
|
||||||
|
|||||||
398
docs/LAUNCH_READY_UI_UX_ROADMAP.md
Normal file
398
docs/LAUNCH_READY_UI_UX_ROADMAP.md
Normal file
@ -0,0 +1,398 @@
|
|||||||
|
# Launch-Ready UI/UX Roadmap
|
||||||
|
|
||||||
|
Last updated: 2026-05-08
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This roadmap defines the path to make the trading web product feel modern, coherent, professional, and launch-ready across the entire app. It covers the product frontend in `learning_ai_invt_trdg` and the shared UI/design-system foundation in `learning_ai_common_plat`.
|
||||||
|
|
||||||
|
The goal is not another styling pass. The goal is a stable product-quality experience with reusable platform components that can be applied to this product and future ByteLyst products.
|
||||||
|
|
||||||
|
## Product Standard
|
||||||
|
|
||||||
|
The target experience is a polished operational SaaS console for trading workflows:
|
||||||
|
|
||||||
|
- Calm, readable, premium, and practical.
|
||||||
|
- Dense enough for portfolio/trading operations without feeling cramped.
|
||||||
|
- Consistent component behavior across all routes.
|
||||||
|
- Clear primary actions and obvious next steps.
|
||||||
|
- Strong loading, empty, error, disabled, and success states.
|
||||||
|
- Responsive across desktop, tablet, and phone without broken menus, overflow, or hidden content.
|
||||||
|
- Accessible by keyboard and readable under normal contrast settings.
|
||||||
|
|
||||||
|
## Current State Summary
|
||||||
|
|
||||||
|
The app is functional, but launch readiness is blocked by inconsistent UI systems:
|
||||||
|
|
||||||
|
- The shared platform primitives exist, but many product screens still use raw page-specific styling.
|
||||||
|
- `web/src/index.css` and `web/src/App.css` carry too much global/product-specific behavior.
|
||||||
|
- Complex screens such as Trade Plans, Portfolio, Backtesting, Settings/Admin, Visual Builder, and Code Editor still contain legacy visual patterns.
|
||||||
|
- Tables, cards, badges, timeline steps, forms, tabs, alerts, and empty states do not yet fully share one design language.
|
||||||
|
- Some routes look modern after recent improvements, while deeper states still look like developer tooling.
|
||||||
|
- Responsive behavior has improved, but the shell still needs a formal contract for desktop/tablet/phone.
|
||||||
|
- Global overlays such as critical alerts and the assistant widget need collision rules.
|
||||||
|
|
||||||
|
## Guiding Principles
|
||||||
|
|
||||||
|
1. Centralize primitives in common platform.
|
||||||
|
2. Compose product screens from approved primitives.
|
||||||
|
3. Prefer quiet utility over decorative UI.
|
||||||
|
4. Keep cards for bounded tools/items, not every page section.
|
||||||
|
5. Use semantic colors only for meaning: success, warning, danger, info.
|
||||||
|
6. Use monospace only for symbols, prices, IDs, and technical values.
|
||||||
|
7. Avoid raw HTML controls in product screens unless wrapped by a shared primitive.
|
||||||
|
8. Every user-facing state needs loading, empty, error, disabled, and success treatment.
|
||||||
|
9. Every route must pass responsive, accessibility, and overflow checks before launch.
|
||||||
|
|
||||||
|
## Phase 0: Stop UI Drift
|
||||||
|
|
||||||
|
Objective: prevent new inconsistent UI while remediation is underway.
|
||||||
|
|
||||||
|
Scope:
|
||||||
|
|
||||||
|
- Expand `pnpm audit:ui` to flag:
|
||||||
|
- raw `<button>`, `<input>`, `<select>`, `<textarea>` in production components
|
||||||
|
- inline `style` usage outside approved exceptions
|
||||||
|
- legacy dark classes such as `bg-black`, `bg-zinc`, `text-gray`, `border-white`
|
||||||
|
- arbitrary `rounded-*`, `tracking-*`, and one-off uppercase styling in product components
|
||||||
|
- direct `@bytelyst/ui` imports outside the product adapter
|
||||||
|
- Quarantine legacy global styles in `App.css`.
|
||||||
|
- Create a migration issue list from audit output.
|
||||||
|
- Require new UI work to use common/product primitives.
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
|
||||||
|
- Audit runs locally and in CI.
|
||||||
|
- New raw controls and legacy surface classes fail the audit.
|
||||||
|
- Known legacy exceptions are documented and temporary.
|
||||||
|
|
||||||
|
## Phase 1: Platform Design System Foundation
|
||||||
|
|
||||||
|
Objective: make `learning_ai_common_plat` the real source of reusable UI quality.
|
||||||
|
|
||||||
|
Common platform components to standardize or add:
|
||||||
|
|
||||||
|
- `AppShell`: navigation, topbar, alert region, right panel slot, responsive contract.
|
||||||
|
- `PageHeader`: title, subtitle, actions, breadcrumbs, compact mode.
|
||||||
|
- `Section`: consistent section spacing, header, actions, description.
|
||||||
|
- `Toolbar`: compact action groups.
|
||||||
|
- `FilterBar`: search, selects, chips, refresh, and responsive wrapping.
|
||||||
|
- `DataTable`: sticky header, density modes, row actions, loading, empty, error, horizontal scroll.
|
||||||
|
- `EntityCard`: reusable card pattern for saved setups, watchlist entries, strategy profiles, and presets.
|
||||||
|
- `FormSection`: sectioned form layout with labels, hints, errors, disabled/read-only states.
|
||||||
|
- `FieldGrid`: responsive field layout.
|
||||||
|
- `StatusBadge`: semantic states with consistent icon/color/label behavior.
|
||||||
|
- `Timeline`: compact and detailed variants.
|
||||||
|
- `AlertBanner`: inline page alerts and global critical alerts.
|
||||||
|
- `Toast`: success/error/status feedback.
|
||||||
|
- `ConfirmDialog`: destructive action confirmation.
|
||||||
|
- `Skeleton`: route, card, table, and form skeleton variants.
|
||||||
|
- `EmptyState`: title, body, action, icon, compact/full variants.
|
||||||
|
|
||||||
|
Token work:
|
||||||
|
|
||||||
|
- Finalize light theme tokens.
|
||||||
|
- Normalize dark theme tokens after light theme is stable.
|
||||||
|
- Define component-level radius, shadow, border, spacing, and typography tokens.
|
||||||
|
- Add token documentation in Storybook.
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
|
||||||
|
- Common UI Storybook demonstrates every primitive with light and dark examples.
|
||||||
|
- Product adapter maps common primitives to the trading app without duplicating logic.
|
||||||
|
- At least 80 percent of product UI composition uses shared primitives or product adapters.
|
||||||
|
|
||||||
|
## Phase 2: Product Shell And Global UX
|
||||||
|
|
||||||
|
Objective: make the app frame stable and professional everywhere.
|
||||||
|
|
||||||
|
Scope:
|
||||||
|
|
||||||
|
- Replace product-local shell with platform `AppShell` or align it to the same contract.
|
||||||
|
- Desktop: full left rail or compact left rail depending on width.
|
||||||
|
- Tablet: compact left rail, no footer nav.
|
||||||
|
- Phone: bottom nav only.
|
||||||
|
- Right panel: desktop only, collapsible later if needed.
|
||||||
|
- Critical alert: inline/sticky shell region, never covering form controls or cards.
|
||||||
|
- Assistant widget: collision-safe offset and route-aware placement.
|
||||||
|
- Route fallback: page-shaped skeleton instead of blank loading.
|
||||||
|
- Global overflow policy: no route can widen the viewport accidentally.
|
||||||
|
- Theme default: stable light mode; dark mode as a tested secondary mode.
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
|
||||||
|
- No route has horizontal page overflow at 390, 768, 1024, 1440 px.
|
||||||
|
- Nav never appears as a broken full-page vertical menu.
|
||||||
|
- Critical alerts and assistant controls do not cover primary actions.
|
||||||
|
|
||||||
|
## Phase 3: Core Trading Screen Rebuild
|
||||||
|
|
||||||
|
Objective: polish the highest-value product workflows first.
|
||||||
|
|
||||||
|
### 3.1 Trade Plans
|
||||||
|
|
||||||
|
Current issues:
|
||||||
|
|
||||||
|
- Complex form sections still feel dense.
|
||||||
|
- Saved setup cards were improved but should become a first-class `EntityCard`.
|
||||||
|
- Metadata and runtime state need better hierarchy.
|
||||||
|
- Timeline should be a shared component.
|
||||||
|
|
||||||
|
Work:
|
||||||
|
|
||||||
|
- Convert create/edit flow into a guided plan builder.
|
||||||
|
- Use `FormSection`, `FieldGrid`, `StatusBadge`, `Timeline`, and `EntityCard`.
|
||||||
|
- Add saved setup filters: active, waiting, filled, closed, needs attention.
|
||||||
|
- Add success toast after save/update/delete.
|
||||||
|
- Add confirm dialog for delete.
|
||||||
|
- Add compact card view and detail drawer for advanced runtime/event history.
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
|
||||||
|
- A new user can create or review a plan without reading dense operational text.
|
||||||
|
- Saved setup cards fit comfortably on tablet and desktop.
|
||||||
|
- Delete/edit/manage actions are clear and not visually dominant.
|
||||||
|
|
||||||
|
### 3.2 Portfolio
|
||||||
|
|
||||||
|
Current issues:
|
||||||
|
|
||||||
|
- `PositionsTab` is large, table-heavy, and still carries legacy dark utility styles.
|
||||||
|
- Diagnostic/status messages can force overflow.
|
||||||
|
- Mobile/tablet experience needs a card alternative.
|
||||||
|
|
||||||
|
Work:
|
||||||
|
|
||||||
|
- Rebuild positions and order activity with common `DataTable`.
|
||||||
|
- Use compact `AlertBanner` for lifecycle/fallback warnings.
|
||||||
|
- Create responsive card view below tablet width.
|
||||||
|
- Replace raw profile filter with `SegmentedControl`.
|
||||||
|
- Add table skeleton, empty, error, and retry states.
|
||||||
|
- Add row-level action menus for plan management.
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
|
||||||
|
- No warning or trade ID can break layout.
|
||||||
|
- Position/order data scans cleanly.
|
||||||
|
- The page remains usable with many rows and long identifiers.
|
||||||
|
|
||||||
|
### 3.3 Research
|
||||||
|
|
||||||
|
Current issues:
|
||||||
|
|
||||||
|
- Strategies tab looks better, but builder/editor/backtest surfaces use different visual systems.
|
||||||
|
- Backtest components are still dark/zinc styled.
|
||||||
|
|
||||||
|
Work:
|
||||||
|
|
||||||
|
- Normalize research tabs with shared `Tabs`.
|
||||||
|
- Convert strategy cards to `EntityCard`.
|
||||||
|
- Convert Visual Builder forms to `FormSection` and common controls.
|
||||||
|
- Convert Code Editor shell to a polished tool panel.
|
||||||
|
- Rebuild Backtest panels with common cards, tables, metric cards, charts, and alerts.
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
|
||||||
|
- Strategies, Visual Builder, Code Editor, Signals, and Backtesting feel like one product.
|
||||||
|
- Backtest UI no longer looks like a separate app.
|
||||||
|
|
||||||
|
### 3.4 Screener
|
||||||
|
|
||||||
|
Current issues:
|
||||||
|
|
||||||
|
- Filter layout has improved but needs a final `FilterBar`.
|
||||||
|
- Table states should use shared `DataTable`.
|
||||||
|
|
||||||
|
Work:
|
||||||
|
|
||||||
|
- Move filters to `FilterBar`.
|
||||||
|
- Convert results to `DataTable`.
|
||||||
|
- Add query count, active filter chips, reset filters.
|
||||||
|
- Improve API error copy and retry.
|
||||||
|
- Add row action affordance for opening chart/research.
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
|
||||||
|
- Filters are readable on tablet.
|
||||||
|
- Result table has consistent empty/loading/error behavior.
|
||||||
|
|
||||||
|
### 3.5 Watchlist
|
||||||
|
|
||||||
|
Current issues:
|
||||||
|
|
||||||
|
- Empty state is improved, but entries should use reusable cards.
|
||||||
|
- Add/edit flow should feel like a proper modal or side panel.
|
||||||
|
|
||||||
|
Work:
|
||||||
|
|
||||||
|
- Convert entry cards to `EntityCard`.
|
||||||
|
- Move add/edit into a modal or drawer.
|
||||||
|
- Add toasts for create/update/delete/clone.
|
||||||
|
- Add confirmation for delete.
|
||||||
|
- Add filter counts.
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
|
||||||
|
- Empty and populated states both look complete.
|
||||||
|
- Entry management feels safe and predictable.
|
||||||
|
|
||||||
|
## Phase 4: Secondary And Admin Surfaces
|
||||||
|
|
||||||
|
Objective: remove remaining developer-tool styling from lower-frequency screens.
|
||||||
|
|
||||||
|
Scope:
|
||||||
|
|
||||||
|
- Settings account page.
|
||||||
|
- Bot Config.
|
||||||
|
- Admin Panel.
|
||||||
|
- Alerts.
|
||||||
|
- Markets and Marketplace.
|
||||||
|
- Login, reset password, auth error states.
|
||||||
|
- Right panel portfolio/news.
|
||||||
|
- Chat assistant panel and floating button.
|
||||||
|
|
||||||
|
Work:
|
||||||
|
|
||||||
|
- Convert Settings/Admin/Config to `FormSection`, `DataTable`, `AlertBanner`, and `ConfirmDialog`.
|
||||||
|
- Add real Alerts filters by severity/type/symbol.
|
||||||
|
- Convert Markets/Marketplace to common cards and skeletons.
|
||||||
|
- Improve right panel empty states and links.
|
||||||
|
- Ensure login/reset flows use common auth UI from platform where possible.
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
|
||||||
|
- No screen looks like raw admin tooling.
|
||||||
|
- Low-frequency screens are safe, clear, and consistent.
|
||||||
|
|
||||||
|
## Phase 5: Interaction And Feedback Quality
|
||||||
|
|
||||||
|
Objective: make the app feel trustworthy under real usage.
|
||||||
|
|
||||||
|
Add consistently:
|
||||||
|
|
||||||
|
- Toasts for create, update, delete, clone, save, refresh.
|
||||||
|
- Confirmation dialogs for destructive actions.
|
||||||
|
- Inline validation for all forms.
|
||||||
|
- Disabled-state explanations.
|
||||||
|
- Retry affordances for network errors.
|
||||||
|
- Skeletons that match final layout.
|
||||||
|
- Empty states with useful next action.
|
||||||
|
- Success states that confirm what changed.
|
||||||
|
- Optimistic or pending states where appropriate.
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
|
||||||
|
- A user never wonders whether an action worked.
|
||||||
|
- Failed network/API states are recoverable.
|
||||||
|
- Destructive actions are protected.
|
||||||
|
|
||||||
|
## Phase 6: Accessibility And Responsive QA
|
||||||
|
|
||||||
|
Objective: launch with confidence across device sizes and input modes.
|
||||||
|
|
||||||
|
Checks:
|
||||||
|
|
||||||
|
- Keyboard-only navigation.
|
||||||
|
- Visible focus states.
|
||||||
|
- ARIA labels for icon buttons.
|
||||||
|
- Form labels and errors.
|
||||||
|
- Color contrast for text, badges, and disabled states.
|
||||||
|
- Reduced motion behavior.
|
||||||
|
- Screen-reader-friendly empty/loading/error states.
|
||||||
|
- No text clipping in buttons, cards, tabs, or chips.
|
||||||
|
- No horizontal viewport overflow.
|
||||||
|
|
||||||
|
Viewport matrix:
|
||||||
|
|
||||||
|
- 390 px phone.
|
||||||
|
- 768 px tablet.
|
||||||
|
- 1024 px small desktop/tablet landscape.
|
||||||
|
- 1440 px desktop.
|
||||||
|
- 1728 px wide desktop.
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
|
||||||
|
- Every route passes visual QA at all target widths.
|
||||||
|
- Every primary workflow works with keyboard navigation.
|
||||||
|
|
||||||
|
## Phase 7: Visual Regression And CI Gates
|
||||||
|
|
||||||
|
Objective: keep the app launch-ready after the cleanup.
|
||||||
|
|
||||||
|
Tests and automation:
|
||||||
|
|
||||||
|
- Playwright route screenshot suite.
|
||||||
|
- Axe accessibility scan.
|
||||||
|
- Horizontal overflow test.
|
||||||
|
- Component smoke tests for common primitives.
|
||||||
|
- Storybook snapshots for shared UI.
|
||||||
|
- CI audit for raw controls, inline styles, and legacy classes.
|
||||||
|
- Route-level tests for empty/loading/error states.
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
|
||||||
|
- CI blocks UI drift.
|
||||||
|
- Screenshots make regressions obvious.
|
||||||
|
- Design system changes can be safely adopted by other products.
|
||||||
|
|
||||||
|
## Suggested Implementation Sequence
|
||||||
|
|
||||||
|
### PR 1: Foundation And Guardrails
|
||||||
|
|
||||||
|
- Expand UI drift audit.
|
||||||
|
- Introduce missing common primitives.
|
||||||
|
- Move product shell toward common shell contract.
|
||||||
|
- Add Storybook coverage for primitives.
|
||||||
|
|
||||||
|
### PR 2: Core Screen Rebuild
|
||||||
|
|
||||||
|
- Trade Plans.
|
||||||
|
- Portfolio.
|
||||||
|
- Watchlist.
|
||||||
|
- Screener.
|
||||||
|
|
||||||
|
### PR 3: Research And Backtesting
|
||||||
|
|
||||||
|
- Research tab shell.
|
||||||
|
- Visual Builder.
|
||||||
|
- Code Editor.
|
||||||
|
- Backtest configuration/results.
|
||||||
|
|
||||||
|
### PR 4: Settings, Admin, Markets, Alerts
|
||||||
|
|
||||||
|
- Settings and Bot Config.
|
||||||
|
- Admin Panel.
|
||||||
|
- Markets and Marketplace.
|
||||||
|
- Alerts.
|
||||||
|
- Right panel.
|
||||||
|
|
||||||
|
### PR 5: Launch QA
|
||||||
|
|
||||||
|
- Visual regression.
|
||||||
|
- Accessibility pass.
|
||||||
|
- Responsive pass.
|
||||||
|
- Final polish and copy pass.
|
||||||
|
|
||||||
|
## Product Launch Readiness Checklist
|
||||||
|
|
||||||
|
- [ ] Shared primitives cover app shell, page header, sections, cards, forms, tables, tabs, badges, timelines, alerts, toasts, modals, skeletons, and empty states.
|
||||||
|
- [ ] No production route uses raw controls outside approved wrappers.
|
||||||
|
- [ ] No production route has accidental horizontal overflow.
|
||||||
|
- [ ] Critical alerts never cover primary content.
|
||||||
|
- [ ] Assistant widget never covers primary actions.
|
||||||
|
- [ ] All destructive actions require confirmation.
|
||||||
|
- [ ] All saves/deletes/updates produce feedback.
|
||||||
|
- [ ] All pages have loading, empty, error, and success states.
|
||||||
|
- [ ] All pages pass keyboard navigation checks.
|
||||||
|
- [ ] All routes are visually checked at phone, tablet, desktop, and wide desktop widths.
|
||||||
|
- [ ] UI screenshots are captured in CI.
|
||||||
|
- [ ] Common platform UI can be reused by another product without trading-specific assumptions.
|
||||||
|
|
||||||
|
## Immediate Next Recommendation
|
||||||
|
|
||||||
|
Start with PR 1 and PR 2 together as the next focused milestone:
|
||||||
|
|
||||||
|
1. Harden the shared design system and audit gates.
|
||||||
|
2. Rebuild Trade Plans and Portfolio using those primitives.
|
||||||
|
|
||||||
|
These two screens carry the most product credibility risk. Once they are polished, the rest of the app can converge around the same patterns faster and with less rework.
|
||||||
@ -9,7 +9,6 @@
|
|||||||
"audit:ui": "sh ./scripts/ui-drift-audit.sh",
|
"audit:ui": "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
10
pnpm-lock.yaml
generated
@ -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': {}
|
||||||
|
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,8 +16,8 @@ const alertTypeIcons: { [key: string]: string } = {
|
|||||||
info: 'ℹ️'
|
info: 'ℹ️'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AlertFeed = ({ alerts }: AlertFeedProps) => {
|
export const AlertFeed = ({ alerts }: AlertFeedProps) => {
|
||||||
const sortedAlerts = [...alerts].reverse().slice(0, 50);
|
const sortedAlerts = [...alerts].reverse().slice(0, 50);
|
||||||
|
|
||||||
const getTimeAgo = (timestamp: number) => {
|
const getTimeAgo = (timestamp: number) => {
|
||||||
const diffMs = Date.now() - timestamp;
|
const diffMs = Date.now() - timestamp;
|
||||||
@ -29,13 +29,19 @@ export const AlertFeed = ({ alerts }: AlertFeedProps) => {
|
|||||||
return `${Math.floor(diffHours / 24)}d ago`;
|
return `${Math.floor(diffHours / 24)}d ago`;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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.map((alert, index) => (
|
{sortedAlerts.length === 0 && (
|
||||||
<div key={index} className={`alert-item alert-${alert.type}`}>
|
<div className="alert-empty-state">
|
||||||
<div className="alert-icon">{alertTypeIcons[alert.type]}</div>
|
<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-content">
|
||||||
<div className="alert-header">
|
<div className="alert-header">
|
||||||
<span className="alert-symbol">{alert.symbol}</span>
|
<span className="alert-symbol">{alert.symbol}</span>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 => (
|
||||||
|
|||||||
@ -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 />
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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 }) {
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
1777
web/src/index.css
1777
web/src/index.css
File diff suppressed because it is too large
Load Diff
@ -2,6 +2,7 @@ import { StrictMode } from 'react'
|
|||||||
import { createRoot } from 'react-dom/client'
|
import { 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';
|
||||||
|
|||||||
@ -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 () => {
|
||||||
|
|||||||
@ -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>
|
</article>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
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>
|
||||||
<div className="flex items-center gap-4">
|
<h2 className="ux-section-title text-lg">Watchlist entries</h2>
|
||||||
<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">
|
<p className="ux-section-copy mt-1">Track manual symbols, paper setups, and real execution candidates.</p>
|
||||||
<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>
|
</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>
|
|
||||||
);
|
|
||||||
|
|||||||
@ -16,8 +16,7 @@ import {
|
|||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
Lightbulb,
|
Lightbulb,
|
||||||
Dna,
|
Cpu,
|
||||||
Cpu,
|
|
||||||
Fingerprint,
|
Fingerprint,
|
||||||
Target,
|
Target,
|
||||||
DollarSign,
|
DollarSign,
|
||||||
@ -328,108 +327,32 @@ 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
|
||||||
}}>
|
</div>
|
||||||
{/* Premium Header - Synchronized with Marketplace/Plans */}
|
<h2>My strategies</h2>
|
||||||
<div style={{
|
<p>
|
||||||
display: 'flex',
|
Monitor active profiles, review recent signals, and create new automated trading workflows.
|
||||||
flexDirection: 'column',
|
</p>
|
||||||
marginBottom: '60px',
|
</div>
|
||||||
padding: '60px 0',
|
|
||||||
borderBottom: '1px solid var(--bl-border-subtle)',
|
<div className="strategy-workspace-actions">
|
||||||
position: 'relative',
|
<div className="strategy-connection-pill" data-connected={botState?.connected ? 'true' : 'false'}>
|
||||||
alignItems: 'flex-start'
|
{botState?.connected ? 'Systems online' : 'Systems disconnected'}
|
||||||
}}>
|
</div>
|
||||||
<div style={{
|
<Button
|
||||||
position: 'absolute',
|
type="button"
|
||||||
top: 0,
|
onClick={() => setShowWizard(true)}
|
||||||
right: 0,
|
variant="primary"
|
||||||
opacity: 0.03,
|
>
|
||||||
pointerEvents: 'none',
|
<Plus size={16} /> New strategy
|
||||||
transform: 'translate(40px, -20px)'
|
</Button>
|
||||||
}}>
|
</div>
|
||||||
<Dna size={320} strokeWidth={1} />
|
</div>
|
||||||
</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>
|
|
||||||
|
|
||||||
{/* 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)}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 & 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', () => {
|
||||||
|
|||||||
@ -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)' }}>
|
</div>
|
||||||
Search an asset to get started
|
<h1 id="home-hero-title">Start with a symbol. See the full trading picture.</h1>
|
||||||
</div>
|
<p>
|
||||||
<div style={{ fontSize: 14, textAlign: 'center', maxWidth: 360 }}>
|
Search or pick a configured asset to open charts, fundamentals, news, alerts, and execution context in one focused workspace.
|
||||||
Type a ticker symbol, crypto pair, or company name in the search bar above to view charts, financials, and news.
|
</p>
|
||||||
</div>
|
<div className="home-hero-actions">
|
||||||
{cryptoMode && (
|
{suggestions.slice(0, 5).map(t => (
|
||||||
<div style={{ fontSize: 12, color: 'var(--muted-foreground)', fontWeight: 600 }}>
|
<Button
|
||||||
Suggested from your crypto bot configuration
|
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>
|
||||||
)}
|
|
||||||
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
|
<div className="home-market-preview" aria-hidden="true">
|
||||||
{suggestions.map(t => (
|
<div className="home-preview-toolbar">
|
||||||
<button
|
<span />
|
||||||
key={t}
|
<span />
|
||||||
onClick={() => onSelect(t)}
|
<span />
|
||||||
style={{
|
</div>
|
||||||
padding: '4px 12px',
|
<div className="home-preview-chart">
|
||||||
background: 'var(--accent-soft)',
|
<svg viewBox="0 0 420 180" role="img">
|
||||||
color: 'var(--primary)',
|
<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" />
|
||||||
border: '1px solid var(--border)',
|
<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" />
|
||||||
borderRadius: 20,
|
</svg>
|
||||||
fontSize: 13,
|
</div>
|
||||||
fontWeight: 600,
|
<div className="home-preview-stats">
|
||||||
cursor: 'pointer',
|
<div>
|
||||||
fontFamily: 'inherit',
|
<span>Readiness</span>
|
||||||
}}
|
<strong>Live</strong>
|
||||||
>
|
</div>
|
||||||
{t}
|
<div>
|
||||||
</button>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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)} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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} />}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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"
|
||||||
],
|
],
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user