Skip to content

Commit 4c157e7

Browse files
committed
feat(layout): inject ThemeManager by default; add global page stores and callbacks
fix(markdown_report): remove header store to avoid duplicate IDs chore: bump version to 1.2.5
1 parent 2afe1da commit 4c157e7

8 files changed

Lines changed: 138 additions & 29 deletions

File tree

README.md

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -191,30 +191,35 @@ uv pip install -e .
191191
Releases are driven by tags. Publishing runs in CI and a smoke test validates PyPI install.
192192

193193
Prerequisites:
194+
194195
- GitHub Actions secret: `PYPI_API_TOKEN` (PyPI API token with upload permission)
195196

196197
Subpackages
198+
197199
- dashkit_table
198-
1) Bump version in `src/dashkit_table/pyproject.toml`
199-
2) Commit and push (if ignored, force add): `git add -f src/dashkit_table/pyproject.toml && git commit -m "release(table): X.Y.Z" && git push`
200-
3) Tag and push: `git tag dashkit_table-vX.Y.Z && git push origin dashkit_table-vX.Y.Z`
200+
1. Bump version in `src/dashkit_table/pyproject.toml`
201+
2. Commit and push (if ignored, force add): `git add -f src/dashkit_table/pyproject.toml && git commit -m "release(table): X.Y.Z" && git push`
202+
3. Tag and push: `git tag dashkit_table-vX.Y.Z && git push origin dashkit_table-vX.Y.Z`
201203
- dashkit_kiboui
202-
1) Bump version in `src/dashkit_kiboui/pyproject.toml`
203-
2) Commit and push
204-
3) Tag and push: `git tag dashkit_kiboui-vX.Y.Z && git push origin dashkit_kiboui-vX.Y.Z`
204+
1. Bump version in `src/dashkit_kiboui/pyproject.toml`
205+
2. Commit and push
206+
3. Tag and push: `git tag dashkit_kiboui-vX.Y.Z && git push origin dashkit_kiboui-vX.Y.Z`
205207

206208
Main package (dash-dashkit)
209+
207210
- Bump version in `pyproject.toml` (update subpackage minimums as needed)
208211
- Commit and push
209-
- Tag and push: `git tag dash-dashkit-vX.Y.Z && git push origin dash-dashkit-vX.Y.Z`
212+
- Tag and push: `git tag dash-dashkit-vX.Y.Z && git push origin dashkit-vX.Y.Z`
210213
- Legacy form `vX.Y.Z` is also supported
211214

212215
CI workflows
216+
213217
- Publish: builds the package for the matching tag and uploads to PyPI
214218
- Smoke: installs the just-published version in a clean venv and imports/instantiates components
215219
- Manual fallback: both workflows support `workflow_dispatch` with `tag_name` if you need to re-run
216220

217221
Notes
222+
218223
- If `src/dashkit_table` is ignored in `.gitignore`, use `git add -f` or remove the ignore entry
219224
- Tag patterns must match exactly as above (component-vX.Y.Z)
220225

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "dash-dashkit"
3-
version = "1.2.4"
3+
version = "1.2.5"
44
description = "Modern dashboard components for Dash applications"
55
readme = "README.md"
66
requires-python = ">=3.10"

scripts/bump_version.py

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
"shadcn": ROOT / "src" / "dashkit_shadcn" / "pyproject.toml",
1515
}
1616

