staticdash 0.3.1__py3-none-any.whl → 0.3.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -104,12 +104,14 @@ body {
104
104
  height: auto !important;
105
105
  }
106
106
 
107
+ /* Tables */
107
108
  table {
108
109
  width: 100%;
109
110
  border-collapse: collapse;
110
111
  margin-top: 1em;
111
112
  background-color: white;
112
113
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
114
+ overflow-x: auto;
113
115
  }
114
116
 
115
117
  thead {
@@ -153,6 +155,7 @@ tbody tr:hover {
153
155
  background-color: #547a9f;
154
156
  }
155
157
 
158
+ /* Sortable Tables */
156
159
  table.sortable th {
157
160
  cursor: pointer;
158
161
  position: relative;
@@ -176,13 +179,89 @@ table.sortable th.sorted-desc::after {
176
179
  content: "\25BC";
177
180
  }
178
181
 
182
+ /* MiniPage Layout */
179
183
  .minipage-row {
180
184
  display: flex;
185
+ width: 100%;
181
186
  gap: 20px;
182
187
  margin-bottom: 20px;
188
+ box-sizing: border-box;
183
189
  }
184
190
 
185
- .minipage {
191
+ .minipage-cell {
192
+ flex: 1 1 0;
193
+ min-width: 0;
186
194
  box-sizing: border-box;
187
195
  padding: 10px;
196
+ overflow-x: auto;
197
+ }
198
+
199
+ .minipage-cell pre {
200
+ flex: 0 1 auto;
201
+ max-height: 400px;
202
+ overflow: auto;
203
+ }
204
+
205
+ .minipage-cell h4 {
206
+ margin-top: 0;
207
+ }
208
+
209
+ .minipage-cell .syntax-block {
210
+ margin-top: 0.5em;
211
+ margin-bottom: 0.5em;
212
+ }
213
+
214
+ .minipage-cell table {
215
+ display: block;
216
+ overflow-x: auto;
217
+ width: 100%;
218
+ }
219
+
220
+ /* Syntax Blocks */
221
+ .syntax-block {
222
+ position: relative;
223
+ margin: 1em 0;
224
+ background: #23272e;
225
+ border-radius: 6px;
226
+ overflow: auto;
227
+ color: #f8f8f2;
228
+ }
229
+
230
+ .syntax-block pre {
231
+ margin: 0;
232
+ padding: 1em;
233
+ background: none;
234
+ color: inherit;
235
+ font-family: 'Fira Mono', 'Consolas', monospace;
236
+ font-size: 0.95em;
237
+ }
238
+
239
+ .code-toolbar {
240
+ position: absolute;
241
+ top: 6px;
242
+ right: 12px;
243
+ z-index: 2;
244
+ }
245
+
246
+ .copy-btn,
247
+ .view-raw-btn {
248
+ color: #fff;
249
+ background: #444;
250
+ border-radius: 4px;
251
+ padding: 2px 8px;
252
+ font-size: 0.85em;
253
+ text-decoration: none;
254
+ margin-left: 4px;
255
+ cursor: pointer;
256
+ transition: background 0.2s;
257
+ }
258
+
259
+ .copy-btn:hover,
260
+ .view-raw-btn:hover {
261
+ background: #1abc9c;
262
+ }
263
+
264
+ .table-wrapper {
265
+ overflow-x: auto;
266
+ width: 100%;
188
267
  }
@@ -11,9 +11,9 @@ document.addEventListener("DOMContentLoaded", () => {
11
11
  });
12
12
  }
13
13
 
14
- function showPage(id) {
14
+ function showPage(pageId) {
15
15
  sections.forEach(section => section.style.display = "none");
16
- const page = document.getElementById(id);
16
+ const page = document.getElementById(pageId);
17
17
  if (page) {
18
18
  page.style.display = "block";
19
19
 
@@ -24,8 +24,12 @@ document.addEventListener("DOMContentLoaded", () => {
24
24
  }
25
25
 
26
26
  links.forEach(link => link.classList.remove("active"));
27
- const activeLink = Array.from(links).find(link => link.dataset.target === id);
27
+ const activeLink = Array.from(links).find(link => link.dataset.target === pageId);
28
28
  if (activeLink) activeLink.classList.add("active");
29
+
30
+ if (window.Prism && typeof Prism.highlightAll === "function") {
31
+ Prism.highlightAll();
32
+ }
29
33
  }
30
34
 
31
35
  links.forEach(link => {
@@ -91,4 +95,32 @@ document.addEventListener("DOMContentLoaded", () => {
91
95
  });
92
96
  });
93
97
  });
98
+
99
+ // Syntax block copy/view raw
100
+ document.querySelectorAll(".copy-btn").forEach(btn => {
101
+ btn.addEventListener("click", e => {
102
+ e.preventDefault();
103
+ const codeId = btn.dataset.target;
104
+ const code = document.getElementById(codeId);
105
+ if (code) {
106
+ navigator.clipboard.writeText(code.textContent);
107
+ btn.textContent = "Copied!";
108
+ setTimeout(() => { btn.textContent = "Copy"; }, 1200);
109
+ }
110
+ });
111
+ });
112
+ document.querySelectorAll(".view-raw-btn").forEach(btn => {
113
+ btn.addEventListener("click", e => {
114
+ e.preventDefault();
115
+ const codeId = btn.dataset.target;
116
+ const code = document.getElementById(codeId);
117
+ if (code) {
118
+ const win = window.open("", "_blank");
119
+ win.document.write("<pre>" + code.textContent.replace(/[<>&]/g, c => ({
120
+ '<': '&lt;', '>': '&gt;', '&': '&amp;'
121
+ }[c])) + "</pre>");
122
+ win.document.close();
123
+ }
124
+ });
125
+ });
94
126
  });
staticdash/dashboard.py CHANGED
@@ -6,6 +6,7 @@ import plotly.graph_objects as go
6
6
  from dominate import document
7
7
  from dominate.tags import div, h1, h2, h3, h4, p, a, script, link
8
8
  from dominate.util import raw as raw_util
9
+ import html
9
10
 
10
11
  class AbstractPage:
11
12
  def __init__(self):
@@ -20,8 +21,8 @@ class AbstractPage:
20
21
  self.elements.append(("text", text))
21
22
 
22
23
  def add_plot(self, plot):
23
- html = raw_util(plot.to_html(full_html=False, include_plotlyjs='cdn', config={'responsive': True}))
24
- self.elements.append(("plot", html))
24
+ html_plot = raw_util(plot.to_html(full_html=False, include_plotlyjs='cdn', config={'responsive': True}))
25
+ self.elements.append(("plot", html_plot))
25
26
 
26
27
  def add_table(self, df, table_id=None, sortable=True):
27
28
  if table_id is None:
@@ -29,8 +30,8 @@ class AbstractPage:
29
30
  classes = "table-hover table-striped"
30
31
  if sortable:
31
32
  classes += " sortable"
32
- html = df.to_html(classes=classes, index=False, border=0, table_id=table_id, escape=False)
33
- self.elements.append(("table", (html, table_id)))
33
+ html_table = df.to_html(classes=classes, index=False, border=0, table_id=table_id, escape=False)
34
+ self.elements.append(("table", (html_table, table_id)))
34
35
 
35
36
  def add_download(self, file_path, label=None):
36
37
  if not os.path.isfile(file_path):
@@ -40,6 +41,9 @@ class AbstractPage:
40
41
  def add_minipage(self, minipage):
41
42
  self.elements.append(("minipage", minipage))
42
43
 
44
+ def add_syntax(self, code, language="python"):
45
+ self.elements.append(("syntax", (code, language)))
46
+
43
47
  class Page(AbstractPage):
44
48
  def __init__(self, slug, title):
45
49
  super().__init__()
@@ -47,89 +51,96 @@ class Page(AbstractPage):
47
51
  self.title = title
48
52
  self.add_header(title, level=1)
49
53
 
50
- def render(self, index):
51
- section = div()
52
- minipage_row = []
54
+ def render(self, index, downloads_dir=None, relative_prefix=""):
55
+ elements = []
53
56
  for kind, content in self.elements:
54
57
  if kind == "minipage":
55
58
  row_div = div(cls="minipage-row")
56
- row_div += content.render(index)
57
- section += row_div
59
+ row_div += content.render(index, downloads_dir=downloads_dir, relative_prefix=relative_prefix)
60
+ elements.append(row_div)
58
61
  elif kind == "header":
59
62
  text, level = content
60
- if level == 1:
61
- section += h1(text)
62
- elif level == 2:
63
- section += h2(text)
64
- elif level == 3:
65
- section += h3(text)
66
- elif level == 4:
67
- section += h4(text)
63
+ elements.append({1: h1, 2: h2, 3: h3, 4: h4}[level](text))
68
64
  elif kind == "text":
69
- section += p(content)
65
+ elements.append(p(content))
70
66
  elif kind == "plot":
71
- section += div(content, cls="plot-container")
67
+ elements.append(div(content, cls="plot-container"))
72
68
  elif kind == "table":
73
69
  table_html, _ = content
74
- section += raw_util(table_html)
70
+ wrapped = div(raw_util(table_html), cls="table-wrapper")
71
+ elements.append(wrapped)
75
72
  elif kind == "download":
76
73
  file_path, label = content
74
+ file_uuid = f"{uuid.uuid4().hex}_{os.path.basename(file_path)}"
75
+ dst_path = os.path.join(downloads_dir, file_uuid)
76
+ shutil.copy2(file_path, dst_path)
77
77
  btn = a(label or os.path.basename(file_path),
78
- href=file_path,
78
+ href=f"{relative_prefix}downloads/{file_uuid}",
79
79
  cls="download-button",
80
80
  download=True)
81
- section += div(btn)
82
- return section
81
+ elements.append(div(btn))
82
+ elif kind == "syntax":
83
+ code, language = content
84
+ code_id = f"code-{uuid.uuid4().hex[:8]}"
85
+ toolbar = div(cls="code-toolbar")
86
+ toolbar += a("Copy", href="#", cls="copy-btn", **{"data-target": code_id})
87
+ toolbar += a("View Raw", href="#", cls="view-raw-btn", **{"data-target": code_id, "style": "margin-left:10px;"})
88
+ escaped_code = html.escape(code)
89
+ block_wrapper = div(
90
+ div(
91
+ toolbar,
92
+ raw_util(f'<pre><code id="{code_id}" class="language-{language}">{escaped_code}</code></pre>'),
93
+ cls="syntax-block"
94
+ )
95
+ )
96
+ elements.append(block_wrapper)
97
+ return elements
83
98
 
84
99
  class MiniPage(AbstractPage):
85
- def __init__(self, width=1.0):
100
+ def __init__(self):
86
101
  super().__init__()
87
- self.width = width
88
102
 
89
- def render(self, index=None):
90
- style = f"flex: 0 0 {self.width * 100}%; max-width: {self.width * 100}%;"
91
- container = div(cls="minipage", style=style)
92
- minipage_row = []
103
+ def render(self, index=None, downloads_dir=None, relative_prefix=""):
104
+ row_div = div(cls="minipage-row")
93
105
  for kind, content in self.elements:
94
- if kind == "minipage":
95
- minipage_row.append(content)
96
- else:
97
- if minipage_row:
98
- row_div = div(cls="minipage-row")
99
- for mp in minipage_row:
100
- row_div += mp.render(index)
101
- container += row_div
102
- minipage_row = []
103
- if kind == "header":
104
- text, level = content
105
- if level == 1:
106
- container += h1(text)
107
- elif level == 2:
108
- container += h2(text)
109
- elif level == 3:
110
- container += h3(text)
111
- elif level == 4:
112
- container += h4(text)
113
- elif kind == "text":
114
- container += p(content)
115
- elif kind == "plot":
116
- container += div(content, cls="plot-container")
117
- elif kind == "table":
118
- table_html, _ = content
119
- container += raw_util(table_html)
120
- elif kind == "download":
121
- file_path, label = content
122
- btn = a(label or os.path.basename(file_path),
123
- href=file_path,
124
- cls="download-button",
125
- download=True)
126
- container += div(btn)
127
- if minipage_row:
128
- row_div = div(cls="minipage-row")
129
- for mp in minipage_row:
130
- row_div += mp.render(index)
131
- container += row_div
132
- return container
106
+ cell = div(cls="minipage-cell")
107
+ if kind == "header":
108
+ text, level = content
109
+ cell += {1: h1, 2: h2, 3: h3, 4: h4}[level](text)
110
+ elif kind == "text":
111
+ cell += p(content)
112
+ elif kind == "plot":
113
+ cell += div(content, cls="plot-container")
114
+ elif kind == "table":
115
+ table_html, _ = content
116
+ cell += div(raw_util(table_html), cls="table-wrapper")
117
+ elif kind == "download":
118
+ file_path, label = content
119
+ file_uuid = f"{uuid.uuid4().hex}_{os.path.basename(file_path)}"
120
+ dst_path = os.path.join(downloads_dir, file_uuid)
121
+ shutil.copy2(file_path, dst_path)
122
+ btn = a(label or os.path.basename(file_path),
123
+ href=f"{relative_prefix}downloads/{file_uuid}",
124
+ cls="download-button",
125
+ download=True)
126
+ cell += div(btn)
127
+ elif kind == "syntax":
128
+ code, language = content
129
+ code_id = f"code-{uuid.uuid4().hex[:8]}"
130
+ toolbar = div(cls="code-toolbar")
131
+ toolbar += a("Copy", href="#", cls="copy-btn", **{"data-target": code_id})
132
+ toolbar += a("View Raw", href="#", cls="view-raw-btn", **{"data-target": code_id, "style": "margin-left:10px;"})
133
+ escaped_code = html.escape(code)
134
+ block_wrapper = div(
135
+ div(
136
+ toolbar,
137
+ raw_util(f'<pre><code id="{code_id}" class="language-{language}">{escaped_code}</code></pre>'),
138
+ cls="syntax-block"
139
+ )
140
+ )
141
+ cell += block_wrapper
142
+ row_div += cell
143
+ return row_div
133
144
 
134
145
  class Dashboard:
135
146
  def __init__(self, title="Dashboard"):
@@ -150,42 +161,34 @@ class Dashboard:
150
161
  os.makedirs(downloads_dir, exist_ok=True)
151
162
  shutil.copytree(assets_src, assets_dst, dirs_exist_ok=True)
152
163
 
164
+ # Per-page HTML
153
165
  for page in self.pages:
154
166
  doc = document(title=page.title)
155
167
  with doc.head:
156
168
  doc.head.add(link(rel="stylesheet", href="../assets/css/style.css"))
157
169
  doc.head.add(script(type="text/javascript", src="../assets/js/script.js"))
158
170
  doc.head.add(script(src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"))
159
-
171
+ doc.head.add(link(rel="stylesheet", href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-tomorrow.min.css"))
172
+ doc.head.add(script(src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/prism.min.js"))
173
+ doc.head.add(script(src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-python.min.js"))
174
+ doc.head.add(script(src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-javascript.min.js"))
160
175
  with doc:
161
- with div(cls="page-section", id=f"page-{page.slug}"):
162
- for kind, content in page.elements:
163
- if kind == "header":
164
- text, level = content
165
- if level == 1:
166
- h1(text)
167
- elif level == 2:
168
- h2(text)
169
- elif level == 3:
170
- h3(text)
171
- elif level == 4:
172
- h4(text)
173
- elif kind == "text":
174
- p(content)
175
- elif kind == "plot":
176
- div(content, cls="plot-container")
177
- elif kind == "table":
178
- table_html, _ = content
179
- doc.add(raw_util(table_html))
180
-
176
+ with div(cls="page-section", id=f"page-{page.slug}") as section:
177
+ for el in page.render(0, downloads_dir=downloads_dir, relative_prefix="../"):
178
+ section += el
181
179
  with open(os.path.join(pages_dir, f"{page.slug}.html"), "w") as f:
182
180
  f.write(str(doc))
183
181
 
182
+ # Main index.html
184
183
  index_doc = document(title=self.title)
185
184
  with index_doc.head:
186
185
  index_doc.head.add(link(rel="stylesheet", href="assets/css/style.css"))
187
186
  index_doc.head.add(script(type="text/javascript", src="assets/js/script.js"))
188
187
  index_doc.head.add(script(src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"))
188
+ index_doc.head.add(link(rel="stylesheet", href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-tomorrow.min.css"))
189
+ index_doc.head.add(script(src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/prism.min.js"))
190
+ index_doc.head.add(script(src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-python.min.js"))
191
+ index_doc.head.add(script(src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-javascript.min.js"))
189
192
 
190
193
  with index_doc:
191
194
  with div(id="sidebar"):
@@ -198,8 +201,8 @@ class Dashboard:
198
201
  with div(id="content"):
199
202
  for idx, page in enumerate(self.pages):
200
203
  with div(id=f"page-{page.slug}", cls="page-section", style="display:none;") as section:
201
- rendered = page.render(idx)
202
- section += rendered # This is correct
204
+ for el in page.render(idx, downloads_dir=downloads_dir, relative_prefix=""):
205
+ section += el
203
206
 
204
207
  with open(os.path.join(output_dir, "index.html"), "w") as f:
205
208
  f.write(str(index_doc))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: staticdash
3
- Version: 0.3.1
3
+ Version: 0.3.3
4
4
  Summary: A lightweight static HTML dashboard generator with Plotly and pandas support.
5
5
  Author-email: Brian Day <brian.day1@gmail.com>
6
6
  License: CC0-1.0
@@ -0,0 +1,8 @@
1
+ staticdash/__init__.py,sha256=KqViaDkiQnhBI8-j3hr14umLDmPgddvdB_G1nJeC5Xs,38
2
+ staticdash/dashboard.py,sha256=lJMRt-3kGucPqGak6bty2y4xcTS4xhRYISoj9YGNVWY,9781
3
+ staticdash/assets/css/style.css,sha256=I0IAJoAxyPjx5DHD4f_D0xKgFGtZOpnOUlD2qomCVfw,3978
4
+ staticdash/assets/js/script.js,sha256=pXqvTyw1LRNYl5u1kQbRtQkwe0dnd1yDs1Jstj7S-GU,4148
5
+ staticdash-0.3.3.dist-info/METADATA,sha256=rCwXd558UO6Gou5pFqfEFan5NyVJzYgtaHCZsnjB7fc,1776
6
+ staticdash-0.3.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
+ staticdash-0.3.3.dist-info/top_level.txt,sha256=3MzZU6SptkUkjcHV1cvPji0H4aRzPphLHnpStgGEcxM,11
8
+ staticdash-0.3.3.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- staticdash/__init__.py,sha256=KqViaDkiQnhBI8-j3hr14umLDmPgddvdB_G1nJeC5Xs,38
2
- staticdash/dashboard.py,sha256=OwQtURb0E5DNm4lyvAzy6WlAqfcFoXsmSIVDwrzuc2U,8212
3
- staticdash/assets/css/style.css,sha256=z3bC-QjAb9f5E3HUkr6R81qQyAfKpMW48yjy11CYH-Q,2779
4
- staticdash/assets/js/script.js,sha256=SMOyh7_E_BlvLYMEwYlvCnV7-GnMR-x8PtEiFbIIAsw,3089
5
- staticdash-0.3.1.dist-info/METADATA,sha256=kXmv9eqBZU0MuQMPfAQGKkWZEaqzxtqRBQMig_3tysk,1776
6
- staticdash-0.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
- staticdash-0.3.1.dist-info/top_level.txt,sha256=3MzZU6SptkUkjcHV1cvPji0H4aRzPphLHnpStgGEcxM,11
8
- staticdash-0.3.1.dist-info/RECORD,,