-
Notifications
You must be signed in to change notification settings - Fork 1
feat(design-system): graduate shared design system into the consumer sync #2508
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| # 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Add blank lines around headings to comply with Markdown style rules (MD022). All section headings require at least one blank line before and after to improve readability and comply with markdownlint standards. Apply the following structure before each ---
### P1 — Light, understated theme by defaultThis affects 8 headings: lines 16, 22, 30, 39, 46, 54, 77, and 88. Also applies to: 22-22, 30-30, 39-39, 46-46, 54-54, 77-77, 88-88 🧰 Tools🪛 markdownlint-cli2 (0.22.1)[warning] 16-16: Headings should be surrounded by blank lines (MD022, blanks-around-headings) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
| **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) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Clarify or implement Line 83 lists Either:
The pattern line in the doc (line 43) notes the approach correctly, so the inventory list on line 83 may simply be aspirational/outdated. 🤖 Prompt for AI Agents |
||
| - `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. | ||
|
|
||
| ## 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._ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| # Shared Design System (staging) | ||
|
|
||
| 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. | | ||
| | `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`). | | ||
|
|
||
| ## 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 | ||
|
|
||
| **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. |
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,95 @@ | ||||||||||||||||
| /* 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 .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} } | ||||||||||||||||
|
Comment on lines
+94
to
+95
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win Add reduced-motion handling for shimmer skeletons. Line 94–95 animates indefinitely with no Proposed patch .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-position: 0 0; }
+}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This syncs the adapter into
design-system/ds_streamlit.py, but the new usage examples tell Streamlit apps to runfrom ds_streamlit import .... From a consumer repo root, Python will not find that module unless every app first mutatessys.path, anddesign-systemcannot be used in a normal dotted import because of the hyphen. Any per-app adoption that follows the shipped docs will fail withModuleNotFoundErrorbefore reaching the helpers; sync it to an importable package/path or include a root shim/loader.Useful? React with 👍 / 👎.