Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
412ef0e
First steps
ponsoc Dec 17, 2025
40a7bf5
Version bump
ponsoc Dec 20, 2025
ce15cf5
Flow and layout are now abstract
ponsoc Dec 20, 2025
58f077f
Component absract
ponsoc Dec 20, 2025
8b158ea
Cleaning the store
ponsoc Dec 20, 2025
489b63e
Sidebar is a component and should be in a layout, not in main app
ponsoc Dec 20, 2025
ae2d619
Second attempt
ponsoc Dec 23, 2025
e91d374
Review improvements
ponsoc Dec 23, 2025
b573115
Update README.md
ponsoc Dec 23, 2025
2296175
Update refresh.py
ponsoc Dec 23, 2025
39da56e
Update metric.py
ponsoc Dec 23, 2025
5fef8ab
No value in current route when properly using the router the layout i…
ponsoc Dec 24, 2025
595912d
Update layout.py
ponsoc Dec 24, 2025
fba86fb
Update refresh.py
ponsoc Dec 24, 2025
2b606ff
Update login_succes.py
ponsoc Dec 24, 2025
fca3bfe
Update component.py
ponsoc Dec 24, 2025
9833822
formatting
ponsoc Dec 24, 2025
fd7676e
formatting
ponsoc Dec 24, 2025
4e71ea9
Resolving first comments
ponsoc Dec 24, 2025
78e2b7f
Formatting and removing unused stuff
ponsoc Dec 24, 2025
65af69f
Cleaning
ponsoc Dec 28, 2025
883a2f0
Added create methods for flow and layout
ponsoc Dec 28, 2025
535b8c3
Introducing toast
ponsoc Dec 28, 2025
3ead59f
Better naming
ponsoc Dec 28, 2025
fb75cd1
Some cleaning considered the new framework rules and started with upd…
ponsoc Dec 28, 2025
c891361
Update login.py
ponsoc Dec 28, 2025
6ed71ce
Cleaning and docs
ponsoc Jan 2, 2026
1df2ab5
Update login.py
ponsoc Jan 2, 2026
23ea5b2
improved visibility functionality
ponsoc Jan 2, 2026
c468adb
doc strings and type hints
ponsoc Jan 2, 2026
5a21bed
final cleaning
ponsoc Jan 2, 2026
7d2c14c
docs strings
ponsoc Jan 2, 2026
790ea1b
Update store.py
ponsoc Jan 2, 2026
c17b510
Merge pull request #21 from ponsoc/improve-router2
ponsoc Jan 2, 2026
d2b4f1e
Merge branch 'improve-router2' into smaller-footprint
ponsoc Jan 2, 2026
789541f
Update README.md
ponsoc Jan 2, 2026
93b99ec
Added migration guide
ponsoc Jan 2, 2026
fdeeee7
Merge pull request #22 from ponsoc/smaller-footprint
ponsoc Jan 2, 2026
180e935
Formatting
ponsoc Jan 2, 2026
5d2fc98
Update README.md
ponsoc Jan 2, 2026
f5356ad
Update README.md
ponsoc Jan 2, 2026
3f6261c
Update README.md
ponsoc Jan 2, 2026
4f9782b
Comments Jamie and event handlers for app and routes
ponsoc Jan 6, 2026
82d1ea2
Final changes, component events work now through events
ponsoc Jan 6, 2026
c330813
Comments Jamie
ponsoc Jan 8, 2026
b9c88d0
Better flow context
ponsoc Jan 8, 2026
c4c6ee2
New Fragment and Dialog component types
ponsoc Jan 8, 2026
ded77d1
Comments Jamie done
ponsoc Jan 9, 2026
02efdc1
formatting
ponsoc Jan 9, 2026
7cfc0eb
minor naming
ponsoc Feb 7, 2026
1b7df3a
update store tests
ponsoc Feb 7, 2026
086bf9d
update router tests
ponsoc Feb 9, 2026
a7a995c
add and update all tests
ponsoc Feb 9, 2026
3534b76
formatting
ponsoc Feb 9, 2026
2460e7f
more formatting
ponsoc Feb 9, 2026
b547c39
missed a file
ponsoc Feb 9, 2026
ce7a54b
fix minor bug and update test
ponsoc Feb 9, 2026
922f688
fix order
ponsoc Feb 9, 2026
872ca97
schedule and rerun
ponsoc Feb 13, 2026
51034f2
Better way of working with the events
ponsoc Mar 6, 2026
2699b22
Update test_component.py
ponsoc Mar 6, 2026
6f7bd9d
Improved tests and example app
ponsoc Mar 6, 2026
c931551
Version and readme
ponsoc Mar 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[flake8]
extend-ignore = E203, E501
extend-ignore = E203, E501, E731
exclude = .github,__pycache__,docs/source/conf.py,old,build,dist,venv,
max-complexity = 10
377 changes: 269 additions & 108 deletions README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion example/src/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

