Skip to content

Commit a7632e0

Browse files
committed
fix: resolve typecheck warnings
1 parent f7bc5db commit a7632e0

6 files changed

Lines changed: 222 additions & 52 deletions

File tree

demo/notifications.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#!/usr/bin/env python3
2+
"""Demo of the Notifications helper."""
3+
4+
import sys
5+
from pathlib import Path
6+
7+
import dash
8+
from dash import Input, Output
9+
import dash_mantine_components as dmc
10+
11+
# Add the src directory to the Python path
12+
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
13+
14+
from dashkit import (
15+
NOTIFICATION_STORE_ID,
16+
create_layout,
17+
notify,
18+
setup_app,
19+
)
20+
21+
app = dash.Dash(__name__)
22+
setup_app(app)
23+
24+
# Simple page with buttons to trigger each variant
25+
content = dmc.Stack(
26+
[
27+
dmc.Button("Success", id="btn-success", color="green"),
28+
dmc.Button("Info", id="btn-info"),
29+
dmc.Button("Warning", id="btn-warning", color="yellow"),
30+
dmc.Button("Error", id="btn-error", color="red"),
31+
],
32+
gap="sm",
33+
)
34+
app.layout = create_layout(content)
35+
36+
37+
@app.callback(
38+
Output(NOTIFICATION_STORE_ID, "data"),
39+
[
40+
Input("btn-success", "n_clicks"),
41+
Input("btn-info", "n_clicks"),
42+
Input("btn-warning", "n_clicks"),
43+
Input("btn-error", "n_clicks"),
44+
],
45+
prevent_initial_call=True,
46+
)
47+
def trigger(*_):
48+
from dash import ctx
49+
50+
match ctx.triggered_id:
51+
case "btn-success":
52+
return notify("success", "Saved", "Record saved successfully")
53+
case "btn-info":
54+
return notify("info", "Heads up", "Here is some information")
55+
case "btn-warning":
56+
return notify("warning", "Warning", "Something needs attention")
57+
case "btn-error":
58+
return notify("error", "Error", "Something went wrong")
59+
return dash.no_update
60+
61+
62+
if __name__ == "__main__":
63+
print("Starting Dashkit Notifications Demo...")
64+
print("Open http://127.0.0.1:8050 in your browser")
65+
app.run(debug=True)

src/dashkit/__init__.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from pathlib import Path
99

10-
from flask import send_from_directory
10+
from flask import Response, send_from_directory
1111

1212
from .buttons import PrimaryButton, SecondaryButton
1313
from .callout import (
@@ -22,6 +22,7 @@
2222
from .header import create_header
2323
from .layout import create_layout
2424
from .markdown_report import MarkdownReport
25+
from .notifications import NOTIFICATION_STORE_ID, NotificationsProvider, notify
2526
from .sidebar import create_sidebar
2627
from .table import Table, TableWithStats
2728

@@ -55,8 +56,8 @@ def setup_app(app, assets_folder=None, include_dashkit_css: bool = True):
5556
route_attr = "_dashkit_assets_route_registered"
5657
if not getattr(app.server, route_attr, False):
5758

58-
@app.server.route("/dashkit-assets/<path:filename>")
59-
def _dashkit_assets(filename: str): # type: ignore
59+
@app.server.route("/dashkit-assets/<path:filename>") # pyright: ignore[reportUntypedFunctionDecorator]
60+
def _dashkit_assets(filename: str) -> Response: # pyright: ignore[reportUnusedFunction]
6061
return send_from_directory(str(pkg_assets), filename)
6162

6263
setattr(app.server, route_attr, True)
@@ -126,6 +127,9 @@ def _dashkit_assets(filename: str): # type: ignore
126127
"ImportantCallout",
127128
"WarningCallout",
128129
"CautionCallout",
130+
"NotificationsProvider",
131+
"notify",
132+
"NOTIFICATION_STORE_ID",
129133
"setup_app",
130134
]
131135

src/dashkit/layout.py

Lines changed: 48 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from dash import Input, Output, callback, clientside_callback, dcc, html
66

77
from .header import create_header
8+
from .notifications import NotificationsProvider
89
from .sidebar import create_sidebar
910
from .theme_manager import ThemeManager
1011

