commit 71803418e5329d8410edcfcfdcdab1c2f8601f71 Author: Jonas Weismueller Date: Mon Mar 23 21:51:59 2026 +0100 Initial commit: Shelly Manager with Textual CLI, Streamlit UI, and comprehensive .gitignore Shelly device management app with mDNS/subnet discovery, inventory, configuration, and mass operations for Gen1/Gen2+ devices. Includes .gitignore excluding runtime data (device DB, user config), AI conversation history, build artifacts, and common Python/OS patterns. diff --git a/.agents/skills/developing-with-streamlit/SKILL.md b/.agents/skills/developing-with-streamlit/SKILL.md new file mode 100644 index 0000000..7fff3c6 --- /dev/null +++ b/.agents/skills/developing-with-streamlit/SKILL.md @@ -0,0 +1,220 @@ +--- +name: developing-with-streamlit +description: "**[REQUIRED]** Use for ALL Streamlit tasks: creating, editing, debugging, beautifying, styling, theming, optimizing, or deploying Streamlit applications. Also required for building custom components (inline or packaged), using st.components.v2, or any HTML/JS/CSS component work. Triggers: streamlit, st., dashboard, app.py, beautify, style, CSS, color, background, theme, button, widget styling, custom component, st.components, packaged component, pyproject.toml, asset_dir, CCv2, HTML/JS component." +--- + +# Developing with Streamlit + +This is a **routing skill** that directs you to specialized sub-skills for Streamlit development. + +## When to Use + +Invoke this skill when the user's request involves: +- Creating a new Streamlit app +- Editing or modifying an existing Streamlit app +- Debugging Streamlit issues (errors, session state bugs, performance problems) +- Beautifying or improving the visual design of a Streamlit app +- Optimizing Streamlit performance (caching, fragments, reruns) +- Deploying Streamlit apps (locally or to Snowflake) +- Styling widgets (button colors, backgrounds, CSS customization) +- Any question about Streamlit widgets, layouts, or components + +**Trigger phrases:** "streamlit", "st.", "dashboard", "app.py", "beautify app", "make it look better", "style", "CSS", "color", "background", "theme", "button", "slow rerun", "session state", "performance", "faster", "cache", "deploy" + +## Workflow + +``` +Step 1: Locate the Streamlit source code + ↓ +Step 2: Identify task type and load appropriate sub-skill(s) + ↓ +Step 3: Apply guidance from sub-skill to edit code + ↓ +Step 4: Check if app is running and offer to run it +``` + +### Step 1: Locate the Streamlit Source Code (if needed) + +**Goal:** Identify the app file(s) to edit. **Skip this step if already clear from context.** + +**When to skip:** +- User mentioned a specific file path (e.g., "edit `src/app.py`") +- User has file(s) already in conversation context +- Working directory has an obvious single entry point (`app.py`, `streamlit_app.py`) + +**When to search:** +- User says "my streamlit app" without specifying which file +- Multiple Python files exist and it's unclear which is the entry point + +**If searching is needed:** + +1. **Quick scan** for Streamlit files: + ```bash + find . -name "*.py" -type f | xargs grep -l "import streamlit\|from streamlit" 2>/dev/null | head -10 + ``` + +2. **Apply entry point heuristics** (in priority order): + - `streamlit_app.py` at root → **this is the entry point** (canonical name) + - `app.py` at root → likely entry point + - File using `st.navigation` → entry point for multi-page apps + - Single `.py` file at root with streamlit import → entry point + - Files in `pages/` or `app_pages/` subdirectory → **NOT entry points** (these are sub-pages) + +3. **If entry point is obvious** → use it, no confirmation needed + + Example: Found `streamlit_app.py` and `pages/metrics.py` → use `streamlit_app.py` + +4. **Only ask if genuinely ambiguous** (e.g., multiple root-level candidates, none named `streamlit_app.py`): + ``` + Found multiple potential entry points: + - dashboard.py + - main.py + + Which is your main app? + ``` + +**Output:** Path to the main Streamlit source file(s) + +### Step 2: Identify Task Type and Route to Sub-Skill + +**Goal:** Determine what the user needs and load the appropriate guidance. + +Use this routing table to select sub-skill(s). **Always read the sub-skill file** before making changes: + +| User Need | Sub-skill to Read | +|-----------|-------------------| +| **Performance issues, slow apps, caching** | `read skills/optimizing-streamlit-performance/SKILL.md` | +| **Building a dashboard with KPIs/metrics** | `read skills/building-streamlit-dashboards/SKILL.md` | +| **Improving visual design, icons, polish** | `read skills/improving-streamlit-design/SKILL.md` | +| **Choosing widgets (selectbox vs radio vs pills)** | `read skills/choosing-streamlit-selection-widgets/SKILL.md` | +| **Styling widgets (button colors, backgrounds, CSS)** | `read skills/creating-streamlit-themes/SKILL.md` | +| **Layouts (columns, tabs, sidebar, containers)** | `read skills/using-streamlit-layouts/SKILL.md` | +| **Displaying data (dataframes, charts)** | `read skills/displaying-streamlit-data/SKILL.md` | +| **Multi-page app architecture** | `read skills/building-streamlit-multipage-apps/SKILL.md` | +| **Session state and callbacks** | `read skills/using-streamlit-session-state/SKILL.md` | +| **Markdown, colored text, badges** | `read skills/using-streamlit-markdown/SKILL.md` | +| **Custom themes and colors** | `read skills/creating-streamlit-themes/SKILL.md` | +| **Comprehensive theme design and brand alignment** | `read skills/creating-streamlit-themes/SKILL.md` | +| **Chat interfaces and AI assistants** | `read skills/building-streamlit-chat-ui/SKILL.md` | +| **Connecting to Snowflake** | `read skills/connecting-streamlit-to-snowflake/SKILL.md` | +| **Building or packaging a custom component, triggering events back to Python from JS/HTML, custom HTML/JS with event handling (CCv2), OR any UI element that doesn't exist as a native Streamlit widget** (e.g., drag-and-drop, custom interactive visualization, canvas drawing) | `read skills/building-streamlit-custom-components-v2/SKILL.md` — **IMPORTANT: `st.components.v1` is deprecated. Never use v1 for new components; always use `st.components.v2.component()`.** | +| **Third-party components** | `read skills/using-streamlit-custom-components/SKILL.md` | +| **Code organization** | `read skills/organizing-streamlit-code/SKILL.md` | +| **Environment setup** | `read skills/setting-up-streamlit-environment/SKILL.md` | +| **CLI commands** | `read skills/using-streamlit-cli/SKILL.md` | + +**Fallback — "this widget doesn't exist in Streamlit":** + +If the user asks for a UI element or interaction that **has never been part of Streamlit's API** and cannot be built with any combination of native widgets (e.g., drag-and-drop, canvas drawing, custom interactive visualizations), **route to the CCv2 sub-skill** (`skills/building-streamlit-custom-components-v2/SKILL.md`). **Do not** route to CCv2 for features that exist in newer Streamlit versions (e.g., `st.connection`, `st.segmented_control`) — suggest upgrading instead. + +**Common combinations:** + +For **beautifying/improving an app**, read in order: +1. `skills/improving-streamlit-design/SKILL.md` +2. `skills/using-streamlit-layouts/SKILL.md` +3. `skills/choosing-streamlit-selection-widgets/SKILL.md` + +For **building a dashboard**, read: +1. `skills/building-streamlit-dashboards/SKILL.md` +2. `skills/displaying-streamlit-data/SKILL.md` + +**IMPORTANT - Use templates:** + +When creating a **new dashboard app**, prefer starting from a template in `templates/apps/`: +- If a template closely matches the request, copy it and adapt: + - `dashboard-metrics` / `dashboard-metrics-snowflake` — KPI cards with time-series charts + - `dashboard-companies` — company/entity comparison + - `dashboard-compute` / `dashboard-compute-snowflake` — resource/credit monitoring + - `dashboard-feature-usage` — feature adoption tracking + - `dashboard-seattle-weather` — public dataset exploration (local only) + - `dashboard-stock-peers` / `dashboard-stock-peers-snowflake` — financial peer analysis +- If no template is a close match, start from scratch but borrow relevant patterns from the templates (e.g., caching with `@st.cache_data`, `filter_by_time_range()`, `st.set_page_config()`, chart utilities, layout structure) +- See `templates/apps/README.md` for template descriptions + +When **editing an existing app**, use templates as reference for best practices: +- Check `templates/apps/` for caching patterns, layout structure, and Snowflake integration +- Apply consistent patterns from templates to improve the existing code + +When applying a **custom theme**, use a template from `templates/themes/`: +- Copy a theme directory (snowflake, dracula, nord, stripe, solarized-light, spotify, github, minimal) +- Themes use Google Fonts for easy setup +- See `templates/themes/README.md` for theme previews + +For **performance optimization**, read: +1. `skills/optimizing-streamlit-performance/SKILL.md` + +### Step 3: Apply Guidance to Edit Code + +**Goal:** Make changes to the Streamlit app following sub-skill best practices. + +**Actions:** + +1. Apply the patterns and recommendations from the loaded sub-skill(s) +2. Make edits to the source file(s) identified in Step 1 +3. Preserve existing functionality while adding improvements + +### Step 4: Check Running Apps and Offer to Run + +**Goal:** Help the user see their changes by checking if their app is running. + +**Actions:** + +1. **Check** for running Streamlit apps on ports 850*: + ```bash + lsof -nP -iTCP -sTCP:LISTEN 2>/dev/null | grep -i python | awk '{print $2, $9}' | grep ':85' || echo "No Streamlit apps detected on ports 850*" + ``` + +2. **Present** findings to user: + + **If app is running:** + ``` + Found Streamlit app running: + - PID: [pid] at http://localhost:[port] + + Your changes should be visible after a page refresh (Streamlit hot-reloads on file save). + ``` + + **If no app is running:** + ``` + No Streamlit app detected on ports 850*. + + Would you like me to run the app? I can start it with: + streamlit run [app_file.py] + ``` + +3. **If user wants to run the app**, start it: + ```bash + streamlit run [path/to/app.py] --server.port 8501 + ``` + +## Stopping Points + +- **Step 2**: If multiple sub-skills seem relevant, ask user which aspect to focus on first +- **Step 4**: Ask before starting the Streamlit app + +## Skill map + +| Skill | Covers | +|-------|--------| +| [building-streamlit-chat-ui](skills/building-streamlit-chat-ui/SKILL.md) | Chat interfaces, streaming responses, message history | +| [building-streamlit-dashboards](skills/building-streamlit-dashboards/SKILL.md) | KPI cards, metrics, dashboard layouts | +| [building-streamlit-multipage-apps](skills/building-streamlit-multipage-apps/SKILL.md) | Page structure, navigation, shared state | +| [building-streamlit-custom-components-v2](skills/building-streamlit-custom-components-v2/SKILL.md) | Streamlit Custom Components v2 (inline and template-based packaged), bidirectional state/trigger callbacks, bundling, theme CSS variables | +| [choosing-streamlit-selection-widgets](skills/choosing-streamlit-selection-widgets/SKILL.md) | Selectbox vs radio vs segmented control vs pills vs multiselect | +| [connecting-streamlit-to-snowflake](skills/connecting-streamlit-to-snowflake/SKILL.md) | st.connection, query caching, credentials | +| [creating-streamlit-themes](skills/creating-streamlit-themes/SKILL.md) | Theme configuration, colors, fonts, light/dark modes, professional brand alignment, CSS avoidance | +| [displaying-streamlit-data](skills/displaying-streamlit-data/SKILL.md) | Dataframes, column config, charts | +| [improving-streamlit-design](skills/improving-streamlit-design/SKILL.md) | Icons, badges, colored text, visual polish | +| [optimizing-streamlit-performance](skills/optimizing-streamlit-performance/SKILL.md) | Caching, fragments, forms, static vs dynamic widgets | +| [organizing-streamlit-code](skills/organizing-streamlit-code/SKILL.md) | When to split into modules, separating UI from logic | +| [setting-up-streamlit-environment](skills/setting-up-streamlit-environment/SKILL.md) | Python environment, dependency management | +| [using-streamlit-custom-components](skills/using-streamlit-custom-components/SKILL.md) | Third-party components from the community | +| [using-streamlit-cli](skills/using-streamlit-cli/SKILL.md) | CLI commands, running apps | +| [using-streamlit-layouts](skills/using-streamlit-layouts/SKILL.md) | Sidebar, columns, containers, tabs, expanders, dialogs, alignment, spacing | +| [using-streamlit-markdown](skills/using-streamlit-markdown/SKILL.md) | Colored text, badges, icons, LaTeX, and all markdown features | +| [using-streamlit-session-state](skills/using-streamlit-session-state/SKILL.md) | Session state, widget keys, callbacks, state persistence | + +## Resources + +- [Streamlit API Reference](https://docs.streamlit.io/develop/api-reference) +- [Streamlit Gallery](https://streamlit.io/gallery) diff --git a/.agents/skills/developing-with-streamlit/skills/building-streamlit-chat-ui/SKILL.md b/.agents/skills/developing-with-streamlit/skills/building-streamlit-chat-ui/SKILL.md new file mode 100644 index 0000000..7e188e9 --- /dev/null +++ b/.agents/skills/developing-with-streamlit/skills/building-streamlit-chat-ui/SKILL.md @@ -0,0 +1,195 @@ +--- +name: building-streamlit-chat-ui +description: Building chat interfaces in Streamlit. Use when creating conversational UIs, chatbots, or AI assistants. Covers st.chat_message, st.chat_input, message history, and streaming responses. +license: Apache-2.0 +--- + +# Streamlit chat interfaces + +Build conversational UIs with Streamlit's chat elements. + +## Basic chat structure + +```python +import streamlit as st + +if "messages" not in st.session_state: + st.session_state.messages = [] + +# Display chat history +for msg in st.session_state.messages: + with st.chat_message(msg["role"]): + st.write(msg["content"]) + +# Handle new input +if prompt := st.chat_input("Ask a question"): + st.session_state.messages.append({"role": "user", "content": prompt}) + + with st.chat_message("user"): + st.write(prompt) + + with st.chat_message("assistant"): + response = get_response(prompt) # Your LLM call + st.write(response) + + st.session_state.messages.append({"role": "assistant", "content": response}) +``` + +## Streaming responses + +Use `st.write_stream` for token-by-token display. Pass any generator that yields strings, including the OpenAI generator directly: + +```python +def get_streaming_response(prompt): + # Replace with your LLM client (OpenAI, Anthropic, Cortex, etc.) + for chunk in your_llm_client.stream(prompt): + yield chunk + +with st.chat_message("assistant"): + response = st.write_stream(get_streaming_response(prompt)) + +st.session_state.messages.append({"role": "assistant", "content": response}) +``` + +With OpenAI, you can pass the stream directly: + +```python +from openai import OpenAI + +client = OpenAI() +with st.chat_message("assistant"): + stream = client.chat.completions.create( + model="gpt-4o", + messages=st.session_state.messages, + stream=True, + ) + response = st.write_stream(stream) +``` + +## Chat message avatars + +Streamlit provides default avatars for "user" and "assistant" roles—only customize if you have a specific need. You can use icons or images: + +```python +# With icons +with st.chat_message("assistant", avatar=":material/robot:"): + st.write(assistant_message) + +# With images +with st.chat_message("user", avatar="https://example.com/avatar.png"): + st.write(user_message) +``` + +## Suggestion chips + +Offer clickable suggestions before the first message. The pills disappear once the user sends a message, creating a clean onboarding experience: + +```python +SUGGESTIONS = { + ":blue[:material/help:] What is Streamlit?": "Explain what Streamlit is", + ":green[:material/code:] Show me an example": "Show a simple Streamlit example", +} + +# Only show before first message - they disappear after +if not st.session_state.messages: + selected = st.pills("Try asking:", list(SUGGESTIONS.keys()), label_visibility="collapsed") + if selected: + # Use the selection as the first prompt + prompt = SUGGESTIONS[selected] + st.session_state.messages.append({"role": "user", "content": prompt}) + st.rerun() +``` + +The `if not st.session_state.messages` check ensures the suggestions only appear on an empty chat. Once a message is added, the pills vanish and the conversation takes over. + +## File uploads + +Enable file attachments with `accept_file`. When enabled, `st.chat_input` returns a dict-like object with `text` and `files` attributes: + +```python +prompt = st.chat_input( + "Ask about an image", + accept_file=True, + file_type=["jpg", "jpeg", "png"], +) + +if prompt: + with st.chat_message("user"): + if prompt.text: + st.write(prompt.text) + if prompt.files: + st.image(prompt.files[0]) + + # Send to vision model + with st.chat_message("assistant"): + response = analyze_image(prompt.files[0], prompt.text) + st.write(response) +``` + +Use `accept_file="multiple"` to allow multiple files. + +## Audio input + +Enable voice recording with `accept_audio`. The recorded audio is available as a WAV file: + +```python +prompt = st.chat_input("Say something", accept_audio=True) + +if prompt: + if prompt.audio: + st.audio(prompt.audio) + if prompt.text: + st.write(prompt.text) +``` + +### Dictation with speech-to-text + +Convert audio to text and inject it back into the chat input: + +```python +prompt = st.chat_input("Say something", accept_audio=True, key="chat") + +if prompt and prompt.audio: + # Transcribe with Whisper or another STT model + transcript = openai.audio.transcriptions.create( + model="whisper-1", + file=prompt.audio, + ) + # Set the transcribed text as the next input + st.session_state.chat = transcript.text + st.rerun() +``` + +## User feedback + +Add thumbs up/down feedback to assistant messages. Also supports `"stars"` and `"faces"` ratings: + +```python +with st.chat_message("assistant"): + st.markdown(response) + feedback = st.feedback("thumbs") + if feedback is not None: + st.toast(f"Feedback received: {'👍' if feedback == 1 else '👎'}") +``` + +## Clear chat + +Add a button to reset the conversation: + +```python +def clear_chat(): + st.session_state.messages = [] + +st.button("Clear chat", on_click=clear_chat) +``` + +## Related skills + +- `connecting-streamlit-to-snowflake`: Database queries and Cortex chat example +- `optimizing-streamlit-performance`: Caching strategies for LLM calls + +## References + +- [st.chat_message](https://docs.streamlit.io/develop/api-reference/chat/st.chat_message) +- [st.chat_input](https://docs.streamlit.io/develop/api-reference/chat/st.chat_input) +- [st.write_stream](https://docs.streamlit.io/develop/api-reference/write-magic/st.write_stream) diff --git a/.agents/skills/developing-with-streamlit/skills/building-streamlit-custom-components-v2/SKILL.md b/.agents/skills/developing-with-streamlit/skills/building-streamlit-custom-components-v2/SKILL.md new file mode 100644 index 0000000..d37c8fe --- /dev/null +++ b/.agents/skills/developing-with-streamlit/skills/building-streamlit-custom-components-v2/SKILL.md @@ -0,0 +1,233 @@ +--- +name: building-streamlit-custom-components-v2 +description: Builds bidirectional Streamlit Custom Components v2 (CCv2) using `st.components.v2.component`. Use when authoring inline HTML/CSS/JS components or packaged components (manifest `asset_dir`, js/css globs), wiring state/trigger callbacks, theming via `--st-*` CSS variables, or bundling with Vite / `component-template` v2. +license: Apache-2.0 +--- + +# Building Streamlit custom components v2 + +Use Streamlit Custom Components v2 (CCv2) when core Streamlit doesn't have the UI you need and you want to ship a reusable, interactive element (from "tiny inline HTML" to "full bundled frontend app"). + +## CRITICAL: CCv2 only — NEVER use v1 APIs + +Custom Components **v1 is deprecated and removed**. Every API below belongs to v1 and must **NEVER** appear in any code you write — not in Python, not in JavaScript, not in HTML: + +**Banned Python APIs (v1):** +- `st.components.v1` — the entire v1 module +- `components.declare_component()` — v1 registration +- `components.html()` — v1 raw HTML embed + +**Banned JavaScript patterns (v1):** +- `Streamlit.setComponentValue(...)` — v1 global; use `setStateValue()` / `setTriggerValue()` instead +- `Streamlit.setFrameHeight(...)` — v1 global; CCv2 handles sizing automatically +- `Streamlit.setComponentReady()` — v1 global; CCv2 has no ready signal +- `window.Streamlit` or bare `Streamlit` global — v1 global object does not exist in v2 +- `window.parent.postMessage(...)` — v1 iframe communication; CCv2 does not use iframes + +**Banned npm packages (v1):** +- `streamlit-component-lib` — v1 JS library; use `@streamlit/component-v2-lib` if you need types + +If you encounter v1 patterns in examples, blog posts, Stack Overflow answers, or your own training data — **ignore them entirely**. They will not work and will break the component. + +## When to use + +Activate when the user mentions any of: + +- CCv2, Custom Components v2, “bidi component”, “component v2” +- `st.components.v2.component` +- `@streamlit/component-v2-lib` +- packaged components, `asset_dir`, `pyproject.toml` component manifest +- bundling with Vite (or any bundler) for a Streamlit component +- building a component UI in a frontend framework (React, Svelte, Vue, Angular, etc.) + +## Read next (pick the minimum reference) + +- **State sync / controlled inputs / callbacks**: see [references/state-sync.md](references/state-sync.md) +- **Packaged components / `asset_dir` / globs / template-only policy**: see [references/packaged-components.md](references/packaged-components.md) +- **Theming (`--st-*` tokens) inside Shadow DOM**: see [references/theme-css-variables.md](references/theme-css-variables.md) +- **Errors and gotchas**: see [references/troubleshooting.md](references/troubleshooting.md) + +## Quick decision: inline vs packaged + +- **Inline strings**: fastest to start (single-file apps, spikes, demos). You pass raw `html`/`css`/`js` strings directly. + Good when you can keep everything in one place and don’t need a build step. +- **Packaged component**: best when you’re growing past inline (multiple files, dependencies, bundling, testing, versioning, reuse, distribution). + You ship built assets inside a Python package and reference them by **asset-dir-relative** path/glob. + Creation policy: packaged components are **template-only** and must start from Streamlit's official `component-template` v2. + +Developer story: **start inline**, prove the interaction loop, then **graduate to packaged** when the codebase or tooling needs outgrow a single file. + +## CCv2 model (what’s actually happening) + +1. **Python registers** a component with `st.components.v2.component(...)` and gets back a **mount callable**. +2. The mount callable **mounts** the component in the app with `data=...`, layout (`width`, `height`), and optional `on__change` callbacks. +3. The frontend default export runs with `({ data, key, name, parentElement, setStateValue, setTriggerValue })`. +4. The component returns a **result object** whose attributes correspond to **state keys** and **trigger keys**. + +## Best practice: wrap the mount callable in your own Python API + +Prefer exposing **your own** Python function that wraps the callable returned by `st.components.v2.component(...)`. + +This gives you a clean, stable API surface for end users (typed parameters, validation, friendly defaults) and keeps `data=...`, `default=...`, and callback wiring as an internal detail. + +Important: + +- Declare the component **once** (usually at module import time). Avoid defining and registering the component inside a function you call multiple times; you can accidentally re-register the component name and get confusing behavior. + +References: + +- [`st.components.v2.component`](https://docs.streamlit.io/develop/api-reference/custom-components/st.components.v2.component) +- [`ComponentRenderer` (mount callable type)](https://docs.streamlit.io/develop/api-reference/custom-components/st.components.v2.types.componentrenderer) + +Example pattern: + +```python +import streamlit as st +from collections.abc import Callable + +_MY_COMPONENT = st.components.v2.component( + "my_inline_component", + html="
", + js=""" +export default function (component) { + const { data, parentElement } = component + parentElement.querySelector("#root").textContent = data?.label ?? "" +} +""", +) + + +def my_component( + label: str, + *, + key: str | None = None, + on_value_change: Callable[[], None] | None = None, + on_submitted_change: Callable[[], None] | None = None, +): + # Callbacks are optional, but if you want result attributes to always exist, + # provide (even empty) callbacks. + if on_value_change is None: + on_value_change = lambda: None + if on_submitted_change is None: + on_submitted_change = lambda: None + + return _MY_COMPONENT( + data={"label": label}, + key=key, + on_value_change=on_value_change, + on_submitted_change=on_submitted_change, + ) +``` + +## Inline quickstart (state + trigger) + +**Reminder: use ONLY v2 APIs.** Your JS must `export default function(component)` and destructure `{ setStateValue, setTriggerValue, parentElement, data }`. NEVER use `Streamlit.setComponentValue()`, `window.Streamlit`, or any v1 pattern. + +This is the minimum "bidi loop": + +- **JS → Python**: emit updates via `setStateValue(...)` (persistent) and `setTriggerValue(...)` (event) +- **Python → JS**: re-hydrate UI via `data=...` on every run + +```python +import streamlit as st + +HTML = """""" + +JS = """\ +export default function (component) { + const { data, parentElement, setStateValue, setTriggerValue } = component + + const input = parentElement.querySelector("#txt") + const btn = parentElement.querySelector("#btn") + if (!input || !btn) return + + const nextValue = (data && data.value) ?? "" + if (input.value !== nextValue) input.value = nextValue + + input.oninput = (e) => { + setStateValue("value", e.target.value) + } + + btn.onclick = () => { + setTriggerValue("submitted", input.value) + } +} +""" + +my_text_input = st.components.v2.component( + "my_inline_text_input", + html=HTML, + js=JS, +) + +KEY = "txt-1" +component_state = st.session_state.get(KEY, {}) +value = component_state.get("value", "") + +result = my_text_input( + key=KEY, + data={"value": value}, + on_value_change=lambda: None, # optional; include to always get `result.value` + on_submitted_change=lambda: None, # optional; include to always get `result.submitted` +) + +st.write("value (state):", result.value) +st.write("submitted (trigger):", result.submitted) +``` + +Notes: + +- **Inline JS/CSS should be multi-line**. CCv2 treats path-like strings as file references; a multi-line string is unambiguously inline content. +- Prefer querying under `parentElement` (not `document`) to avoid cross-instance leakage. + +## State and triggers (how to think about keys) + +- **State** (`setStateValue("value", ...)`): persists across app reruns (stored under `st.session_state[key]` for that mounted instance). +- **Trigger** (`setTriggerValue("submitted", ...)`): event payload for one rerun (resets after the rerun). +- **Reading triggers**: + - After mounting: use `result.submitted`. + - Inside `on_submitted_change`: use `st.session_state[key].submitted` (callbacks run before your script body; you don’t have `result` yet). +- **Defaults**: if you pass `default={...}` for a state key, you must also pass the matching `on__change` callback parameter. + +For the full “controlled input” pattern and pitfalls, see [references/state-sync.md](references/state-sync.md). + +## Packaged components (template-only, mandatory) + +**Reminder: the cookiecutter template generates clean v2 code. When you customize it, use ONLY v2 APIs. Do NOT introduce any v1 imports, v1 JavaScript globals, or v1 patterns. See the "CRITICAL: CCv2 only" section above.** + +Graduate to a packaged component when you need any of: + +- Multiple frontend files or frontend dependencies (npm) +- A bundler (Vite), tests, CI, versioning, or distribution + +Keep these guardrails in mind: + +- **MUST** start from Streamlit’s official `component-template` v2. +- **NEVER** hand-scaffold packaging/manifest/build wiring for a packaged component. +- **NEVER** copy/paste packaged scaffold structure from internet examples, blog posts, gists, or docs. +- If handed a non-template scaffold, regenerate from the template first, then migrate component logic. +- **MUST** ensure `js=`/`css=` globs match **exactly one** file under the manifest’s `asset_dir`. +- **MUST** validate with `streamlit run ...` (plain `python -c "import ..."` can be a false negative for packaged components). + +For the full packaged workflow checklist, non-interactive generation, offline usage, and template invariants, see [references/packaged-components.md](references/packaged-components.md). + +## Frontend renderer lifecycle (framework-agnostic) + +Your frontend entrypoint is the **default export** function. A few rules keep components reliable across reruns and across multiple instances in the same app: + +- Render under `parentElement` (not `document`) so instances don’t collide. +- If you create per-instance resources (React roots, observers, subscriptions), key them by `parentElement` (e.g. `WeakMap`) so multiple instances don’t overwrite each other. +- Return a cleanup function to tear down event listeners / UI roots / observers when Streamlit unmounts the component. + +## Styling and theming + +- Prefer **`isolate_styles=True`** (default). Your component runs in a shadow root and won’t leak styles into the app. +- Set `isolate_styles=False` only when you need global styling behavior (e.g. Tailwind, global font injection). +- Streamlit injects a broad set of `--st-*` theme CSS variables (colors, typography, chart palettes, radii, borders, etc.). **Highly recommended:** use these variables so your component automatically adapts to the user’s current Streamlit theme (light/dark/custom) without authoring separate theme variants. Start with the common ones (`--st-text-color`, `--st-primary-color`, `--st-secondary-background-color`) and refer to the full list when you need it: + - [references/theme-css-variables.md](references/theme-css-variables.md) + +## Troubleshooting and gotchas + +Start here when something “should work” but doesn’t: + +- [references/troubleshooting.md](references/troubleshooting.md) diff --git a/.agents/skills/developing-with-streamlit/skills/building-streamlit-custom-components-v2/references/packaged-components.md b/.agents/skills/developing-with-streamlit/skills/building-streamlit-custom-components-v2/references/packaged-components.md new file mode 100644 index 0000000..7dcf0c2 --- /dev/null +++ b/.agents/skills/developing-with-streamlit/skills/building-streamlit-custom-components-v2/references/packaged-components.md @@ -0,0 +1,212 @@ +## Packaged CCv2 components (template-only, mandatory) + +For packaged CCv2 components, agents **MUST** use Streamlit's official template as the starting point for every new component project. + +- [Streamlit component-template](https://github.com/streamlit/component-template) + +Never hand-scaffold the package/manifest/build layout and never copy/paste a packaged component scaffold from blog posts, gists, docs, or other internet sources. + +If a request starts from a non-template scaffold, stop and regenerate from the template first, then port logic into the generated project. + +Follow your generated project's README. **Only keep reading if you need to debug template wiring or customize behavior after template generation.** + +## Contents + +- Agent policy: template-only (mandatory) +- Prerequisites (packaged components) +- Start inline, then graduate to packaged +- Frontend framework note (React is optional) +- TypeScript support (recommended) +- Generate a new CCv2 component project + - Non-interactive generation (cookiecutter keys) + - Offline/airgapped +- Dev loop (template default) +- Verify the build output (prevents most load failures) +- Template invariants (don’t break these) +- Rename checklist (avoid placeholder-name drift) +- If you intentionally deviate from the template +- Verification recommendation + +### Agent policy: template-only (mandatory) + +If the request is for a packaged CCv2 component: + +- Start from the official template first (no exceptions). +- Never manually scaffold a custom package/manifest/build layout before template generation. +- Never copy a packaged component scaffold from the internet, even as a "starting point." +- If given existing non-template scaffolding, regenerate from the template and migrate code into it. +- Customize only after generation so you retain known-good packaging defaults. + +### Prerequisites (packaged components) + +- **Python build tooling**: `uv` (recommended) + `cookiecutter`. +- **Frontend build tooling**: Node.js + npm. + +### Start inline, then graduate to packaged + +Inline components are great for getting started quickly. Move to a packaged component when you hit any of these: + +- You need **multiple frontend files** (components/modules) instead of one big string. +- You want to pull in **frontend libraries** (npm deps) and run a bundler. +- You need **tests**, CI, versioning, or distribution (PyPI/private index). + +### Frontend framework note (React is optional) + +The official Streamlit `component-template` v2 supports both **React + TypeScript (Vite)** and **Pure TypeScript (Vite)** (no React). CCv2 also works with **any frontend framework that compiles to JavaScript** (Svelte, Vue, Angular, vanilla TS/JS, etc.). + +The only requirement is that you produce JS/CSS assets into your component’s `asset_dir`, then register them from Python via `html=...`, `js="..."`, and `css="..."` using **asset-dir-relative** paths/globs. + +### TypeScript support (recommended) + +For end-to-end type safety while authoring the frontend, install `@streamlit/component-v2-lib`: + +- [npm package](https://www.npmjs.com/package/@streamlit/component-v2-lib) +- [docs](https://docs.streamlit.io/develop/api-reference/custom-components/component-v2-lib) + +It provides TypeScript types like `FrontendRenderer` / `FrontendRendererArgs` so your `export default` renderer gets a **typed** `data` payload and typed state/trigger keys via generics. + +### Generate a new CCv2 component project + +This command is the required starting point for every packaged CCv2 component: + +```bash +uvx --from cookiecutter cookiecutter gh:streamlit/component-template --directory cookiecutter/v2 +``` + +If you run this non-interactively, pass explicit cookiecutter values (do not rely on defaults): + +Template keys: + +- `author_name` +- `author_email` +- `project_name` +- `package_name` +- `import_name` +- `description` +- `open_source_license` +- `framework` + +Recommended non-interactive invocation: + +This sample uses a **hypothetical breadcrumb component** name so the values are concrete and meaningful: + +```bash +uvx --from cookiecutter cookiecutter gh:streamlit/component-template \ + --directory cookiecutter/v2 \ + --no-input \ + author_name="Your Name" \ + author_email="you@example.com" \ + project_name="Streamlit Breadcrumbs" \ + package_name="streamlit-breadcrumbs" \ + import_name="streamlit_breadcrumbs" \ + description="Packaged Streamlit CCv2 breadcrumb component" \ + open_source_license="Apache-2.0" \ + framework="React + Typescript" +``` + +Notes: + +- Choice values must match template options exactly (`framework` is `"React + Typescript"` or `"Pure Typescript"`). +- Passing all keys avoids template placeholder names and post-generation rename churn. + +Offline/airgapped: + +```bash +uvx --from cookiecutter cookiecutter /path/to/component-template --directory cookiecutter/v2 +``` + +### Dev loop (template default) + +From the generated project: + +1. Activate the target project environment before Python/uv commands: + + ```bash + source /path/to/project/.venv/bin/activate + ``` + +2. Build the frontend assets (from `/frontend`): + + ```bash + npm i + npm run build + ``` + +3. Editable install (project root containing `pyproject.toml`): + + ```bash + uv pip install -e . --force-reinstall + ``` + +4. Run the example app with Streamlit: + + ```bash + streamlit run example.py + ``` + +Why this order: + +- Building first ensures `asset_dir` contains the expected files before install/use. +- Reinstalling editable after key renames keeps metadata and import paths in sync. + +### Packaged component workflow (copy/paste checklist) + +Use this when debugging or customizing after generation; it's designed to prevent the common "built assets exist but Streamlit can't load them" failure modes. + +``` +Packaged CCv2 checklist +- [ ] Generate project from `component-template` v2 +- [ ] Confirm this is template-generated (not hand-scaffolded, not copied from internet snippets) +- [ ] Activate the target project environment before Python/uv commands +- [ ] Rename template defaults (`streamlit-component-x`, `streamlit_component_x`, etc.) if needed +- [ ] Build frontend assets into the manifest’s `asset_dir` (template: `frontend/build/`) +- [ ] Editable install the Python package (`uv pip install -e . --force-reinstall`) +- [ ] Verify `js=`/`css=` globs match exactly one file each under `asset_dir` +- [ ] Run via `streamlit run ...` and confirm the component renders/events work +- [ ] If something breaks: read `references/troubleshooting.md`, fix, rebuild, re-verify glob uniqueness +``` + +### Verify the build output (prevents most load failures) + +- Ensure the manifest’s `asset_dir` exists and contains the built assets. +- Ensure each glob you register from Python matches **exactly one** file under `asset_dir`: + - Typical: `js="index-*.js"` and `css="index-*.css"` + - If multiple matches: clean the build output (template: `npm run clean`) and rebuild. + +### Template invariants (don’t break these) + +You typically shouldn’t need to touch these, but they explain most “why won’t this load?” failures: + +- **Component key**: the Python registration key must match the manifest: `"."`. +- **Manifest must ship inside the Python package**: the template places a minimal CCv2 manifest at `/pyproject.toml` with `asset_dir = "frontend/build"`. +- **Asset paths are asset-dir-relative strings**: `js="index-*.js"` (template default output) or `js="assets/index-*.js"` (if you configured an `assets/` subdir). +- **Globs must match exactly one file**: if `index-*.js` matches multiple hashed builds, clean the build output (`npm run clean`) and rebuild. + +### Rename checklist (avoid placeholder-name drift) + +Template defaults like `streamlit-component-x` / `streamlit_component_x` should be replaced everywhere early. + +Rename all of these together: + +- Root folder name (optional but recommended for clarity). +- Distribution name (`[project].name`) in root `pyproject.toml`. +- Import package directory (`streamlit_`). +- In-package manifest file and contents (`/pyproject.toml`). +- Wrapper registration key: + - `st.components.v2.component(".", ...)` +- `MANIFEST.in` and `[tool.setuptools.*]` references. +- README/example imports and frontend package name. + +### Allowed customizations (after template generation only) + +Keep the blast radius small: + +- If you change output layout, update only the `js=`/`css=` asset-dir-relative globs in the Python wrapper. +- For Vite, keep `base: "./"` so relative URLs work when served from Streamlit’s component URLs. + +### Verification recommendation + +Validate packaged components with `streamlit run ...`, not plain `python -c "import ..."` checks. + +- Streamlit discovers component manifests as part of runtime setup. +- Plain import checks can report false-negative `asset_dir` registration errors for otherwise-correct packaged components. diff --git a/.agents/skills/developing-with-streamlit/skills/building-streamlit-custom-components-v2/references/state-sync.md b/.agents/skills/developing-with-streamlit/skills/building-streamlit-custom-components-v2/references/state-sync.md new file mode 100644 index 0000000..9319754 --- /dev/null +++ b/.agents/skills/developing-with-streamlit/skills/building-streamlit-custom-components-v2/references/state-sync.md @@ -0,0 +1,149 @@ +## State sync patterns (JS ↔ Python) + +This reference shows the canonical CCv2 “controlled component” loop and the most common pitfalls when syncing state between JavaScript and Python. + +## Contents + +- Mental model +- Canonical pattern: controlled text input + - JavaScript (hydrate from `data`, emit via `setStateValue`) + - Python wrapper (feed state back down via `data`) +- Defaults: when to use `default=...` (and why it fails) +- Python → JS hydration: initial-only vs true sync +- Session State timing: don’t mutate after mount +- Troubleshooting checklist + +### Mental model + +- **Frontend state emission (JS → Python)**: you explicitly call `setStateValue(key, value)` or `setTriggerValue(key, value)`. +- **Frontend state hydration (Python → JS)**: your JS reads `component.data` and updates the DOM accordingly. +- There is no built-in “two-way binding”: you must implement both sides. + +### Canonical pattern: controlled text input + +This is modeled after Streamlit’s own CCv2 e2e example. + +#### JavaScript (hydrate from `data`, emit via `setStateValue`) + +Key guideline: only assign to the input when it’s different, or you’ll fight the user’s cursor. + +```js +export default function (component) { + const { parentElement, data, setStateValue } = component + + const label = parentElement.querySelector("label") + const input = parentElement.querySelector("input") + if (!label || !input) return + + label.innerText = data.label + + const nextValue = data.value ?? "" + if (input.value !== nextValue) { + input.value = nextValue + } + + input.onkeydown = e => { + if (e.key === "Enter") { + setStateValue("value", e.target.value) + } + } +} +``` + +#### Python wrapper (feed state back down via `data`) + +```python +import streamlit as st + +_COMPONENT = st.components.v2.component( + "interactive_text_input", + html=""" + + + """, + js=JS, # inline JS string from above +) + + +def interactive_text_input(*, label: str, initial_value: str, key: str): + # 1) Read current component state from Session State (if it exists) + component_state = st.session_state.get(key, {}) + + # 2) Compute the value you want the UI to display + value = component_state.get("value", initial_value) + + # 3) Send it down to the frontend via `data` + return _COMPONENT( + key=key, + data={"label": label, "value": value}, + ) + + +KEY = "my_text_input" + +if st.button("Make it say Hello World"): + st.session_state.setdefault(KEY, {})["value"] = "Hello World" + +interactive_text_input(label="Enter something", initial_value="Initial Text", key=KEY) +``` + +### Defaults: when to use `default=...` (and why it fails) + +`default={...}` is optional. Use it when you want Streamlit to initialize missing state keys for a mounted instance. + +Rules: + +- Defaults apply only to **state** keys (not triggers). +- Every key in `default` must have a corresponding `on__change` callback parameter when mounting, or Streamlit raises. + +Pattern: + +```python +result = _COMPONENT( + key=key, + data={"value": value}, + default={"value": value}, + on_value_change=lambda: None, # required if using default["value"] +) +``` + +### Python → JS hydration: initial-only vs true sync + +You’ll see two patterns in the wild: + +- **Initial-only hydration**: JS reads `data.initialX` on first mount only. This is useful for *initialization* but it will **not** reflect later Python changes. +- **True sync (controlled)**: JS reconciles its UI from `data.value` on every render, and only writes when changed. + +Initial-only example (pitfall for sync): + +```js +// If you guard hydration with hasMounted, Python changes won't propagate. +if (typeof data?.initialText !== "undefined" && !hasMountedForKey) { + input.value = String(data.initialText) +} +hasMounted[key] = true +``` + +True sync approach (recommended when Python can update the UI): + +```js +const nextValue = data.value ?? "" +if (input.value !== nextValue) input.value = nextValue +``` + +### Session State timing: don’t mutate after mount + +Streamlit may raise if you modify `st.session_state..` **after** the component with that key has been instantiated in the same run. + +Safe patterns: + +- Update `st.session_state[key][...]` **before** mounting the component (e.g., in a button handler placed above the mount call). +- Or update state in a different run (trigger a rerun after setting state). + +### Troubleshooting checklist + +- **Cursor jumps / typing feels broken**: ensure your JS only assigns `input.value` when it differs from the `data` value. +- **Python updates don’t reflect in UI**: confirm you pass the updated values via `data` every run; avoid initial-only hydration guards if you want true sync. +- **`default` raises**: ensure every default key has a corresponding `on__change` callback parameter. +- **Session state mutation error**: move `st.session_state[key][...] = ...` earlier in the script (before mount), or restructure into a two-run flow (set state then rerun). + diff --git a/.agents/skills/developing-with-streamlit/skills/building-streamlit-custom-components-v2/references/theme-css-variables.md b/.agents/skills/developing-with-streamlit/skills/building-streamlit-custom-components-v2/references/theme-css-variables.md new file mode 100644 index 0000000..66adc52 --- /dev/null +++ b/.agents/skills/developing-with-streamlit/skills/building-streamlit-custom-components-v2/references/theme-css-variables.md @@ -0,0 +1,243 @@ +## Streamlit theme CSS variables for CCv2 components (`--st-*`) + +Streamlit injects a set of `--st-*` CSS custom properties into CCv2 components so your component can match the app’s active theme from within your component CSS (including when `isolate_styles=True` and you’re rendering inside a shadow root). + +**Recommendation:** Prefer `var(--st-…)` over hard-coded colors and sizes. These variables automatically adapt to a user's current Streamlit theme (light/dark/custom), so most components **do not** need separate “dark mode vs light mode” styling. + +## Contents + +- Quick start +- Gotchas: serialization + fallbacks +- Cheat sheet: the 90% variables (intent → `--st-*`) +- Foundation tokens (surfaces, text, borders, shape) +- Typography tokens +- Data display tokens (dataframes/tables) +- Data visualization tokens (chart palettes) +- Semantic/status palette (red/green/etc.) +- Appendix: full variable index (alphabetical) + +### Quick start + +Use them like any CSS variable: + +```css +.card { + background: var(--st-secondary-background-color); + color: var(--st-text-color); + border: 1px solid var(--st-border-color); + border-radius: var(--st-base-radius); +} + +.primaryButton { + background: var(--st-primary-color); + color: var(--st-background-color); + border-radius: var(--st-button-radius); +} +``` + +### Gotchas: serialization rules + safe fallbacks + +These variables originate from Streamlit’s theme object and are serialized into strings: + +- **Strings**: passed through (e.g. `--st-primary-color: #ff4b4b`). +- **Numbers**: stringified (e.g. `--st-base-font-weight: 400`). +- **Booleans**: become `"1"` or `"0"` (e.g. `--st-link-underline`). +- **Arrays**: become a comma-joined string (e.g. `--st-heading-font-sizes: 2.75rem,2.25rem,...`). + - If you need individual items in JS, split on `","`. +- **Missing values (`null` / `undefined`)**: become `unset` so consumers fall back to initial/inherited CSS behavior. + +### Cheat sheet: the 90% variables (intent → `--st-*`) + +Use this section as your starting point. It covers what most components need most of the time. + +| Intent | Variables | +| ------------------------- | ---------------------------------------------------------------------- | +| App/page background | `--st-background-color` | +| Panel/card background | `--st-secondary-background-color` | +| Body text | `--st-text-color` | +| Headings | `--st-heading-color`, `--st-heading-font` | +| Primary accent / emphasis | `--st-primary-color` | +| Links | `--st-link-color`, `--st-link-underline` | +| Borders / dividers | `--st-border-color`, `--st-border-color-light` | +| Widget outline borders | `--st-widget-border-color` | +| Corner radius | `--st-base-radius`, `--st-button-radius` | +| Code blocks | `--st-code-background-color`, `--st-code-text-color`, `--st-code-font` | + +### Foundation tokens (surfaces, text, borders, shape) + +#### Surfaces and content colors + +- `--st-background-color` (page background) +- `--st-secondary-background-color` (panels, cards, containers) +- `--st-text-color` (default text) +- `--st-heading-color` (derived heading color) +- `--st-primary-color` (brand / accent) +- `--st-link-color` (link color) +- `--st-link-underline` (boolean serialized to `"1"` / `"0"`) + +#### Borders and separators + +- `--st-border-color` (default borders/dividers) +- `--st-border-color-light` (derived lighter border) +- `--st-widget-border-color` (widget borders) + +#### Shape (radii) + +- `--st-base-radius` +- `--st-button-radius` + +#### Code block colors + +- `--st-code-background-color` +- `--st-code-text-color` + +### Typography tokens + +#### Font families + +- `--st-font` (body font) +- `--st-heading-font` +- `--st-code-font` + +#### Body sizing and weights + +- `--st-base-font-size` +- `--st-base-font-weight` (number) + +#### Headings (H1–H6) + +Arrays (comma-joined): + +- `--st-heading-font-sizes` (array; typically 6 values for H1–H6) +- `--st-heading-font-weights` (array; typically 6 values for H1–H6) + +Per-level convenience variables: + +- `--st-heading-font-size-1` +- `--st-heading-font-size-2` +- `--st-heading-font-size-3` +- `--st-heading-font-size-4` +- `--st-heading-font-size-5` +- `--st-heading-font-size-6` +- `--st-heading-font-weight-1` (number) +- `--st-heading-font-weight-2` (number) +- `--st-heading-font-weight-3` (number) +- `--st-heading-font-weight-4` (number) +- `--st-heading-font-weight-5` (number) +- `--st-heading-font-weight-6` (number) + +#### Code typography + +- `--st-code-font-size` +- `--st-code-font-weight` (number) + +### Data display tokens (dataframes/tables) + +- `--st-dataframe-border-color` +- `--st-dataframe-header-background-color` + +### Data visualization tokens (chart palettes) + +Chart palette variables are **arrays serialized as comma-joined strings**: + +- `--st-chart-categorical-colors` (discrete series) +- `--st-chart-sequential-colors` (low → high) +- `--st-chart-diverging-colors` (negative ↔ positive around a midpoint) + +If you need the palette values in JS, split on commas: + +```js +export default function (component) { + const { parentElement } = component; + + const host = parentElement.host ?? parentElement; + const raw = getComputedStyle(host) + .getPropertyValue('--st-chart-categorical-colors') + .trim(); + const palette = raw ? raw.split(',') : []; +} +``` + +### Semantic/status palette + +These are best for status UI (badges, alerts, validation messages), not for primary layout surfaces. + +Each “family” typically comes in three variants: + +- **Base**: `--st--color` +- **Background**: `--st--background-color` +- **Text**: `--st--text-color` + +Families: + +- Red: `--st-red-color`, `--st-red-background-color`, `--st-red-text-color` +- Orange: `--st-orange-color`, `--st-orange-background-color`, `--st-orange-text-color` +- Yellow: `--st-yellow-color`, `--st-yellow-background-color`, `--st-yellow-text-color` +- Blue: `--st-blue-color`, `--st-blue-background-color`, `--st-blue-text-color` +- Green: `--st-green-color`, `--st-green-background-color`, `--st-green-text-color` +- Violet: `--st-violet-color`, `--st-violet-background-color`, `--st-violet-text-color` +- Gray: `--st-gray-color`, `--st-gray-background-color`, `--st-gray-text-color` + +### Appendix: full variable index (alphabetical) + +- `--st-background-color` +- `--st-base-font-size` +- `--st-base-font-weight` +- `--st-base-radius` +- `--st-blue-background-color` +- `--st-blue-color` +- `--st-blue-text-color` +- `--st-border-color` +- `--st-border-color-light` +- `--st-button-radius` +- `--st-chart-categorical-colors` +- `--st-chart-diverging-colors` +- `--st-chart-sequential-colors` +- `--st-code-background-color` +- `--st-code-font` +- `--st-code-font-size` +- `--st-code-font-weight` +- `--st-code-text-color` +- `--st-dataframe-border-color` +- `--st-dataframe-header-background-color` +- `--st-font` +- `--st-gray-background-color` +- `--st-gray-color` +- `--st-gray-text-color` +- `--st-green-background-color` +- `--st-green-color` +- `--st-green-text-color` +- `--st-heading-color` +- `--st-heading-font` +- `--st-heading-font-size-1` +- `--st-heading-font-size-2` +- `--st-heading-font-size-3` +- `--st-heading-font-size-4` +- `--st-heading-font-size-5` +- `--st-heading-font-size-6` +- `--st-heading-font-sizes` +- `--st-heading-font-weight-1` +- `--st-heading-font-weight-2` +- `--st-heading-font-weight-3` +- `--st-heading-font-weight-4` +- `--st-heading-font-weight-5` +- `--st-heading-font-weight-6` +- `--st-heading-font-weights` +- `--st-link-color` +- `--st-link-underline` +- `--st-orange-background-color` +- `--st-orange-color` +- `--st-orange-text-color` +- `--st-primary-color` +- `--st-red-background-color` +- `--st-red-color` +- `--st-red-text-color` +- `--st-secondary-background-color` +- `--st-text-color` +- `--st-violet-background-color` +- `--st-violet-color` +- `--st-violet-text-color` +- `--st-widget-border-color` +- `--st-yellow-background-color` +- `--st-yellow-color` +- `--st-yellow-text-color` diff --git a/.agents/skills/developing-with-streamlit/skills/building-streamlit-custom-components-v2/references/troubleshooting.md b/.agents/skills/developing-with-streamlit/skills/building-streamlit-custom-components-v2/references/troubleshooting.md new file mode 100644 index 0000000..6ddbc14 --- /dev/null +++ b/.agents/skills/developing-with-streamlit/skills/building-streamlit-custom-components-v2/references/troubleshooting.md @@ -0,0 +1,121 @@ +## Troubleshooting CCv2 components + +## Contents + +- Packaged assets and manifests (`asset_dir`, component key) +- Renaming / placeholder drift +- Inline strings vs file-backed assets (path heuristic) +- Globs (0 matches or multiple matches) +- Defaults, callbacks, and missing result attributes +- Keys (Python `key=` vs frontend `key`) +- Shadow DOM / `isolate_styles` surprises +- Frontend build (Vite) gotchas +- DOM clobbering (overwriting injected HTML/CSS) + +### Packaged assets and manifests (`asset_dir`, component key) + +#### “Component '' must be declared in pyproject.toml with asset_dir to use file-backed js/css.” + +You passed a **path-like** `js=`/`css=` string (like `index-*.js` or `assets/index-*.js`) but Streamlit can’t find an `asset_dir` for this component key. + +Fix: + +- If you want **inline JS/CSS**, pass a **multi-line** string with the actual code (not a path). +- If you want **packaged assets**, ensure: + - Your wheel includes a `pyproject.toml` with `[[tool.streamlit.component.components]] ... asset_dir = ...` + - You call `st.components.v2.component(".", js="...", css="...")` with the matching fully-qualified key. + +Important context: + +- This error is expected if you test packaged wrappers via plain Python import in some environments. +- Prefer `streamlit run ...` for packaged verification because manifest discovery is part of Streamlit runtime initialization. + +### Inline strings vs file-backed assets (path heuristic) + +CCv2 uses a heuristic: strings that “look like” paths are treated as file references. A multi-line string is always treated as inline content. + +Fix: + +- Prefer triple-quoted multi-line strings for inline `html`/`css`/`js`. +- Avoid single-line minified JS/CSS in `js=`/`css=`; add a newline if you must. + +### Globs (0 matches or multiple matches) + +Globs must match **exactly one** file under `asset_dir`. + +Fix: + +- Clean the build output directory before rebuilding. +- Make your bundler output a predictable `index-.js` / `index-.css` (or `assets/index-...` if you emit into an `assets/` subdir). +- If you started from Streamlit’s `component-template`, run `npm run clean` from your `frontend/` directory to clear the `build/` output so `index-*.js` matches exactly one file. + +### Renamed project/package still shows old template names + +Symptoms: + +- Old names like `streamlit-component-x` / `streamlit_component_x` remain in paths or metadata. +- Imports, manifest component keys, and registration keys no longer align. + +Fix: + +- Rename/update all related surfaces together: + - root `pyproject.toml` project name + - import package folder and `MANIFEST.in` paths + - `[tool.setuptools.packages.find]` and `[tool.setuptools.package-data]` + - in-package manifest (`/pyproject.toml`) + - wrapper registration key (`"."`) + - README/example imports and install commands +- Rebuild frontend and reinstall editable package after rename. + +### Defaults, callbacks, and missing result attributes + +#### `default={...}` doesn’t apply / missing result attributes + +Defaults only apply to **state keys**, and Streamlit expects those keys to be declared via `on__change` callback parameters at mount time. + +Fix: + +- If you pass `default={"value": ...}`, also pass `on_value_change=lambda: None`. +- For triggers, don’t expect defaults; triggers are transient and default to `None`. + +### Keys (Python `key=` vs frontend `key`) + +- Python `key=` is the user-visible Streamlit element key. +- The frontend also receives a `key` string that is **generated by Streamlit** and is not the same as the Python `key` unless you explicitly pass the user key through `data`. + +Fix: + +- If your frontend needs a stable identifier, pass it in `data={"user_key": key, ...}`. + +### Shadow DOM / `isolate_styles` surprises + +#### Shadow DOM (`isolate_styles=True`) surprises + +With `isolate_styles=True` (default): + +- Your component is mounted in a **shadow root**. +- `parentElement` is a `ShadowRoot`. +- Global CSS (like Tailwind injected into the document) won’t automatically style your component. + +Fix: + +- Keep `isolate_styles=True` for safety and use CSS variables and component-local styles. +- Use `isolate_styles=False` only when you intentionally need global styling behavior. + +### Frontend build gotchas (Vite) + +If you deviate from the template’s Vite config (or you’re wiring Vite into an existing repo), these are the common footguns: + +- **Missing `base: "./"`**: relative asset URLs can break when served from Streamlit’s component URL path. +- **Stale build artifacts**: Vite outputs hashed filenames; if you keep old builds around, `index-*.js` can match multiple files. Clean the build dir before rebuilding. + +### DOM clobbering (overwriting injected HTML/CSS) + +#### Accidentally overwriting your component DOM + +If you directly set `parentElement.innerHTML = ...`, you can overwrite the HTML/CSS that Streamlit injected from your `html=`/`css=` arguments. + +Fix: + +- Prefer `querySelector` + modifying children. +- If you need dynamic HTML, create a new child element and set **that** element’s `innerHTML`. diff --git a/.agents/skills/developing-with-streamlit/skills/building-streamlit-dashboards/SKILL.md b/.agents/skills/developing-with-streamlit/skills/building-streamlit-dashboards/SKILL.md new file mode 100644 index 0000000..ae8d038 --- /dev/null +++ b/.agents/skills/developing-with-streamlit/skills/building-streamlit-dashboards/SKILL.md @@ -0,0 +1,147 @@ +--- +name: building-streamlit-dashboards +description: Building dashboards in Streamlit. Use when creating KPI displays, metric cards, or data-heavy layouts. Covers borders, cards, responsive layouts, and dashboard composition. +license: Apache-2.0 +--- + +# Streamlit dashboards + +Compose metrics, charts, and data into clean dashboard layouts. + +## Cards with borders + +Use `border=True` to create visual cards. Supported on `st.container`, `st.metric`, `st.columns`, and `st.form`: + +```python +# Container card +with st.container(border=True): + st.subheader("Sales Overview") + st.line_chart(sales_data) + +# Metric card +st.metric("Revenue", "$1.2M", "+12%", border=True) + +# Column cards +for col in st.columns(3, border=True): + with col: + st.metric("Users", "1.2k") +``` + +## Card labels + +Add context to cards with headers or bold text: + +```python +# With subheader +with st.container(border=True): + st.subheader("Monthly Trends") + st.line_chart(data) + +# With bold label +with st.container(border=True): + st.markdown("**Top Products**") + st.dataframe(top_products) +``` + +## KPI rows + +Use horizontal containers for responsive metric rows: + +```python +with st.container(horizontal=True): + st.metric("Revenue", "$1.2M", "-7%", border=True) + st.metric("Users", "762k", "+12%", border=True) + st.metric("Orders", "1.4k", "+5%", border=True) +``` + +Horizontal containers wrap on smaller screens. Prefer them over `st.columns` for metric rows. + +## Metrics with sparklines + +Add trend context with `chart_data`: + +```python +weekly_values = [700, 720, 715, 740, 762, 755, 780] + +st.metric( + "Active Users", + "780k", + "+3.2%", + border=True, + chart_data=weekly_values, + chart_type="line", # or "bar" +) +``` + +Sparklines show y-values only—use for evenly-spaced data like daily/weekly snapshots. + +## Dashboard layout + +Combine cards into a dashboard: + +```python +# KPI row +with st.container(horizontal=True): + st.metric("Revenue", "$1.2M", "-7%", border=True, chart_data=rev_trend, chart_type="line") + st.metric("Users", "762k", "+12%", border=True, chart_data=user_trend, chart_type="line") + st.metric("Orders", "1.4k", "+5%", border=True, chart_data=order_trend, chart_type="bar") + +# Charts row +col1, col2 = st.columns(2) +with col1: + with st.container(border=True): + st.subheader("Revenue by Region") + st.bar_chart(region_data, x="region", y="revenue") + +with col2: + with st.container(border=True): + st.subheader("Monthly Trend") + st.line_chart(monthly_data, x="month", y="value") + +# Data table +with st.container(border=True): + st.subheader("Recent Orders") + st.dataframe(orders_df, hide_index=True) +``` + +## Sidebar filters + +Put filters in the sidebar to maximize dashboard space: + +```python +with st.sidebar: + date_range = st.date_input("Date range", value=(start, end)) + region = st.multiselect("Region", regions, default=regions) + +# Main area is all dashboard content +``` + +## Dashboard templates + +Ready-to-use dashboard templates are available in `templates/apps/`: + +| Template | Features | +|----------|----------| +| `dashboard-metrics` | Metric cards with sparklines, date filtering, focus mode | +| `dashboard-metrics-snowflake` | Same as above, with Snowflake connection | +| `dashboard-companies` | Company comparison, filterable data tables | +| `dashboard-compute` | `@st.fragment` for independent updates, popover filters | +| `dashboard-compute-snowflake` | Same as above, with Snowflake connection | +| `dashboard-feature-usage` | Feature adoption tracking, trend analysis | +| `dashboard-seattle-weather` | Weather data visualization | +| `dashboard-stock-peers` | Stock peer comparison | +| `dashboard-stock-peers-snowflake` | Same as above, with Snowflake connection | + +Each template uses synthetic data that can be replaced with real queries. See `templates/apps/README.md` for setup instructions. + +## Related skills + +- `using-streamlit-layouts`: Columns, containers, tabs, dialogs +- `displaying-streamlit-data`: Charts, dataframes, column configuration +- `optimizing-streamlit-performance`: Caching and fragments for heavy dashboards + +## References + +- [st.container](https://docs.streamlit.io/develop/api-reference/layout/st.container) +- [st.metric](https://docs.streamlit.io/develop/api-reference/data/st.metric) +- [st.columns](https://docs.streamlit.io/develop/api-reference/layout/st.columns) diff --git a/.agents/skills/developing-with-streamlit/skills/building-streamlit-multipage-apps/SKILL.md b/.agents/skills/developing-with-streamlit/skills/building-streamlit-multipage-apps/SKILL.md new file mode 100644 index 0000000..025b7a9 --- /dev/null +++ b/.agents/skills/developing-with-streamlit/skills/building-streamlit-multipage-apps/SKILL.md @@ -0,0 +1,218 @@ +--- +name: building-streamlit-multipage-apps +description: Building multi-page Streamlit apps. Use when creating apps with multiple pages, setting up navigation, or managing state across pages. +license: Apache-2.0 +--- + +# Streamlit multi-page apps + +Structure and navigation for apps with multiple pages. + +## Directory structure + +``` +streamlit_app.py # Main entry point +app_pages/ + home.py + analytics.py + settings.py +``` + +**Important:** Name your pages directory `app_pages/` (not `pages/`). Using `pages/` conflicts with Streamlit's old auto-discovery API and can cause unexpected behavior. + +## Main module + +```python +# streamlit_app.py +import streamlit as st + +# Initialize global state (shared across pages) +if "api_client" not in st.session_state: + st.session_state.api_client = init_api_client() + +# Define navigation +page = st.navigation([ + st.Page("app_pages/home.py", title="Home", icon=":material/home:"), + st.Page("app_pages/analytics.py", title="Analytics", icon=":material/bar_chart:"), + st.Page("app_pages/settings.py", title="Settings", icon=":material/settings:"), +]) + +# App-level UI runs before page content +# Useful for shared elements like titles +st.title(f"{page.icon} {page.title}") + +page.run() +``` + +**Note:** When you handle titles in `streamlit_app.py`, individual pages should NOT use `st.title` again. + +## Navigation position + +**Few pages (3-7) → Top navigation:** + +```python +page = st.navigation([...], position="top") +``` + +Creates a clean horizontal menu. Great for simple apps. Sections are supported too—they appear as dropdowns in the top nav. + +**Many pages or nested sections → Sidebar:** + +```python +page = st.navigation({ + "Main": [ + st.Page("app_pages/home.py", title="Home"), + st.Page("app_pages/analytics.py", title="Analytics"), + ], + "Admin": [ + st.Page("app_pages/settings.py", title="Settings"), + st.Page("app_pages/users.py", title="Users"), + ], +}, position="sidebar") +``` + +**Mixed: Some pages ungrouped:** + +Use an empty string key `""` for pages that shouldn't be in a section. These ungrouped pages always appear first, before any named groups. Put all ungrouped pages in a single `""` key: + +```python +page = st.navigation({ + "": [ + st.Page("app_pages/home.py", title="Home"), + st.Page("app_pages/about.py", title="About"), + ], + "Analytics": [ + st.Page("app_pages/dashboard.py", title="Dashboard"), + st.Page("app_pages/reports.py", title="Reports"), + ], +}, position="top") +``` + +## Page modules + +```python +# app_pages/analytics.py +import streamlit as st + +# Access global state +api = st.session_state.api_client +user = st.session_state.user + +# Page-specific content (title is handled in streamlit_app.py) +data = api.fetch_analytics(user.id) +st.line_chart(data) +``` + +## Global state + +Initialize state in the main module only if it's needed across multiple pages: + +```python +# streamlit_app.py +st.session_state.api = init_client() +st.session_state.user = get_user() +st.session_state.settings = load_settings() +``` + +**Tip:** Use `st.session_state.setdefault("key", default_value)` to initialize values only if they don't exist. + +**Why main module (for global state):** +- Runs before every page +- Ensures state is initialized +- Single source of truth + +## Page-specific state + +Use prefixed keys for page-specific state: + +```python +# app_pages/analytics.py +if "analytics_date_range" not in st.session_state: + st.session_state.analytics_date_range = default_range() +``` + +## Share elements between pages + +Put shared UI in the entrypoint (before `page.run()`) so it appears on all pages: + +```python +# streamlit_app.py +import streamlit as st + +pages = [...] +page = st.navigation(pages) + +# Shared title +st.title(f"{page.icon} {page.title}") + +# Shared sidebar widgets +with st.sidebar: + st.selectbox("Theme", ["Light", "Dark"]) + +page.run() +``` + +## Programmatic navigation + +Navigate to another page programmatically: + +```python +if st.button("Go to Settings"): + st.switch_page("app_pages/settings.py") +``` + +Create navigation links with `st.page_link`: + +```python +st.page_link("app_pages/home.py", label="Home", icon=":material/home:") +st.page_link("https://example.com", label="External", icon=":material/open_in_new:") +``` + +> **Note:** Prefer `st.navigation` over `st.page_link` for standard navigation. Do not use `st.page_link` to recreate the nav bar you get with `st.navigation`. Only use `st.page_link` when linking to pages from somewhere other than the sidebar, or when building a more complex navigation menu. + +## Conditional pages + +Show different pages based on user role, authentication, or any other condition by building the pages list dynamically: + +```python +# streamlit_app.py +import streamlit as st + +pages = [st.Page("app_pages/home.py", title="Home", icon=":material/home:")] + +if st.user.is_logged_in: + pages.append(st.Page("app_pages/dashboard.py", title="Dashboard", icon=":material/bar_chart:")) + +if st.session_state.get("is_admin"): + pages.append(st.Page("app_pages/admin.py", title="Admin", icon=":material/settings:")) + +page = st.navigation(pages) +page.run() +``` + +Common conditions for showing/hiding pages: +- `st.user.is_logged_in` for authenticated users +- `st.session_state` flags (roles, permissions, feature flags) +- Environment variables or secrets +- Time-based access (e.g., beta features) + +## Imports from pages + +When importing from page files in `app_pages/`, always import from the root directory perspective: + +```python +# app_pages/dashboard.py - GOOD +from utils.data import load_sales_data + +# app_pages/dashboard.py - BAD (don't use relative imports) +from ..utils.data import load_sales_data +``` + +## References + +- [Multipage apps docs](https://docs.streamlit.io/develop/concepts/multipage-apps) +- [st.navigation](https://docs.streamlit.io/develop/api-reference/navigation/st.navigation) +- [st.Page](https://docs.streamlit.io/develop/api-reference/navigation/st.page) +- [st.session_state](https://docs.streamlit.io/develop/api-reference/caching-and-state/st.session_state) +- [st.switch_page](https://docs.streamlit.io/develop/api-reference/navigation/st.switch_page) +- [st.page_link](https://docs.streamlit.io/develop/api-reference/widgets/st.page_link) diff --git a/.agents/skills/developing-with-streamlit/skills/choosing-streamlit-selection-widgets/SKILL.md b/.agents/skills/developing-with-streamlit/skills/choosing-streamlit-selection-widgets/SKILL.md new file mode 100644 index 0000000..aa89880 --- /dev/null +++ b/.agents/skills/developing-with-streamlit/skills/choosing-streamlit-selection-widgets/SKILL.md @@ -0,0 +1,139 @@ +--- +name: choosing-streamlit-selection-widgets +description: Choosing the right Streamlit selection widget. Use when deciding between radio buttons, selectbox, segmented control, pills, or other option selection widgets. Helps pick the right widget for the number of options and selection type. +license: Apache-2.0 +--- + +# Streamlit selection widgets + +The right selection widget for the job. Streamlit has evolved—many old patterns are now anti-patterns. + +## When to use what + +Use `st.segmented_control` or `st.pills` when you want all options visible at once. Use `st.selectbox` or `st.multiselect` when options should be hidden in a dropdown. + +| Widget | Best For | +|--------|----------| +| `st.segmented_control` | 2-5 options, single select, all visible | +| `st.pills` | 2-5 options, multi-select, all visible | +| `st.selectbox` | Many options, single select, dropdown | +| `st.multiselect` | Many options, multi-select, dropdown | + +## Segmented control (options visible, single select) + +```python +# BAD +status = st.radio("Status", ["Draft", "Published"], horizontal=True) + +# GOOD +status = st.segmented_control("Status", ["Draft", "Published"]) +``` + +For vertical layouts, `st.radio(..., horizontal=False)` is still a great choice. + +Cleaner, more modern look than horizontal radio buttons. + +## Pills (options visible, multi-select) + +```python +# Multi-select with few options +selected = st.pills( + "Tags", + ["Python", "SQL", "dbt", "Streamlit"], + selection_mode="multi" +) +``` + +Can also be used to mimic an "example" widget, especially with `label_visibility="collapsed"`: +```python +st.pills("Examples", ["Show me sales data", "Top customers"], label_visibility="collapsed") +``` + +More visual and easier to use than `st.multiselect` for small option sets. + +## Selectbox (many options, single select) + +```python +country = st.selectbox( + "Select a country", + ["USA", "UK", "Canada", "Germany", "France", ...] +) +``` + +Dropdowns scale better than radio/pills for long lists. + +## Multiselect (many options, multi-select) + +```python +countries = st.multiselect( + "Select countries", + ["USA", "UK", "Canada", "Germany", "France", ...] +) +``` + +## Toggle vs checkbox + +Use `st.toggle` for settings that trigger changes in the app. Reserve `st.checkbox` for forms. + +```python +# GOOD: Toggle for app settings +dark_mode = st.toggle("Dark mode") +show_advanced = st.toggle("Show advanced options") + +# GOOD: Checkbox in forms +with st.form("signup"): + agree = st.checkbox("I agree to the terms") + st.form_submit_button("Sign up") +``` + +## Forms with border=False + +Remove the default form border for cleaner inline forms. Keep the border for longer forms where visual grouping helps. + +```python +# Inline form without border +with st.form(key="add_item", border=False): + with st.container(horizontal=True, vertical_alignment="bottom"): + st.text_input("New item", label_visibility="collapsed", placeholder="Add item") + st.form_submit_button("Add", icon=":material/add:") + +# Longer form - keep the border for visual grouping +with st.form("signup"): + st.text_input("Name") + st.text_input("Email") + st.selectbox("Role", ["Admin", "User"]) + st.form_submit_button("Submit") +``` + +## Custom options in selectbox and multiselect + +Allow users to add their own options with `accept_new_options`: + +```python +# Works with multiselect +tickers = st.multiselect( + "Stock tickers", + options=["AAPL", "MSFT", "GOOGL", "NVDA"], + default=["AAPL"], + accept_new_options=True, + placeholder="Choose stocks or type your own" +) + +# Also works with selectbox +country = st.selectbox( + "Country", + options=["USA", "UK", "Canada"], + accept_new_options=True, + placeholder="Select or type a country" +) +``` + +## References + +- [st.segmented_control](https://docs.streamlit.io/develop/api-reference/widgets/st.segmented_control) +- [st.pills](https://docs.streamlit.io/develop/api-reference/widgets/st.pills) +- [st.selectbox](https://docs.streamlit.io/develop/api-reference/widgets/st.selectbox) +- [st.multiselect](https://docs.streamlit.io/develop/api-reference/widgets/st.multiselect) +- [st.toggle](https://docs.streamlit.io/develop/api-reference/widgets/st.toggle) +- [st.checkbox](https://docs.streamlit.io/develop/api-reference/widgets/st.checkbox) +- [st.form](https://docs.streamlit.io/develop/api-reference/execution-flow/st.form) diff --git a/.agents/skills/developing-with-streamlit/skills/connecting-streamlit-to-snowflake/SKILL.md b/.agents/skills/developing-with-streamlit/skills/connecting-streamlit-to-snowflake/SKILL.md new file mode 100644 index 0000000..c451c2c --- /dev/null +++ b/.agents/skills/developing-with-streamlit/skills/connecting-streamlit-to-snowflake/SKILL.md @@ -0,0 +1,188 @@ +--- +name: connecting-streamlit-to-snowflake +description: Connecting Streamlit apps to Snowflake. Use when setting up database connections, managing secrets, or querying Snowflake from a Streamlit app. +license: Apache-2.0 +--- + +# Streamlit Snowflake connection + +Connect your Streamlit app to Snowflake the right way. + +## Use st.connection + +Always use `st.connection("snowflake")` instead of raw connectors. + +```python +import streamlit as st + +conn = st.connection("snowflake") + +# Query data +df = conn.query("SELECT * FROM my_table LIMIT 100") +st.dataframe(df) +``` + +**Why st.connection:** +- Automatic connection pooling +- Built-in caching +- Handles reconnection +- Works with st.secrets + +## Caller's rights connection (Streamlit 1.53+) + +For apps running in Snowflake, use caller's rights to run queries with the viewer's permissions instead of the app owner's: + +```python +conn = st.connection("snowflake", type="snowflake-callers-rights") +``` + +This is useful when: +- Different users should see different data based on their Snowflake roles +- You want row-level security to apply based on the viewer +- You don't want the app to have elevated permissions + +## Cached queries + +Use the built-in `ttl` parameter to cache query results: + +```python +from datetime import timedelta + +conn = st.connection("snowflake") + +# Cache for 10 minutes +df = conn.query("SELECT * FROM metrics", ttl=timedelta(minutes=10)) + +# Cache for 1 hour +df = conn.query("SELECT * FROM reference_data", ttl=3600) +``` + +## Configure with st.secrets + +Store credentials in `.streamlit/secrets.toml` (never commit this file). + +**CRITICAL**: Derive the `account` and `host` values from the user's Snowflake CLI connection config. Run `snow connection list` and use the exact values. A wrong `account` will redirect to the wrong login page. + +```toml +# .streamlit/secrets.toml +[connections.snowflake] +account = "ORGNAME-ACCTNAME" # from `snow connection list` +host = "myaccount.snowflakecomputing.com" # from `snow connection list` (include if present) +user = "your_user" +authenticator = "externalbrowser" +warehouse = "your_warehouse" +database = "your_database" +schema = "your_schema" +``` + +Add to `.gitignore`: +``` +.streamlit/secrets.toml +``` + +## Parameterized queries + +Use parameters to prevent SQL injection: + +```python +conn = st.connection("snowflake") + +# Safe: parameterized +df = conn.query( + "SELECT * FROM users WHERE region = :region", + params={"region": selected_region} +) + +# UNSAFE: string formatting - don't do this +# df = conn.query(f"SELECT * FROM users WHERE region = '{selected_region}'") +``` + +## Write data + +Use the session for write operations: + +```python +conn = st.connection("snowflake") +session = conn.session() + +# Write a dataframe +session.write_pandas(df, "MY_TABLE", auto_create_table=True) + +# Execute statements +session.sql("INSERT INTO logs VALUES (:ts, :msg)", params={...}).collect() +``` + +## Multiple connections + +Define multiple connections in secrets: + +```toml +# .streamlit/secrets.toml +[connections.snowflake] +account = "prod_account" +# ... prod credentials + +[connections.snowflake_staging] +account = "staging_account" +# ... staging credentials +``` + +```python +prod_conn = st.connection("snowflake") +staging_conn = st.connection("snowflake_staging") +``` + +## Chat with Cortex + +Build a chat interface using Snowflake Cortex LLMs: + +```python +import streamlit as st +from snowflake.cortex import complete + +st.set_page_config(page_title="AI Assistant", page_icon=":sparkles:") + +if "messages" not in st.session_state: + st.session_state.messages = [] + +for msg in st.session_state.messages: + with st.chat_message(msg["role"]): + st.write(msg["content"]) + +if prompt := st.chat_input("Ask anything"): + st.session_state.messages.append({"role": "user", "content": prompt}) + + with st.chat_message("user"): + st.write(prompt) + + with st.chat_message("assistant"): + response = st.write_stream( + complete( + "claude-3-5-sonnet", + prompt, + session=st.connection("snowflake").session(), + stream=True, + ) + ) + + st.session_state.messages.append({"role": "assistant", "content": response}) +``` + +See `building-streamlit-chat-ui` for more chat patterns (avatars, suggestions, history management). + +## Python 3.12+ dependency caveat + +`streamlit[snowflake]` gates `snowflake-connector-python` on `python_version < "3.12"`. On Python 3.12+, the connector is silently skipped and you get `No module named 'snowflake'` at runtime. Always add `snowflake-connector-python>=3.3.0` as an explicit dependency in `pyproject.toml`: + +```toml +dependencies = [ + "snowflake-connector-python>=3.3.0", + "streamlit[snowflake]>=1.54.0", +] +``` + +## References + +- [st.connection](https://docs.streamlit.io/develop/api-reference/connections/st.connection) +- [SnowflakeConnection](https://docs.streamlit.io/develop/api-reference/connections/st.connections.snowflakeconnection) +- [st.secrets](https://docs.streamlit.io/develop/api-reference/connections/st.secrets) diff --git a/.agents/skills/developing-with-streamlit/skills/creating-streamlit-themes/SKILL.md b/.agents/skills/developing-with-streamlit/skills/creating-streamlit-themes/SKILL.md new file mode 100644 index 0000000..da348da --- /dev/null +++ b/.agents/skills/developing-with-streamlit/skills/creating-streamlit-themes/SKILL.md @@ -0,0 +1,486 @@ +--- +name: creating-streamlit-themes +description: Creating and customizing Streamlit themes. Use when changing app colors, fonts, or appearance, or aligning apps to brand guidelines. Covers config.toml configuration, design principles, and CSS avoidance. +license: Apache-2.0 +--- + +# Creating Streamlit themes + +Build professional, brand-aligned themes using `.streamlit/config.toml`. This skill covers design principles and complete configuration for polished, cohesive themes. + +## Theme file setup + +Theme options go in Streamlit's `config.toml` under the `[theme]` section: + +## Theme inheritance + +Start from a built-in theme or external file: + +```toml +[theme] +base = "light" # or "dark" +# base = "./my-base-theme.toml" # Local file +# base = "https://example.com/theme.toml" # Remote URL +``` + +When using `base`, you only need to override the values you want to change. Theme files referenced via `base` can only contain a single `[theme]` section—`[theme.light]` and `[theme.dark]` variants are not supported in external theme files. + +## Color configuration + +### Theme colors + +```toml +[theme] +primaryColor = "#0969da" # Buttons, links, active elements +backgroundColor = "#ffffff" # Main content background +secondaryBackgroundColor = "#f6f8fa" # Widget backgrounds, code blocks +textColor = "#1F2328" # Body text + +# Optional refinements +linkColor = "#0969da" # Markdown links (defaults to primaryColor) +codeTextColor = "#1F2328" # Inline code text +codeBackgroundColor = "#f6f8fa" # Code block background +borderColor = "#d0d7de" # Widget borders +``` + +**Design principle:** Choose a `primaryColor` dark enough to contrast with white text. Streamlit renders the text of primary buttons white against the primary color. + +### Color palette + +Define semantic colors for status indicators, markdown text coloring, and sparklines: + +```toml +[theme] +redColor = "#cf222e" +orangeColor = "#bf8700" +yellowColor = "#dbab09" +greenColor = "#1a7f37" +blueColor = "#0969da" +violetColor = "#8250df" +grayColor = "#57606a" +``` + +Each color supports background and text variants (auto-derived if not set): + +```toml +[theme] +greenColor = "#1a7f37" +greenBackgroundColor = "#dafbe1" # Light tint for badges +greenTextColor = "#116329" # Darkened for readability +``` + +### Chart colors + +Define colors for Plotly, Altair, and Vega-Lite charts: + +```toml +[theme] +# Categorical data (bars, pie slices, series) +chartCategoricalColors = ["#0969da", "#1a7f37", "#bf3989", "#8250df", "#cf222e", "#bf8700", "#57606a"] + +# Sequential/gradient data (heatmaps) - exactly 10 colors required +chartSequentialColors = ["#f0f6fc", "#c8e1ff", "#79c0ff", "#58a6ff", "#388bfd", "#1f6feb", "#1158c7", "#0d419d", "#0a3069", "#04244a"] +``` + +### Dataframe styling + +```toml +[theme] +dataframeBorderColor = "#d0d7de" +dataframeHeaderBackgroundColor = "#f6f8fa" +``` + +Ensure `textColor` is readable against `dataframeHeaderBackgroundColor`—headers use the main text color. + +## Typography + +### Font families + +Use built-in fonts, load from Google Fonts, or define custom fonts from font files (see below): + +```toml +[theme] +# Built-in options +font = "sans-serif" # or "serif" or "monospace" + +# Google Fonts +font = "Inter:https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" + +# Font with spaces in name +font = "'IBM Plex Sans':https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&display=swap" +``` + +### Self-hosting custom fonts + +Use `[[theme.fontFaces]]` tables to load fonts via Streamlit's static file serving. Font files must be placed in a `static/` directory and served through the app—they cannot be arbitrary local file paths. + +**Before adding fonts to config.toml:** Verify the font files exist in the static directory. + +```toml +[[theme.fontFaces]] +family = "CustomFont" +url = "app/static/CustomFont-Regular.woff2" +weight = 400 + +[[theme.fontFaces]] +family = "CustomFont" +url = "app/static/CustomFont-Bold.woff2" +weight = 700 + +[theme] +font = "CustomFont" +``` + +**Attributes:** `family` (name), `url` (path to OTF/TTF/WOFF/WOFF2), `weight` (400, "200 800", or "bold"), `style` ("normal"/"italic"/"oblique"), `unicodeRange` (e.g., "U+0000-00FF"). + +Changes to `fontFaces` require a server restart. + +### Heading and code fonts + +```toml +[theme] +headingFont = "Inter:https://fonts.googleapis.com/css2?family=Inter:wght@600;700&display=swap" +codeFont = "'JetBrains Mono':https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap" +``` + +### Font sizing and weight + +```toml +[theme] +baseFontSize = 14 # Root size in pixels (default: 16) +baseFontWeight = 400 # Normal weight +codeFontSize = "0.875rem" # Relative to base, or use "13px" +codeFontWeight = 400 + +# Heading hierarchy (h1 through h6), or use a single value for all +headingFontSizes = ["32px", "24px", "20px", "16px", "14px", "12px"] +headingFontWeights = [600, 600, 600, 500, 500, 500] +``` + +### Link styling + +```toml +[theme] +linkUnderline = false # Remove underlines for cleaner look +``` + +## Border and radius + +```toml +[theme] +baseRadius = "8px" # All components (none/small/medium/large/full/px/rem) +buttonRadius = "8px" # Buttons specifically (defaults to baseRadius) +showWidgetBorder = true # Show borders on unfocused widgets +showSidebarBorder = true # Show divider between sidebar and content +``` + +**Radius keywords:** `"none"` (0), `"small"` (4px), `"medium"` (8px), `"large"` (12px), `"full"` (pill shape). + +## Sidebar customization + +Style the sidebar independently: + +```toml +[theme.sidebar] +backgroundColor = "#f6f8fa" +secondaryBackgroundColor = "#eaeef2" +codeBackgroundColor = "#eaeef2" +textColor = "#1F2328" +borderColor = "#d0d7de" +primaryColor = "#0969da" # Active elements in sidebar +``` + +## Light and dark modes + +Define separate themes for each mode: + +```toml +[theme.light] +primaryColor = "#0969da" +backgroundColor = "#ffffff" +secondaryBackgroundColor = "#f6f8fa" +textColor = "#1F2328" + +[theme.dark] +primaryColor = "#58a6ff" +backgroundColor = "#0d1117" +secondaryBackgroundColor = "#161b22" +textColor = "#e6edf3" + +[theme.light.sidebar] +backgroundColor = "#f6f8fa" + +[theme.dark.sidebar] +backgroundColor = "#010409" +``` + +Users can switch between modes in the app settings menu only if both `[theme.light]` and `[theme.dark]` are defined. A custom theme with just `[theme]` locks the app to a single mode. + +## Detecting current theme + +Use `st.context.theme.base` to adapt your app to the active theme. Useful for: + +- Adjusting specific chart colors for better contrast +- Swapping logos or images (e.g., dark logo on light, light logo on dark) +- Styling third-party components that don't auto-adapt +- Applying conditional CSS or custom styling + +```python +if st.context.theme.base == "dark": + # Do something for dark mode +``` + +## Design principles + +### Color contrast + +Ensure WCAG AA compliance (4.5:1 ratio for text): +- Light themes: Dark text (#1F2328) on light backgrounds (#ffffff) +- Dark themes: Light text (#e6edf3) on dark backgrounds (#0d1117) +- Primary colors must contrast with white button text + +### Color harmony + +Build cohesive palettes using these approaches: + +**Monochromatic:** Single hue with varying lightness (e.g., shadcn's zinc grays) +```toml +primaryColor = "#18181B" +textColor = "#09090B" +borderColor = "#E4E4E7" +grayColor = "#71717A" +``` + +**Brand accent:** Neutral base with one brand color (e.g., Stripe's purple) +```toml +primaryColor = "#635bff" # Brand purple +backgroundColor = "#ffffff" +textColor = "#425466" # Neutral gray +``` + +**Complementary:** Brand primary with supporting accent colors +```toml +primaryColor = "#29B5E8" # Brand blue (Snowflake) +textColor = "#11567F" # Darker blue for text +greenColor = "#36B37E" # Success states +redColor = "#DE350B" # Error states +``` + +### Typography guidelines + +- **Body text:** 14-16px, weight 400 +- **Headings:** Decreasing scale from h1 (28-40px) to h6 (12-14px) +- **Code:** Monospace font, slightly smaller than body (0.85-0.875rem) +- **Font pairing:** Use the same font for body and headings for consistency, or pair complementary fonts (e.g., serif headings with sans-serif body). Code should always use a distinct monospace font. + +### Visual hierarchy + +Create depth with background layers: +``` +Main content: #ffffff (lightest) +Secondary elements: #f6f8fa (slightly darker) +Sidebar: #f6f8fa or contrasting brand color +Code blocks: #f6f8fa (matches secondary or distinct) +``` + +## Example: Snowflake brand theme + +Clean, professional theme with brand blue accents: + +```toml +[theme] +primaryColor = "#29B5E8" +backgroundColor = "#ffffff" +secondaryBackgroundColor = "#f4f9fc" +codeBackgroundColor = "#e8f4f8" +textColor = "#11567F" +linkColor = "#29B5E8" +borderColor = "#d0e8f2" +showWidgetBorder = true +showSidebarBorder = true +baseRadius = "8px" +buttonRadius = "8px" + +font = "'Inter':https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" +codeFont = "'JetBrains Mono':https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap" +codeFontSize = "13px" +codeTextColor = "#11567F" +baseFontSize = 14 +baseFontWeight = 400 +headingFontSizes = ["32px", "24px", "20px", "16px", "14px", "12px"] +headingFontWeights = [600, 600, 600, 500, 500, 500] +linkUnderline = false + +chartCategoricalColors = ["#29B5E8", "#11567F", "#71C8E5", "#174D6A", "#A5DDF2", "#0E4D6B", "#52B8D9"] + +blueColor = "#29B5E8" +greenColor = "#36B37E" +yellowColor = "#FFAB00" +redColor = "#DE350B" +violetColor = "#6554C0" + +dataframeBorderColor = "#d0e8f2" +dataframeHeaderBackgroundColor = "#e8f4f8" + +[theme.sidebar] +backgroundColor = "#11567F" +secondaryBackgroundColor = "#174D6A" +codeBackgroundColor = "#0E4D6B" +textColor = "#ffffff" +borderColor = "#1E6D94" +``` + +## Example: VS Code dark theme + +Developer-focused dark theme with syntax-inspired colors: + +```toml +[theme] +base = "dark" +primaryColor = "#0078d4" +backgroundColor = "#1e1e1e" +secondaryBackgroundColor = "#252526" +codeBackgroundColor = "#1e1e1e" +textColor = "#cccccc" +linkColor = "#3794ff" +borderColor = "#3c3c3c" +showWidgetBorder = true +showSidebarBorder = true +baseRadius = "4px" +buttonRadius = "4px" + +font = "'Segoe UI', 'Open Sans':https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700&display=swap" +codeFont = "'Fira Code':https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500&display=swap" +codeFontSize = "13px" +codeTextColor = "#d4d4d4" +baseFontSize = 14 +baseFontWeight = 400 +headingFontSizes = ["28px", "22px", "18px", "16px", "14px", "12px"] +headingFontWeights = [600, 600, 600, 600, 600, 600] +linkUnderline = false + +chartCategoricalColors = ["#0078d4", "#4ec9b0", "#dcdcaa", "#ce9178", "#c586c0", "#569cd6", "#6a9955"] + +blueColor = "#569cd6" +greenColor = "#6a9955" +yellowColor = "#dcdcaa" +orangeColor = "#ce9178" +violetColor = "#c586c0" + +[theme.sidebar] +backgroundColor = "#252526" +secondaryBackgroundColor = "#333333" +codeBackgroundColor = "#1e1e1e" +borderColor = "#3c3c3c" +``` + +## Common mistakes + +### Primary color too light + +```toml +# BAD: White text on yellow is unreadable +primaryColor = "#FFEB3B" + +# GOOD: Use a darker shade +primaryColor = "#F59E0B" +``` + +### Insufficient contrast + +```toml +# BAD: Light gray text on white +textColor = "#CCCCCC" +backgroundColor = "#FFFFFF" + +# GOOD: Dark text on light background +textColor = "#1F2328" +backgroundColor = "#FFFFFF" +``` + +### Mismatched backgrounds + +```toml +# BAD: Secondary lighter than primary +backgroundColor = "#f6f8fa" +secondaryBackgroundColor = "#ffffff" + +# GOOD: Secondary should be darker/distinct +backgroundColor = "#ffffff" +secondaryBackgroundColor = "#f6f8fa" +``` + +### Forgetting sidebar contrast + +When using a dark sidebar with a light main section, adjust all sidebar colors—not just `textColor`: + +```toml +# BAD: Only changed backgroundColor +[theme.sidebar] +backgroundColor = "#11567F" + +# GOOD: Adjust all colors for dark sidebar +[theme.sidebar] +backgroundColor = "#11567F" +secondaryBackgroundColor = "#174D6A" +textColor = "#ffffff" +borderColor = "#1E6D94" +... +``` + +## IMPORTANT: No custom CSS unless explicitly requested + +**DO NOT use custom CSS or HTML for theming.** This includes: +- `st.markdown(..., unsafe_allow_html=True)` with `""") +``` + +**Never use CSS for theming (colors, backgrounds, fonts) unless explicitly asked. Use config.toml instead.** + +## Development workflow + +Most theme options update live after saving `config.toml` and rerunning. Font-related options (`fontFaces`) require a server restart. + +Test your theme with: buttons (primary contrast), forms (borders, focus), dataframes (headers), code blocks, charts, and sidebar. + +## Theme templates + +Ready-to-use themes with bundled fonts are available in `templates/themes/`: + +| Theme | Base | Primary Color | Fonts | +|-------|------|---------------|-------| +| **snowflake** | Light | `#29B5E8` (cyan) | Inter, JetBrains Mono | +| **dracula** | Dark | `#BD93F9` (purple) | Fira Sans, JetBrains Mono | +| **nord** | Dark | `#88C0D0` (frost blue) | Inter, JetBrains Mono | +| **stripe** | Light | `#635BFF` (indigo) | Inter, Source Code Pro | +| **solarized-light** | Light | `#268BD2` (blue) | Source Sans 3, Source Code Pro | +| **spotify** | Dark | `#1DB954` (green) | Inter, Fira Code | +| **github** | Light | `#0969DA` (blue) | Inter, JetBrains Mono | +| **minimal** | Dark | `#6366f1` (indigo) | Inter, JetBrains Mono | + +Each theme uses Google Fonts for easy setup. See `templates/themes/README.md`. + +## Related skills + +- [improving-streamlit-design](../improving-streamlit-design/SKILL.md) - Visual polish with icons, badges, spacing + +## References + +- [Theming overview](https://docs.streamlit.io/develop/concepts/configuration/theming) +- [Colors and borders](https://docs.streamlit.io/develop/concepts/configuration/theming-customize-colors-and-borders) +- [Fonts](https://docs.streamlit.io/develop/concepts/configuration/theming-customize-fonts) +- [config.toml reference](https://docs.streamlit.io/develop/api-reference/configuration/config.toml) +- [st.context](https://docs.streamlit.io/develop/api-reference/caching-and-state/st.context) diff --git a/.agents/skills/developing-with-streamlit/skills/displaying-streamlit-data/SKILL.md b/.agents/skills/developing-with-streamlit/skills/displaying-streamlit-data/SKILL.md new file mode 100644 index 0000000..b924d95 --- /dev/null +++ b/.agents/skills/developing-with-streamlit/skills/displaying-streamlit-data/SKILL.md @@ -0,0 +1,199 @@ +--- +name: displaying-streamlit-data +description: Displaying charts, dataframes, and metrics in Streamlit. Use when visualizing data, configuring dataframe columns, or adding sparklines to metrics. Covers native charts, Altair, and column configuration. +license: Apache-2.0 +--- + +# Streamlit charts & data + +Present data clearly. + +## Choosing display elements + +| Element | Use Case | +|---------|----------| +| `st.dataframe` | Interactive exploration, sorting, filtering | +| `st.data_editor` | User-editable tables | +| `st.table` | Static display, no interaction needed | +| `st.metric` | KPIs with delta indicators | +| `st.json` | Structured data inspection | + +## Native charts first + +Prefer Streamlit's native charts for simple cases. + +```python +st.line_chart(df, x="date", y="revenue") +st.bar_chart(df, x="category", y="count") +st.scatter_chart(df, x="age", y="salary") +st.area_chart(df, x="date", y="value") +``` + +Native charts support additional parameters: `color` for series grouping, `stack` for bar/area stacking, `size` for scatter point sizing, `horizontal` for horizontal bars. See the [chart API reference](https://docs.streamlit.io/develop/api-reference/charts) for full options. + +## Human-readable labels + +Use clear labels—not column names or abbreviations. Skip `x_label`/`y_label` if the column names are already readable. + +```python +# BAD: cryptic column names without labels +st.line_chart(df, x="dt", y="rev") + +# GOOD: readable columns, no labels needed +st.line_chart(df, x="date", y="revenue") + +# GOOD: cryptic columns, add labels +st.line_chart(df, x="dt", y="rev", x_label="Date", y_label="Revenue") +``` + +## Altair for complex charts + +Use Altair when you need more control. Altair is bundled with Streamlit (no extra install), while Plotly requires an additional package. Pick one and stay consistent throughout your app. + +```python +import altair as alt + +chart = alt.Chart(df).mark_line().encode( + x=alt.X("date:T", title="Date"), + y=alt.Y("revenue:Q", title="Revenue ($)"), + color="region:N" +) +st.altair_chart(chart) +``` + +**When to use Altair:** +- Custom axis formatting +- Multiple series with legends +- Interactive tooltips +- Layered visualizations + +## Dataframe column configuration + +Use `column_config` where it adds value—formatting currencies, showing progress bars, displaying links or images. Don't add config just for labels or tooltips that don't meaningfully improve readability. Works with both `st.dataframe` and `st.data_editor`. + +```python +st.dataframe( + df, + column_config={ + "revenue": st.column_config.NumberColumn( + "Revenue", + format="$%.2f" + ), + "completion": st.column_config.ProgressColumn( + "Progress", + min_value=0, + max_value=100 + ), + "url": st.column_config.LinkColumn("Website"), + "logo": st.column_config.ImageColumn("Logo"), + "created_at": st.column_config.DatetimeColumn( + "Created", + format="MMM DD, YYYY" + ), + "internal_id": None, # Hide non-essential columns + }, + hide_index=True, +) +``` + +**Note on hiding columns:** Setting a column to `None` hides it from the UI, but the data is still sent to the frontend. For truly sensitive data, pre-filter the DataFrame before displaying. + +**Dataframe best practices:** +- **Hide useless index:** `hide_index=True` +- **Or make index meaningful:** `df = df.set_index("customer_name")` before displaying +- **Hide internal/technical columns:** Set column to `None` in config (but pre-filter for sensitive data) +- **Use visual column types where they help:** sparklines for trends, progress bars for completion, images for logos + +**Column types:** +- `AreaChartColumn` → Area sparklines +- `BarChartColumn` → Bar sparklines +- `CheckboxColumn` → Boolean as checkbox +- `DateColumn` → Date only (no time) +- `DatetimeColumn` → Dates with formatting +- `ImageColumn` → Images +- `JSONColumn` → Display JSON objects +- `LineChartColumn` → Sparkline charts +- `LinkColumn` → Clickable links +- `ListColumn` → Display lists/arrays +- `MultiselectColumn` → Multi-value selection +- `NumberColumn` → Numbers with formatting +- `ProgressColumn` → Progress bars +- `SelectboxColumn` → Editable dropdown +- `TextColumn` → Text with formatting +- `TimeColumn` → Time only (no date) + +## Pinned columns + +Keep important columns visible while scrolling horizontally: + +```python +st.dataframe( + df, + column_config={ + "Title": st.column_config.TextColumn(pinned=True), # Always visible + "Rating": st.column_config.ProgressColumn(min_value=0, max_value=10), + }, + hide_index=True, +) +``` + +## Data editor + +Use `st.data_editor` when users need to edit data directly: + +```python +edited_df = st.data_editor( + df, + num_rows="dynamic", # Allow adding/deleting rows + column_config={ + "status": st.column_config.SelectboxColumn( + "Status", + options=["pending", "approved", "rejected"] + ), + }, +) + +# React to edits +if not edited_df.equals(df): + save_changes(edited_df) +``` + +## JSON display + +For structured data inspection. Accepts dicts, lists, or any JSON-serializable object: + +```python +st.json({"name": "John", "scores": [95, 87, 92]}) +``` + +## Sparklines in metrics + +Add `chart_data` and `chart_type` to metrics for visual context. + +```python +values = [700, 720, 715, 740, 762, 755, 780] + +st.metric( + label="Developers", + value="762k", + delta="-7.42% (MoM)", + delta_color="inverse", + chart_data=values, + chart_type="line" # or "bar" +) +``` + +**Note:** Sparklines only show y-values and ignore x-axis spacing. Use them for evenly-spaced data (like daily or weekly snapshots). For irregularly-spaced time series, use a proper chart instead. + +See `building-streamlit-dashboards` for composing metrics into dashboard layouts. + +## References + +- [st.dataframe](https://docs.streamlit.io/develop/api-reference/data/st.dataframe) +- [st.data_editor](https://docs.streamlit.io/develop/api-reference/data/st.data_editor) +- [st.column_config](https://docs.streamlit.io/develop/api-reference/data/st.column_config) +- [st.metric](https://docs.streamlit.io/develop/api-reference/data/st.metric) +- [st.json](https://docs.streamlit.io/develop/api-reference/data/st.json) +- [st.line_chart](https://docs.streamlit.io/develop/api-reference/charts/st.line_chart) +- [st.bar_chart](https://docs.streamlit.io/develop/api-reference/charts/st.bar_chart) +- [st.altair_chart](https://docs.streamlit.io/develop/api-reference/charts/st.altair_chart) diff --git a/.agents/skills/developing-with-streamlit/skills/improving-streamlit-design/SKILL.md b/.agents/skills/developing-with-streamlit/skills/improving-streamlit-design/SKILL.md new file mode 100644 index 0000000..24705b8 --- /dev/null +++ b/.agents/skills/developing-with-streamlit/skills/improving-streamlit-design/SKILL.md @@ -0,0 +1,191 @@ +--- +name: improving-streamlit-design +description: Improving visual design in Streamlit apps. Use when polishing apps with icons, badges, spacing, or text styling. Covers Material icons, badge syntax, divider alternatives, and text casing conventions. +license: Apache-2.0 +--- + +# Streamlit visual design + +Small touches that make apps feel polished. + +**Related skills:** Visual design works hand-in-hand with other skills: +- `choosing-streamlit-selection-widgets` → Choosing the right widget (segmented control, pills, toggle) +- `displaying-streamlit-data` → Column config, sparklines, bordered metrics +- `using-streamlit-layouts` → Containers, alignment, dashboard cards + +## Page config + +Set browser tab title, icon, and layout. Place this at the top of your script to avoid visual blinking: + +```python +st.set_page_config( + page_title="My Dashboard", + page_icon=":material/analytics:", + layout="wide", # Use "wide" for dashboards with lots of data +) +``` + +**Layout options:** +- `layout="centered"` (default) → Best for most apps, content is constrained to a readable width +- `layout="wide"` → Full-width, good for dashboards and data-heavy apps + +## App logo + +Add a logo to the sidebar/header: + +```python +st.logo("logo.png") +``` + +## Icons over emojis + +Use Material icons for a cleaner, more professional look. + +```python +# GOOD: Material icons +st.markdown(":material/settings:") +st.markdown(":material/calendar_today:") +st.markdown(":material/dashboard:") +st.markdown(":material/person:") + +# SPARINGLY: Emojis for special occasions +st.markdown("Celebration! 🎉") +``` + +Format: `:material/icon_name:` + +Find icons: https://fonts.google.com/icons + +**Popular icons by category:** + +| Category | Icons | +|----------|-------| +| Navigation | `home`, `arrow_back`, `menu`, `settings`, `search` | +| Actions | `send`, `play_arrow`, `refresh`, `download`, `upload`, `save`, `delete`, `edit` | +| Status | `check_circle`, `error`, `warning`, `info`, `pending` | +| Data | `table_chart`, `bar_chart`, `analytics`, `query_stats`, `database` | +| Content | `chat`, `code`, `description`, `article`, `folder` | +| UI | `visibility`, `build`, `tune`, `filter_list` | + +## Badges for status + +For standalone badges: +```python +st.badge("Active", icon=":material/check:", color="green") +st.badge("Pending", icon=":material/schedule:", color="orange") +st.badge("Deprecated", color="red") +``` + +For inline badges in text: +```python +st.markdown(""" +:green-badge[Active] :orange-badge[Pending] :red-badge[Deprecated] :blue-badge[New] +""") +``` + +Avoid the old verbose syntax: +```python +# OLD (still works but cluttered) +st.markdown(":orange-background[:orange[Pending]]") +``` + +## Spacing: remove dividers + +Dividers (`st.divider()` or `---`) look heavy. Just remove them—Streamlit's default spacing is usually enough. + +```python +# BAD +st.header("Section 1") +st.write("Content") +st.divider() # Too heavy +st.header("Section 2") + +# GOOD +st.header("Section 1") +st.write("Content") +st.header("Section 2") +``` + +If you genuinely need spacing: +```python +st.space("small") # Small gap +st.space("medium") # Medium gap +st.space("large") # Large gap +st.space(50) # Custom pixels for fine-tuning +``` + +**Don't** systematically replace dividers with `st.space()`—it can look weird too. + +## Sentence casing + +Use sentence casing for titles and labels. Title Case Feels Shouty. + +```python +# GOOD +st.title("Upload your data") +st.selectbox("Select a region", options) +st.button("Save changes") + +# BAD +st.title("Upload Your Data") +st.selectbox("Select A Region", options) +``` + +## Caption over info + +`st.info()` is too heavy for simple informational text. + +```python +# GOOD: Lighter +st.caption("Data last updated 5 minutes ago") + +# BAD: Too heavy +st.info("Data last updated 5 minutes ago") +``` + +**When to use what:** +- `st.caption` → Simple info, metadata, timestamps +- `st.info` → Important instructions +- `st.warning` → Caution, potential issues +- `st.error` → Errors that block progress +- `st.success` → Confirmation of action +- `st.toast` → Lightweight confirmation that auto-dismisses + +## Text alignment + +Use `text_alignment` for text elements: + +```python +st.title("Centered title", text_alignment="center") +st.write("Right aligned", text_alignment="right") +st.caption("Justified text", text_alignment="justify") +``` + +Options: `"left"` (default), `"center"`, `"right"`, `"justify"` + +**Note:** `horizontal_alignment` on containers positions elements but also sets their `text_alignment`. If you need different text alignment within a horizontally-aligned container, override `text_alignment` on the text element itself. + +## Icons in callouts and expanders + +Material icons can make callouts and expanders look nicer: + +```python +st.info("Processing complete", icon=":material/check_circle:") +st.warning("Rate limit approaching", icon=":material/warning:") +st.error("Connection failed", icon=":material/error:") +st.success("Saved!", icon=":material/thumb_up:") + +with st.expander("Settings", icon=":material/settings:"): + st.write("Configure your preferences") +``` + +Other elements like `st.button` and `st.tabs` also support icons—worth considering when it adds clarity. + +## References + +- [st.set_page_config](https://docs.streamlit.io/develop/api-reference/configuration/st.set_page_config) +- [st.logo](https://docs.streamlit.io/develop/api-reference/media/st.logo) +- [st.badge](https://docs.streamlit.io/develop/api-reference/text/st.badge) +- [st.space](https://docs.streamlit.io/develop/api-reference/layout/st.space) +- [st.markdown](https://docs.streamlit.io/develop/api-reference/text/st.markdown) +- [st.toast](https://docs.streamlit.io/develop/api-reference/status/st.toast) diff --git a/.agents/skills/developing-with-streamlit/skills/optimizing-streamlit-performance/SKILL.md b/.agents/skills/developing-with-streamlit/skills/optimizing-streamlit-performance/SKILL.md new file mode 100644 index 0000000..1b2ffeb --- /dev/null +++ b/.agents/skills/developing-with-streamlit/skills/optimizing-streamlit-performance/SKILL.md @@ -0,0 +1,323 @@ +--- +name: optimizing-streamlit-performance +description: Optimizing Streamlit app performance. Use when apps are slow, rerunning too often, or loading heavy content. Covers caching, fragments, and static vs dynamic widget choices. +license: Apache-2.0 +--- + +# Streamlit performance + +Performance is the biggest win. Without caching and fragments, your app reruns everything on every interaction. + +## Caching + +### @st.cache_data for data + +Use for any function that loads or computes data. + +```python +# BAD: Recomputes on every rerun +def load_data(path): + return pd.read_csv(path) + +# GOOD: Cached +@st.cache_data +def load_data(path): + return pd.read_csv(path) +``` + +### @st.cache_resource for connections + +Use for connections, API clients, ML models—objects that can't be serialized. + +```python +@st.cache_resource +def get_connection(): + return st.connection("snowflake") + +@st.cache_resource +def load_model(): + return torch.load("model.pt") +``` + +**Critical warning:** Never mutate `@st.cache_resource` returns—changes affect all users: + +```python +# BAD: Mutating shared resource +@st.cache_resource +def get_config(): + return {"setting": "default"} + +config = get_config() +config["setting"] = "custom" # Affects ALL users! + +# GOOD: Copy before modifying +config = get_config().copy() +config["setting"] = "custom" +``` + +**Cleanup with `on_release`:** Clean up resources when evicted from cache: + +```python +def cleanup_connection(conn): + conn.close() + +@st.cache_resource(on_release=cleanup_connection) +def get_database(): + return create_connection() +``` + +### TTL for fresh data + +```python +@st.cache_data(ttl="5m") # 5 minutes +def get_metrics(): + return api.fetch() + +@st.cache_data(ttl="1h") # 1 hour +def load_reference_data(): + return pd.read_csv("large_reference.csv") +``` + +**Guidelines:** +- Real-time dashboards → `ttl="1m"` or less +- Metrics/reports → `ttl="5m"` to `ttl="15m"` +- Reference data → `ttl="1h"` or more +- Static data → No TTL + +### Prevent unbounded cache growth + +**Important:** Caches without `ttl` or `max_entries` can grow indefinitely and cause memory issues. For any cached function that stores changing objects (user-specific data, parameterized queries), set limits: + +```python +# BAD: Unbounded cache - memory will grow indefinitely +@st.cache_data +def get_user_data(user_id): + return fetch_user(user_id) + +# GOOD: Bounded cache with TTL +@st.cache_data(ttl="1h") +def get_user_data(user_id): + return fetch_user(user_id) + +# GOOD: Bounded cache with max entries +@st.cache_data(max_entries=100) +def get_user_data(user_id): + return fetch_user(user_id) +``` + +Use `ttl` for time-based expiration OR `max_entries` for size-based limits. You usually don't need both. + +### Caching anti-patterns + +**Don't cache functions that read widgets:** + +```python +# BAD: Widget inside cached function +@st.cache_data +def filtered_data(): + query = st.text_input("Query") # Widget inside cached function! + return df[df["name"].str.contains(query)] + +# GOOD: Pass widget values as parameters +@st.cache_data +def filtered_data(query: str): + return df[df["name"].str.contains(query)] + +query = st.text_input("Query") +result = filtered_data(query) +``` + +**Cache at the right granularity:** + +```python +# BAD: Caching too much - new cache entry per filter value +@st.cache_data +def get_and_filter_data(filter_value): + data = load_all_data() # Expensive! + return data[data["col"] == filter_value] + +# GOOD: Cache the expensive part, filter separately +@st.cache_data(ttl="1h") +def load_all_data(): + return fetch_from_database() + +data = load_all_data() +filtered = data[data["col"] == filter_value] +``` + +## Fragments + +Use `@st.fragment` to isolate reruns for self-contained UI pieces. + +```python +# BAD: Full app reruns +st.metric("Users", get_count()) +if st.button("Refresh"): + st.rerun() + +# GOOD: Only fragment reruns +@st.fragment +def live_metrics(): + st.metric("Users", get_count()) + st.button("Refresh") + +live_metrics() +``` + +For auto-refreshing metrics, use `run_every`: +```python +@st.fragment(run_every="30s") +def auto_refresh_metrics(): + st.metric("Users", get_count()) + +auto_refresh_metrics() +``` + +Use for: live metrics, refresh buttons, interactive charts that don't affect global state. + +## Forms to batch interactions + +By default, every widget interaction triggers a full rerun. Use `st.form` to batch multiple inputs and only rerun on submit. + +```python +# BAD: Reruns on every keystroke and selection +name = st.text_input("Name") +email = st.text_input("Email") +role = st.selectbox("Role", ["Admin", "User"]) + +# GOOD: Single rerun when user clicks Submit +with st.form("user_form"): + name = st.text_input("Name") + email = st.text_input("Email") + role = st.selectbox("Role", ["Admin", "User"]) + submitted = st.form_submit_button("Submit") + +if submitted: + save_user(name, email, role) +``` + +Use `border=False` for seamless inline forms that don't look like forms: + +```python +with st.form("search", border=False): + with st.container(horizontal=True): + query = st.text_input("Search", label_visibility="collapsed") + st.form_submit_button(":material/search:") +``` + +**When to use forms:** +- Multiple related inputs (signup, filters, settings) +- Text inputs where typing triggers expensive operations +- Any UI where "submit" semantics make sense + +**When NOT to use forms:** If inputs depend on each other (e.g., selecting a country should update available cities), forms won't work since there's no rerun until submit. + +## Conditional rendering + +**This is critical and often missed.** + +Layout containers like `st.tabs`, `st.expander`, and `st.popover` always render all their content, even when hidden or collapsed. + +To render content only when needed, use elements like `st.segmented_control`, `st.toggle`, or `@st.dialog` with conditional logic: + +```python +# BAD: Heavy content loads even when tab not visible +tab1, tab2 = st.tabs(["Light", "Heavy"]) +with tab2: + expensive_chart() # Always computed! + +# GOOD: Content only loads when selected +view = st.segmented_control("View", ["Light", "Heavy"]) +if view == "Heavy": + expensive_chart() # Only computed when selected +``` + +```python +# BAD: Expander content always loads +with st.expander("Advanced options"): + heavy_computation() # Runs even when collapsed! + +# GOOD: Toggle controls loading +if st.toggle("Show advanced options"): + heavy_computation() # Only runs when toggled on +``` + +## Pre-computation + +Move expensive work outside the main flow: +- Compute aggregations in SQL/dbt, not Python +- Pre-compute metrics in scheduled jobs +- Use materialized views for complex queries + +## Large data handling + +### For datasets under ~100M rows + +```python +@st.cache_data +def load_data(): + return pd.read_parquet("large_file.parquet") +``` + +### For very large datasets (over ~100M rows) + +> **Note:** This is only an escape hatch when serialization becomes too slow. In most cases, data this large shouldn't be loaded entirely into memory—prefer using a database that queries and loads data on demand. + +`@st.cache_data` uses pickle which slows with huge data. Use `@st.cache_resource` instead: + +```python +@st.cache_resource # No serialization overhead +def load_huge_data(): + return pd.read_parquet("huge_file.parquet") + +# WARNING: Don't mutate the returned DataFrame! +``` + +### Sampling for exploration + +When exploring large datasets, load a random sample instead of the full data: + +```python +@st.cache_data(ttl="1h") +def load_sample(n=10000): + df = pd.read_parquet("huge.parquet") + return df.sample(n=n) +``` + +## Multithreading + +Custom threads cannot call Streamlit commands (no session context). + +```python +import threading + +def fetch_in_background(url, results, index): + results[index] = requests.get(url).json() # No st.* calls! + +# Collect results, then display in main thread +results = [None] * len(urls) +threads = [ + threading.Thread(target=fetch_in_background, args=(url, results, i)) + for i, url in enumerate(urls) +] +for t in threads: + t.start() +for t in threads: + t.join() + +# Now display in main thread +for result in results: + st.write(result) +``` + +**Prefer alternatives when possible:** +- `@st.cache_data` for expensive computations +- `@st.fragment(run_every="5s")` for periodic updates + +## References + +- [Caching overview](https://docs.streamlit.io/develop/concepts/architecture/caching) +- [st.cache_data](https://docs.streamlit.io/develop/api-reference/caching-and-state/st.cache_data) +- [st.cache_resource](https://docs.streamlit.io/develop/api-reference/caching-and-state/st.cache_resource) +- [st.fragment](https://docs.streamlit.io/develop/api-reference/execution-flow/st.fragment) +- [st.form](https://docs.streamlit.io/develop/api-reference/execution-flow/st.form) \ No newline at end of file diff --git a/.agents/skills/developing-with-streamlit/skills/organizing-streamlit-code/SKILL.md b/.agents/skills/developing-with-streamlit/skills/organizing-streamlit-code/SKILL.md new file mode 100644 index 0000000..9225f0e --- /dev/null +++ b/.agents/skills/developing-with-streamlit/skills/organizing-streamlit-code/SKILL.md @@ -0,0 +1,91 @@ +--- +name: organizing-streamlit-code +description: Organizing Streamlit code for maintainability. Use when structuring apps with separate modules and utilities. Covers separation of concerns, keeping UI code clean, and import patterns. +license: Apache-2.0 +--- + +# Streamlit code organization + +For most simple apps, keep everything in one file—it's cleaner and more straightforward. The app file should read like a normal Python script for data processing, with a few Streamlit commands sprinkled in. + +Name the main file `streamlit_app.py` (Streamlit's default). + +## When to split + +**Keep in one file (most apps):** +- Apps under ~1000 lines +- One-off scripts and prototypes +- Apps where logic is straightforward + +**Consider splitting when:** +- Data processing is complex (50+ lines of non-UI code) +- Multiple pages share logic +- You want to test business logic separately + +If splitting makes sense, here's how to organize it. + +## Directory structure + +``` +my-app/ +├── streamlit_app.py # Main entry point +├── app_pages/ # Page UI modules +│ ├── dashboard.py +│ └── settings.py +└── utils/ # Business logic & helpers + ├── data.py + └── api.py +``` + +## Separating UI from logic + +When you do split, keep Streamlit files focused on UI and move complex logic to utility modules: + +```python +# streamlit_app.py - UI-focused +import streamlit as st +from utils.data import load_sales_data, compute_metrics + +st.title("Sales Dashboard") + +start = st.date_input("Start") +end = st.date_input("End") + +data = load_sales_data(start, end) +metrics = compute_metrics(data) + +st.metric("Revenue", f"${metrics['revenue']:,.0f}") +st.dataframe(data) +``` + +## Avoid if __name__ == "__main__" + +Streamlit apps run the entire file on each interaction. Don't use the main guard in Streamlit files. + +```python +# BAD - don't do this in streamlit_app.py or pages +if __name__ == "__main__": + main() + +# GOOD - just put the code directly +import streamlit as st + +st.title("My App") +``` + +The main guard is fine in utility modules for quick testing: + +```python +# utils/data.py +def load_data(path): + ... + +# Optional: test this module directly with `python utils/data.py` +if __name__ == "__main__": + print(load_data("test.csv")) +``` + +## References + +- [Multipage apps](https://docs.streamlit.io/develop/concepts/multipage-apps) +- [st.cache_data](https://docs.streamlit.io/develop/api-reference/caching-and-state/st.cache_data) diff --git a/.agents/skills/developing-with-streamlit/skills/setting-up-streamlit-environment/SKILL.md b/.agents/skills/developing-with-streamlit/skills/setting-up-streamlit-environment/SKILL.md new file mode 100644 index 0000000..bd5aa43 --- /dev/null +++ b/.agents/skills/developing-with-streamlit/skills/setting-up-streamlit-environment/SKILL.md @@ -0,0 +1,128 @@ +--- +name: setting-up-streamlit-environment +description: Setting up Python environments for Streamlit apps. Use when creating a new project or managing dependencies. Covers uv for dependency management and running apps. +license: Apache-2.0 +--- + +# Streamlit environment + +Use whatever dependency management the project already has (pip, poetry, conda, etc.). If starting fresh and uv is available, it's a good default—fast, reliable, and creates isolated environments automatically. + +If uv is not installed, ask the user before installing it. + +## CRITICAL: Always Use Latest Streamlit + +**Always specify `streamlit>=1.53.0`** (or latest) in dependencies. Many Streamlit features and patterns in these skills require recent versions. Older streamlit versions will cause errors with: +- Material icons (`:material/icon_name:`) +- `st.pills()`, `st.segmented_control()` +- Modern caching decorators +- Navigation APIs + +When setting up a new project or fixing an existing one, **always check and update the streamlit version**. + +## Using uv + +If uv is available, here's how to set up a Streamlit project. + +### Quick start (venv only) + +For simple apps, just create a virtual environment: + +```bash +uv venv +source .venv/bin/activate # or .venv\Scripts\activate on Windows +uv pip install streamlit +``` + +Run with: + +```bash +streamlit run streamlit_app.py +``` + +## Full project setup + +For larger projects or when you need reproducible builds: + +```bash +uv init my-streamlit-app +cd my-streamlit-app +uv add streamlit +``` + +This creates: +- `pyproject.toml` with dependencies +- `uv.lock` for reproducible builds +- `.venv/` virtual environment + +Run with: + +```bash +uv run streamlit run streamlit_app.py +``` + +## With options + +Avoid setting options unless you have a specific reason: + +```bash +streamlit run streamlit_app.py --server.headless true # Only for automated/CI environments +``` + +## Add dependencies + +```bash +# With venv approach +uv pip install plotly snowflake-connector-python + +# With full project (uv init) +uv add plotly snowflake-connector-python +``` + +## Project structure + +Keep it simple. For most apps: + +``` +my-streamlit-app/ +├── .venv/ +└── streamlit_app.py +``` + +Only add more when needed: +- `app_pages/` → Only for multi-page apps +- `.streamlit/config.toml` → Only if customizing theme or settings +- `.streamlit/secrets.toml` → Only if using secrets (add to `.gitignore`) +- `pyproject.toml` → Only if using `uv init` for reproducible builds + +## Convention + +Name your main file `streamlit_app.py` for consistency. This is what Streamlit expects by default. + +**What goes in the main module:** +- When using navigation: it's a router that defines pages and runs them +- When there's no navigation: it's the home page with your main content + +## pyproject.toml Example + +```toml +[project] +name = "my-streamlit-app" +version = "0.1.0" +requires-python = ">=3.11" +dependencies = [ + "streamlit>=1.53.0", + "plotly>=5.0.0", + "snowflake-connector-python>=3.0.0", +] + +[tool.uv] +dev-dependencies = [ + "pytest>=8.0.0", +] +``` + +## References + +- [uv documentation](https://docs.astral.sh/uv/) +- [Streamlit installation](https://docs.streamlit.io/get-started/installation) diff --git a/.agents/skills/developing-with-streamlit/skills/using-streamlit-cli/SKILL.md b/.agents/skills/developing-with-streamlit/skills/using-streamlit-cli/SKILL.md new file mode 100644 index 0000000..40c5f0b --- /dev/null +++ b/.agents/skills/developing-with-streamlit/skills/using-streamlit-cli/SKILL.md @@ -0,0 +1,165 @@ +--- +name: using-streamlit-cli +description: Documents Streamlit CLI commands for running apps, managing configuration, and diagnostics. Use when starting Streamlit apps, configuring runtime options, or troubleshooting CLI issues. +--- + +# Using the Streamlit CLI + +The Streamlit CLI is the primary tool for running Streamlit applications and managing configuration. This skill covers all essential commands and configuration options. + +## Running Streamlit apps + +### Basic syntax + +```bash +streamlit run [] [-- config options] [script args] +``` + +### Entrypoint options + +| Argument | Behavior | +|----------|----------| +| (none) | Looks for `streamlit_app.py` in current directory | +| Directory path | Runs `streamlit_app.py` within that directory | +| File path | Runs the specified file directly | +| URL | Runs a remote script (e.g., from GitHub) | + +### Examples + +```bash +# Run default app in current directory +streamlit run + +# Run a specific file +streamlit run app.py + +# Run from a URL +streamlit run https://raw.githubusercontent.com/streamlit/demo-uber-nyc-pickups/master/streamlit_app.py + +# Alternative: run as Python module (useful for IDE configuration) +python -m streamlit run app.py +``` + +### Running with `uv` (recommended) + +Use `uv run` to run Streamlit in a virtual environment with automatic dependency management: + +```bash +# Run with uv (automatically uses/creates virtual environment) +uv run streamlit run app.py + +# With configuration options +uv run streamlit run app.py --server.headless=true + +# With script arguments +uv run streamlit run app.py -- arg1 arg2 +``` + +Using `uv run` is the recommended approach because it: +- Automatically manages virtual environments +- Resolves and installs dependencies from `pyproject.toml` +- Ensures reproducible environments across machines +- Avoids manual activation/deactivation of virtual environments + +## Setting configuration with `streamlit run` + +Configuration options follow the pattern `--
.