app = MainApp()

app.run()
app.app.run()
46 changes: 45 additions & 1 deletion example/src/assets/style.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,47 @@
html, body, * {
font-style: italic
}
}

.status-icon {
width: 20px;
height: 20px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: bold;
color: white;
}

/* RUNNING (spinner) */
.running {
border: 3px solid rgba(0,0,0,0.1);
border-left-color: #4CAF50;
animation: spin 1s linear infinite;
}

/* STATIC STATES */
.success {
background-color: #4CAF50;
}

.error {
background-color: #F44336;
}

.info {
background-color: #2196F3;
}

/* Spin animation */
@keyframes spin {
0% { transform: rotate(0deg);}
100% { transform: rotate(360deg);}
}

.status-row {
display: flex;
align-items: center;
gap: 10px;
}
14 changes: 12 additions & 2 deletions example/src/components/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@
from .sidebar import SidebarComponent
from .data_viewer import DataViewerComponent
from .metric import MetricComponent
from .toast import ToastComponent
from .button import ButtonComponent
from .status import StatusComponent


__all__ = [LoginDialogComponent, SidebarComponent, DataViewerComponent, MetricComponent]
__all__ = [
LoginDialogComponent,
SidebarComponent,
DataViewerComponent,
MetricComponent,
ToastComponent,
ButtonComponent,
StatusComponent
]
22 changes: 22 additions & 0 deletions example/src/components/button.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import streamlit as st
from ststeroids import Component, Flow


class ButtonComponent(Component):

EVENT_ClICK = "click"

def __init__(self, button_text: str):
self.button_text = button_text

def _handle_click(self):
self.trigger(self.EVENT_ClICK)

def display(self):
st.button(self.button_text, on_click=self._handle_click)

def on_click(self, flow: Flow) -> None:
"""
Register a flow to be executed when the user clicks the button.
"""
self.on(self.EVENT_ClICK, flow)
10 changes: 4 additions & 6 deletions example/src/components/data_viewer.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,27 @@
import uuid
import streamlit as st
from ststeroids import Component


class DataViewerComponent(Component):
def __init__(
self,
component_id: str,
header: str,
column_config: dict = {},
column_order: list = [],
):
super().__init__(component_id, {"data": None, "dek": uuid.uuid4()})
self.header = header
self.column_config = column_config
self.column_order = column_order
self.data = None

def render(self):
def display(self):
st.subheader(self.header)
st.dataframe(
self.state.data,
self.data,
hide_index=True,
column_config=self.column_config,
column_order=self.column_order,
)

def set_data(self, data):
self.state.data = data
self.data = data
50 changes: 22 additions & 28 deletions example/src/components/login_dialog.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,31 @@
import streamlit as st
from ststeroids import Component, Flow
from ststeroids import Dialog, Flow


class LoginDialogComponent(Component):
class LoginDialogComponent(Dialog):

EVENT_LOGIN = "login"

def __init__(
self,
component_id: str,
login_flow: Flow,
login_success_flow: Flow,
header: str = "Enter username/password",
):
super().__init__(component_id, {"visible": False})
self.header = header
self.login_flow = login_flow
self.login_success_flow = login_success_flow
self.error_message = None
self.hide()

def render(self):
if self.state.visible:
username = st.text_input("Username")
password = st.text_input("Password", type="password")
if st.button("Login", use_container_width=True):
login_succes = self.login_flow.execute_run(username, password)
if login_succes:
self.login_success_flow.execute_run()
else:
st.error("Login failed, please check your username and password.")
def display(self):
self.username = st.text_input("Username")
self.password = st.text_input("Password", type="password")
if st.button("Login", use_container_width=True):
self.trigger(self.EVENT_LOGIN)
if self.error_message:
st.error(self.error_message)
self.error_message = None

def show(self):
if self.state.visible is False:
self.state.visible = True
st.rerun()
def on_login(self, flow: Flow) -> None:
"""
Register a flow to be executed when the user clicks the login button.
"""
self.on(self.EVENT_LOGIN, flow)

def hide(self):
if self.state.visible is True:
self.state.visible = False
st.rerun()
def set_error(self, message: str):
self.error_message = message
13 changes: 6 additions & 7 deletions example/src/components/metric.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import streamlit as st
from ststeroids import Component
from ststeroids import Fragment


class MetricComponent(Component):
class MetricComponent(Fragment):
def __init__(
self,
component_id: str,
header: str,
):
super().__init__(component_id, {"value": None})
self.header = header
self.value = 0

def render(self):
st.metric(self.header, self.state.value)
def display(self):
st.metric(self.header, self.value)

