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>",
Comment on lines +115 to +121

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate notice() usages and nearby arguments.
rg -nP --type=py -C3 '\bnotice\s*\('
rg -nP --type=py -C3 '\baction\s*='

Repository: stranske/Fine-Art-Archive

Length of output: 163


🌐 Web query:

In Streamlit, when using st.markdown(..., unsafe_allow_html=True), is Markdown syntax inside an injected HTML <div> rendered as Markdown (e.g., links), or displayed literally?

💡 Result:

In Streamlit, when using st.markdown(..., unsafe_allow_html=True), Markdown syntax inside an HTML

is generally treated as raw HTML and may not be parsed as Markdown [1][2]. Historically, this behavior has been a known limitation because Streamlit's Markdown parser (typically react-markdown) often treats content inside block-level HTML tags like
as raw HTML, causing Markdown features—such as links or bold text—within those tags to be displayed literally rather than rendered [1][2]. However, this behavior is subject to updates and improvements. For example, as of early 2026, specific issues regarding the failure of Markdown links to render inside
blocks have been addressed via improvements to Streamlit's internal processing, which pre-processes such content to ensure Markdown-style links are converted correctly before the final rendering [2]. If you encounter issues where Markdown syntax is not rendering inside HTML tags: - Ensure you are using an up-to-date version of Streamlit [2]. - Note that Streamlit often requires content to be correctly structured (e.g., proper spacing or paragraph breaks) for the parser to identify and render Markdown elements correctly within HTML blocks [1][3]. - For pure HTML or complex layouts, consider using st.html instead of st.markdown, as st.html is designed specifically for rendering HTML and does not attempt to parse Markdown [4][5]. Streamlit also does not support "open-ended" HTML tags that span across multiple st.markdown calls, as each command creates its own isolated container [6][7]. If you are attempting to wrap other Streamlit elements (like charts or widgets) inside a single
, this is not natively supported by st.markdown [6][7].

Citations:


🏁 Script executed:

#!/bin/bash
# First, locate and read the notice() function definition
fd -e py "ds_streamlit" | head -5

Repository: stranske/Fine-Art-Archive

Length of output: 193


🏁 Script executed:

#!/bin/bash
# Read the notice function with context around line 111-121
sed -n '100,130p' design-system/ds_streamlit.py

Repository: stranske/Fine-Art-Archive

Length of output: 1397


🏁 Script executed on selected repositories:

#!/bin/bash
# Search for notice() usages in related repos
rg -nP --type=py '\bnotice\s*\(' -A2 -B2 || true

Repositories: stranske/Workflows, stranske/Template

Length of output: 416


🏁 Script executed:

#!/bin/bash
# Check if there's any recent commit history or changes context
git log --oneline -n 20 design-system/ds_streamlit.py 2>/dev/null || echo "git log unavailable"

Repository: stranske/Fine-Art-Archive

Length of output: 222


🏁 Script executed:

#!/bin/bash
# Check the full notice() function including docstring to confirm markdown claim
sed -n '111,122p' design-system/ds_streamlit.py

Repository: stranske/Fine-Art-Archive

Length of output: 771


Docstring-implementation mismatch: action parameter escapes HTML but is documented as markdown.

Line 111's docstring states action is "optional remediation (markdown)", but line 116 HTML-escapes the action parameter before rendering inside a <div>. In Streamlit, Markdown syntax inside HTML block elements is treated as literal text rather than parsed, so callers cannot pass markdown-formatted remediation text (e.g., links like [click here](url)) as the docstring implies.

Either update the docstring to document that action accepts literal text only, or clarify the intentional security-first tradeoff if the escape is non-negotiable. This change appears intentional for XSS hardening, but the contract with callers needs alignment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@design-system/ds_streamlit.py` around lines 115 - 121, The docstring for the
`action` parameter indicates it accepts markdown, but the implementation
HTML-escapes the action parameter before inserting it into the div element,
which prevents markdown syntax from being parsed. Update the docstring to
accurately reflect that the `action` parameter accepts literal text only (not
markdown), or if markdown support is intended, remove the escape call on the
action parameter and add a comment explaining any security considerations or
trade-offs associated with this change. Ensure the documentation matches the
actual behavior that callers will experience.

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