17-
VERSION_RE = re.compile(r'^(\s*version\s*=\s*")(?P<ver>\d+\.\d+\.\d+)("\s*)$', re.MULTILINE)
17+
VERSION_RE = re.compile(
18+
r'^(\s*version\s*=\s*")(?P<ver>\d+\.\d+\.\d+)("\s*)$', re.MULTILINE
19+
)
1820
PKG_CONSTRAINTS = {
1921
"table": "dashkit_table",
2022
"kiboui": "dashkit_kiboui",
@@ -41,12 +43,24 @@ def parse_args():
4143
help="Prompt to select repos and bump type (default when no args)",
4244
)
4345
# Release/VC options
44-
p.add_argument("--commit", action="store_true", help="Create a Git commit with the changes")
46+
p.add_argument(
47+
"--commit", action="store_true", help="Create a Git commit with the changes"
48+
)
4549
p.add_argument("--tag", action="store_true", help="Create a Git tag after commit")
46-
p.add_argument("--tag-name", default=None, help="Explicit tag name to create (overrides prefix+version)")
47-
p.add_argument("--tag-prefix", default="v", help="Prefix for tag when --tag-name not provided (default: v)")
50+
p.add_argument(
51+
"--tag-name",
52+
default=None,
53+
help="Explicit tag name to create (overrides prefix+version)",
54+
)
55+
p.add_argument(
56+
"--tag-prefix",
57+
default="v",
58+
help="Prefix for tag when --tag-name not provided (default: v)",
59+
)
4860
p.add_argument("--push", action="store_true", help="Push commit and tag to remote")
49-
p.add_argument("--remote", default="origin", help="Remote name for push (default: origin)")
61+
p.add_argument(
62+
"--remote", default="origin", help="Remote name for push (default: origin)"
63+
)
5064
return p.parse_args()
5165

5266

@@ -100,7 +114,7 @@ def update_root_constraints(root_path: Path, new_versions: dict[str, str]) -> bo
100114
pattern = re.compile(rf'("{re.escape(pkg)}>=)(\d+\.\d+\.\d+)(")')
101115

102116
def repl(mm: re.Match) -> str:
103-
return f"{mm.group(1)}{new_ver}{mm.group(3)}"
117+
return f"{mm.group(1)}{new_ver}{mm.group(3)}" # noqa: B023
104118

105119
new_content, n = pattern.subn(repl, content)
106120
if n:
@@ -113,8 +127,14 @@ def repl(mm: re.Match) -> str:
113127

114128
# ---------------- Git helpers -----------------
115129