def set_value(self, value: int):
self.state.value = value
self.value = value
17 changes: 8 additions & 9 deletions example/src/components/sidebar.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import streamlit as st
from ststeroids import Component, Router
from ststeroids import Component


class SidebarComponent(Component):

def __init__(self, component_id: str, router: Router):
super().__init__(component_id)
self.router = router

def render(self):
def display(self):
with st.sidebar:
st.page_link("pages/dashboard.py", icon=":material/search:", label="Dashboard")
st.page_link("pages/manage.py", icon=":material/bar_chart:", label="Manage data")

st.page_link(
"pages/dashboard.py", icon=":material/search:", label="Dashboard"
)
st.page_link(
"pages/manage.py", icon=":material/bar_chart:", label="Manage data"
)
32 changes: 32 additions & 0 deletions example/src/components/status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from typing import Literal

import streamlit as st
from ststeroids import Component


class StatusComponent(Component):

def __init__(
self,
message: str = None,
type: Literal["running", "info", "error", "success"] = "info",
):
self.message = message
self.type = type

def display(self):
if self.message:
st.markdown(f"<div class='status-row'>{self._status_icon(self.type)}<div>{self.message}</div></div>", unsafe_allow_html=True)

def set_status(self, message: str, type: Literal["running", "info", "error", "success"] = "info"):
self.message = message
self.type = type

def clear(self):
self.message = None
self.type = None

def _status_icon(self, state: str):
icons = {"success": "✓", "error": "✕", "info": "i", "running": ""}

return f"<div class='status-icon {state}'>{icons.get(state, '')}</div>"
18 changes: 18 additions & 0 deletions example/src/components/toast.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import streamlit as st
from ststeroids import Component


class ToastComponent(Component):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

self.visible is currently commented, either implement or remove comments

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Doen

def __init__(
self,
):
self.message = None
self.hide()

def display(self):
st.toast(self.message)
self.hide()

def set_message(self, message: str):
self.message = message
self.show()
6 changes: 4 additions & 2 deletions example/src/flows/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from .login import LoginFlow
from .login_succes import LoginSuccessFlow
from .refresh import RefreshFlow
from .app_setup import SetupFlow
from .logout import LogoutFlow
from .long_running import LongRunningFlow

__all__ = [LoginFlow, LoginSuccessFlow, RefreshFlow]
__all__ = [LoginFlow, RefreshFlow, SetupFlow, LogoutFlow, LongRunningFlow]
6 changes: 6 additions & 0 deletions example/src/flows/app_setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from ststeroids import Flow, FlowContext


class SetupFlow(Flow):
def run(self, _ctx: FlowContext):
print("I'm a flow setting up the app per user")
61 changes: 53 additions & 8 deletions example/src/flows/login.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,62 @@
from service import MockBackendService
from ststeroids import Flow, Store
from ststeroids import Flow, Store, FlowContext
from components import (
LoginDialogComponent,
DataViewerComponent,
MetricComponent,
ToastComponent,
)
from shared import ComponentIDs
import streamlit as st


class LoginFlow(Flow):
def __init__(self, session_store: Store, backend_service: MockBackendService):
super().__init__()
self.session_store = session_store
self.backend_service = backend_service

def run(self, username: str, password: str):
response = self.backend_service.authenticate(username, password)
@property
def cp_login_dialog(self):
return LoginDialogComponent.get(ComponentIDs.dialog_login)

@property
def cp_data_viewer(self):
return DataViewerComponent.get(ComponentIDs.data_viewer)

@property
def cp_total_movies(self):
return MetricComponent.get(ComponentIDs.total_movies)

@property
def cp_toast(self):
return ToastComponent.get(ComponentIDs.toast)

def run(self, _ctx: FlowContext):
response = self.backend_service.authenticate(
self.cp_login_dialog.username, self.cp_login_dialog.password
)
if response.ok:
token_data = response.json()
self.session_store.set_property("access_token", token_data["access_token"])
return True
return False
self._login_success(response)
else:
self._login_failed()

def _login_success(self, response):
token_data = response.json()
self.session_store.set_property("access_token", token_data["access_token"])
self.cp_login_dialog.hide()
response = self.backend_service.get_movies()
# enable the line below for example of an error scenario
# response.ok = False

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this comment still relevant?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Yes

if response.ok:
data = response.json()
self.session_store.set_property(
"data", data
) # Store the data in the session_store for later use in more complex applications
self.cp_total_movies.set_value(len(data))
self.cp_data_viewer.set_data(data)
else:
self.cp_toast.set_message("error")
st.switch_page("pages/dashboard.py")

def _login_failed(self):
self.cp_login_dialog.set_error("Login failed, check your username and password")
Loading