better-rtplot 0.5.0__tar.gz → 0.6.0__tar.gz
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.
- {better_rtplot-0.5.0 → better_rtplot-0.6.0}/PKG-INFO +2 -1
- {better_rtplot-0.5.0 → better_rtplot-0.6.0}/README.md +1 -0
- {better_rtplot-0.5.0 → better_rtplot-0.6.0}/pyproject.toml +1 -1
- {better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/client.py +22 -7
- {better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/server_browser.py +5 -1
- {better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/index.html +90 -32
- better_rtplot-0.6.0/rtplot/static/presentation.css +168 -0
- {better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/ui-view.js +51 -9
- {better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/ui_state.py +64 -3
- {better_rtplot-0.5.0 → better_rtplot-0.6.0}/LICENSE +0 -0
- {better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/interactive_test.py +0 -0
- {better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/saved_plots/.gitignore +0 -0
- {better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/server.py +0 -0
- {better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/server_browser_gui.py +0 -0
- {better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/auto-render.min.js +0 -0
- {better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- {better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- {better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- {better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- {better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- {better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- {better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- {better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- {better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- {better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- {better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- {better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- {better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- {better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- {better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- {better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- {better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- {better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- {better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- {better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- {better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/katex.min.css +0 -0
- {better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/katex.min.js +0 -0
- {better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/uPlot.iife.min.js +0 -0
- {better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/uPlot.min.css +0 -0
- {better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/ui-view.css +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: better-rtplot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary:
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -132,6 +132,7 @@ A rising line now draws itself in the browser tab.
|
|
|
132
132
|
- **Remote-friendly.** Sender or plot host can bind. Live Bind /
|
|
133
133
|
Connect buttons retarget without restart.
|
|
134
134
|
- **Config lives with the data.** The sender declares plot layout.
|
|
135
|
+
- **Responsive presentation.** Native phone sizing, essential actions, compact navigation, and explicit semantic styles. See the [presentation guide](docs/presentation.md) and [before/after screenshots](docs/presentation-screenshots/README.md).
|
|
135
136
|
- **Sections and runtime state.** Optional collapsible sections, persistent status controls, and presentation updates that preserve streaming history. See the [section API](docs/sections.md) and [workflow example](examples/06_sections/README.md).
|
|
136
137
|
- **Interactive controls.** Buttons, sliders, dials, displays — polled
|
|
137
138
|
from your loop, no threads, no callbacks.
|
|
@@ -107,6 +107,7 @@ A rising line now draws itself in the browser tab.
|
|
|
107
107
|
- **Remote-friendly.** Sender or plot host can bind. Live Bind /
|
|
108
108
|
Connect buttons retarget without restart.
|
|
109
109
|
- **Config lives with the data.** The sender declares plot layout.
|
|
110
|
+
- **Responsive presentation.** Native phone sizing, essential actions, compact navigation, and explicit semantic styles. See the [presentation guide](docs/presentation.md) and [before/after screenshots](docs/presentation-screenshots/README.md).
|
|
110
111
|
- **Sections and runtime state.** Optional collapsible sections, persistent status controls, and presentation updates that preserve streaming history. See the [section API](docs/sections.md) and [workflow example](examples/06_sections/README.md).
|
|
111
112
|
- **Interactive controls.** Buttons, sliders, dials, displays — polled
|
|
112
113
|
from your loop, no threads, no callbacks.
|
|
@@ -3,7 +3,7 @@ import numpy as np
|
|
|
3
3
|
import time
|
|
4
4
|
import uuid
|
|
5
5
|
import warnings
|
|
6
|
-
from .ui_state import META_KEY, declarations, merge_state
|
|
6
|
+
from .ui_state import META_KEY, declarations, merge_state, presentation_requested
|
|
7
7
|
from collections import OrderedDict, namedtuple
|
|
8
8
|
from dataclasses import dataclass, field
|
|
9
9
|
from typing import List, Optional, Tuple, Union
|
|
@@ -162,6 +162,7 @@ class Plot:
|
|
|
162
162
|
xrange: Optional[int] = None
|
|
163
163
|
height: Optional[float] = None
|
|
164
164
|
section: Optional[str] = None
|
|
165
|
+
min_height: Optional[int] = None
|
|
165
166
|
|
|
166
167
|
def to_dict(self):
|
|
167
168
|
return _drop_none({
|
|
@@ -176,7 +177,7 @@ class Plot:
|
|
|
176
177
|
"yrange": _range_to_list(self.yrange),
|
|
177
178
|
"xrange": self.xrange,
|
|
178
179
|
"height": self.height,
|
|
179
|
-
"section": self.section,
|
|
180
|
+
"section": self.section, "min_height": self.min_height,
|
|
180
181
|
})
|
|
181
182
|
|
|
182
183
|
|
|
@@ -209,11 +210,12 @@ class Button:
|
|
|
209
210
|
label: str
|
|
210
211
|
color: Optional[str] = None
|
|
211
212
|
height: Optional[float] = None
|
|
213
|
+
appearance: Optional[str] = None
|
|
212
214
|
|
|
213
215
|
def to_dict(self):
|
|
214
216
|
return _drop_none({
|
|
215
217
|
"type": "button", "id": self.id, "label": self.label,
|
|
216
|
-
"color": self.color, "height": self.height,
|
|
218
|
+
"color": self.color, "height": self.height, "appearance": self.appearance,
|
|
217
219
|
})
|
|
218
220
|
|
|
219
221
|
|
|
@@ -267,11 +269,12 @@ class Display:
|
|
|
267
269
|
label: str
|
|
268
270
|
format: Optional[str] = None
|
|
269
271
|
height: Optional[float] = None
|
|
272
|
+
appearance: Optional[str] = None
|
|
270
273
|
|
|
271
274
|
def to_dict(self):
|
|
272
275
|
return _drop_none({
|
|
273
276
|
"type": "display", "id": self.id, "label": self.label,
|
|
274
|
-
"format": self.format, "height": self.height,
|
|
277
|
+
"format": self.format, "height": self.height, "appearance": self.appearance,
|
|
275
278
|
})
|
|
276
279
|
|
|
277
280
|
|
|
@@ -281,11 +284,12 @@ class Text:
|
|
|
281
284
|
label: str
|
|
282
285
|
value: str = ""
|
|
283
286
|
height: Optional[float] = None
|
|
287
|
+
appearance: Optional[str] = None
|
|
284
288
|
|
|
285
289
|
def to_dict(self):
|
|
286
290
|
return _drop_none({
|
|
287
291
|
"type": "text", "id": self.id, "label": self.label,
|
|
288
|
-
"value": self.value, "height": self.height,
|
|
292
|
+
"value": self.value, "height": self.height, "appearance": self.appearance,
|
|
289
293
|
})
|
|
290
294
|
|
|
291
295
|
|
|
@@ -312,12 +316,14 @@ class ControlsRow:
|
|
|
312
316
|
controls: List[Union[Button, Slider, Dial, Display, Text, TextInput, dict]] = field(default_factory=list)
|
|
313
317
|
|
|
314
318
|
section: Optional[str] = None
|
|
319
|
+
title: Optional[str] = None
|
|
320
|
+
exclusive: Optional[bool] = None
|
|
315
321
|
|
|
316
322
|
def to_dict(self):
|
|
317
323
|
return _drop_none({"controls": [
|
|
318
324
|
c.to_dict() if hasattr(c, "to_dict") else c
|
|
319
325
|
for c in self.controls
|
|
320
|
-
], "section": self.section})
|
|
326
|
+
], "section": self.section, "title": self.title, "exclusive": self.exclusive})
|
|
321
327
|
|
|
322
328
|
|
|
323
329
|
@dataclass
|
|
@@ -328,6 +334,9 @@ class Section:
|
|
|
328
334
|
collapsible: bool = False
|
|
329
335
|
collapsed: bool = False
|
|
330
336
|
visible: bool = True
|
|
337
|
+
navigation: str = "primary"
|
|
338
|
+
density: str = "normal"
|
|
339
|
+
show_heading: bool = True
|
|
331
340
|
|
|
332
341
|
def to_dict(self):
|
|
333
342
|
return dict(vars(self))
|
|
@@ -337,11 +346,12 @@ class Section:
|
|
|
337
346
|
class View:
|
|
338
347
|
sections: List[Union[Section, dict]]
|
|
339
348
|
persistent_section: Optional[str] = None
|
|
349
|
+
essential_controls: Optional[List[str]] = None
|
|
340
350
|
|
|
341
351
|
def to_dict(self):
|
|
342
352
|
return _drop_none({"sections": [s.to_dict() if hasattr(s, "to_dict") else s
|
|
343
353
|
for s in self.sections],
|
|
344
|
-
"persistent_section": self.persistent_section})
|
|
354
|
+
"persistent_section": self.persistent_section, "essential_controls": self.essential_controls})
|
|
345
355
|
|
|
346
356
|
|
|
347
357
|
def local_plot():
|
|
@@ -600,6 +610,11 @@ def initialize_plots(plot_descriptions=1, handshake_timeout=2.0, *, view=None, u
|
|
|
600
610
|
"confirm support: layout may be flat and disabled states are NOT enforced.",
|
|
601
611
|
RuntimeWarning, stacklevel=2)
|
|
602
612
|
|
|
613
|
+
if presentation_requested(plot_desc_dict, _view) and "presentation_v1" not in _server_capabilities:
|
|
614
|
+
warnings.warn("Presentation options require rtplot browser server >= 0.6.0. This server did not "
|
|
615
|
+
"confirm support; essential placement, groups, and styling may be ignored.",
|
|
616
|
+
RuntimeWarning, stacklevel=2)
|
|
617
|
+
|
|
603
618
|
|
|
604
619
|
def _send_initialize_with_handshake(cfg, timeout):
|
|
605
620
|
"""Publish ``cfg`` and block until the server acks it.
|
|
@@ -622,6 +622,8 @@ def control_allowed(tab, payload, kind):
|
|
|
622
622
|
state = {**CONTROL_DEFAULTS, **tab.ui_state.get("controls", {}).get(cid, {})}
|
|
623
623
|
if not state["enabled"] or not state["visible"]:
|
|
624
624
|
return False
|
|
625
|
+
if cid in tab.view.get("essential_controls", []):
|
|
626
|
+
return True
|
|
625
627
|
sid = info.get("section")
|
|
626
628
|
section = next((s for s in tab.view.get("sections", []) if s["id"] == sid), {})
|
|
627
629
|
return tab.ui_state.get("sections", {}).get(sid, {}).get("visible", section.get("visible", True))
|
|
@@ -649,7 +651,8 @@ def _expand_config(config):
|
|
|
649
651
|
plots.append((f"{key}/{i}", child))
|
|
650
652
|
layout.append({"kind": "row", "columns": columns, "plots": indices, "section": entry.get("section")})
|
|
651
653
|
elif "controls" in entry:
|
|
652
|
-
layout.append({"kind": "controls", "index": len(controls), "section": entry.get("section")
|
|
654
|
+
layout.append({"kind": "controls", "index": len(controls), "section": entry.get("section"),
|
|
655
|
+
"title": entry.get("title"), "exclusive": entry.get("exclusive", False)})
|
|
653
656
|
controls.append(entry["controls"])
|
|
654
657
|
else:
|
|
655
658
|
layout.append({"kind": "plot", "index": len(plots), "section": entry.get("section")})
|
|
@@ -1574,6 +1577,7 @@ _INDEX_HTML = _read_static_asset("index.html")
|
|
|
1574
1577
|
for _asset in (
|
|
1575
1578
|
"ui-view.js",
|
|
1576
1579
|
"ui-view.css",
|
|
1580
|
+
"presentation.css",
|
|
1577
1581
|
"uPlot.min.css",
|
|
1578
1582
|
"uPlot.iife.min.js",
|
|
1579
1583
|
"katex/katex.min.css",
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
5
6
|
<title>rtplot</title>
|
|
6
7
|
<link rel="stylesheet" href="/static/uPlot.min.css" />
|
|
7
8
|
<link rel="stylesheet" href="/static/ui-view.css" />
|
|
@@ -143,6 +144,7 @@
|
|
|
143
144
|
.res-stats .rs-tab .rs-hz { color: #2a5db0; font-weight: 500; }
|
|
144
145
|
.res-unavail { font-size: calc(11px * var(--ui-scale)); color: #999; font-style: italic; }
|
|
145
146
|
</style>
|
|
147
|
+
<link rel="stylesheet" href="/static/presentation.css" />
|
|
146
148
|
</head>
|
|
147
149
|
<body>
|
|
148
150
|
<div id="tabbar"></div>
|
|
@@ -154,12 +156,13 @@
|
|
|
154
156
|
</div>
|
|
155
157
|
<div id="header">
|
|
156
158
|
<h1>rtplot</h1>
|
|
157
|
-
<div id="status" class="green">Rate: -- Hz</div>
|
|
158
|
-
<div id="zmq-mode">ZMQ: --</div>
|
|
159
159
|
<div id="ws-status">connecting...</div>
|
|
160
160
|
<button id="menu-btn" class="btn" title="Settings" aria-label="Settings">☰</button>
|
|
161
161
|
</div>
|
|
162
162
|
<div id="menu-panel" aria-hidden="true">
|
|
163
|
+
<h2>Connection and performance</h2>
|
|
164
|
+
<div id="status" class="green">Rate: -- Hz</div>
|
|
165
|
+
<div id="zmq-mode">ZMQ: --</div>
|
|
163
166
|
<h2>Settings</h2>
|
|
164
167
|
<div class="menu-row">
|
|
165
168
|
<label for="menu-font">UI font scale</label>
|
|
@@ -391,12 +394,25 @@
|
|
|
391
394
|
}
|
|
392
395
|
menuBtn.addEventListener('click', (e) => {
|
|
393
396
|
e.stopPropagation();
|
|
394
|
-
menuPanel.classList.toggle('open');
|
|
397
|
+
const open = menuPanel.classList.toggle('open');
|
|
398
|
+
const top = Math.max(document.getElementById('header').getBoundingClientRect().bottom,
|
|
399
|
+
viewManager?.sticky.getBoundingClientRect().bottom || 0) + 8;
|
|
400
|
+
menuPanel.style.top = `${top}px`;
|
|
401
|
+
menuPanel.style.maxHeight = `calc(100dvh - ${top + 8}px)`;
|
|
402
|
+
menuPanel.setAttribute('aria-hidden', String(!open));
|
|
403
|
+
menuBtn.setAttribute('aria-expanded', String(open));
|
|
404
|
+
});
|
|
405
|
+
document.addEventListener('keydown', e => {
|
|
406
|
+
if (e.key === 'Escape' && menuPanel.classList.contains('open')) {
|
|
407
|
+
menuPanel.classList.remove('open'); menuPanel.setAttribute('aria-hidden', 'true');
|
|
408
|
+
menuBtn.setAttribute('aria-expanded', 'false'); menuBtn.focus();
|
|
409
|
+
}
|
|
395
410
|
});
|
|
396
411
|
document.addEventListener('click', (e) => {
|
|
397
412
|
if (!menuPanel.classList.contains('open')) return;
|
|
398
413
|
if (menuPanel.contains(e.target) || menuBtn.contains(e.target)) return;
|
|
399
414
|
menuPanel.classList.remove('open');
|
|
415
|
+
menuPanel.setAttribute('aria-hidden', 'true'); menuBtn.setAttribute('aria-expanded', 'false');
|
|
400
416
|
});
|
|
401
417
|
menuFontInput.addEventListener('input', () => {
|
|
402
418
|
settings.fontScale = Number(menuFontInput.value);
|
|
@@ -445,14 +461,22 @@
|
|
|
445
461
|
let activeLayout = null;
|
|
446
462
|
const controlElements = { displays: {}, displayFormats: {}, displayKinds: {}, sliders: {}, textInputs: {} };
|
|
447
463
|
|
|
464
|
+
const presentationTargets = '.section-nav,.section-header,#header,#menu-panel,#tabbar,.uplot';
|
|
465
|
+
let presentationPointer = false;
|
|
466
|
+
document.addEventListener('pointerdown', e => {
|
|
467
|
+
presentationPointer = Boolean(e.target.closest(presentationTargets));
|
|
468
|
+
setTimeout(() => { presentationPointer = false; }, 0);
|
|
469
|
+
}, true);
|
|
448
470
|
function sendCtrl(msg) {
|
|
449
471
|
if (msg.type.startsWith('control_')) {
|
|
450
|
-
if (!viewManager || !viewManager.canInteract(msg.id)) return;
|
|
472
|
+
if (!viewManager || !viewManager.canInteract(msg.id)) return false;
|
|
451
473
|
msg = {...msg, session: activeLayout?.session, generation: activeLayout?.generation};
|
|
452
474
|
}
|
|
453
475
|
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
454
476
|
socket.send(JSON.stringify(msg));
|
|
477
|
+
return true;
|
|
455
478
|
}
|
|
479
|
+
return false;
|
|
456
480
|
}
|
|
457
481
|
|
|
458
482
|
function parseDisplayFormat(fmt) {
|
|
@@ -678,7 +702,7 @@
|
|
|
678
702
|
const b = document.createElement('button');
|
|
679
703
|
b.className = 'ctrl-btn';
|
|
680
704
|
b.textContent = el.label || el.id;
|
|
681
|
-
if (el.color != null) {
|
|
705
|
+
if (el.color != null && (!el.appearance || el.appearance === 'default')) {
|
|
682
706
|
const c = resolveColor(el.color);
|
|
683
707
|
b.style.backgroundColor = c;
|
|
684
708
|
b.style.borderColor = c;
|
|
@@ -735,9 +759,16 @@
|
|
|
735
759
|
if (el.max_length !== undefined && Number.isFinite(Number(el.max_length))) {
|
|
736
760
|
input.maxLength = Number(el.max_length);
|
|
737
761
|
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
762
|
+
let committed = input.value;
|
|
763
|
+
const commit = () => {
|
|
764
|
+
if (input.value === committed) return;
|
|
765
|
+
if (sendCtrl({ type: 'control_text', id: el.id, value: input.value })) committed = input.value;
|
|
766
|
+
};
|
|
767
|
+
input.addEventListener('blur', e => {
|
|
768
|
+
// Moving into presentation UI must preserve a draft without sending it.
|
|
769
|
+
if (presentationPointer || e.relatedTarget?.closest(presentationTargets)) return;
|
|
770
|
+
commit();
|
|
771
|
+
});
|
|
741
772
|
input.addEventListener('keydown', (e) => {
|
|
742
773
|
if (e.key === 'Enter') {
|
|
743
774
|
e.preventDefault();
|
|
@@ -765,12 +796,20 @@
|
|
|
765
796
|
}
|
|
766
797
|
}
|
|
767
798
|
|
|
768
|
-
function buildControlRow(row, section) {
|
|
799
|
+
function buildControlRow(row, section, options={}) {
|
|
769
800
|
const div = document.createElement('div');
|
|
770
801
|
div.className = 'ctrl-row';
|
|
802
|
+
if (options.title) {
|
|
803
|
+
const title = document.createElement('h3'); title.className = 'control-group-title'; title.textContent = options.title;
|
|
804
|
+
div.appendChild(title); div.setAttribute('role', 'group'); div.setAttribute('aria-label', options.title);
|
|
805
|
+
}
|
|
806
|
+
if (options.exclusive) div.classList.add('exclusive-group');
|
|
771
807
|
row.forEach(el => {
|
|
772
808
|
const item = buildControlElement(el);
|
|
773
809
|
applyElementSize(item, el);
|
|
810
|
+
item.dataset.appearance = el.appearance || 'default';
|
|
811
|
+
if (el.appearance === 'state') item.setAttribute('role', 'status');
|
|
812
|
+
item.querySelectorAll('input,svg.ctrl-dial').forEach(input => input.setAttribute('aria-label', el.label || el.id));
|
|
774
813
|
div.appendChild(item);
|
|
775
814
|
viewManager.register(el.id, item, section);
|
|
776
815
|
});
|
|
@@ -850,11 +889,11 @@
|
|
|
850
889
|
|
|
851
890
|
const series = [{}];
|
|
852
891
|
for (let t = 0; t < traceCount; t++) {
|
|
853
|
-
const dash = (
|
|
892
|
+
const dash = ({'-': [10, 5], dashed: [10, 5], dotted: [2, 4], dashdot: [10, 4, 2, 4]})[styles[t]] || null;
|
|
854
893
|
series.push({
|
|
855
894
|
label: pcfg.names[t],
|
|
856
|
-
stroke: resolveColor(colors[t]),
|
|
857
|
-
width: widths[t] || 1,
|
|
895
|
+
stroke: resolveColor(colors[t] || DEFAULT_COLORS[t % DEFAULT_COLORS.length]),
|
|
896
|
+
width: (Array.isArray(widths) ? widths[t] : widths) || 1,
|
|
858
897
|
dash: dash || undefined,
|
|
859
898
|
points: { show: false },
|
|
860
899
|
spanGaps: true,
|
|
@@ -864,16 +903,15 @@
|
|
|
864
903
|
|
|
865
904
|
const baseHeight = rowLayout ? 260 : 320;
|
|
866
905
|
const heightMul = Number(pcfg.height);
|
|
867
|
-
const plotHeight = (Number.isFinite(heightMul) && heightMul > 0)
|
|
868
|
-
? Math.round(baseHeight * heightMul)
|
|
869
|
-
: baseHeight;
|
|
906
|
+
const plotHeight = Math.max(pcfg.min_height || 160, (Number.isFinite(heightMul) && heightMul > 0)
|
|
907
|
+
? Math.round(baseHeight * heightMul) : baseHeight);
|
|
870
908
|
const wrap = document.createElement('div');
|
|
871
909
|
wrap.className = 'plot-wrap';
|
|
872
910
|
parent.appendChild(wrap);
|
|
873
911
|
|
|
874
912
|
let plotState = null;
|
|
875
913
|
const opts = {
|
|
876
|
-
width: Math.max(
|
|
914
|
+
width: Math.max(120, wrap.clientWidth - 16),
|
|
877
915
|
height: plotHeight,
|
|
878
916
|
title: pcfg.title || '',
|
|
879
917
|
scales: {
|
|
@@ -886,6 +924,14 @@
|
|
|
886
924
|
],
|
|
887
925
|
series: series,
|
|
888
926
|
legend: { show: true },
|
|
927
|
+
hooks: { ready: [u => u.root.querySelectorAll('.u-legend .u-series').forEach((tr, i) => {
|
|
928
|
+
if (!i) return;
|
|
929
|
+
if (traceCount === 1 && pcfg.title === pcfg.names[0]) tr.querySelector('.u-label').classList.add('sr-only');
|
|
930
|
+
tr.tabIndex = 0; tr.setAttribute('role', 'button'); tr.setAttribute('aria-pressed', String(u.series[i].show)); tr.setAttribute('aria-label', `Toggle ${pcfg.names[i-1]}`);
|
|
931
|
+
tr.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); tr.querySelector('.u-label').click(); } });
|
|
932
|
+
})], setSeries: [u => u.root.querySelectorAll('.u-legend .u-series').forEach((tr, i) => {
|
|
933
|
+
if (i) tr.setAttribute('aria-pressed', String(u.series[i].show));
|
|
934
|
+
})] },
|
|
889
935
|
cursor: { drag: { x: false, y: false } },
|
|
890
936
|
};
|
|
891
937
|
|
|
@@ -929,7 +975,7 @@
|
|
|
929
975
|
plots.forEach(p => {
|
|
930
976
|
const visible = p.wrap.getClientRects().length > 0;
|
|
931
977
|
if (visible) {
|
|
932
|
-
const width = Math.max(
|
|
978
|
+
const width = Math.max(120, p.wrap.clientWidth - 16);
|
|
933
979
|
if (!p.visible || Math.abs(p.uplot.width - width) > 1) {
|
|
934
980
|
p.uplot.setSize({width, height: p.height});
|
|
935
981
|
p.dataDirty = true; p.forceKeepScales = true;
|
|
@@ -956,6 +1002,7 @@
|
|
|
956
1002
|
function newRow(count, parent) {
|
|
957
1003
|
const row = document.createElement('div'); row.className = 'plot-row';
|
|
958
1004
|
row.style.setProperty('--plot-columns', Math.max(1, Math.floor(count)));
|
|
1005
|
+
row.style.setProperty('--compact-columns', Math.max(1, Math.min(2, Math.floor(count))));
|
|
959
1006
|
parent.appendChild(row); return row;
|
|
960
1007
|
}
|
|
961
1008
|
// Trace offsets come ONLY from wire declaration order, never section/DOM order.
|
|
@@ -976,9 +1023,10 @@
|
|
|
976
1023
|
entry.plots.forEach(index => buildOnePlot(cfg.plots[index], entry.columns === 1, offsets[index], row));
|
|
977
1024
|
} else if (entry.kind === 'controls') {
|
|
978
1025
|
implicitRows.delete(parent);
|
|
979
|
-
parent.appendChild(buildControlRow(controls[entry.index], entry.section));
|
|
1026
|
+
parent.appendChild(buildControlRow(controls[entry.index], entry.section, entry));
|
|
980
1027
|
}
|
|
981
1028
|
});
|
|
1029
|
+
viewManager.finish();
|
|
982
1030
|
applySliderValues(cfg.slider_values); applyTextValues(cfg.text_values); applyDisplayValues(cfg.display_values);
|
|
983
1031
|
viewManager.apply(cfg.ui_state || {}, cfg.ui_revision ?? 0);
|
|
984
1032
|
renderLatexIn(plotsDiv); applyVisibleSamples(); refreshPresentation(); scheduleRender();
|
|
@@ -1140,8 +1188,10 @@
|
|
|
1140
1188
|
|
|
1141
1189
|
// Stacked labels: name on top, endpoint on a muted subline so
|
|
1142
1190
|
// users can see at a glance which tab is wired to which device.
|
|
1143
|
-
const labels = document.createElement('
|
|
1144
|
-
labels.className = 'tab-labels';
|
|
1191
|
+
const labels = document.createElement('button');
|
|
1192
|
+
labels.className = 'tab-labels tab-select';
|
|
1193
|
+
labels.setAttribute('aria-label', `View ${t.name || id}`);
|
|
1194
|
+
labels.addEventListener('click', e => { e.stopPropagation(); if (id !== activeTab) switchTab(id); });
|
|
1145
1195
|
const name = document.createElement('span');
|
|
1146
1196
|
name.className = 'tab-name';
|
|
1147
1197
|
name.textContent = t.name || id;
|
|
@@ -1154,10 +1204,10 @@
|
|
|
1154
1204
|
labels.appendChild(ep);
|
|
1155
1205
|
el.appendChild(labels);
|
|
1156
1206
|
|
|
1157
|
-
const reconnect = document.createElement('
|
|
1207
|
+
const reconnect = document.createElement('button');
|
|
1158
1208
|
reconnect.className = 'tab-reconnect';
|
|
1159
1209
|
reconnect.textContent = '\u21BB';
|
|
1160
|
-
reconnect.title = 'Reconnect';
|
|
1210
|
+
reconnect.title = 'Reconnect'; reconnect.setAttribute('aria-label', `Reconnect ${t.name || id}`);
|
|
1161
1211
|
reconnect.addEventListener('click', (e) => {
|
|
1162
1212
|
e.stopPropagation();
|
|
1163
1213
|
sendCtrl({ type: 'tab_reconnect', id: id });
|
|
@@ -1171,17 +1221,17 @@
|
|
|
1171
1221
|
el.appendChild(reconnect);
|
|
1172
1222
|
|
|
1173
1223
|
if (id !== 'bind_me') {
|
|
1174
|
-
const ren = document.createElement('
|
|
1224
|
+
const ren = document.createElement('button');
|
|
1175
1225
|
ren.className = 'tab-rename';
|
|
1176
1226
|
ren.textContent = '\u270E';
|
|
1177
|
-
ren.title = 'Rename';
|
|
1227
|
+
ren.title = 'Rename'; ren.setAttribute('aria-label', `Rename ${t.name || id}`);
|
|
1178
1228
|
ren.addEventListener('click', (e) => { e.stopPropagation(); beginRename(el, id, t.name); });
|
|
1179
1229
|
el.appendChild(ren);
|
|
1180
1230
|
|
|
1181
|
-
const close = document.createElement('
|
|
1231
|
+
const close = document.createElement('button');
|
|
1182
1232
|
close.className = 'tab-close';
|
|
1183
1233
|
close.textContent = '\u00D7';
|
|
1184
|
-
close.title = 'Close tab';
|
|
1234
|
+
close.title = 'Close tab'; close.setAttribute('aria-label', `Close ${t.name || id}`);
|
|
1185
1235
|
close.addEventListener('click', (e) => {
|
|
1186
1236
|
e.stopPropagation();
|
|
1187
1237
|
if (confirm(`Close tab "${t.name || id}"?`)) {
|
|
@@ -1216,6 +1266,7 @@
|
|
|
1216
1266
|
add.className = 'tab-add';
|
|
1217
1267
|
add.textContent = '+ new device';
|
|
1218
1268
|
add.title = 'Add a tab that connects to a remote device';
|
|
1269
|
+
add.setAttribute('aria-label', 'Add data source');
|
|
1219
1270
|
add.addEventListener('click', openCreateForm);
|
|
1220
1271
|
tabbar.appendChild(add);
|
|
1221
1272
|
|
|
@@ -1231,10 +1282,15 @@
|
|
|
1231
1282
|
input.type = 'text';
|
|
1232
1283
|
input.className = 'tab-name-input';
|
|
1233
1284
|
input.value = currentName || '';
|
|
1234
|
-
nameSpan.
|
|
1285
|
+
const selector = nameSpan.closest('.tab-select');
|
|
1286
|
+
const original = selector || nameSpan;
|
|
1287
|
+
original.replaceWith(input);
|
|
1235
1288
|
input.focus();
|
|
1236
1289
|
input.select();
|
|
1290
|
+
let finished = false;
|
|
1237
1291
|
const commit = () => {
|
|
1292
|
+
if (finished) return;
|
|
1293
|
+
finished = true;
|
|
1238
1294
|
const newName = input.value.trim();
|
|
1239
1295
|
if (newName && newName !== currentName) {
|
|
1240
1296
|
sendCtrl({ type: 'tab_rename', id: id, name: newName });
|
|
@@ -1243,14 +1299,16 @@
|
|
|
1243
1299
|
const span = document.createElement('span');
|
|
1244
1300
|
span.className = 'tab-name';
|
|
1245
1301
|
span.textContent = currentName || '';
|
|
1246
|
-
input.replaceWith(span);
|
|
1302
|
+
if (selector) { nameSpan.textContent = currentName || ''; input.replaceWith(selector); } else input.replaceWith(span);
|
|
1247
1303
|
}
|
|
1248
1304
|
};
|
|
1249
1305
|
const cancel = () => {
|
|
1306
|
+
if (finished) return;
|
|
1307
|
+
finished = true;
|
|
1250
1308
|
const span = document.createElement('span');
|
|
1251
1309
|
span.className = 'tab-name';
|
|
1252
1310
|
span.textContent = currentName || '';
|
|
1253
|
-
input.replaceWith(span);
|
|
1311
|
+
if (selector) { nameSpan.textContent = currentName || ''; input.replaceWith(selector); } else input.replaceWith(span);
|
|
1254
1312
|
};
|
|
1255
1313
|
input.addEventListener('keydown', (e) => {
|
|
1256
1314
|
if (e.key === 'Enter') { e.preventDefault(); commit(); }
|
|
@@ -1339,13 +1397,13 @@
|
|
|
1339
1397
|
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
1340
1398
|
socket = new WebSocket(proto + '//' + location.host + '/ws');
|
|
1341
1399
|
socket.binaryType = 'arraybuffer';
|
|
1342
|
-
socket.onopen = () => { wsStatus.textContent = 'connected'; };
|
|
1400
|
+
socket.onopen = () => { wsStatus.textContent = 'Server connected'; wsStatus.dataset.health = 'connected'; };
|
|
1343
1401
|
socket.onclose = () => {
|
|
1344
|
-
wsStatus.textContent = '
|
|
1402
|
+
wsStatus.textContent = 'Reconnecting…'; wsStatus.dataset.health = 'disconnected';
|
|
1345
1403
|
if (viewManager) viewManager.connection(false);
|
|
1346
1404
|
setTimeout(connect, 1000);
|
|
1347
1405
|
};
|
|
1348
|
-
socket.onerror = () => { wsStatus.textContent = 'error'; };
|
|
1406
|
+
socket.onerror = () => { wsStatus.textContent = 'Connection error'; wsStatus.dataset.health = 'disconnected'; };
|
|
1349
1407
|
socket.onmessage = (ev) => {
|
|
1350
1408
|
if (typeof ev.data === 'string') {
|
|
1351
1409
|
let msg;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/* Responsive presentation. No element is duplicated across breakpoints. */
|
|
2
|
+
:root { --ctrl-unit-h: 44px; --persistent-budget: 144px; --presentation-offset: 120px; }
|
|
3
|
+
* { box-sizing: border-box; }
|
|
4
|
+
html { scroll-padding-top: var(--presentation-offset); }
|
|
5
|
+
body { min-width: 0; }
|
|
6
|
+
button, input, select, summary, [role=button], .section-nav a { min-width: 44px; min-height: 44px; }
|
|
7
|
+
button, input, select, textarea { font: inherit; }
|
|
8
|
+
:where(button,input,select,textarea,summary,a,[tabindex]):focus-visible { outline: 3px solid #185f9f; outline-offset: 2px; }
|
|
9
|
+
:where(button,input,select,textarea,summary,a,[tabindex]) { scroll-margin-top: var(--presentation-offset); scroll-margin-bottom: 12px; }
|
|
10
|
+
#header { min-height: 48px; gap: 8px; padding: 2px 12px; }
|
|
11
|
+
#header h1 { font-size: 17px; }
|
|
12
|
+
#ws-status { font-size: 13px; color: #35485a; overflow-wrap: anywhere; }
|
|
13
|
+
#menu-btn { width: 44px; height: 44px; margin: 0; flex: 0 0 44px; }
|
|
14
|
+
#menu-panel { max-width: calc(100vw - 16px); min-width: 0; width: 340px; max-height: calc(100dvh - 64px); overflow-y: auto; top: 52px; right: 8px; padding: 12px; }
|
|
15
|
+
#menu-panel #status, #menu-panel #zmq-mode { margin-bottom: 8px; overflow-wrap: anywhere; }
|
|
16
|
+
#menu-panel .menu-ctrl { min-width: 0; }
|
|
17
|
+
#menu-panel input[type=range] { min-width: 44px; width: 100%; }
|
|
18
|
+
#menu-panel input[type=number] { font-size: 16px; }
|
|
19
|
+
#menu-panel .menu-hint, #menu-panel label, #menu-panel .menu-reset { color: #465767; font-size: 13px; }
|
|
20
|
+
#tabbar { max-width: 100%; padding-top: 0; }
|
|
21
|
+
.tab { min-height: 44px; padding: 0 8px; gap: 4px; }
|
|
22
|
+
.tab .tab-endpoint { display: none; }
|
|
23
|
+
.tab .tab-reconnect, .tab .tab-rename, .tab .tab-close { opacity: 1; border: 0; background: transparent; font-size: 16px; color: #405569; padding: 0; flex: 0 0 44px; }
|
|
24
|
+
.tab .tab-name { max-width: min(260px, 55vw); }
|
|
25
|
+
#tab-create.open { flex-wrap: wrap; max-width: 100%; }
|
|
26
|
+
#tab-create input { max-width: 100%; min-width: 0; font-size: 16px; }
|
|
27
|
+
#plots { padding: 8px 12px 24px; min-width: 0; }
|
|
28
|
+
.plot-row { grid-template-columns: repeat(var(--plot-columns, 1), minmax(0, 1fr)); overflow: visible; }
|
|
29
|
+
.plot-wrap { box-sizing: content-box; }
|
|
30
|
+
.uplot { max-width: 100%; }
|
|
31
|
+
.uplot .u-title { white-space: normal; overflow-wrap: anywhere; font-size: 17px; }
|
|
32
|
+
.uplot .u-legend { display: block; white-space: normal; padding: 4px 0; font-size: 13px; }
|
|
33
|
+
.uplot .u-legend tbody { display: flex; justify-content: center; flex-wrap: wrap; gap: 2px 8px; }
|
|
34
|
+
.uplot .u-legend .u-series { display: inline-flex; align-items: center; min-height: 44px; max-width: 100%; margin: 0; }
|
|
35
|
+
.uplot .u-legend .u-label { white-space: normal; overflow-wrap: anywhere; max-width: 180px; }
|
|
36
|
+
.uplot .u-legend .u-value { overflow-wrap: anywhere; }
|
|
37
|
+
.ui-sticky { overflow: visible; max-height: none; background: #fafafa; }
|
|
38
|
+
.essential-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; padding: 4px 0; min-width: 0; border-bottom: 1px solid #ccd5de; }
|
|
39
|
+
.essential-controls .ctrl-item { flex: 0 1 auto; min-width: 0; margin: 0; }
|
|
40
|
+
.essential-controls .ctrl-text { flex: 1 1 150px; }
|
|
41
|
+
.essential-controls .ctrl-text label { font-size: 12px; }
|
|
42
|
+
.essential-controls .ctrl-textval { padding: 0; min-width: 0; }
|
|
43
|
+
.persistent-overflow { max-height: max(44px, calc(var(--persistent-budget) - 52px - var(--essential-height, 0px) - var(--source-height, 0px))); overflow-y: auto; overscroll-behavior: contain; }
|
|
44
|
+
.ui-source-status { padding: 4px 8px; font-size: 13px; color: #78260e; }
|
|
45
|
+
.section-nav { gap: 4px; padding: 0; align-items: flex-start; }
|
|
46
|
+
.section-nav a, .section-more summary { display: inline-flex; align-items: center; gap: 6px; font-size: 14px; line-height: 1.2; padding: 6px 10px; color: #174f80; border: 0; border-radius: 4px; background: #eaf0f6; }
|
|
47
|
+
.section-nav a:hover, .section-more summary:hover { background: #dce8f3; }
|
|
48
|
+
.section-more summary { cursor: pointer; }
|
|
49
|
+
.section-more summary::after { content: ' ▾'; }
|
|
50
|
+
.section-more[open] summary::after { content: ' ▴'; }
|
|
51
|
+
.secondary-links { display: flex; flex-wrap: wrap; gap: 4px; padding-top: 4px; }
|
|
52
|
+
.nav-status { color: #435b6e; font-size: 12px; }
|
|
53
|
+
.section-nav [data-status=warning] .nav-status, .section-nav [data-status=error] .nav-status { color: #923616; }
|
|
54
|
+
.section-header h2 { font-size: 17px; line-height: 1.4; }
|
|
55
|
+
.heading-optional { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
|
|
56
|
+
.heading-optional:focus-within { position: static; width: auto; height: auto; clip-path: none; }
|
|
57
|
+
.section-description, .section-message { color: #435363; line-height: 1.5; overflow-wrap: anywhere; }
|
|
58
|
+
.view-section[data-density=compact] .ctrl-row { padding: 4px 8px; gap: 6px 10px; }
|
|
59
|
+
.view-section[data-density=compact] .section-body { gap: 6px; }
|
|
60
|
+
.ctrl-row { gap: 10px 12px; padding: 10px; min-width: 0; }
|
|
61
|
+
.control-group-title { flex: 0 0 100%; margin: 0; font-size: 14px; font-weight: 600; color: #35485a; }
|
|
62
|
+
.ctrl-item { min-width: 0; max-width: 100%; flex-wrap: wrap; line-height: 1.4; }
|
|
63
|
+
.ctrl-item.flex { min-width: 0; }
|
|
64
|
+
.ctrl-item label { font-size: 14px; color: #35485a; }
|
|
65
|
+
.ctrl-item input { font-size: 16px; }
|
|
66
|
+
.ctrl-textinput { min-width: 0; max-width: 100%; width: 100%; flex: 1 1 180px; }
|
|
67
|
+
.ctrl-numinput { width: 76px; flex: 0 0 76px; }
|
|
68
|
+
.ctrl-nudgebtn { width: 44px; height: 44px; flex: 0 0 44px; }
|
|
69
|
+
.ctrl-slider { flex-wrap: wrap; }
|
|
70
|
+
.ctrl-slider label { flex-basis: 100%; }
|
|
71
|
+
.ctrl-slider .ctrl-rangeinput { min-width: 80px; flex: 1 1 100px; max-width: 100%; }
|
|
72
|
+
.ctrl-btn { min-height: 44px; font-size: 15px; padding: 8px 14px; white-space: normal; overflow-wrap: anywhere; }
|
|
73
|
+
.ctrl-btn:disabled, .ctrl-nudgebtn:disabled { color: #616b75; background: #eff1f3; border-color: #b6c0c9; cursor: default; }
|
|
74
|
+
.ctrl-val, .ctrl-textval { min-width: 0; max-width: 100%; white-space: normal; overflow-wrap: anywhere; }
|
|
75
|
+
.ctrl-textval { font-size: 15px; }
|
|
76
|
+
.ctrl-reason { color: #7d3217; line-height: 1.4; }
|
|
77
|
+
.ctrl-busy, .ctrl-selected { color: #245779; }
|
|
78
|
+
.ctrl-item[data-appearance=help] { flex: 1 1 100%; }
|
|
79
|
+
.ctrl-item[data-appearance=help] .ctrl-textval { background: transparent; color: #465767; border: 0; padding: 0; font-size: 14px; }
|
|
80
|
+
.ctrl-item[data-appearance=state] .ctrl-textval { background: #e9f4ef; color: #175a3d; border: 0; font-weight: 600; }
|
|
81
|
+
.ctrl-item[data-appearance=primary] .ctrl-btn:not(:disabled) { background: #175f9d; border-color: #175f9d; color: white; }
|
|
82
|
+
.ctrl-item[data-appearance=warning] .ctrl-btn:not(:disabled) { background: #fff1c9; border-color: #9d6d00; color: #654200; }
|
|
83
|
+
.ctrl-item[data-appearance=danger] .ctrl-btn:not(:disabled) { background: #a32624; border-color: #a32624; color: white; font-weight: 650; }
|
|
84
|
+
.ctrl-item[data-appearance=choice] .ctrl-btn { border-color: #8195a8; background: #f3f6fa; color: #284862; }
|
|
85
|
+
.exclusive-group { gap: 4px; }
|
|
86
|
+
.exclusive-group .ctrl-item { flex: 0 1 auto; }
|
|
87
|
+
.exclusive-group .ui-selected, .ctrl-item[data-appearance=choice].ui-selected { outline: 0; }
|
|
88
|
+
.exclusive-group .ui-selected .ctrl-btn, .ctrl-item[data-appearance=choice].ui-selected .ctrl-btn { background: #d5e8f8; color: #123e64; border: 2px solid #235d8a; }
|
|
89
|
+
.exclusive-group .ctrl-selected, .ctrl-item[data-appearance=choice] .ctrl-selected { display: none; }
|
|
90
|
+
.exclusive-group .ui-selected .ctrl-btn::before, .ctrl-item[data-appearance=choice].ui-selected .ctrl-btn::before { content: '✓ '; margin-right: 4px; }
|
|
91
|
+
@media (min-width: 640px) and (max-width: 1023px) {
|
|
92
|
+
.plot-row { grid-template-columns: repeat(var(--compact-columns, 2), minmax(0, 1fr)); }
|
|
93
|
+
.ctrl-item.flex { flex-basis: 240px; }
|
|
94
|
+
}
|
|
95
|
+
@media (max-width: 639px) {
|
|
96
|
+
:root { --persistent-budget: 180px; }
|
|
97
|
+
#plots { padding: 6px 8px 24px; gap: 8px; }
|
|
98
|
+
#header { padding-inline: 8px; }
|
|
99
|
+
.plot-row { grid-template-columns: minmax(0, 1fr); }
|
|
100
|
+
.ctrl-row > .ctrl-item { flex: 1 1 100%; }
|
|
101
|
+
.exclusive-group > .ctrl-item { flex: 0 1 auto; }
|
|
102
|
+
.ctrl-slider .ctrl-rangeinput { flex-basis: 100%; }
|
|
103
|
+
.section-nav a, .section-more summary { font-size: 13px; padding: 6px; }
|
|
104
|
+
.nav-status { font-size: 11px; }
|
|
105
|
+
.section-nav a { flex-direction: column; gap: 1px; justify-content: center; }
|
|
106
|
+
.section-header { gap: 6px; }
|
|
107
|
+
}
|
|
108
|
+
@media (max-height: 480px) and (min-width: 640px) {
|
|
109
|
+
:root { --persistent-budget: 120px; }
|
|
110
|
+
#header { min-height: 44px; padding-block: 0; }
|
|
111
|
+
.essential-controls { padding-block: 2px; }
|
|
112
|
+
}
|
|
113
|
+
/* A quiet, consistent surface keeps live data and intentional actions prominent. */
|
|
114
|
+
:root { color-scheme: light; }
|
|
115
|
+
body { background: #f4f6f8; color: #223348; font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; line-height: 1.45; }
|
|
116
|
+
#header { background: #fff; border-color: #dde4ec; box-shadow: 0 2px 8px #263b5110; }
|
|
117
|
+
#header h1 { color: #123f64; font-weight: 750; letter-spacing: -.5px; }
|
|
118
|
+
#menu-btn { border-color: transparent; border-radius: 8px; color: #36536d; }
|
|
119
|
+
#ws-status { display: flex; align-items: center; gap: 6px; }
|
|
120
|
+
#ws-status::before { content: ''; width: 7px; height: 7px; flex: 0 0 7px; border-radius: 50%; background: #a36b14; }
|
|
121
|
+
#ws-status[data-health=connected]::before { background: #22764e; }
|
|
122
|
+
#ws-status[data-health=disconnected]::before { background: #a32624; }
|
|
123
|
+
#tabbar { background: #e9eef3; border-color: #d6e0e9; }
|
|
124
|
+
.tab, .tab.bind-me { background: transparent; border-color: transparent; border-radius: 0; top: 0; }
|
|
125
|
+
.tab.active, .tab.bind-me.active { background: #fff; color: #174b75; box-shadow: inset 0 -3px #296a9e; border-color: transparent; }
|
|
126
|
+
.tab-add { border: 0; border-radius: 0; color: #40586f; }
|
|
127
|
+
.ui-sticky { background: #f4f6f8; }
|
|
128
|
+
.essential-controls { padding-inline: 8px; background: #fff; border: 1px solid #d9e3ec; border-radius: 9px; box-shadow: 0 2px 8px #253d5510; }
|
|
129
|
+
.essential-controls .ctrl-textval { background: transparent; }
|
|
130
|
+
.section-nav a { background: transparent; }
|
|
131
|
+
.section-more summary { background: #e8eef4; border-radius: 7px; }
|
|
132
|
+
.section-header h2 { color: #263f56; letter-spacing: -.2px; font-weight: 650; }
|
|
133
|
+
.section-header button { background: transparent; border-color: transparent; padding-left: 0; }
|
|
134
|
+
.section-status { color: #426078; }
|
|
135
|
+
.ctrl-row, .plot-wrap { border-color: #dce4ed; border-radius: 10px; box-shadow: 0 1px 3px #1d39530a; }
|
|
136
|
+
.ctrl-btn, .ctrl-textinput, .ctrl-numinput, .ctrl-nudgebtn { border-radius: 7px; }
|
|
137
|
+
.ctrl-textinput, .ctrl-numinput { border-color: #a9bacb; }
|
|
138
|
+
.ctrl-btn { transition: background-color .12s ease, border-color .12s ease; }
|
|
139
|
+
.ctrl-btn:not(:disabled):hover { box-shadow: 0 2px 5px #15395320; }
|
|
140
|
+
.ctrl-item[data-appearance=primary] .ctrl-btn:not(:disabled):hover { background: #124b7d; }
|
|
141
|
+
.ctrl-item[data-appearance=danger] .ctrl-btn:not(:disabled):hover { background: #851e1d; }
|
|
142
|
+
.ctrl-item[data-appearance=warning] .ctrl-btn:not(:disabled):hover { background: #ffe7a3; }
|
|
143
|
+
.ctrl-item[data-appearance=help] .ctrl-textval { line-height: 1.55; }
|
|
144
|
+
.uplot .u-title { color: #2c445b; font-weight: 600; }
|
|
145
|
+
#menu-panel { border-color: #d2dce7; border-radius: 10px; box-shadow: 0 8px 24px #15395322; }
|
|
146
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation: none !important; transition: none !important; } }
|
|
147
|
+
#menu-panel { top: max(52px, var(--presentation-offset)); max-height: calc(100dvh - max(52px, var(--presentation-offset)) - 8px); }
|
|
148
|
+
.sr-only { position: absolute !important; width: 1px !important; height: 1px !important; padding: 0 !important; margin: -1px !important; overflow: hidden !important; clip-path: inset(50%); white-space: nowrap !important; border: 0 !important; }
|
|
149
|
+
.uplot .u-legend .u-series:first-child { display: none; }
|
|
150
|
+
.ctrl-item label, .control-group-title, .section-header h2 { overflow-wrap: anywhere; max-width: 100%; }
|
|
151
|
+
.ctrl-btn { font-size: max(15px, calc(15px * var(--ui-scale))); }
|
|
152
|
+
.ctrl-item input { font-size: max(16px, calc(16px * var(--ui-scale))); }
|
|
153
|
+
#tabbar { flex-wrap: wrap; overflow: visible; }
|
|
154
|
+
.tab { max-width: 100%; flex-wrap: wrap; }
|
|
155
|
+
.ctrl-item[data-appearance=state] .ctrl-val { background: #e9f4ef; color: #175a3d; border: 0; font-weight: 600; }
|
|
156
|
+
.tab .tab-select { border: 0; background: transparent; padding: 2px 4px; color: inherit; font: inherit; text-align: left; justify-content: center; }
|
|
157
|
+
@media (max-width: 639px) {
|
|
158
|
+
.tab-add { flex: 0 0 44px; width: 44px; padding: 0; font-size: 0; }
|
|
159
|
+
.tab-add::after { content: '+'; font-size: 22px; }
|
|
160
|
+
}
|
|
161
|
+
/* The page, not a viewport-height body box, contains the sticky header. */
|
|
162
|
+
body { height: auto; min-height: 100%; }
|
|
163
|
+
.ctrl-item[data-appearance=state] .ctrl-val { background: #eaf2fa; color: #244f74; }
|
|
164
|
+
.uplot .u-series:has(.sr-only) th::after { content: ''; }
|
|
165
|
+
#empty-state, .res-unavail { color: #586979; }
|
|
166
|
+
.ctrl-textinput, .ctrl-numinput, .ctrl-nudgebtn { border-color: #738799; }
|
|
167
|
+
|
|
168
|
+
@media (max-height: 480px) and (min-width: 640px) { .ui-source-status { padding-block: 2px; font-size: 12px; line-height: 1.2; } }
|
|
@@ -9,28 +9,41 @@
|
|
|
9
9
|
this.state = {controls: {}, sections: {}}; this.revision = -1;
|
|
10
10
|
this.available = cfg.source_available !== false; this.connected = true;
|
|
11
11
|
this.reason = cfg.source_reason || '';
|
|
12
|
+
this.essentialIds = new Set(cfg.view?.essential_controls || []);
|
|
12
13
|
this.key = `rtplotSections.v1:${cfg.tab}`;
|
|
13
14
|
try { this.prefs = JSON.parse(sessionStorage.getItem(this.key) || '{}'); } catch (_) { this.prefs = {}; }
|
|
14
15
|
this.sticky = document.createElement('div'); this.sticky.className = 'ui-sticky'; root.appendChild(this.sticky);
|
|
15
16
|
this.banner = document.createElement('div'); this.banner.className = 'ui-source-status';
|
|
16
17
|
this.banner.setAttribute('role', 'status'); this.sticky.appendChild(this.banner);
|
|
17
|
-
this.nav = document.createElement('nav'); this.nav.className = 'section-nav'; this.nav.setAttribute('aria-label', 'Sections');
|
|
18
|
+
this.nav = document.createElement('nav'); this.nav.className = 'section-nav'; this.nav.setAttribute('aria-label', 'Sections'); root.appendChild(this.nav);
|
|
19
|
+
this.more = document.createElement('details'); this.more.className = 'section-more';
|
|
20
|
+
const summary = document.createElement('summary'); summary.textContent = 'More'; this.more.appendChild(summary);
|
|
21
|
+
this.secondary = document.createElement('div'); this.secondary.className = 'secondary-links'; this.more.appendChild(this.secondary);
|
|
22
|
+
this.essentials = document.createElement('div'); this.essentials.className = 'essential-controls';
|
|
23
|
+
this.essentials.setAttribute('role', 'group'); this.essentials.setAttribute('aria-label', 'Essential actions and state');
|
|
24
|
+
this.sticky.appendChild(this.essentials);
|
|
25
|
+
this.overflow = document.createElement('div'); this.overflow.className = 'persistent-overflow'; this.sticky.appendChild(this.overflow);
|
|
18
26
|
const view = cfg.view || {};
|
|
19
27
|
(view.sections || []).forEach((s, index) => {
|
|
20
28
|
const node = document.createElement('section'); node.className = 'view-section'; node.dataset.section = s.id;
|
|
21
29
|
node.id = `rtplot-section-${index}`;
|
|
30
|
+
node.dataset.density = s.density || 'normal';
|
|
31
|
+
node.setAttribute('aria-label', s.title);
|
|
22
32
|
const header = document.createElement('div'); header.className = 'section-header';
|
|
23
|
-
const heading = document.createElement('h2');
|
|
33
|
+
const heading = document.createElement('h2'); heading.tabIndex = -1;
|
|
24
34
|
const toggle = document.createElement(s.collapsible && s.id !== view.persistent_section ? 'button' : 'span');
|
|
25
35
|
toggle.textContent = s.title; heading.appendChild(toggle); header.appendChild(heading); node.appendChild(header);
|
|
36
|
+
if (s.show_heading === false) header.classList.add('heading-optional');
|
|
26
37
|
if (s.description) { const d = document.createElement('p'); d.className = 'section-description'; d.textContent = s.description; node.appendChild(d); }
|
|
27
38
|
const status = document.createElement('span'); status.className = 'section-status'; header.appendChild(status);
|
|
28
39
|
const message = document.createElement('p'); message.className = 'section-message'; message.setAttribute('role', 'status'); node.appendChild(message);
|
|
29
40
|
const body = document.createElement('div'); body.className = 'section-body'; body.id = `${node.id}-body`; node.appendChild(body);
|
|
30
|
-
const link = document.createElement('a'); link.textContent = s.title; link.href = `#${node.id}`;
|
|
31
|
-
const
|
|
41
|
+
const link = document.createElement('a'); link.textContent = s.title; link.href = `#${node.id}`;
|
|
42
|
+
const navStatus = document.createElement('span'); navStatus.className = 'nav-status'; link.appendChild(navStatus);
|
|
43
|
+
(s.navigation === 'secondary' ? this.secondary : this.nav).appendChild(link);
|
|
44
|
+
const section = {declaration: s, node, body, toggle, link, navStatus, heading, status, message, collapsed: false};
|
|
32
45
|
this.sections.set(s.id, section);
|
|
33
|
-
if (s.id === view.persistent_section) { node.classList.add('persistent-section'); this.
|
|
46
|
+
if (s.id === view.persistent_section) { node.classList.add('persistent-section'); this.overflow.appendChild(node); }
|
|
34
47
|
else root.appendChild(node);
|
|
35
48
|
const canCollapse = toggle.tagName === 'BUTTON';
|
|
36
49
|
if (canCollapse) {
|
|
@@ -41,19 +54,45 @@
|
|
|
41
54
|
link.addEventListener('click', e => {
|
|
42
55
|
e.preventDefault(); this.collapse(s.id, false, true);
|
|
43
56
|
// A declaration/runtime-hidden section has no navigation link.
|
|
44
|
-
this.
|
|
57
|
+
this.more.open = false;
|
|
58
|
+
this.changed(); requestAnimationFrame(() => {
|
|
59
|
+
if (s.id === view.persistent_section && this.overflow.hidden) {
|
|
60
|
+
this.essentials.tabIndex = -1; this.essentials.focus({preventScroll: true});
|
|
61
|
+
} else {
|
|
62
|
+
heading.focus({preventScroll: true}); node.scrollIntoView({block: 'start'});
|
|
63
|
+
}
|
|
64
|
+
});
|
|
45
65
|
});
|
|
46
66
|
});
|
|
67
|
+
this.nav.appendChild(this.more); this.more.hidden = !this.secondary.children.length;
|
|
47
68
|
this.nav.hidden = !this.sections.size;
|
|
48
69
|
this.refreshAvailability();
|
|
49
70
|
this.observer = new ResizeObserver(() => this.offsets());
|
|
50
71
|
this.observer.observe(this.sticky); this.observer.observe(document.getElementById('header'));
|
|
72
|
+
this.onFocus = e => {
|
|
73
|
+
if (!root.contains(e.target) || this.sticky.contains(e.target)) return;
|
|
74
|
+
const top = document.getElementById('header').getBoundingClientRect().bottom + this.sticky.getBoundingClientRect().height;
|
|
75
|
+
if (e.target.getBoundingClientRect().top < top) e.target.scrollIntoView({block: 'center'});
|
|
76
|
+
};
|
|
77
|
+
root.addEventListener('focusin', this.onFocus);
|
|
51
78
|
this.offsets();
|
|
52
79
|
}
|
|
80
|
+
finish() {
|
|
81
|
+
// Move the existing elements once, preserving declared control order.
|
|
82
|
+
// Resizing and state updates never reparent, clone, or replace them.
|
|
83
|
+
this.controls.forEach((c, id) => { if (this.essentialIds.has(id)) this.essentials.appendChild(c.item); });
|
|
84
|
+
this.essentials.hidden = !this.essentials.children.length;
|
|
85
|
+
this.overflow.querySelectorAll('.ctrl-row').forEach(row => { if (!row.querySelector('.ctrl-item')) row.hidden = true; });
|
|
86
|
+
this.overflow.hidden = !this.overflow.querySelector('.ctrl-row:not([hidden])');
|
|
87
|
+
this.refreshAvailability(); this.offsets();
|
|
88
|
+
}
|
|
53
89
|
offsets() {
|
|
54
90
|
const header = document.getElementById('header').getBoundingClientRect().height;
|
|
55
91
|
this.sticky.style.top = `${header}px`;
|
|
56
92
|
const margin = header + this.sticky.getBoundingClientRect().height + 12;
|
|
93
|
+
document.documentElement.style.setProperty('--presentation-offset', `${margin}px`);
|
|
94
|
+
this.sticky.style.setProperty('--source-height', `${this.banner.getBoundingClientRect().height}px`);
|
|
95
|
+
this.sticky.style.setProperty('--essential-height', `${this.essentials.getBoundingClientRect().height}px`);
|
|
57
96
|
this.sections.forEach(s => { s.node.style.scrollMarginTop = `${margin}px`; });
|
|
58
97
|
}
|
|
59
98
|
parent(section) { return this.sections.get(section)?.body || this.root; }
|
|
@@ -78,7 +117,7 @@
|
|
|
78
117
|
const c = this.controls.get(id); if (!c) return false;
|
|
79
118
|
const v = {...defaults, ...this.state.controls?.[id]};
|
|
80
119
|
const s = this.sections.get(c.section);
|
|
81
|
-
return this.connected && this.available && v.enabled && v.visible && (!s || (!s.node.hidden && !s.collapsed));
|
|
120
|
+
return this.connected && this.available && v.enabled && v.visible && (this.essentialIds.has(id) || !s || (!s.node.hidden && !s.collapsed));
|
|
82
121
|
}
|
|
83
122
|
applyControl(id) {
|
|
84
123
|
const c = this.controls.get(id); if (!c) return;
|
|
@@ -100,7 +139,10 @@
|
|
|
100
139
|
const s = this.sections.get(id); if (!s) return;
|
|
101
140
|
const v = {visible: s.declaration.visible, status: 'idle', message: '', ...this.state.sections?.[id]};
|
|
102
141
|
s.node.hidden = !v.visible; s.link.hidden = !v.visible;
|
|
142
|
+
this.more.hidden = !Array.from(this.secondary.children).some(link => !link.hidden);
|
|
103
143
|
s.status.textContent = statusLabels[v.status] || v.status;
|
|
144
|
+
s.navStatus.textContent = statusLabels[v.status] || v.status;
|
|
145
|
+
s.link.dataset.status = v.status;
|
|
104
146
|
s.status.dataset.status = v.status; s.message.textContent = v.message; s.message.hidden = !v.message;
|
|
105
147
|
}
|
|
106
148
|
apply(snapshot, revision) {
|
|
@@ -124,7 +166,7 @@
|
|
|
124
166
|
this.banner.hidden = this.connected && this.available;
|
|
125
167
|
this.banner.textContent = !this.connected ? 'Disconnected from server — application controls unavailable' :
|
|
126
168
|
this.reason || 'Source unavailable — application controls disabled';
|
|
127
|
-
this.sticky.hidden = !this.sections.size && this.connected && this.available;
|
|
169
|
+
this.sticky.hidden = !this.sections.size && !this.essentialIds.size && this.connected && this.available;
|
|
128
170
|
this.controls.forEach((_, id) => this.applyControl(id)); this.offsets();
|
|
129
171
|
}
|
|
130
172
|
source(available, reason) {
|
|
@@ -132,7 +174,7 @@
|
|
|
132
174
|
this.available = available; this.reason = reason; this.refreshAvailability();
|
|
133
175
|
}
|
|
134
176
|
connection(connected) { this.connected = connected; this.refreshAvailability(); }
|
|
135
|
-
destroy() { this.observer.disconnect(); }
|
|
177
|
+
destroy() { this.observer.disconnect(); this.root.removeEventListener('focusin', this.onFocus); }
|
|
136
178
|
}
|
|
137
179
|
window.RtplotView = RtplotView;
|
|
138
180
|
})();
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
from copy import deepcopy
|
|
3
3
|
|
|
4
4
|
META_KEY = "__rtplot_view__"
|
|
5
|
-
CAPABILITIES = ["sections", "ui_state_v1"]
|
|
5
|
+
CAPABILITIES = ["sections", "ui_state_v1", "presentation_v1"]
|
|
6
6
|
CONTROL_DEFAULTS = {"enabled": True, "visible": True, "busy": False, "selected": False, "reason": ""}
|
|
7
7
|
SECTION_DEFAULTS = {"visible": True, "status": "idle", "message": ""}
|
|
8
8
|
STATUSES = {"idle", "ready", "busy", "complete", "warning", "error"}
|
|
@@ -29,13 +29,20 @@ def declarations(config, view=None):
|
|
|
29
29
|
raise ValueError("Section IDs must be unique non-empty strings")
|
|
30
30
|
ids.add(sid)
|
|
31
31
|
s = {"id": sid, "title": sid, "description": "", "collapsible": False,
|
|
32
|
-
"collapsed": False, "visible": True,
|
|
32
|
+
"collapsed": False, "visible": True, "navigation": "primary", "density": "normal",
|
|
33
|
+
"show_heading": True, **section}
|
|
33
34
|
for key in ("title", "description"):
|
|
34
35
|
if not isinstance(s[key], str):
|
|
35
36
|
raise ValueError(f"Section {key} must be a string")
|
|
36
37
|
for key in ("collapsible", "collapsed", "visible"):
|
|
37
38
|
if not isinstance(s[key], bool):
|
|
38
39
|
raise ValueError(f"Section {key} must be boolean")
|
|
40
|
+
for key, default, allowed in (("navigation", "primary", {"primary", "secondary"}),
|
|
41
|
+
("density", "normal", {"normal", "compact"})):
|
|
42
|
+
if s.get(key, default) not in allowed:
|
|
43
|
+
raise ValueError(f"Invalid section {key}")
|
|
44
|
+
if type(s.get("show_heading", True)) is not bool:
|
|
45
|
+
raise ValueError("show_heading must be boolean")
|
|
39
46
|
if not s["collapsible"]:
|
|
40
47
|
s["collapsed"] = False
|
|
41
48
|
normalized.append(s)
|
|
@@ -53,14 +60,35 @@ def declarations(config, view=None):
|
|
|
53
60
|
raise ValueError("The persistent section can contain only control rows")
|
|
54
61
|
if "plots" in row and any(p.get("section") is not None for p in row["plots"]):
|
|
55
62
|
raise ValueError("Assign section on the PlotRow, not its child plots")
|
|
63
|
+
if row.get("title") is not None and not isinstance(row["title"], str):
|
|
64
|
+
raise ValueError("Control group title must be a string")
|
|
65
|
+
if type(row.get("exclusive", False)) is not bool:
|
|
66
|
+
raise ValueError("Control group exclusive must be boolean")
|
|
67
|
+
for plot in row.get("plots", [row]):
|
|
68
|
+
if "min_height" in plot and (type(plot["min_height"]) is not int or plot["min_height"] < 160):
|
|
69
|
+
raise ValueError("Plot min_height must be an integer >= 160 CSS pixels")
|
|
56
70
|
for control in row.get("controls", []):
|
|
57
71
|
cid = control.get("id")
|
|
58
72
|
if not isinstance(cid, str) or not cid:
|
|
59
73
|
raise ValueError("Control IDs must be non-empty strings")
|
|
60
74
|
if cid in controls:
|
|
61
75
|
raise ValueError(f"Duplicate control ID: {cid}")
|
|
76
|
+
if control.get("appearance", "default") not in {"default", "help", "state", "primary", "choice", "warning", "danger"}:
|
|
77
|
+
raise ValueError("Unknown control appearance")
|
|
78
|
+
if row.get("exclusive") and control.get("type") != "button":
|
|
79
|
+
raise ValueError("Exclusive groups may contain only buttons")
|
|
62
80
|
controls[cid] = {"type": control.get("type"), "section": sid}
|
|
63
|
-
|
|
81
|
+
if row.get("exclusive"):
|
|
82
|
+
controls[cid]["exclusive_group"] = key
|
|
83
|
+
essentials = view.get("essential_controls", [])
|
|
84
|
+
if not isinstance(essentials, list) or any(not isinstance(cid, str) or cid not in controls for cid in essentials):
|
|
85
|
+
raise ValueError("essential_controls must list existing control IDs")
|
|
86
|
+
if len(set(essentials)) != len(essentials):
|
|
87
|
+
raise ValueError("essential_controls must not contain duplicate IDs")
|
|
88
|
+
result = {"sections": normalized, "persistent_section": persistent} if view else {}
|
|
89
|
+
if essentials:
|
|
90
|
+
result["essential_controls"] = essentials
|
|
91
|
+
return result, controls
|
|
64
92
|
|
|
65
93
|
|
|
66
94
|
def merge_state(current, patch, controls, view):
|
|
@@ -68,6 +96,19 @@ def merge_state(current, patch, controls, view):
|
|
|
68
96
|
if not isinstance(patch, dict) or set(patch) - {"controls", "sections"}:
|
|
69
97
|
raise ValueError("UI state must contain only controls and sections maps")
|
|
70
98
|
result = deepcopy(current)
|
|
99
|
+
selected_groups = {}
|
|
100
|
+
for cid, values in patch.get("controls", {}).items() if isinstance(patch.get("controls", {}), dict) else []:
|
|
101
|
+
group = controls.get(cid, {}).get("exclusive_group")
|
|
102
|
+
if group and isinstance(values, dict) and values.get("selected") is True:
|
|
103
|
+
if group in selected_groups:
|
|
104
|
+
raise ValueError("An exclusive group may have only one selected choice")
|
|
105
|
+
selected_groups[group] = cid
|
|
106
|
+
for group, selected in selected_groups.items():
|
|
107
|
+
for cid, info in controls.items():
|
|
108
|
+
if cid != selected and info.get("exclusive_group") == group:
|
|
109
|
+
result.setdefault("controls", {}).get(cid, {}).pop("selected", None)
|
|
110
|
+
if not result["controls"].get(cid):
|
|
111
|
+
result["controls"].pop(cid, None)
|
|
71
112
|
section_ids = {s["id"] for s in view.get("sections", [])}
|
|
72
113
|
for group, changes in patch.items():
|
|
73
114
|
if not isinstance(changes, dict):
|
|
@@ -100,3 +141,23 @@ def merge_state(current, patch, controls, view):
|
|
|
100
141
|
else:
|
|
101
142
|
target.pop(ident, None)
|
|
102
143
|
return result
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def presentation_requested(config, view):
|
|
147
|
+
"""Whether a layout relies on options introduced in presentation_v1."""
|
|
148
|
+
if view.get("essential_controls"):
|
|
149
|
+
return True
|
|
150
|
+
if any(s.get("navigation", "primary") != "primary" or s.get("density", "normal") != "normal"
|
|
151
|
+
or not s.get("show_heading", True) for s in view.get("sections", [])):
|
|
152
|
+
return True
|
|
153
|
+
for key, row in config.items():
|
|
154
|
+
if key == META_KEY:
|
|
155
|
+
continue
|
|
156
|
+
if row.get("title") and "controls" in row or row.get("exclusive"):
|
|
157
|
+
return True
|
|
158
|
+
if any(c.get("appearance", "default") != "default" for c in row.get("controls", [])):
|
|
159
|
+
return True
|
|
160
|
+
for plot in row.get("plots", [row]):
|
|
161
|
+
if plot.get("min_height") or any(style in ("dashed", "dotted", "dashdot") for style in (plot.get("line_style") or [])):
|
|
162
|
+
return True
|
|
163
|
+
return False
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/fonts/KaTeX_AMS-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/fonts/KaTeX_Caligraphic-Bold.woff2
RENAMED
|
File without changes
|
|
File without changes
|
{better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/fonts/KaTeX_Fraktur-Bold.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/fonts/KaTeX_Fraktur-Regular.woff2
RENAMED
|
File without changes
|
|
File without changes
|
{better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/fonts/KaTeX_Main-BoldItalic.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/fonts/KaTeX_Main-Italic.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/fonts/KaTeX_Main-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/fonts/KaTeX_Math-BoldItalic.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/fonts/KaTeX_Math-Italic.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/fonts/KaTeX_SansSerif-Bold.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/fonts/KaTeX_SansSerif-Italic.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/fonts/KaTeX_SansSerif-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/fonts/KaTeX_Script-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/fonts/KaTeX_Size1-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/fonts/KaTeX_Size2-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/fonts/KaTeX_Size3-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/fonts/KaTeX_Size4-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.5.0 → better_rtplot-0.6.0}/rtplot/static/katex/fonts/KaTeX_Typewriter-Regular.woff2
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|