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@c2537cc959f2ce05926c4639d25b90678abc97bc" # 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@c2537cc959f2ce05926c4639d25b90678abc97bc" # v1
uses: "stranske/Workflows/.github/actions/setup-api-client@44965d8d784573c119fb63828c05c89256c5f3e1" # v1
with:
secrets: ${{ toJSON(secrets) }}
github_token: ${{ github.token }}
Expand Down
105 changes: 105 additions & 0 deletions design-system/PRESENTATION_PATTERNS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Presentation Patterns — the fleet design-system rollout standard

The cross-repo UX-Review baseline (2026-06-22, 8 apps) found that **every app's engine works; the
failures are presentation + deployment.** Deployment is a separate program. This doc covers the
**presentation** class: it collapses the ~12 recurring presentation findings into **6 reusable
patterns**, each with a rule, a design-system component (`components.css` / `tokens.css`), a
per-app-type application (Streamlit / web-SPA / static-PWA), and the fleet findings it closes.

**Principle:** apps should consume a *pattern*, not invent a bespoke fix. One pattern, applied
everywhere, beats N one-off PRs — and it's why this is a design-system rollout, not a bug list.

---

## 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`).
- **Streamlit:** `ds_streamlit.empty_state(title, desc, cta_label, on_click)`.
- **Fixes:** TMP #5628 (Run-analysis CTA above results), PA #2021 (Results "Outputs.xlsx not found"),
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).
- **Streamlit:** `ds_streamlit.error(message, remediation)` backed by `translate_error(exc)` (maps
known fields, e.g. `financing_mode` → "Financing mode is required").
- **Fixes:** PA #2021 (raw `ModelConfig financing_mode` / margin pydantic errors), Inv-Man-Intake (item_id),
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").
- **Streamlit:** `ds_streamlit.availability_badge(label)` in the tab title / disabled control caption.
- **Fixes:** TMP #5629 (4/6 Results tabs empty — fixed by labelling, the canonical example), PA #2026
(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`).

---

## Finding → pattern map (presentation class)

| Finding | Pattern(s) |
|---|---|
| TMP #5628 empty-state CTA above results | P2 |
| TMP #5629 unmarked empty tabs *(fixed)* | P5 |
| Manager-Database #1214 empty default views | P2 |
| Manager-Database #1215 auth notice in UI | P4 |
| Inv-Man-Intake #629 non-actionable raw-JSON queue | P6 (+ a real action, app-specific) |
| Inv-Man-Intake #630 trace-sink + raw filenames | P4, P6 |
| Portable-Alpha #2021 raw errors / empty states | P2, P3 |
| Portable-Alpha #2026 upload-only, no sample | P5 |
| Pension-Data #594 *(deployment program — offline)* | — |
| LMS #351 root JSON 404 / empty surfaces | P2 (+ deployment) |
| Default dark theme (TMP/PA/MD/IMI) | P1 |

## Streamlit design kit (most of the fleet is Streamlit)
The CSS components above cover the web apps (Pension-Data, trip-planner, LMS). The four Streamlit
Tier-A apps need a Streamlit-native equivalent — ship a shared `ds_streamlit.py` alongside the CSS:
- `inject_theme()` — applies the `theme-air` palette (P1); pairs with `.streamlit/config.toml`.
- `empty_state(title, desc, cta_label=None, on_click=None)` (P2)
- `notice(kind, title, body, action=None)` and `error(message, remediation=None)` + `translate_error(exc)` (P3)
- `dev_note(msg)` → logging; `diagnostics_expander()` (P4)
- `availability_badge(label)` (P5)
- `humanize_id(raw, mapping)` (P6)
Graduate this kit + the CSS into `Workflows/templates/consumer-repo/design-system/` so maint-68 syncs it fleet-wide.
Comment on lines +83 to +92

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add blank line before heading for consistency.

Missing blank line before the "Streamlit design kit" heading at Line 83.

📝 Proposed fix
 | Default dark theme (TMP/PA/MD/IMI) | P1 |
 
 ## Streamlit design kit (most of the fleet is Streamlit)
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 83-83: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

🤖 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/PRESENTATION_PATTERNS.md` around lines 83 - 92, Add a blank
line before the "## Streamlit design kit" heading to ensure consistent
formatting and spacing with other section headings in the markdown file.
Currently the heading appears immediately after the previous section without any
whitespace separator, which breaks the standard markdown formatting pattern
where headings should be preceded by blank lines for readability.


## Rollout sequence
1. **Graduate** `tokens.css` + `components.css` (with the new patterns) + `ds_streamlit.py` into the Workflows consumer-repo design-system; let the existing sync (maint-68) distribute it.
2. **Apply per app, highest-ROI first** — close each open finding by adopting its pattern (not a bespoke fix), one small PR per app with the pattern's named test gate:
- TMP #5628 → P2 (closest to a clean pass)
- Manager-Database #1214 → P2, #1215 → P4
- Inv-Man-Intake #630 → P4/P6 (#629 also needs a real queue action)
- Portable-Alpha #2021 → P2/P3, #2026 → P5
- Pension-Data / LMS web surfaces → P2/P3 via the CSS components
3. **Theme pass (P1)** across the Streamlit apps once the kit is synced.

_Authored 2026-06-22 from the UX-Review fleet baseline. Components live in `components.css`/`tokens.css`
(this dir); see each repo's `docs/ux-review/REVIEW_LOG.md` for its findings + scores._
50 changes: 50 additions & 0 deletions design-system/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Shared Design System

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

- **`theme-air`** (Ink & Air) — default; work / outward-facing tools.
- **`theme-paper`** (Warm Paper) — friendlier apps (Reader, LMS).
- **`density-compact`** — tightens spacing for data-dense screens (e.g. Trend tables) **without** changing the theme.

## Use

```html
<link rel="stylesheet" href="tokens.css">
<link rel="stylesheet" href="components.css">

<body class="theme-air"> <!-- or theme-paper; add density-compact for dense screens -->
<div class="ds">
...components (.panel, .kpi, .appbar, table, .btns, .callout, ...)...
</div>
</body>
```

- Components are scoped under `.ds` so they never leak into a host app's styles.
- `components.css` is theme-agnostic — it only reads tokens. Don't fork it.

## Per-app customization

Override any token in an app stylesheet loaded **after** `tokens.css`:

```css
.theme-air { --accent: #0f6f6a; } /* this app wants a teal accent */
```

That's the "default with per-app customization" model: the base is canonical; an app changes only the tokens it needs.

## Files

| File | Role |
|---|---|
| `tokens.css` | Variables — themes + density. **Source of truth for the look.** |
| `components.css` | Component styles, token-driven and theme-agnostic. |
| `ds_streamlit.py` | Streamlit-native helpers for the same presentation patterns. |

## Theme → app mapping (proposed)

- **Ink & Air:** Trend_Model_Project, Counter_Risk, Manager-Database, Inv-Man-Intake, Pension-Data, Portable-Alpha-Extension-Model, trip-planner, Travel-Plan-Permission. Use `density-compact` on Trend/Counter_Risk data-dense screens.
- **Warm Paper:** Reader, learning-management-system.

## Status & next step

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.
99 changes: 99 additions & 0 deletions design-system/components.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/* components.css — component layer. Token-driven and theme-AGNOSTIC.
* Requires tokens.css + a theme class (.theme-air | .theme-paper) on an ancestor.
* Everything is scoped under .ds so it never leaks into a host app's own styles.
*/
.ds { color:var(--text); font-family:var(--font-body); font-size:var(--fs-base); line-height:1.5; background:var(--bg); }
.ds *, .ds *::before, .ds *::after { box-sizing:border-box; }
.ds h3 { font-family:var(--font-heading); font-weight:var(--heading-weight); margin:0 0 var(--space-3); font-size:15px; letter-spacing:-.005em; }
.ds .sub { color:var(--muted); font-size:var(--fs-sm); margin:calc(-1 * var(--space-2)) 0 var(--space-3); }

.ds .panel { background:var(--surface); border:var(--card-border); border-radius:var(--radius); box-shadow:var(--shadow); padding:var(--card-pad); margin-bottom:var(--space-4); }

.ds .appbar { display:flex; align-items:center; gap:var(--space-4); padding:var(--control-pad-y) var(--card-pad); background:var(--surface); border:var(--card-border); border-radius:var(--radius); box-shadow:var(--shadow); margin-bottom:var(--space-4); }
.ds .appbar .brand { font-family:var(--font-heading); font-weight:700; font-size:15px; }
.ds .appbar nav { display:flex; gap:var(--space-1); flex-wrap:wrap; }
.ds .appbar nav a { font-size:var(--fs-sm); text-decoration:none; color:var(--muted); padding:5px 10px; border-radius:var(--radius-sm); cursor:pointer; }
.ds .appbar nav a.active { color:var(--accent); background:var(--accent-weak); font-weight:600; }
.ds .appbar .spacer { flex:1; }

.ds .kpis { display:flex; flex-wrap:wrap; gap:var(--space-3); margin-bottom:var(--space-4); }
.ds .kpi { flex:1; min-width:150px; background:var(--surface); border:var(--card-border); border-radius:var(--radius); box-shadow:var(--shadow); padding:var(--space-3) var(--space-4); }
.ds .kpi .label { font-size:var(--fs-sm); color:var(--muted); }
.ds .kpi .val { font-size:var(--kpi-size); font-weight:700; letter-spacing:-.02em; margin-top:4px; font-family:var(--font-heading); }
.ds .kpi .delta { font-size:var(--fs-sm); font-weight:600; margin-top:2px; }
.ds .up { color:var(--pos); } .ds .down { color:var(--neg); }

.ds .grid2 { display:grid; grid-template-columns:1.3fr 1fr; gap:var(--space-4); }
@media (max-width:720px){ .ds .grid2 { grid-template-columns:1fr; } }

.ds table { width:100%; border-collapse:collapse; font-size:var(--fs-sm); }
.ds thead th { text-align:left; color:var(--muted); font-weight:600; font-size:var(--fs-xs); text-transform:uppercase; letter-spacing:.04em; padding:var(--row-pad-y) var(--row-pad-x); border-bottom:1px solid var(--border); }
.ds tbody td { padding:var(--row-pad-y) var(--row-pad-x); border-bottom:1px solid var(--row-border); }
.ds tbody tr:nth-child(even) { background:var(--zebra); }
.ds td.num, .ds th.num { text-align:right; font-variant-numeric:tabular-nums; }

.ds .field { margin-bottom:var(--space-3); }
.ds label { display:block; font-size:var(--fs-sm); font-weight:600; margin-bottom:5px; }
.ds label .help { font-weight:400; color:var(--muted); }
.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; }
.ds .b-primary { background:var(--btn-primary-bg); color:var(--btn-primary-contrast); border-color:var(--btn-primary-bg); }
.ds .b-secondary { background:var(--surface); color:var(--text); border:1px solid var(--border); }
.ds .b-ghost { background:transparent; color:var(--accent); }
.ds .b-danger { background:transparent; color:var(--neg); border:1px solid var(--neg-border); }
.ds button[disabled] { opacity:.45; cursor:not-allowed; }

