Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/agents-guard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ jobs:
github.event_name == 'pull_request_target' &&
steps.eligibility.outputs.should-run == 'true' &&
steps.api_client_base.outputs.available != 'true'
uses: "stranske/Workflows/.github/actions/setup-api-client@62ed0a86b5d57062ac3d04f4519e3998858e2d96" # v1
uses: "stranske/Workflows/.github/actions/setup-api-client@44965d8d784573c119fb63828c05c89256c5f3e1" # v1
with:
secrets: ${{ toJSON(secrets) }}
github_token: ${{ github.token }}
Expand Down Expand Up @@ -180,7 +180,7 @@ jobs:
steps.eligibility.outputs.should-run == 'true' &&
github.event_name == 'pull_request' &&
steps.api_client_head.outputs.available != 'true'
uses: "stranske/Workflows/.github/actions/setup-api-client@62ed0a86b5d57062ac3d04f4519e3998858e2d96" # v1
uses: "stranske/Workflows/.github/actions/setup-api-client@44965d8d784573c119fb63828c05c89256c5f3e1" # v1
with:
secrets: ${{ toJSON(secrets) }}
github_token: ${{ github.token }}
Expand Down
6 changes: 6 additions & 0 deletions design-system/PRESENTATION_PATTERNS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ everywhere, beats N one-off PRs — and it's why this is a design-system rollout
## The 6 patterns

### P1 — Light, understated theme by default

**Rule:** never ship a default dark theme; default to `theme-air` (Ink & Air). Dark is opt-in only.
- **web:** load `tokens.css` + `components.css`; root element `class="ds theme-air"` (`theme-paper` for friendlier apps).
- **Streamlit:** `.streamlit/config.toml` `[theme] base="light"` + the shared `ds_streamlit.inject_theme()` (maps the `theme-air` tokens to Streamlit's theme). *Already prototyped for TMP.*
- **Fixes:** default-dark on Trend_Model_Project / Portable-Alpha / Manager-Database / Inv-Man-Intake.

### P2 — Empty state = title + reason + next action

**Rule:** a "no data yet" surface ALWAYS shows a title, a one-line reason, and a **next-action CTA**.
NEVER a bare prompt sitting above already-rendered content, and NEVER an internal filename/path.
- **web:** `.ds .empty-state` (`.es-icon/.es-title/.es-desc/.es-cta`).
Expand All @@ -28,6 +30,7 @@ NEVER a bare prompt sitting above already-rendered content, and NEVER an interna
Manager-Database #1214 (empty default date / "Recent Activity"), LMS #351 (root `/` JSON 404).

### P3 — Errors are human messages + remediation

**Rule:** NEVER surface a raw exception, pydantic/validation dump, internal field name, or stack to a
user. Translate at the boundary to a plain message + a recovery action.
- **web:** `.ds .notice--error` (`.ic` + `.body strong` headline + `.body .act` remediation).
Expand All @@ -37,13 +40,15 @@ user. Translate at the boundary to a plain message + a recovery action.
the broad "raw error" class across the fleet.

### P4 — Dev/diagnostic notices go to logs, not the UI

**Rule:** auth-bypass / trace-sink / observability / "dev mode" notices NEVER render in the main
content. Use logging, or at most a collapsed "Diagnostics" expander.
- **web:** n/a in the main flow; use a `<details>`/debug panel.
- **Streamlit:** `ds_streamlit.dev_note(msg)` → `logging` (not `st.warning`/`st.write`); diagnostics behind `st.expander("Diagnostics")`.
- **Fixes:** Manager-Database #1215 (auth-bypass `st.warning`), Inv-Man-Intake #630 (trace-sink banner).

### P5 — Feature-availability markers (no silent dead-ends)

**Rule:** a tab/control that isn't applicable in the current mode states so up front (a badge/label),
rather than opening into a silent empty/disabled surface.
- **web:** `.ds .badge` on the tab/control (e.g. "multi-period only", "needs setup").
Expand All @@ -52,6 +57,7 @@ rather than opening into a silent empty/disabled surface.
(upload-only pages with no sample path → mark/offer the sample).

### P6 — No raw internal identifiers in user surfaces

**Rule:** decode internal IDs / fixture filenames / record keys into human-readable labels before display.
- **web/Streamlit:** a display-name mapping; keep the raw id as secondary/`title=` metadata if useful.
- **Fixes:** Inv-Man-Intake #629 (opaque `item_id`), #630 (raw fixture filename in selector), PA Results (`Outputs.xlsx`).
Expand Down
7 changes: 3 additions & 4 deletions design-system/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Shared Design System (staging)
# Shared Design System

Two themes on one token foundation, plus an orthogonal density axis.

Expand Down Expand Up @@ -38,8 +38,7 @@ That's the "default with per-app customization" model: the base is canonical; an
|---|---|
| `tokens.css` | Variables — themes + density. **Source of truth for the look.** |
| `components.css` | Component styles, token-driven and theme-agnostic. |
| `index.html` | Link-based reference page. Open locally, or import into Claude Design. |
| `preview.html` | Generated self-contained snapshot for sharing (regenerate with `build_preview.py`). |
| `ds_streamlit.py` | Streamlit-native helpers for the same presentation patterns. |

## Theme → app mapping (proposed)

Expand All @@ -48,4 +47,4 @@ That's the "default with per-app customization" model: the base is canonical; an

## Status & next step

**STAGED** here in the tracker. Graduates to `Workflows/templates/consumer-repo/design-system/` with a `sync-manifest.yml` entry (`is_directory: true`; base tokens `template_sync: exact`) as a deliberate Workflows PR — see [`../PLAN.md`](../PLAN.md) §3.3. Claude Design imports from the Workflows path once graduated, enabling the prototype-in-Design → land-in-repo round-trip.
This kit is managed from `Workflows/templates/consumer-repo/design-system/` and distributed by Maint 68 through `.github/sync-manifest.yml`. Update it here first, then let the sync workflow replace consumer copies.
4 changes: 4 additions & 0 deletions design-system/components.css
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
.ds .q { display:inline-flex; width:15px; height:15px; border-radius:50%; border:1px solid var(--border); color:var(--muted); font-size:10px; align-items:center; justify-content:center; vertical-align:1px; margin-left:5px; cursor:help; }
.ds input[type=text], .ds select { width:100%; font:inherit; font-size:var(--fs-base); padding:var(--control-pad-y) var(--control-pad-x); color:var(--text); background:var(--input-bg); border:1px solid var(--border); border-radius:var(--input-radius); }
.ds input[type=range] { width:100%; accent-color:var(--accent); }
.ds a:focus-visible, .ds button:focus-visible, .ds input:focus-visible, .ds select:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }

.ds .btns { display:flex; flex-wrap:wrap; gap:var(--space-2); margin-top:var(--space-1); }
.ds button { font:inherit; font-size:var(--fs-sm); font-weight:600; padding:var(--control-pad-y) 15px; border-radius:var(--btn-radius); cursor:pointer; border:1px solid transparent; }
Expand Down Expand Up @@ -93,3 +94,6 @@
/* skeleton — loading placeholder so a pending surface never looks empty/broken. */
.ds .skeleton { background:linear-gradient(90deg,var(--panel) 25%,var(--border) 37%,var(--panel) 63%); background-size:400% 100%; border-radius:var(--radius-sm); animation:ds-shimmer 1.3s ease infinite; min-height:14px; }
@keyframes ds-shimmer { 0%{background-position:100% 0} 100%{background-position:0 0} }
@media (prefers-reduced-motion: reduce) {
.ds .skeleton { animation:none; background:var(--panel); }
}
38 changes: 27 additions & 11 deletions design-system/ds_streamlit.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@

import logging
from collections.abc import Callable, Mapping
from contextlib import contextmanager
from html import escape
from typing import Any

logger = logging.getLogger("ds")
Expand Down Expand Up @@ -88,9 +90,12 @@ def empty_state(
clicked. NEVER pass an internal filename/path as `desc`."""
import streamlit as st

safe_icon = escape(str(icon))
safe_title = escape(str(title))
safe_desc = escape(str(desc))
st.markdown(
f"<div class='ds-empty'><div style='font-size:22px;opacity:.6'>{icon}</div>"
f"<div class='t'>{title}</div><div class='d'>{desc}</div></div>",
f"<div class='ds-empty'><div style='font-size:22px;opacity:.6'>{safe_icon}</div>"
f"<div class='t'>{safe_title}</div><div class='d'>{safe_desc}</div></div>",
unsafe_allow_html=True,
)
if cta_label:
Expand All @@ -107,12 +112,13 @@ def notice(kind: str, title: str = "", body: str = "", action: str | None = None
import streamlit as st

color, bg, ic = _NOTICE_STYLE.get(kind, _NOTICE_STYLE["info"])
head = f"<strong>{title}</strong><br>" if title else ""
act = f"<div style='margin-top:6px'>{action}</div>" if action else ""
head = f"<strong>{escape(str(title))}</strong><br>" if title else ""
act = f"<div style='margin-top:6px'>{escape(str(action))}</div>" if action else ""
safe_body = escape(str(body))
st.markdown(
f"<div class='ds-notice' style='background:{bg};border-color:{color}33'>"
f"<span class='ic' style='color:{color}'>{ic}</span>"
f"<div>{head}{body}{act}</div></div>",
f"<div>{head}{safe_body}{act}</div></div>",
unsafe_allow_html=True,
)

Expand All @@ -128,18 +134,19 @@ def translate_error(exc: Exception) -> tuple[str, str | None]:
Falls back to a generic message; the raw text is logged, not shown."""
logger.warning("ds.translate_error: %s", exc, exc_info=True)
text = str(exc)
text_lower = text.lower()
# Known field-required cases (extend per app as needed).
if "financing_mode" in text:
if "financing_mode" in text_lower:
return (
"Financing mode isn't set for this run.",
"Choose a financing mode (e.g. per-path) and run again.",
)
if "exceeds total capital" in text or "capital buffer" in text:
if "exceeds total capital" in text_lower or "capital buffer" in text_lower:
return (
"The capital allocation isn't feasible.",
"Reduce the internal allocation or volatility multiple to leave margin headroom.",
)
if "No investable funds" in text or "NO_FUNDS" in text:
if "no investable funds" in text_lower or "no_funds" in text_lower:
return (
"No funds matched the selection filters.",
"Try another preset or relax the selection settings.",
Expand All @@ -155,16 +162,25 @@ def dev_note(msg: str) -> None:
logger.info("ds.dev_note: %s", msg)


@contextmanager
def diagnostics_expander(label: str = "Diagnostics", *, expanded: bool = False):
"""P4 — explicit opt-in container for diagnostics that must be visible."""
import streamlit as st

with st.expander(label, expanded=expanded):
yield


def availability_badge(label: str) -> str:
"""P5 — markup for a small availability marker (use in a tab title/caption),
"""P5 — plain Streamlit-safe availability marker for tab titles/captions,
e.g. tab label f"Export {availability_badge('multi-period only')}"."""
return f"<span class='ds-badge'>{label}</span>"
return f" · {str(label).strip()}"


def humanize_id(raw: str, mapping: Mapping[str, str] | None = None) -> str:
"""P6 — decode an internal id to a human label; never show raw keys."""
if mapping and raw in mapping:
return mapping[raw]
# Best-effort: take a trailing human-ish segment, strip hashes.
tail = str(raw).replace("_", " ").split(":")[0].strip()
tail = str(raw).replace("_", " ").split(":")[-1].strip()
return tail or "item"
4 changes: 2 additions & 2 deletions design-system/tokens.css
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
--radius: 10px; --radius-sm: 8px; --btn-radius: 8px; --input-radius: 8px;

/* ---- type ---- */
--font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--font-body: system-ui, -apple-system, "BlinkMacSystemFont", "Segoe UI", "Roboto", "Helvetica", "Arial", sans-serif;
--font-heading: var(--font-body);
--heading-weight: 700;
}
Expand Down Expand Up @@ -71,6 +71,6 @@
--pos-weak:#eef3e6; --neg-weak:#f6e9e6; --warn-weak:#f7efe1; --info-weak:var(--accent-weak);
--card-border: 1px solid var(--border); --shadow: 0 1px 2px rgba(80,60,30,.06);
--radius:8px; --radius-sm:7px; --btn-radius:7px; --input-radius:7px;
--font-heading: Georgia, "Times New Roman", serif;
--font-heading: "Georgia", "Times New Roman", serif;
--heading-weight:600;
}
Loading