@@ -55,51 +56,53 @@ def create_layout(
5556
}
5657

5758
return dmc.MantineProvider(
58-
html.Div(
59-
[
60-
# Theme and location management
61-
ThemeManager() if include_theme_manager else None,
62-
# Global page stores for header + page config
63-
dcc.Store(id="page_header_config", data={}),
64-
dcc.Store(id="page_config", data={}),
65-
# Sidebar
66-
create_sidebar(
67-
brand_name=sidebar_config["brand_name"],
68-
brand_initial=sidebar_config["brand_initial"],
69-
),
70-
# Right side: navbar + content (full width minus sidebar)
71-
html.Div(
72-
[
73-
# Header/navbar - spans full width of content area
74-
create_header(
75-
page_title=header_config["page_title"],
76-
page_icon=header_config["page_icon"],
77-
search_placeholder=header_config.get(
78-
"search_placeholder", "Search..."
59+
NotificationsProvider(
60+
html.Div(
61+
[
62+
# Theme and location management
63+
ThemeManager() if include_theme_manager else None,
64+
# Global page stores for header + page config
65+
dcc.Store(id="page_header_config", data={}),
66+
dcc.Store(id="page_config", data={}),
67+
# Sidebar
68+
create_sidebar(
69+
brand_name=sidebar_config["brand_name"],
70+
brand_initial=sidebar_config["brand_initial"],
71+
),
72+
# Right side: navbar + content (full width minus sidebar)
73+
html.Div(
74+
[
75+
# Header/navbar - spans full width of content area
76+
create_header(
77+
page_title=header_config["page_title"],
78+
page_icon=header_config["page_icon"],
79+
search_placeholder=header_config.get(
80+
"search_placeholder", "Search..."
81+
),
82+
actions=header_config.get("actions"),
83+
filter_items=header_config.get("filter_items"),
7984
),
80-
actions=header_config.get("actions"),
81-
filter_items=header_config.get("filter_items"),
82-
),
83-
# Content with max-width constraint
84-
html.Main(
85-
[
86-
html.Div(
87-
[content],
88-
id="main-content-container",
89-
style={
90-
"maxWidth": "calc(100vw - var(--dashkit-sidebar-width))",
91-
"width": "100%",
92-
},
93-
className=f"dark:text-white {content_padding} prose prose-sm dark:prose-invert",
94-
)
95-
],
96-
className="flex-1 overflow-auto dark:bg-dashkit-surface ",
97-
),
98-
],
99-
className="main-content-area flex-1 flex flex-col",
100-
),
101-
],
102-
className="flex h-screen bg-white dark:bg-dashkit-surface font-sans",
85+
# Content with max-width constraint
86+
html.Main(
87+
[
88+
html.Div(
89+
[content],
90+
id="main-content-container",
91+
style={
92+
"maxWidth": "calc(100vw - var(--dashkit-sidebar-width))",
93+
"width": "100%",
94+
},
95+
className=f"dark:text-white {content_padding} prose prose-sm dark:prose-invert",
96+
)
97+
],
98+
className="flex-1 overflow-auto dark:bg-dashkit-surface ",
99+
),
100+
],
101+
className="main-content-area flex-1 flex flex-col",
102+
),
103+
],
104+
className="flex h-screen bg-white dark:bg-dashkit-surface font-sans",
105+
)
103106
)
104107
)
105108

@@ -112,7 +115,7 @@ def create_layout(
112115
],
113116
Input("url", "pathname", allow_optional=True),
114117
)
115-
def _update_page_config(pathname: str | None):
118+
def _update_page_config(pathname: str | None) -> tuple[dict[str, str], dict[str, str]]: # pyright: ignore[reportUnusedFunction]
116119
"""Update header + page config stores from Dash page registry.
117120
118121
This runs whenever the URL changes. If `url` isn't present, it will be

src/dashkit/notifications.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""Notifications and toast helpers for Dashkit.
2+
3+
Provides a global ``NotificationsProvider`` that should wrap the app layout and a
4+
simple ``notify`` helper that can be returned from any callback to enqueue a
5+
toast.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
from uuid import uuid4
11+
12+
import dash_mantine_components as dmc
13+
from dash import Input, Output, clientside_callback, dcc, html
14+
15+
# ID for the dcc.Store that transports notification payloads
16+
NOTIFICATION_STORE_ID = "dashkit_notifications"
17+
18+
19+
def NotificationsProvider(
20+
child: html.Div | None = None,
21+
*,
22+
position: str = "top-right",
23+
limit: int = 3,
24+
) -> html.Div:
25+
"""Wrap children with a Mantine notifications provider.
26+
27+
Args:
28+
child: Optional layout content to wrap.
29+
position: Corner of the viewport where toasts appear.
30+
limit: Maximum number of visible notifications.
31+
"""
32+
33+
provider = getattr(dmc, "NotificationProvider") # noqa: B009
34+
elements = [
35+
provider(position=position, zIndex=1000, limit=limit),
36+
dcc.Store(id=NOTIFICATION_STORE_ID),
37+
]
38+
if child:
39+
elements.append(child)
40+
return html.Div(elements)
41+
42+
43+
def notify(
44+
kind: str,
45+
message: str,
46+
description: str | None = None,
47+
action: dict[str, str] | None = None,
48+
*,
49+
duration: int = 5000,
50+
) -> dict[str, str | int | None]:
51+
"""Create a notification payload.
52+
53+
Returns a dictionary intended for ``Output(NOTIFICATION_STORE_ID, "data")``
54+
that triggers the clientside toast renderer.
55+
"""
56+
57+
return {
58+
"id": str(uuid4()),
59+
"kind": kind,
60+
"message": message,
61+
"description": description,
62+
"action": action,
63+
"duration": duration,
64+
}
65+
66+
67+
# Clientside callback that renders toasts when the store is updated
68+
clientside_callback(
69+
"""
70+
function(data) {
71+
if (!data) {
72+
return window.dash_clientside.no_update;
73+
}
74+
const colorMap = {success: 'green', info: 'blue', warning: 'yellow', error: 'red'};
75+
const opts = {
76+
color: colorMap[data.kind] || 'blue',
77+
message: data.message,
78+
autoClose: data.duration || 5000,
79+
};
80+
if (data.description) {
81+
opts.title = data.message;
82+
opts.message = data.description;
83+
}
84+
if (data.action) {
85+
opts.action = {
86+
label: data.action.label,
87+
onClick: () => console.log('notification action', data.action.label)
88+
};
89+
}
90+
window.dash_mantine_components?.notifications.show(opts);
91+
return null;
92+
}
93+
""",
94+
Output(NOTIFICATION_STORE_ID, "data", allow_duplicate=True),
95+
Input(NOTIFICATION_STORE_ID, "data"),
96+
prevent_initial_call=True,
97+
)

src/dashkit/sidebar.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -534,7 +534,7 @@ def _safe_order(value: Any) -> int:
534534

535535
# Sort sections by order, then alphabetically
536536
def _section_sort_key(item):
537-
section_title, pages = item
537+
section_title, _ = item
538538
return (section_orders.get(section_title, 999), section_title)
539539

540540
for section_title, pages in sorted(pages_by_section.items(), key=_section_sort_key):

src/dashkit/table.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
from dash import html
44

55
try:
6-
from dashkit_table import DashkitTable as CustomDashkitTable # type: ignore
6+
from dashkit_table.DashkitTable import (
7+
DashkitTable as CustomDashkitTable, # type: ignore
8+
)
79
except Exception as _e:
810
CustomDashkitTable = None # type: ignore[assignment]
911
_import_error = _e
@@ -59,8 +61,7 @@ def Table(
5961
# Using our custom table component with latest Handsontable v16.0.1
6062
if CustomDashkitTable is None:
6163
raise ImportError(
62-
"dashkit_table is not installed. Install it with `pip install dashkit_table` "
63-
"or install extras: `pip install dash-dashkit[table]`."
64+
"dashkit_table is not installed. Install it with `pip install dashkit_table` or install extras: `pip install dash-dashkit[table]`."
6465
) from _import_error
6566
return CustomDashkitTable(
6667
id=id,

0 commit comments

Comments
 (0)