diff --git a/docs/frontend_architecture.md b/docs/frontend_architecture.md index 85dadf7..43118b3 100644 --- a/docs/frontend_architecture.md +++ b/docs/frontend_architecture.md @@ -125,9 +125,8 @@ The application uses a **single-page template** (`index.html`) with tab-based na ``` ┌─────────────────────────────────────────────────────────┐ │ Header (site-header) │ -│ ├── Brand (icon + title + subtitle) │ -│ ├── Parameters bar [ticker] [Run] │ ← not a tab, always visible -│ └── Theme toggle │ +│ Brand (icon+title, left) [ticker] [✅/❌] [Run] (center) │ ← not a tab, always visible +│ Theme toggle (right)│ ├─────────────────────────────────────────────────────────┤ │ Sidebar │ Main Panel │ │ (tab-nav) │ (tab-content) │ @@ -144,16 +143,31 @@ The application uses a **single-page template** (`index.html`) with tab-based na ``` The **Parameters bar** (`templates/partials/parameters_bar.html`, batch B6; folded into -the header post-B10) renders inside `.site-header`, between the brand and the theme -toggle, and owns exactly one visible input — `ticker` — plus the Run button and the -ticker-validation badges. Every other parameter lives in its module's toolbar (batch -B7); the bar also carries two hidden `start_time`/`end_time` inputs that `POST /` -validates and uses to size the readiness prefetch, kept in sync by -`state/marketParamsState.js`. It is **not** a tab: it survives tab switches, and it is -always visible — there is no collapse toggle. Because `.site-header` stays dark in both -themes, the bar's own controls (label, input) use light-on-dark styling rather than the -page's light-theme tokens; the ticker-validation badges and the error alert keep their -own self-contained colors, so they read the same as before. +the header post-B10) renders inside `.site-header` and owns exactly one visible input — +`ticker` — plus the Run button and a validity icon per ticker. `.header-inner` is a +3-column grid (`1fr auto 1fr`: brand / bar / actions); the bar sits in the `auto` middle +column, so it is truly centered on the row regardless of how wide the brand or the theme +toggle are, not just left-packed next to the brand. +`.parameters-bar-main` (the label + input + validity icons + Run) is deliberately +`flex-wrap: nowrap`: a multi-line flex container's intrinsic width is only its widest +single child, not the sum, which would starve the grid column of the width it needs — +wrapping only comes back below the 768px breakpoint, where the bar is stretched to the +header's full width by `grid-template-areas` instead of being intrinsically sized, so +wrapping there is safe. + +Every other parameter lives in its module's toolbar (batch B7); the bar also carries two +hidden `start_time`/`end_time` inputs that `POST /` validates and uses to size the +readiness prefetch, kept in sync by `state/marketParamsState.js`. It is **not** a tab: it +survives tab switches, and it is always visible — there is no collapse toggle. Because +`.site-header` stays dark in both themes, the bar's own controls (label, input) use +light-on-dark styling rather than the page's light-theme tokens. + +**Ticker validity feedback** (`#ticker-badges`, `static/main.js::validateTicker`) is a +single ✅/❌ glyph per parsed ticker, rendered immediately to the right of the input — +not the pre-existing pill badges (ticker + price, colored background) and not the +`▸ N ticker(s) valid` sentence that used to sit below the input; both are gone. The glyph +carries the ticker (and price, if valid) as its `title` tooltip. Debounced 500ms after +input, same as before. ### Peek Sidebar diff --git a/static/main.js b/static/main.js index 0edf00a..d902771 100644 --- a/static/main.js +++ b/static/main.js @@ -103,14 +103,14 @@ const FormManager = { } }; +// Validity feedback is a single ✅/❌ icon per ticker, rendered to the right +// of the input (#ticker-badges) — no separate status sentence. let validationTimeout; function validateTicker() { const rawInput = document.getElementById('ticker').value.trim().toUpperCase(); - const validationDiv = document.getElementById('ticker-validation'); const badgesDiv = document.getElementById('ticker-badges'); if (!rawInput) { - if (validationDiv) validationDiv.innerHTML = ''; if (badgesDiv) badgesDiv.innerHTML = ''; currentPrice = null; return; @@ -120,10 +120,12 @@ function validateTicker() { validationTimeout = setTimeout(() => { const tickers = parseTickers(rawInput); if (tickers.length === 0) { - if (validationDiv) validationDiv.innerHTML = 'No valid symbols'; + if (badgesDiv) { + badgesDiv.innerHTML = ''; + } return; } - if (validationDiv) validationDiv.innerHTML = 'Validating...'; + if (badgesDiv) badgesDiv.innerHTML = ''; fetch('/api/validate_tickers', { method: 'POST', @@ -137,25 +139,15 @@ function validateTicker() { if (badgesDiv) { badgesDiv.innerHTML = Object.entries(results).map(([t, info]) => { const cls = info.valid ? 'ticker-badge valid' : 'ticker-badge invalid'; + const symbol = info.valid ? '✅' : '❌'; const priceTxt = info.valid && info.price ? ` $${info.price.toFixed(2)}` : ''; - return `${escapeHtml(t)}${priceTxt}`; - }).join(' '); + const title = `${escapeHtml(t)}${priceTxt}`; + return `${symbol}`; + }).join(''); } const firstValid = Object.entries(results).find(([, info]) => info.valid); currentPrice = firstValid ? firstValid[1].price : null; - const validCount = Object.values(results).filter(r => r.valid).length; - const totalCount = Object.keys(results).length; - if (validationDiv) { - if (validCount === totalCount) { - validationDiv.innerHTML = `${validCount} ticker(s) valid`; - validationDiv.className = 'ticker-validation valid'; - } else { - validationDiv.innerHTML = `${validCount}/${totalCount} valid`; - validationDiv.className = 'ticker-validation warning'; - } - } - const validTickers = Object.entries(results) .filter(([, info]) => info.valid) .map(([t]) => t); @@ -163,9 +155,8 @@ function validateTicker() { } }) .catch(() => { - if (validationDiv) { - validationDiv.innerHTML = 'Error'; - validationDiv.className = 'ticker-validation warning'; + if (badgesDiv) { + badgesDiv.innerHTML = ''; } currentPrice = null; }); diff --git a/static/styles.css b/static/styles.css index a1cb8c1..da84d00 100644 --- a/static/styles.css +++ b/static/styles.css @@ -155,15 +155,18 @@ body { box-shadow: 0 1px 0 rgba(255, 255, 255, .06), var(--shadow-md); } +/* Three columns (brand / ticker form / actions) so the middle column — + the Parameters bar — sits truly centered on the row, not just left-packed + next to the brand: the two outer `1fr` tracks absorb the leftover space + equally regardless of how wide the brand or the actions are. */ .header-inner { width: 100%; max-width: 1600px; margin: 0 auto; padding: .6rem 1.5rem; - display: flex; - flex-wrap: wrap; + display: grid; + grid-template-columns: 1fr auto 1fr; align-items: center; - justify-content: space-between; gap: .6rem 1rem; } @@ -172,6 +175,8 @@ body { align-items: center; gap: .75rem; color: #fff; + justify-self: start; + min-width: 0; } .brand-icon { @@ -212,6 +217,7 @@ body { align-items: center; gap: .9rem; flex-shrink: 0; + justify-self: end; } /* Theme toggle — sits on the sticky header, which stays dark in BOTH @@ -609,24 +615,6 @@ body { border-radius: var(--radius-sm); } -/* Ticker validation */ -.ticker-validation { - font-size: .75rem; - font-weight: 500; -} - -.ticker-validation.valid { - color: var(--success); -} - -.ticker-validation.invalid { - color: var(--error); -} - -.ticker-validation.warning { - color: var(--warning); -} - /* ── Collapsible Positions ── */ .form-card.collapsible .form-card-title { display: flex; @@ -1116,11 +1104,33 @@ body { display: none; } - /* Brand + theme toggle share the first line; the ticker form (flex-basis - 100%) is forced onto its own line below, same as the old separate bar. */ + /* Brand + theme toggle share the first line (their own grid track each); + the ticker form drops to a second, full-width row below — same as the + old separate bar — instead of trying to stay centered on one line. */ + .header-inner { + grid-template-columns: 1fr auto; + grid-template-areas: + "brand actions" + "ticker ticker"; + } + + .header-brand { + grid-area: brand; + } + + .header-actions { + grid-area: actions; + } + .parameters-bar { - order: 3; - flex-basis: 100%; + grid-area: ticker; + justify-self: stretch; + } + + /* Full-width here (not intrinsically sized), so wrapping is safe and + needed on narrow phones. */ + .parameters-bar-main { + flex-wrap: wrap; } } @@ -1895,35 +1905,19 @@ body { .ticker-badges { display: flex; flex-wrap: wrap; - gap: .35rem; - margin-top: .4rem; + align-items: center; + gap: .25rem; } +/* One ✅/❌ glyph per ticker — the emoji itself carries the valid/invalid + color, so `.valid`/`.invalid` are kept only as hooks (title tooltip has + the ticker + price / reason) rather than for extra styling. */ .ticker-badge { display: inline-flex; align-items: center; - gap: .25rem; - font-size: .73rem; - font-weight: 600; - padding: .2rem .55rem; - border-radius: 999px; - white-space: nowrap; -} - -.ticker-badge.valid { - background: #ecfdf5; - color: #065f46; - border: 1px solid #a7f3d0; -} - -.ticker-badge.invalid { - background: #fef2f2; - color: #991b1b; - border: 1px solid #fecaca; -} - -.ticker-badge i { - font-size: .65rem; + font-size: 1rem; + line-height: 1; + cursor: default; } /* Ticker tab navigation (for multi-ticker results) */ @@ -3175,14 +3169,21 @@ textarea:focus-visible, display: flex; flex-direction: column; gap: 4px; - flex: 1 1 320px; + justify-self: center; min-width: 0; font-family: var(--font); } +/* `flex-wrap: nowrap` here is load-bearing for the grid `auto` column above: + a multi-line flex container's intrinsic (max-content) width is only its + *widest single child*, not the sum of all of them, which collapsed this + row to one item's width and wrapped the rest. Single-line intrinsic width + is the sum, which is what actually sizes the column to fit the row. Wrap + is re-enabled in the mobile media query, where this row is stretched to + the header's full width instead of being intrinsically sized. */ .parameters-bar-main { display: flex; - flex-wrap: wrap; + flex-wrap: nowrap; align-items: center; gap: 10px; } @@ -3222,14 +3223,6 @@ textarea:focus-visible, margin: 0; } -.parameters-bar .ticker-validation { - color: var(--slate-300); -} - -.parameters-bar .btn-primary { - margin-left: auto; -} - /* ============================================================ Module toolbars (batch B7) — each module owns its parameters. They sit under the panel header; the values travel on the module's diff --git a/templates/partials/parameters_bar.html b/templates/partials/parameters_bar.html index da46c77..6d5132d 100644 --- a/templates/partials/parameters_bar.html +++ b/templates/partials/parameters_bar.html @@ -2,7 +2,8 @@ index.html header comment). Owns exactly ONE input that every data module shares: `ticker` — plus the Run - button and the ticker-validation badges. It is not a tab: it renders inside + button and a validity icon (✅/❌ per ticker, static/main.js::validateTicker) + rendered to the input's right. It is not a tab: it renders inside `.site-header`, so it stays put while the user switches tabs and never hides. Batch B7 moved every other parameter to the module that owns it: the horizon @@ -19,7 +20,7 @@ -
+
@@ -28,7 +29,6 @@ {% if error %}
{{ error }}
{% endif %} -
{# See the header comment: submit-only mirror of the marketParams store. #}