130+
116131
def _run_git(*args: str) -> subprocess.CompletedProcess:
117-
return subprocess.run(["git", *args], cwd=str(ROOT), stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
132+
return subprocess.run(
133+
["git", *args],
134+
cwd=str(ROOT),
135+
capture_output=True,
136+
text=True,
137+
)
118138

119139

120140
def _ensure_git_repo() -> bool:
@@ -145,9 +165,12 @@ def _git_push(remote: str, push_tags: bool) -> None:
145165

146166
# ---------------- Interactive helpers -----------------
147167

168+
148169
def _prompt_select_repos() -> list[str]:
149170
keys = list(REPOS.keys())
150-
print("Select repos to bump (comma-separated numbers or names; 'all' for all; blank to cancel):")
171+
print(
172+
"Select repos to bump (comma-separated numbers or names; 'all' for all; blank to cancel):"
173+
)
151174
for i, k in enumerate(keys, 1):
152175
print(f" {i}) {k}")
153176
sel = input("> ").strip()
@@ -175,7 +198,14 @@ def _prompt_select_repos() -> list[str]:
175198
def _prompt_bump_type() -> str:
176199
print("Select bump type [1] patch [2] minor [3] major (default: 1):")
177200
sel = input("> ").strip()
178-
mapping = {"1": "patch", "2": "minor", "3": "major", "patch": "patch", "minor": "minor", "major": "major"}
201+
mapping = {
202+
"1": "patch",
203+
"2": "minor",
204+
"3": "major",
205+
"patch": "patch",
206+
"minor": "minor",
207+
"major": "major",
208+
}
179209
if not sel:
180210
return "patch"
181211
kind = mapping.get(sel.lower())
@@ -232,7 +262,9 @@ def main():
232262
bump_type = args.type
233263
unknown = [r for r in targets if r not in REPOS]
234264
if unknown:
235-
raise SystemExit(f"Unknown repos: {', '.join(unknown)}. Valid: {', '.join(REPOS)}")
265+
raise SystemExit(
266+
f"Unknown repos: {', '.join(unknown)}. Valid: {', '.join(REPOS)}"
267+
)
236268

237269
bumped: dict[str, str] = {}
238270
changed_paths: list[Path] = []
@@ -257,7 +289,9 @@ def main():
257289
# Optionally commit, tag, and push
258290
if commit_flag or tag_flag or push_flag:
259291
if not _ensure_git_repo():
260-
raise SystemExit("Not a Git repo (or Git not available); cannot commit/tag/push.")
292+
raise SystemExit(
293+
"Not a Git repo (or Git not available); cannot commit/tag/push."
294+
)
261295
if changed_paths:
262296
_git_add(changed_paths)
263297
commit_msg_parts = [f"{name} {ver}" for name, ver in bumped.items()]
@@ -279,13 +313,15 @@ def main():
279313
tags_to_create: list[tuple[str, str]] = [] # (tag_name, message)
280314
if tag_name and len(bumped) == 1:
281315
# Single repo: respect explicit tag name
282-
only_repo = next(iter(bumped.keys()))
316+
next(iter(bumped.keys()))
283317
tn = tag_name
284318
msg = f"Release {tn} ({'; '.join(commit_msg_parts)})"
285319
tags_to_create.append((tn, msg))
286320
else:
287321
if tag_name and len(bumped) > 1:
288-
print("Note: --tag-name ignored when tagging multiple repos; using per-repo conventions.")
322+
print(
323+
"Note: --tag-name ignored when tagging multiple repos; using per-repo conventions."
324+
)
289325
for repo_key, ver in bumped.items():
290326
prefix = TAG_PREFIXES.get(repo_key, tag_prefix)
291327
tn = f"{prefix}{ver}"

src/dashkit/__init__.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"""
77

88
from pathlib import Path
9+
910
from flask import send_from_directory
1011

1112
from .buttons import PrimaryButton, SecondaryButton
@@ -45,9 +46,11 @@ def setup_app(app, assets_folder=None, include_dashkit_css: bool = True):
4546
if include_dashkit_css and pkg_assets.exists():
4647
route_attr = "_dashkit_assets_route_registered"
4748
if not getattr(app.server, route_attr, False):
49+
4850
@app.server.route("/dashkit-assets/<path:filename>")
4951
def _dashkit_assets(filename: str): # type: ignore
5052
return send_from_directory(str(pkg_assets), filename)
53+
5154
setattr(app.server, route_attr, True)
5255

5356
app.index_string = """
@@ -81,12 +84,18 @@ def _dashkit_assets(filename: str): # type: ignore
8184
</footer>
8285
</body>
8386
</html>
84-
""".replace("{dashkit_css}", "<link href=\"/dashkit-assets/style.css\" rel=\"stylesheet\">" if include_dashkit_css and pkg_assets.exists() else "")
87+
""".replace(
88+
"{dashkit_css}",
89+
'<link href="/dashkit-assets/style.css" rel="stylesheet">'
90+
if include_dashkit_css and pkg_assets.exists()
91+
else "",
92+
)
8593

8694

8795
# Resolve version dynamically from installed package metadata
8896
try:
89-
from importlib.metadata import PackageNotFoundError, version as _pkg_version
97+
from importlib.metadata import version as _pkg_version
98+
9099
__version__ = _pkg_version("dash-dashkit")
91100
except Exception:
92101
__version__ = "0.0.0"

src/dashkit/charts.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
from dashkit_shadcn import AreaChart, BarChart, ChartContainer # noqa: F401
33

44
# Re-export for convenience
5-
__all__ = ["AreaChart", "BarChart", "ChartContainer"]
5+
__all__ = ["AreaChart", "BarChart", "ChartContainer"]

src/dashkit/layout.py

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
from typing import Any
22

3+
import dash
34
import dash_mantine_components as dmc
4-
from dash import html
5+
from dash import Input, Output, callback, clientside_callback, dcc, html
56

67
from .header import create_header
78
from .sidebar import create_sidebar
9+
from .theme_manager import ThemeManager
810

911

1012
def create_layout(
1113
content: html.Div | None = None,
1214
sidebar_config: dict[str, Any] | None = None,
1315
header_config: dict[str, Any] | None = None,
1416
content_padding: str = "p-8",
17+
include_theme_manager: bool = True,
1518
) -> html.Div:
1619
"""Create the main layout with configurable sidebar and header.
1720
@@ -20,6 +23,7 @@ def create_layout(
2023
sidebar_config: Configuration for sidebar with brand name and initial
2124
header_config: Configuration for header with page_title, actions, etc.
2225
content_padding: CSS class for content padding (default: "p-8")
26+
include_theme_manager: When True, injects ThemeManager (Location + theme stores/callbacks)
2327
"""
2428
if content is None:
2529
content = html.Div(
@@ -53,6 +57,11 @@ def create_layout(
5357
return dmc.MantineProvider(
5458
html.Div(
5559
[
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={}),
5665
# Sidebar
5766
create_sidebar(
5867
brand_name=sidebar_config["brand_name"],
@@ -93,3 +102,53 @@ def create_layout(
93102
className="flex h-screen bg-white dark:bg-dashkit-surface font-sans",
94103
)
95104
)
105+
106+
107+
# Register page state callbacks at import time so apps don't need to define them
108+
@callback(
109+
[
110+
Output("page_header_config", "data"),
111+
Output("page_config", "data"),
112+
],
113+
Input("url", "pathname", allow_optional=True),
114+
)
115+
def _update_page_config(pathname: str | None):
116+
"""Update header + page config stores from Dash page registry.
117+
118+
This runs whenever the URL changes. If `url` isn't present, it will be
119+
skipped due to allow_optional.
120+
"""
121+
header_config = {"title": "Dashboard", "icon": ""}
122+
page_config = {"content_padding": "p-8"}
123+
124+
if pathname:
125+
for page in dash.page_registry.values():
126+
if page.get("path") == pathname:
127+
header_config = {
128+
"title": page.get("title", ""),
129+
"icon": page.get("icon", ""),
130+
}
131+
page_config = {"content_padding": page.get("content_padding", "p-8")}
132+
break
133+
134+
return header_config, page_config
135+
136+
137+
# Clientside callback to apply content padding class to main content container
138+
clientside_callback(
139+
r"""
140+
function(page_config) {
141+
if (page_config && page_config.content_padding) {
142+
const element = document.getElementById('main-content-container');
143+
if (element) {
144+
element.className = element.className.replace(/p-\d+|p-0/g, '');
145+
element.className = element.className + ' ' + page_config.content_padding;
146+
}
147+
}
148+
return window.dash_clientside.no_update;
149+
}
150+
""",
151+
Output("main-content-container", "className"),
152+
Input("page_config", "data"),
153+
prevent_initial_call=False,
154+
)

src/dashkit/logo.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def BrandHeader(brand_name, icon=None, subtitle=None):
7070

7171
if icon:
7272
# Check if icon is a dash_iconify icon name (contains ':') or emoji/text
73-
if ':' in icon:
73+
if ":" in icon:
7474
header_content.append(
7575
dash_iconify.DashIconify(
7676
icon=icon,

src/dashkit/markdown_report.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,17 @@ def MarkdownReport(
1212
1313
Args:
1414
content: Markdown content to render
15-
title: Optional title rendered within the report body and used in header
15+
title: Optional title rendered within the report body
1616
className: Additional CSS classes
1717
1818
Returns:
1919
html.Div: Styled markdown report component
2020
"""
2121
children: list[Any] = []
2222

23-
# If title provided, use it to update header via Store consumed by header
23+
# Render title inside the markdown when provided (header is managed globally)
2424
if title:
25-
children.append(dcc.Store(id="page_header_config", data={"title": title}))
25+
children.append(html.H1(title))
2626

2727
children.append(
2828
dcc.Markdown(content, className="prose prose-sm dark:prose-invert max-w-none")

0 commit comments

Comments
 (0)