.ds .callout { display:flex; gap:10px; padding:11px 13px; border-radius:var(--radius); background:var(--accent-weak); border:1px solid var(--accent-line); font-size:var(--fs-sm); }
.ds .callout .ic { color:var(--accent); font-weight:700; }
.ds .empty { text-align:center; color:var(--muted); border:1px dashed var(--border); border-radius:var(--radius); padding:20px; font-size:var(--fs-sm); }
.ds .chart { color:var(--accent); display:block; }

/* ============================================================================
* PRESENTATION-STATE PATTERNS (added 2026-06-22 from the cross-fleet UX-Review
* baseline). These standardize the recurring failure classes: empty/dead-end
* views, raw errors, dev/diagnostic leaks, and unavailable features.
* See PRESENTATION_PATTERNS.md for the rule behind each + per-app-type usage.
* ========================================================================== */

/* notice — the ONE container for user-facing messages. Never render a raw
* exception / internal field name / filename / CLI flag; translate to a human
* message + remediation and put it here. Variants by semantics. */
.ds .notice { display:flex; gap:10px; align-items:flex-start; padding:11px 13px; border-radius:var(--radius); font-size:var(--fs-sm); border:1px solid var(--border); background:var(--panel); }
.ds .notice .ic { font-weight:700; flex:none; line-height:1.4; }
.ds .notice .body { flex:1; }
.ds .notice .body strong { display:block; margin-bottom:2px; }
.ds .notice .body .act { margin-top:6px; } /* remediation link/button slot */
.ds .notice--info { background:var(--info-weak); border-color:var(--accent-line); }
.ds .notice--info .ic { color:var(--info); }
.ds .notice--warn { background:var(--warn-weak); border-color:var(--warn); }
.ds .notice--warn .ic { color:var(--warn); }
.ds .notice--error { background:var(--neg-weak); border-color:var(--neg-border); }
.ds .notice--error .ic { color:var(--neg); }
.ds .notice--ok { background:var(--pos-weak); border-color:var(--pos); }
.ds .notice--ok .ic { color:var(--pos); }

/* empty-state — for "no data yet" surfaces. ALWAYS a title + one-line reason +
* a next-action; NEVER a bare prompt sitting above already-rendered results,
* and NEVER an internal filename/path. */
.ds .empty-state { text-align:center; border:1px dashed var(--border); border-radius:var(--radius); padding:28px 20px; }
.ds .empty-state .es-icon { font-size:22px; opacity:.6; }
.ds .empty-state .es-title { font-family:var(--font-heading); font-weight:var(--heading-weight); font-size:15px; margin:8px 0 4px; }
.ds .empty-state .es-desc { color:var(--muted); font-size:var(--fs-sm); max-width:42ch; margin:0 auto 12px; }
.ds .empty-state .es-cta { display:inline-flex; gap:8px; }

/* badge — small availability/status marker (e.g. "multi-period only",
* "needs setup") so a tab/control states its applicability up front. */
.ds .badge { display:inline-block; font-size:var(--fs-xs); font-weight:600; padding:1px 7px; border-radius:999px; background:var(--panel); color:var(--muted); border:1px solid var(--border); vertical-align:middle; }
.ds .badge--muted { opacity:.8; }

/* 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); }
}
Loading
Loading