From e7a8a2a5da5f7dde0d3d4a934f8915f19421ecf9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 23 Jun 2026 08:28:17 +0000 Subject: [PATCH] chore: sync workflow templates from Workflows repo Automated sync from stranske/Workflows Template hash: d0d4ad2bdd25 Changes synced from sync-manifest.yml --- .github/workflows/agents-guard.yml | 4 +-- design-system/PRESENTATION_PATTERNS.md | 6 ++++ design-system/README.md | 7 ++--- design-system/components.css | 4 +++ design-system/ds_streamlit.py | 38 ++++++++++++++++++-------- design-system/tokens.css | 4 +-- 6 files changed, 44 insertions(+), 19 deletions(-) diff --git a/.github/workflows/agents-guard.yml b/.github/workflows/agents-guard.yml index 90bf8f4e..8d1e6740 100644 --- a/.github/workflows/agents-guard.yml +++ b/.github/workflows/agents-guard.yml @@ -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 }} @@ -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 }} diff --git a/design-system/PRESENTATION_PATTERNS.md b/design-system/PRESENTATION_PATTERNS.md index 77bf1062..87ad7410 100644 --- a/design-system/PRESENTATION_PATTERNS.md +++ b/design-system/PRESENTATION_PATTERNS.md @@ -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`). @@ -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). @@ -37,6 +40,7 @@ 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 `
`/debug panel. @@ -44,6 +48,7 @@ content. Use logging, or at most a collapsed "Diagnostics" expander. - **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"). @@ -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`). diff --git a/design-system/README.md b/design-system/README.md index aa50075a..29a86d59 100644 --- a/design-system/README.md +++ b/design-system/README.md @@ -1,4 +1,4 @@ -# Shared Design System (staging) +# Shared Design System Two themes on one token foundation, plus an orthogonal density axis. @@ -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) @@ -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. diff --git a/design-system/components.css b/design-system/components.css index 42a2b368..e62ee650 100644 --- a/design-system/components.css +++ b/design-system/components.css @@ -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; } @@ -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); } +} diff --git a/design-system/ds_streamlit.py b/design-system/ds_streamlit.py index 6ba94ce8..60a6df2a 100644 --- a/design-system/ds_streamlit.py +++ b/design-system/ds_streamlit.py @@ -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") @@ -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"
{icon}
" - f"
{title}
{desc}
", + f"
{safe_icon}
" + f"
{safe_title}
{safe_desc}
", unsafe_allow_html=True, ) if cta_label: @@ -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"{title}
" if title else "" - act = f"
{action}
" if action else "" + head = f"{escape(str(title))}
" if title else "" + act = f"
{escape(str(action))}
" if action else "" + safe_body = escape(str(body)) st.markdown( f"
" f"{ic}" - f"
{head}{body}{act}
", + f"
{head}{safe_body}{act}
", unsafe_allow_html=True, ) @@ -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.", @@ -155,10 +162,19 @@ 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"{label}" + return f" · {str(label).strip()}" def humanize_id(raw: str, mapping: Mapping[str, str] | None = None) -> str: @@ -166,5 +182,5 @@ def humanize_id(raw: str, mapping: Mapping[str, str] | None = None) -> str: 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" diff --git a/design-system/tokens.css b/design-system/tokens.css index 830d3052..f08f5597 100644 --- a/design-system/tokens.css +++ b/design-system/tokens.css @@ -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; } @@ -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; }