dynamic-systems-testing-framework 1.0.1__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.
- dstf/__init__.py +41 -0
- dstf/__main__.py +10 -0
- dstf/authoring/__init__.py +13 -0
- dstf/authoring/builtin/__init__.py +32 -0
- dstf/authoring/builtin/accept.py +205 -0
- dstf/authoring/builtin/edit_criteria.py +479 -0
- dstf/authoring/builtin/edit_family.py +228 -0
- dstf/authoring/builtin/edit_on_plot.py +1354 -0
- dstf/authoring/builtin/edit_tree.py +1399 -0
- dstf/authoring/builtin/enqueue.py +144 -0
- dstf/authoring/capabilities.py +463 -0
- dstf/authoring/errors.py +49 -0
- dstf/authoring/family_rows.py +526 -0
- dstf/authoring/forms.py +631 -0
- dstf/authoring/layout.py +104 -0
- dstf/authoring/live_rows.py +337 -0
- dstf/authoring/live_runs.py +385 -0
- dstf/authoring/needle.py +127 -0
- dstf/authoring/picker_rows.py +92 -0
- dstf/authoring/queue_rows.py +354 -0
- dstf/authoring/session.py +3016 -0
- dstf/authoring/shell/__init__.py +9 -0
- dstf/authoring/shell/actions.py +702 -0
- dstf/authoring/shell/app.py +53 -0
- dstf/authoring/shell/drawer_controls.py +190 -0
- dstf/authoring/shell/family_fold.py +468 -0
- dstf/authoring/shell/figure_hook.py +249 -0
- dstf/authoring/shell/grips.js +1000 -0
- dstf/authoring/shell/header.py +783 -0
- dstf/authoring/shell/live_view.py +93 -0
- dstf/authoring/shell/picker.py +267 -0
- dstf/authoring/shell/placing.py +122 -0
- dstf/authoring/shell/plot_card.py +1171 -0
- dstf/authoring/shell/queue_panel.py +433 -0
- dstf/authoring/shell/rail.py +593 -0
- dstf/authoring/shell/shell.py +528 -0
- dstf/authoring/shell/stats_card.py +501 -0
- dstf/authoring/shell/strip.py +204 -0
- dstf/authoring/shell/style.py +721 -0
- dstf/authoring/shell/suite.py +277 -0
- dstf/authoring/shell/suite_grid.js +414 -0
- dstf/authoring/shell/suite_grid.py +373 -0
- dstf/authoring/shell/tab_theme.py +110 -0
- dstf/authoring/shell/tabstate.py +146 -0
- dstf/authoring/shell/test_list.py +179 -0
- dstf/authoring/shell/test_page.py +1404 -0
- dstf/authoring/shell/tree_card.py +1171 -0
- dstf/authoring/shell/variables_drawer.py +589 -0
- dstf/authoring/shell/widgets.py +188 -0
- dstf/authoring/situation.py +287 -0
- dstf/authoring/spec_document.py +215 -0
- dstf/authoring/spec_preview.py +83 -0
- dstf/authoring/suite_rows.py +99 -0
- dstf/authoring/tree_move.py +516 -0
- dstf/authoring/tree_pointer.py +33 -0
- dstf/authoring/tree_text.py +61 -0
- dstf/authoring/typed_number.js +64 -0
- dstf/authoring/typed_number.py +76 -0
- dstf/authoring/unscorable.py +141 -0
- dstf/authoring/variable_rows.py +216 -0
- dstf/backends/__init__.py +283 -0
- dstf/backends/base.py +1117 -0
- dstf/backends/common/__init__.py +1 -0
- dstf/backends/common/json_driver.py +484 -0
- dstf/backends/common/live_mat.py +369 -0
- dstf/backends/common/mat_reader.py +632 -0
- dstf/backends/common/proc_kill.py +238 -0
- dstf/backends/common/proc_output.py +20 -0
- dstf/backends/common/result_files.py +109 -0
- dstf/backends/common/result_variables.py +329 -0
- dstf/backends/common/statistics.py +39 -0
- dstf/backends/cross_backend.py +193 -0
- dstf/backends/dymola/__init__.py +5 -0
- dstf/backends/dymola/backend.py +776 -0
- dstf/backends/dymola/interface_loader.py +241 -0
- dstf/backends/dymola/log_parser.py +396 -0
- dstf/backends/dymola/persistent_backend.py +949 -0
- dstf/backends/fmpy/__init__.py +11 -0
- dstf/backends/fmpy/backend.py +443 -0
- dstf/backends/julia/__init__.py +27 -0
- dstf/backends/julia/backend.py +156 -0
- dstf/backends/julia/persistent_backend.py +541 -0
- dstf/backends/julia/run_persistent.jl +167 -0
- dstf/backends/julia/run_test.jl +146 -0
- dstf/backends/openmodelica/__init__.py +21 -0
- dstf/backends/openmodelica/backend.py +368 -0
- dstf/backends/openmodelica/init_xml.py +76 -0
- dstf/backends/openmodelica/log_parser.py +119 -0
- dstf/backends/openmodelica/mos_generator.py +228 -0
- dstf/backends/openmodelica/persistent_backend.py +1045 -0
- dstf/backends/openmodelica/session_loader.py +63 -0
- dstf/backends/persistent.py +1811 -0
- dstf/backends/pool.py +602 -0
- dstf/backends/progress.py +395 -0
- dstf/backends/python/__init__.py +11 -0
- dstf/backends/python/backend.py +141 -0
- dstf/backends/python/run_test.py +141 -0
- dstf/backends/run_metadata.py +103 -0
- dstf/backends/types.py +310 -0
- dstf/cli/__init__.py +12 -0
- dstf/cli/author_cmd.py +61 -0
- dstf/cli/baseline_cmds.py +341 -0
- dstf/cli/common.py +231 -0
- dstf/cli/diag_cmds.py +350 -0
- dstf/cli/main.py +561 -0
- dstf/cli/review.py +398 -0
- dstf/cli/run_cmds.py +589 -0
- dstf/cli/spec_cmds.py +123 -0
- dstf/comparison/__init__.py +13 -0
- dstf/comparison/algorithms.py +2127 -0
- dstf/comparison/builtins.py +291 -0
- dstf/comparison/comparator.py +1189 -0
- dstf/comparison/geometry.py +164 -0
- dstf/comparison/handles.py +1194 -0
- dstf/comparison/metric_tree.py +332 -0
- dstf/comparison/mode_schema.py +176 -0
- dstf/comparison/modes.py +788 -0
- dstf/comparison/registry.py +230 -0
- dstf/comparison/seeds.py +281 -0
- dstf/comparison/sides.py +158 -0
- dstf/comparison/snap.py +62 -0
- dstf/comparison/strict_keys.py +246 -0
- dstf/comparison/summaries.py +354 -0
- dstf/comparison/tree_eval.py +868 -0
- dstf/comparison/tree_spec.py +678 -0
- dstf/comparison/types.py +194 -0
- dstf/comparison/validator.py +232 -0
- dstf/config.py +815 -0
- dstf/discovery/__init__.py +0 -0
- dstf/discovery/json_recognizer.py +702 -0
- dstf/discovery/library_state.py +248 -0
- dstf/discovery/mo_parser.py +794 -0
- dstf/discovery/patch_apply.py +308 -0
- dstf/discovery/patch_envelope.py +63 -0
- dstf/discovery/plots.py +318 -0
- dstf/discovery/recognizer.py +103 -0
- dstf/discovery/scan_cache.py +154 -0
- dstf/discovery/spec_parser.py +538 -0
- dstf/discovery/spec_shape.py +122 -0
- dstf/discovery/spec_update.py +493 -0
- dstf/discovery/test_registry.py +488 -0
- dstf/discovery/unit_tests.py +95 -0
- dstf/figures/__init__.py +14 -0
- dstf/figures/assemble.py +48 -0
- dstf/figures/decimate.py +150 -0
- dstf/figures/families.py +289 -0
- dstf/figures/palette.py +170 -0
- dstf/figures/plotly.py +1413 -0
- dstf/figures/view.py +233 -0
- dstf/pipeline.py +435 -0
- dstf/plugins.py +104 -0
- dstf/reporting/__init__.py +0 -0
- dstf/reporting/console_report.py +187 -0
- dstf/reporting/context_builder.py +979 -0
- dstf/reporting/dashboard_render.py +534 -0
- dstf/reporting/junit_report.py +192 -0
- dstf/reporting/overlay_loader.py +708 -0
- dstf/reporting/report_suite.py +1059 -0
- dstf/reporting/rows.py +458 -0
- dstf/reporting/schema_export.py +413 -0
- dstf/reporting/stats_rows.py +321 -0
- dstf/reporting/status.py +388 -0
- dstf/reporting/templates/axes.js +61 -0
- dstf/reporting/templates/dashboard.html +284 -0
- dstf/reporting/templates/dashboard.js +604 -0
- dstf/reporting/templates/filters.js +100 -0
- dstf/reporting/templates/interactive.html +420 -0
- dstf/reporting/templates/report.js +116 -0
- dstf/reporting/templates/vendor/plotly.min.js +8 -0
- dstf/reporting/theme.py +324 -0
- dstf/reporting/tree_rows.py +73 -0
- dstf/reporting/verdicts.py +124 -0
- dstf/storage/__init__.py +0 -0
- dstf/storage/accept.py +216 -0
- dstf/storage/baseline.py +43 -0
- dstf/storage/batch_manifest.py +204 -0
- dstf/storage/ref_index.py +295 -0
- dstf/storage/reference_store.py +661 -0
- dstf/storage/roles.py +576 -0
- dstf/storage/run_stats.py +85 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/METADATA +266 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/RECORD +185 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/WHEEL +4 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/entry_points.txt +2 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
"""The shell's stylesheet on top of `reporting.theme.css_variables()`
|
|
2
|
+
(DESIGN.md "Typography", "Header", "Spacing, shape, elevation", "Status
|
|
3
|
+
badges"): the font token on the body and on Quasar's controls — the
|
|
4
|
+
roadmap left the tool on Quasar's stack until this shell applied it — the
|
|
5
|
+
off-white header with its hairline, the 32 px rows with their status tint,
|
|
6
|
+
the filled badge, the sentence beside a greyed button. Every colour is a
|
|
7
|
+
token; nothing here names a hex."""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from ...reporting.theme import STATUS
|
|
12
|
+
|
|
13
|
+
_PILLS = "".join(
|
|
14
|
+
f".dstf-pill-{status} {{ background: var(--dstf-{status}-pill); "
|
|
15
|
+
f"color: var(--dstf-{status}-dark); border-color: var(--dstf-{status}-border); }}\n"
|
|
16
|
+
for status in ("pass", "fail", "sim-fail", "no-ref", "not-run", "warn", "queued")
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
#: A criterion's swatch (legend b) in the colour its pill's border uses:
|
|
20
|
+
#: filled while its drawing shows, and `.dstf-swatch-off` empties it.
|
|
21
|
+
_SWATCHES = "".join(
|
|
22
|
+
f".dstf-swatch-{status} {{ border-color: var(--dstf-{status}-border); "
|
|
23
|
+
f"background: var(--dstf-{status}-border); }}\n"
|
|
24
|
+
for status in ("pass", "fail", "warn", "queued")
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
_TINTS = "".join(
|
|
28
|
+
f".dstf-tint-{status} {{ background: var(--dstf-{status}-tint); }}\n"
|
|
29
|
+
for status in STATUS
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
#: What Quasar draws itself, under a theme (D148; `Shell.page_setup` lays it
|
|
33
|
+
#: after `STYLE`). Under `body--dark` — which `TabTheme` sets for a dark
|
|
34
|
+
#: theme — Quasar paints its own surfaces from `--q-dark` / `--q-dark-page`
|
|
35
|
+
#: (the palette's) and these are the pieces that name a colour of their own.
|
|
36
|
+
QUASAR = """
|
|
37
|
+
/* A filled primary button's text is the token's, not Quasar's white: on the
|
|
38
|
+
dark accent white reads 2.2:1. Quasar's `.text-white` is `!important`
|
|
39
|
+
INSIDE NiceGUI's `quasar_importants` layer, and a layered important beats
|
|
40
|
+
any unlayered one — the rule joins that layer and wins there on
|
|
41
|
+
specificity (unlayered, the button stayed rgb(255, 255, 255) in Chromium). */
|
|
42
|
+
@layer quasar_importants {
|
|
43
|
+
.q-btn.bg-primary { color: var(--dstf-on-accent) !important; }
|
|
44
|
+
/* A disabled item of a bar's ⋮ menu reads its reason under its name:
|
|
45
|
+
Quasar dims the whole item to 60 % — an `!important` of that layer —
|
|
46
|
+
which put the reason, already the muted text, at 2:1. The name is
|
|
47
|
+
dimmed below; the sentence is not. */
|
|
48
|
+
.dstf-menu-action.disabled { opacity: 1 !important; }
|
|
49
|
+
}
|
|
50
|
+
.dstf-menu-action.disabled .dstf-menu-name { opacity: 0.6; }
|
|
51
|
+
/* The window's own ground follows the tab's theme: `first_paint` gave it
|
|
52
|
+
the project's before this stylesheet arrived. */
|
|
53
|
+
html { background: var(--dstf-page); }
|
|
54
|
+
body.body--dark { background: var(--dstf-page); color: var(--dstf-text); }
|
|
55
|
+
.body--dark .q-card, .body--dark .q-menu, .body--dark .q-dialog__inner > div,
|
|
56
|
+
.body--dark .q-drawer { background: var(--dstf-surface); color: var(--dstf-text); }
|
|
57
|
+
.body--dark .q-tooltip { background: var(--dstf-surface-active); color: var(--dstf-text); }
|
|
58
|
+
.body--dark .q-field__native, .body--dark .q-field__input,
|
|
59
|
+
.body--dark .q-field__label, .body--dark .q-item__label--caption {
|
|
60
|
+
color: var(--dstf-text-secondary); }
|
|
61
|
+
.body--dark .q-separator { background: var(--dstf-border); }
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
STYLE = (
|
|
65
|
+
"""
|
|
66
|
+
body, .q-header, .q-drawer, .q-btn, .q-field, .q-badge, .q-dialog, .q-tooltip, .q-item {
|
|
67
|
+
font-family: var(--dstf-font);
|
|
68
|
+
}
|
|
69
|
+
body { background: var(--dstf-page); color: var(--dstf-text); }
|
|
70
|
+
.dstf-header.q-header {
|
|
71
|
+
background: var(--dstf-page) !important; color: var(--dstf-text) !important;
|
|
72
|
+
border-bottom: 1px solid var(--dstf-border); box-shadow: none;
|
|
73
|
+
/* No padding of its own — NiceGUI's `.nicegui-header` brings 16 px on
|
|
74
|
+
every side — because each ROW carries the side padding instead: on the
|
|
75
|
+
header it inset the sub-header's border-top, which then stopped short
|
|
76
|
+
of both edges while the header's own border-bottom ran to them. */
|
|
77
|
+
padding: 0;
|
|
78
|
+
/* NiceGUI's own `.nicegui-header { flex-direction: row }` outranks
|
|
79
|
+
Quasar's `.column`: without this the Test page's second row renders to
|
|
80
|
+
the RIGHT of the first with Rerun / Accept clipped off-screen, and
|
|
81
|
+
every simulation test still passes. */
|
|
82
|
+
flex-direction: column; flex-wrap: nowrap; align-items: stretch;
|
|
83
|
+
}
|
|
84
|
+
.dstf-header-row { padding: 4px 24px; min-height: 40px; }
|
|
85
|
+
.dstf-subheader {
|
|
86
|
+
border-top: 1px solid var(--dstf-border); padding: 4px 24px; min-height: 40px;
|
|
87
|
+
}
|
|
88
|
+
/* The Test page's subheader WRAPS: kept to one line beside a real
|
|
89
|
+
library's model id, it put Accept and Rerun past a 900 px window (wholly
|
|
90
|
+
off it on a TRANSFORM-length id) and squeezed Rerun to seven lines. The
|
|
91
|
+
id breaks at any character when a line cannot hold it. The TEST actions
|
|
92
|
+
are the status strip's now, below it. */
|
|
93
|
+
.dstf-subheader .dstf-model { overflow-wrap: anywhere; }
|
|
94
|
+
/* The Test page's status strip: a third row of the header, its tone a
|
|
95
|
+
3 px left border and a tint. The sentence takes what is left and wraps;
|
|
96
|
+
its buttons keep to the right as one group, below it when they do not
|
|
97
|
+
fit. */
|
|
98
|
+
.dstf-strip {
|
|
99
|
+
border-top: 1px solid var(--dstf-border); border-left: 3px solid;
|
|
100
|
+
padding: 4px 24px 4px 21px; min-height: 36px; row-gap: 4px;
|
|
101
|
+
}
|
|
102
|
+
.dstf-strip-grey { background: var(--dstf-queued-tint); border-left-color: var(--dstf-queued-border); }
|
|
103
|
+
.dstf-strip-red { background: var(--dstf-fail-tint); border-left-color: var(--dstf-fail-border); }
|
|
104
|
+
.dstf-strip-amber { background: var(--dstf-warn-tint); border-left-color: var(--dstf-warn-border); }
|
|
105
|
+
.dstf-strip-text { flex: 1 1 20em; }
|
|
106
|
+
.dstf-strip > .dstf-strip-actions { margin-left: auto; }
|
|
107
|
+
/* A pill is at least as wide as WARN, its text centred: PASS and FAIL are
|
|
108
|
+
one width, so the names after them on a plot's criteria rows start in
|
|
109
|
+
one column (D140; the report's `.node-status` keeps the same). */
|
|
110
|
+
.dstf-pill {
|
|
111
|
+
font-size: 10.5px; font-weight: 700; text-transform: uppercase;
|
|
112
|
+
letter-spacing: .03em; padding: 1px 6px; border-radius: 3px; border: 1px solid;
|
|
113
|
+
min-width: 4.8em; text-align: center;
|
|
114
|
+
}
|
|
115
|
+
/* A pill that carries its variable's name ("x FAIL", on a card drawing
|
|
116
|
+
more than one series): the name keeps the case the test wrote it in. */
|
|
117
|
+
.dstf-pill-named { text-transform: none; }
|
|
118
|
+
.dstf-tree-line, .dstf-stats-line { min-height: 32px; }
|
|
119
|
+
.dstf-tree-row {
|
|
120
|
+
min-height: 32px; border-top: 1px solid var(--dstf-border-hairline);
|
|
121
|
+
}
|
|
122
|
+
.dstf-tree-row:hover { background: var(--dstf-surface-hover); }
|
|
123
|
+
/* A row whose click opens something (U5): a criterion's editor, a k-of-n's
|
|
124
|
+
or a weighted's fields — a button to the mouse and the keyboard. */
|
|
125
|
+
.dstf-tree-row[role="button"] { cursor: pointer; }
|
|
126
|
+
.dstf-tree-row[role="button"]:focus-visible {
|
|
127
|
+
outline: 2px solid var(--dstf-accent); outline-offset: -2px;
|
|
128
|
+
}
|
|
129
|
+
/* Where a row sits (U5, Q1): a 6 px bar per level, 2 px apart, the row's
|
|
130
|
+
full height, in place of an indent; the hovered group's branch takes its
|
|
131
|
+
level's deeper shade on that level's bar (`.dstf-lvl-on`, set by the
|
|
132
|
+
page's script on the BAR, never the row). Then the chevron's place,
|
|
133
|
+
kept on every row so the pills stay in line. The bars cover the row's
|
|
134
|
+
top border too (the -1 px): stretched inside it they stopped 1 px short
|
|
135
|
+
of the row above's, a gap in every branch. */
|
|
136
|
+
.dstf-bars {
|
|
137
|
+
display: flex; flex: none; align-self: stretch; gap: 2px; margin-top: -1px;
|
|
138
|
+
}
|
|
139
|
+
.dstf-bar { width: 6px; flex: none; }
|
|
140
|
+
.dstf-lvl-1 { background: var(--dstf-level-1); }
|
|
141
|
+
.dstf-lvl-2 { background: var(--dstf-level-2); }
|
|
142
|
+
.dstf-lvl-3 { background: var(--dstf-level-3); }
|
|
143
|
+
.dstf-lvl-4 { background: var(--dstf-level-4); }
|
|
144
|
+
.dstf-lvl-1.dstf-lvl-on { background: var(--dstf-level-1-on); }
|
|
145
|
+
.dstf-lvl-2.dstf-lvl-on { background: var(--dstf-level-2-on); }
|
|
146
|
+
.dstf-lvl-3.dstf-lvl-on { background: var(--dstf-level-3-on); }
|
|
147
|
+
.dstf-lvl-4.dstf-lvl-on { background: var(--dstf-level-4-on); }
|
|
148
|
+
.dstf-chev-slot { width: 28px; flex: none; display: inline-flex; justify-content: center; }
|
|
149
|
+
/* A group's chevron (U5, Q3): a 28 px target, an 18 px glyph, turned
|
|
150
|
+
right while its row is folded; the row then says what it hides. */
|
|
151
|
+
.dstf-chev {
|
|
152
|
+
width: 28px; height: 28px; padding: 0; border: 0; border-radius: 50%;
|
|
153
|
+
background: transparent; color: var(--dstf-text-secondary); cursor: pointer;
|
|
154
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
155
|
+
}
|
|
156
|
+
.dstf-chev:hover { background: var(--dstf-surface-active); }
|
|
157
|
+
.dstf-chev:focus-visible { outline: 2px solid var(--dstf-accent); }
|
|
158
|
+
.dstf-chev .q-icon { font-size: 18px; transition: transform .12s; }
|
|
159
|
+
.dstf-tree-row[data-folded="1"] .dstf-chev .q-icon { transform: rotate(-90deg); }
|
|
160
|
+
.dstf-fold-summary { color: var(--dstf-text-secondary); font-size: 12px; }
|
|
161
|
+
/* A long row never widens the page: its text wraps — anywhere, as a
|
|
162
|
+
TRANSFORM name has no space to break at — and its readout keeps one
|
|
163
|
+
line, cut with an ellipsis, the whole of it in its title (`_Row.paint`).
|
|
164
|
+
At 900 px a deep criterion's name and readout pushed the page 48 px
|
|
165
|
+
sideways. The readout shrinks with the name down to 80 px and no
|
|
166
|
+
further: shrinking from 0, a 200-character name left it 71 px at 900
|
|
167
|
+
(J2); kept whole up to 40% of the row, it left the J2 tests' names
|
|
168
|
+
119-143 px at 900, five lines and more, where now they have 290-350. */
|
|
169
|
+
.dstf-tree-row > .dstf-tree-text { min-width: 0; overflow-wrap: anywhere; }
|
|
170
|
+
.dstf-tree-row > .dstf-tree-readout {
|
|
171
|
+
flex: 0 1 auto; min-width: 80px;
|
|
172
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
173
|
+
}
|
|
174
|
+
.dstf-tree-row:not([data-folded="1"]) .dstf-fold-summary { display: none; }
|
|
175
|
+
/* The root's tag (Q2): "and · 5 criteria · root". */
|
|
176
|
+
.dstf-root-tag { color: var(--dstf-text-muted); font-size: 12px; }
|
|
177
|
+
.dstf-root-tag::before { content: "· "; }
|
|
178
|
+
/* The drag (U4): a handle before the pill, its place kept while hidden so
|
|
179
|
+
the pills stay in line (`.dstf-grip-off`: never Quasar's `.hidden`, whose
|
|
180
|
+
cascade layer beats an unlayered `!important`). Over a row: a line at the row's indent where the
|
|
181
|
+
node would go before it, an outline onto it, a red outline where the
|
|
182
|
+
move is refused — its reason then fills the card's header line, in place
|
|
183
|
+
of the line's text (`.dstf-line-why`): said in the row it reflowed the
|
|
184
|
+
row's icons. */
|
|
185
|
+
.dstf-tree-row { position: relative; }
|
|
186
|
+
.dstf-grip {
|
|
187
|
+
width: 20px; flex: none; display: inline-flex; justify-content: center;
|
|
188
|
+
color: var(--dstf-text-muted); cursor: grab;
|
|
189
|
+
}
|
|
190
|
+
.dstf-grip-off { visibility: hidden; }
|
|
191
|
+
.dstf-drop-before::before {
|
|
192
|
+
content: ""; position: absolute; top: -2px; right: 0;
|
|
193
|
+
left: var(--indent, 0px); height: 3px; background: var(--dstf-accent);
|
|
194
|
+
pointer-events: none;
|
|
195
|
+
}
|
|
196
|
+
.dstf-drop-onto { outline: 2px solid var(--dstf-accent); outline-offset: -2px; }
|
|
197
|
+
.dstf-drop-no {
|
|
198
|
+
outline: 2px solid var(--dstf-fail-border); outline-offset: -2px;
|
|
199
|
+
cursor: not-allowed;
|
|
200
|
+
}
|
|
201
|
+
.dstf-tree-line.dstf-line-why > * { display: none !important; }
|
|
202
|
+
.dstf-tree-line.dstf-line-why::before {
|
|
203
|
+
content: attr(data-why); color: var(--dstf-fail-dark);
|
|
204
|
+
}
|
|
205
|
+
/* After a move (V4): its row flashed once as it comes into view, and one
|
|
206
|
+
line under it saying what the move changed. */
|
|
207
|
+
@keyframes dstf-flash { from { background: var(--dstf-selection); } }
|
|
208
|
+
.dstf-flash { animation: dstf-flash 1.2s ease-out; }
|
|
209
|
+
/* Its scroll margin: what is above the tree settles a pixel lower after
|
|
210
|
+
the line is scrolled to (the page's header row and strip repaint). */
|
|
211
|
+
.dstf-moved { color: var(--dstf-text-secondary); scroll-margin-bottom: 8px; }
|
|
212
|
+
/* The tree card's live region (`TreeCard.live`): read, never seen. */
|
|
213
|
+
.dstf-live {
|
|
214
|
+
position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
|
|
215
|
+
overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
|
|
216
|
+
}
|
|
217
|
+
/* Move to… (U4): a place per line, its node's text cut in the middle,
|
|
218
|
+
then where in it — "— before" whole and the row it names cut in the
|
|
219
|
+
middle too; the menu no wider than the page allows. Kept whole, a
|
|
220
|
+
100-character name ran past the menu's edge and squeezed the node's
|
|
221
|
+
text to 0 px. The named row takes what the node's text and "— before"
|
|
222
|
+
leave (a basis of 0), so it is cut first; the node's text is cut only
|
|
223
|
+
past that, to no less than 6ch — shrunk alike, a k-of-n's read only
|
|
224
|
+
its tail. "Where" lays its parts out in the label's own row
|
|
225
|
+
(`contents`): as a box of its own, its minimum width was its whole
|
|
226
|
+
text, cut or not. */
|
|
227
|
+
.dstf-move-menu { max-width: min(560px, 92vw); }
|
|
228
|
+
.dstf-move-label {
|
|
229
|
+
display: flex; gap: .4em; min-width: 0; white-space: nowrap;
|
|
230
|
+
}
|
|
231
|
+
.dstf-move-label > .dstf-mid { flex: 0 1 auto; min-width: 6ch; }
|
|
232
|
+
.dstf-move-where { display: contents; }
|
|
233
|
+
.dstf-move-where > * { flex: none; }
|
|
234
|
+
.dstf-move-where > .dstf-mid { flex: 1 1 0; min-width: 0; }
|
|
235
|
+
/* A combinator's kind dropdown, in its row's lead place: a dense Quasar
|
|
236
|
+
field is 40 px tall, which stood a combinator's row well above a
|
|
237
|
+
leaf's; at 28 px the rows read as one list. */
|
|
238
|
+
.dstf-kind .q-field__control, .dstf-kind .q-field__native,
|
|
239
|
+
.dstf-kind .q-field__marginal { min-height: 28px; height: 28px; }
|
|
240
|
+
/* A plot card: its title row, its plot, its criteria header and rows at
|
|
241
|
+
the card's own spacing (the brief's "col gap 10px"). */
|
|
242
|
+
.dstf-plot { gap: 10px; }
|
|
243
|
+
/* A card's header row wraps (3c's gate over a real library: at 900 px
|
|
244
|
+
beside a drawer, a long title, its pills and the toolbar in one row
|
|
245
|
+
that neither wrapped nor shrank scrolled the page sideways). The title
|
|
246
|
+
and its pills take what is left and wrap inside it, a long name cut in
|
|
247
|
+
its middle (`.dstf-mid`); the toolbar wraps as one unit, pushed to the
|
|
248
|
+
right. No
|
|
249
|
+
`no-wrap` on the row: kept to one line, the title shrank instead (to
|
|
250
|
+
three and four lines at 900 px beside a drawer). */
|
|
251
|
+
.dstf-card-head { row-gap: 4px; }
|
|
252
|
+
/* `min-width: 0`: a title cut in its middle is one unbreakable line, and
|
|
253
|
+
the row's automatic minimum — that whole line — pushed the page sideways
|
|
254
|
+
(1477 px on a 150-character title). */
|
|
255
|
+
.dstf-card-title { flex: 1 1 auto; min-width: 0; }
|
|
256
|
+
.dstf-card-tools { flex: none; margin-left: auto; }
|
|
257
|
+
.dstf-plot-slot { min-height: 320px; --toolbar-gutter: 34px; }
|
|
258
|
+
/* The plot's vertical toolbar stands in a gutter of its own, right of the
|
|
259
|
+
figure, instead of over the error axis's tick labels and title: a margin
|
|
260
|
+
reserved inside the figure could not know how wide those labels grow
|
|
261
|
+
(an error around 1e-5 outgrew every reserve measured). The plot is the
|
|
262
|
+
gutter's width narrower than its slot: `width: auto` undoes the plot's
|
|
263
|
+
`w-full`, so the margin narrows the plot instead of hanging the toolbar
|
|
264
|
+
15 px past the card. Plotly pins the toolbar's container with an inline
|
|
265
|
+
`right: 0px`, hence the `!important`. The width is not a `--dstf-`
|
|
266
|
+
token: those are the theme's table (theme.py). */
|
|
267
|
+
.dstf-plot-slot > .js-plotly-plot {
|
|
268
|
+
width: auto; margin-right: var(--toolbar-gutter);
|
|
269
|
+
}
|
|
270
|
+
.dstf-plot-slot .js-plotly-plot .modebar-container {
|
|
271
|
+
right: calc(-1 * var(--toolbar-gutter)) !important;
|
|
272
|
+
}
|
|
273
|
+
/* A tolerance grip — the arrow outside each side of the open criterion's
|
|
274
|
+
tolerance — is hidden at rest: `grips.js` marks each grip's path with its
|
|
275
|
+
group (`data-grip`) and, of the cluster of groups the pointer is in,
|
|
276
|
+
every one (`data-grip-on`). `display: none`, never `visibility` or `opacity`:
|
|
277
|
+
Plotly writes inline `opacity: 1; pointer-events: all` on every editable
|
|
278
|
+
shape, so a grip hidden either way was still grabbed where nothing
|
|
279
|
+
showed. Without a hover — a touch screen — they are always shown.
|
|
280
|
+
But an arrow whose edge is OFF the plot (`data-grip-off`, marked by
|
|
281
|
+
`grips.js` on every redraw): that one is not displayed at all, its
|
|
282
|
+
group shown or not, hover or none. One rule for an edge that is not on
|
|
283
|
+
the plot — no arrow. Left Plotly's clip instead, an arrow was still
|
|
284
|
+
painted wherever the clip cut nothing: whole, inside the panel, when it
|
|
285
|
+
is drawn inward of an edge 2 px off it; a sliver on the border beside
|
|
286
|
+
a checkpoint just past it. And the hover label gave way next to it. */
|
|
287
|
+
.js-plotly-plot .shapelayer path[data-grip]:not([data-grip-on]) { display: none; }
|
|
288
|
+
.js-plotly-plot .shapelayer path[data-grip-off] { display: none; }
|
|
289
|
+
/* And a grip is not clipped to the plot area. Plotly clips every shape to
|
|
290
|
+
it, and an arrow stands 10 to 17 px OUTSIDE its edge: with that edge at
|
|
291
|
+
the plot's edge, where any zoom can put it, the arrow was all in the
|
|
292
|
+
margin — not painted, and a press where it belonged landed on the axis
|
|
293
|
+
under it. Unclipped it is drawn whole in the margin, over the tick
|
|
294
|
+
labels, and takes the press: the shape layer lies above the axes' drag
|
|
295
|
+
layer. Only the svg's own edge cuts it. Every other shape keeps its clip
|
|
296
|
+
— a ring too: centred on its point, its inner half is drawn and pressed
|
|
297
|
+
while the point is on the plot.
|
|
298
|
+
Plotly writes the clip as the `clip-path` ATTRIBUTE, in two places: on
|
|
299
|
+
a shape's GROUP as it is drawn — every redraw draws new groups — and,
|
|
300
|
+
from a press to its release, on the held PATH itself, for every axis
|
|
301
|
+
whose range is set (any zoom sets them). A property of a stylesheet
|
|
302
|
+
beats a presentation attribute wherever it is written and whenever, so
|
|
303
|
+
these two rules are the whole mechanism: nothing takes an attribute
|
|
304
|
+
off, and an arrow pressed in the margin of a zoomed plot stays painted
|
|
305
|
+
while it is held. A browser without `:has` (Firefox before 121) drops
|
|
306
|
+
the group's rule, and the path's alone un-clips nothing: there an
|
|
307
|
+
arrow at the plot's edge is clipped as Plotly draws it.
|
|
308
|
+
The un-clip is for the 10 to 17 px an arrow stands outside an edge that
|
|
309
|
+
is ON the plot. An arrow whose edge is off it — the arrows of a
|
|
310
|
+
checkpoint zoomed just off the plot were painted in the margin beside
|
|
311
|
+
the axis title, and took a press — is not displayed (above), so no
|
|
312
|
+
rule here needs to leave it out. */
|
|
313
|
+
.js-plotly-plot .shapelayer path[data-grip] { clip-path: none; }
|
|
314
|
+
.js-plotly-plot .shapelayer g.shape-group:has(> path[data-grip]) { clip-path: none; }
|
|
315
|
+
/* An arrow that would be drawn over another of its cluster is moved ACROSS
|
|
316
|
+
its own axis by whole steps of 9 px (`grips.js` `stagger`, which marks
|
|
317
|
+
it with the steps: `data-grip-shift`, -3, -1, 1 or 3): a time or a
|
|
318
|
+
frequency arrow up or down, a value arrow left or right. A `transform`
|
|
319
|
+
of the stylesheet, so the path Plotly drew and drags is untouched: held,
|
|
320
|
+
it follows the pointer from where it is painted, and a release says its
|
|
321
|
+
edge. The 9 is `grips.js`'s STEP — it works out which arrows clear which
|
|
322
|
+
at that step. */
|
|
323
|
+
.js-plotly-plot .shapelayer path[data-grip-shift="-3"] { --grip-across: -27px; }
|
|
324
|
+
.js-plotly-plot .shapelayer path[data-grip-shift="-1"] { --grip-across: -9px; }
|
|
325
|
+
.js-plotly-plot .shapelayer path[data-grip-shift="1"] { --grip-across: 9px; }
|
|
326
|
+
.js-plotly-plot .shapelayer path[data-grip-shift="3"] { --grip-across: 27px; }
|
|
327
|
+
.js-plotly-plot .shapelayer path[data-grip-axis="x"][data-grip-shift] {
|
|
328
|
+
transform: translate(0, var(--grip-across));
|
|
329
|
+
}
|
|
330
|
+
.js-plotly-plot .shapelayer path[data-grip-axis="y"][data-grip-shift] {
|
|
331
|
+
transform: translate(var(--grip-across), 0);
|
|
332
|
+
}
|
|
333
|
+
/* The cursor over an arrow is the way it moves — up and down for a value
|
|
334
|
+
side, left and right for a time or a frequency side — by the axis
|
|
335
|
+
`grips.js` marks it with. Plotly's own for a draggable shape is `move`,
|
|
336
|
+
set as the pointer comes by a CLASS on the path (`.js-plotly-plot .plotly
|
|
337
|
+
.cursor-move`, specificity 0,3,0), never inline: these rules — two
|
|
338
|
+
classes, an attribute and an element, 0,3,1 — outrank it as they
|
|
339
|
+
stand. */
|
|
340
|
+
.js-plotly-plot .shapelayer path[data-grip-axis="y"] { cursor: ns-resize; }
|
|
341
|
+
.js-plotly-plot .shapelayer path[data-grip-axis="x"] { cursor: ew-resize; }
|
|
342
|
+
/* While the pointer is near a shown arrow or the ring its group stands
|
|
343
|
+
around (`grips.js` marks the graph div), the plot's hover label is not
|
|
344
|
+
shown: coming at a checkpoint from above or below, the unified label's
|
|
345
|
+
box and its spike line covered the arrows and half the ring. It is back
|
|
346
|
+
as soon as the pointer is away from them — arrows shown or not. By
|
|
347
|
+
`visibility`: the layer keeps its layout, which Plotly measures to size
|
|
348
|
+
a label it draws meanwhile. */
|
|
349
|
+
.js-plotly-plot[data-grip-near] .hoverlayer { visibility: hidden; }
|
|
350
|
+
/* A shape no editor moves — a box, a window, a result line, another
|
|
351
|
+
criterion's bound — takes no pointer: `grips.js` marks it `data-inert`
|
|
352
|
+
from what the figure says is draggable. Plotly makes EVERY shape of an
|
|
353
|
+
editable figure draggable (inline `pointer-events: all`, hence the
|
|
354
|
+
`!important`), so such a shape showed a resize cursor, moved under the
|
|
355
|
+
mouse and snapped back on release with nothing said. A press on it is now
|
|
356
|
+
the plot's own. A line is dragged by a helper group of its own, marked
|
|
357
|
+
too — and that helper is not clipped to the plot area as the line is:
|
|
358
|
+
a bound line an editor DOES move is marked the same while it is off
|
|
359
|
+
the plot whole, where its helper alone was left to press. */
|
|
360
|
+
.js-plotly-plot .shapelayer [data-inert],
|
|
361
|
+
.js-plotly-plot .shapelayer [data-inert] * { pointer-events: none !important; }
|
|
362
|
+
@media (hover: none), (pointer: coarse) {
|
|
363
|
+
.js-plotly-plot .shapelayer path[data-grip]:not([data-grip-on]):not([data-grip-off]) {
|
|
364
|
+
display: inline;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
.dstf-crit {
|
|
368
|
+
min-height: 36px; padding: 4px 8px; cursor: pointer;
|
|
369
|
+
border-top: 1px solid var(--dstf-border-hairline);
|
|
370
|
+
}
|
|
371
|
+
.dstf-crit:hover { background: var(--dstf-surface-hover); }
|
|
372
|
+
/* A row's name (metric · variable) is one line: beside an open drawer a
|
|
373
|
+
card is ~540 px at 900 px, and it broke inside itself ("tube" over
|
|
374
|
+
"· x"). The readout beside it may wrap. */
|
|
375
|
+
.dstf-crit-name { white-space: nowrap; }
|
|
376
|
+
/* A variable label cut in the MIDDLE (D140, Q20; `widgets.MiddleCut`): the
|
|
377
|
+
head ellipsized, the tail (`[3]`, the element) whole. Two inline-blocks
|
|
378
|
+
in an inline-block — never a flex pair — so innerText and a copy read the
|
|
379
|
+
whole label; the head is capped at the element's width less the tail's
|
|
380
|
+
(`--tail`, its length in ch), and the tail is at least that wide: in a
|
|
381
|
+
proportional font `[3]` is narrower than 3ch, and a head capped by more
|
|
382
|
+
than the tail took was cut with room to spare. In a flex row the element
|
|
383
|
+
shrinks: `overflow: hidden` makes its minimum width 0. */
|
|
384
|
+
.dstf-mid {
|
|
385
|
+
display: inline-block; max-width: 100%; overflow: hidden;
|
|
386
|
+
white-space: nowrap; vertical-align: bottom;
|
|
387
|
+
}
|
|
388
|
+
.dstf-mid-head {
|
|
389
|
+
display: inline-block; max-width: calc(100% - var(--tail, 0ch));
|
|
390
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
391
|
+
vertical-align: bottom;
|
|
392
|
+
}
|
|
393
|
+
.dstf-mid-tail {
|
|
394
|
+
display: inline-block; min-width: var(--tail, 0ch); white-space: nowrap;
|
|
395
|
+
vertical-align: bottom;
|
|
396
|
+
}
|
|
397
|
+
/* A row's breadcrumb — its ancestors' kinds, `and › k-of-n ›` — leads it,
|
|
398
|
+
muted and on one line: a plot's rows are its variable's leaves from
|
|
399
|
+
anywhere in the tree, and the crumb says where each sits. Its minimum
|
|
400
|
+
width is its card's longest crumb, in ch (`PlotCard`), so a plot's
|
|
401
|
+
`and ›` and `and › k-of-n ›` rows start their names in one column (the
|
|
402
|
+
report's `.node-crumbs` does the same). */
|
|
403
|
+
.dstf-crumb { white-space: nowrap; }
|
|
404
|
+
/* A plot's context series (grey on the plot, not scored), listed above it
|
|
405
|
+
each with the × that takes it off: Plotly's legend cannot hold a
|
|
406
|
+
button. */
|
|
407
|
+
.dstf-context { padding: 0 8px; }
|
|
408
|
+
.dstf-context-chip {
|
|
409
|
+
border: 1px solid var(--dstf-border-hairline); border-radius: 4px;
|
|
410
|
+
padding: 0 0 0 6px;
|
|
411
|
+
}
|
|
412
|
+
/* A long criterion name keeps its one line, so at narrow widths the row
|
|
413
|
+
wraps (it carries no `no-wrap`): the readout and the node's controls move
|
|
414
|
+
to a second line, the controls at its right end, instead of pushing the
|
|
415
|
+
page sideways (3c's gate over a real library, 900 px beside a drawer). */
|
|
416
|
+
.dstf-crit > .dstf-node-host { margin-left: auto; }
|
|
417
|
+
/* A plot's criteria header: a muted "Criteria" and, at its right end, what
|
|
418
|
+
attaches at VARIABLE (the plot's +), level with the criteria rows'
|
|
419
|
+
controls below it. */
|
|
420
|
+
.dstf-criteria-head { min-height: 32px; padding: 0 8px; }
|
|
421
|
+
.dstf-criteria-head > .dstf-node-host { margin-left: auto; }
|
|
422
|
+
/* It wraps (no `no-wrap`): under a lock the + gives way to "… is running
|
|
423
|
+
on <model>", longer than a card beside a drawer at 900 px. Its form
|
|
424
|
+
row's slot, below it, holds the host's hidden error labels even when no
|
|
425
|
+
form is open, so it is never `:empty` — it sits flush under the header
|
|
426
|
+
instead, and the card's gap is not paid twice. */
|
|
427
|
+
.dstf-criteria-head + .dstf-criteria-slot { margin-top: -10px; }
|
|
428
|
+
/* The open criterion's row, and a row with a form open under it, stay
|
|
429
|
+
lit. */
|
|
430
|
+
.dstf-crit-open, .dstf-row-open { background: var(--dstf-selection); }
|
|
431
|
+
/* A criterion's swatch: its status colour, filled while its drawing shows,
|
|
432
|
+
an outline of it while hidden; the open criterion's is locked on. */
|
|
433
|
+
.dstf-swatch {
|
|
434
|
+
width: 12px; height: 12px; border-radius: 2px; flex: none;
|
|
435
|
+
border: 2px solid; cursor: pointer; box-sizing: border-box;
|
|
436
|
+
}
|
|
437
|
+
.dstf-swatch-locked { cursor: default; opacity: .6; }
|
|
438
|
+
/* A family's folded criteria row (D152): its chevron sits in the swatch's
|
|
439
|
+
place — a 20 px target in the swatch's 12 px, so every row's name starts
|
|
440
|
+
in one column — turned right while the row is folded. A member's row is
|
|
441
|
+
a criterion's row, indented under its fold with a rule down its left;
|
|
442
|
+
folded away it is `hidden`, an attribute the row's `display: flex` would
|
|
443
|
+
otherwise beat. */
|
|
444
|
+
.dstf-fold-chev.dstf-chev { width: 20px; height: 20px; margin: 0 -4px; flex: none; }
|
|
445
|
+
.dstf-fold[data-folded="1"] .dstf-chev .q-icon { transform: rotate(-90deg); }
|
|
446
|
+
.dstf-crit[data-member] {
|
|
447
|
+
margin-left: 14px; width: calc(100% - 14px);
|
|
448
|
+
border-left: 2px solid var(--dstf-border);
|
|
449
|
+
}
|
|
450
|
+
.dstf-criteria > [hidden] { display: none !important; }
|
|
451
|
+
/* The editor belongs to the row above it: indented under it, with the
|
|
452
|
+
accent rule down its left saying how far it reaches. A node's form row
|
|
453
|
+
belongs to its row the same way (the 2026-09-21 board, 1A). */
|
|
454
|
+
.dstf-editor, .dstf-form-row {
|
|
455
|
+
margin: 0 0 8px 16px; padding: 12px;
|
|
456
|
+
border-left: 3px solid var(--dstf-accent);
|
|
457
|
+
border-top: 1px solid var(--dstf-border-hairline);
|
|
458
|
+
border-bottom: 1px solid var(--dstf-border-hairline);
|
|
459
|
+
}
|
|
460
|
+
/* The criterion editor's fields in ONE wrapping row (the board: the plot
|
|
461
|
+
and its editor fit a screen together). `.dstf-form` is the mode form's
|
|
462
|
+
slot, which hands its controls straight to that row rather than stacking
|
|
463
|
+
them in a column of its own; `.dstf-field` is one control, wide enough
|
|
464
|
+
for its label ("Width mode" clipped to "Wid…" in the Task 6 screenshot);
|
|
465
|
+
`.dstf-field-wide` is a multi-line one (a JSON list), on its own line. */
|
|
466
|
+
.dstf-fields { row-gap: 8px; }
|
|
467
|
+
.dstf-form { display: contents; }
|
|
468
|
+
.dstf-field { min-width: 140px; }
|
|
469
|
+
/* `order`, not source order: a tube's "Control points" is declared in the
|
|
470
|
+
MIDDLE of its mode form, and a full-width box there broke the row into
|
|
471
|
+
three lines with two single-line fields stranded below it. */
|
|
472
|
+
.dstf-fields .dstf-field-wide { order: 1; }
|
|
473
|
+
.dstf-field-wide { width: 100%; }
|
|
474
|
+
/* A tolerance's two sides (`forms.sides_control`): the lower field, the
|
|
475
|
+
link toggle and the upper field, one group on one line, the toggle level
|
|
476
|
+
with the two input lines. A group with a hint ("in effect: …") makes its
|
|
477
|
+
first field wide enough for it on one line — never wider than the row
|
|
478
|
+
leaves beside the toggle and the second field. Under 420 px of row (the
|
|
479
|
+
editor beside an open drawer in a 900 px window: 415) that width is
|
|
480
|
+
dropped rather than push the editor wider than its card: the group takes
|
|
481
|
+
the row, its first field what the row has left, and it wraps where even
|
|
482
|
+
that is too little. The row a group sits in is the container that says
|
|
483
|
+
how much room there is: the form's, or a table's rows. */
|
|
484
|
+
.dstf-fields, .dstf-table-scroll { container-type: inline-size; }
|
|
485
|
+
.dstf-sides { flex-wrap: nowrap; gap: 4px; }
|
|
486
|
+
.dstf-sides > .q-btn { flex: none; margin-top: 8px; }
|
|
487
|
+
/* The toggle's two states at a glance, as the board drew them: linked is
|
|
488
|
+
the accent, unlinked muted. Read off `aria-pressed` — an attribute the
|
|
489
|
+
server renders, not a class. */
|
|
490
|
+
.dstf-sides > .q-btn[aria-pressed="true"] { color: var(--dstf-accent); }
|
|
491
|
+
.dstf-sides > .q-btn[aria-pressed="false"] { color: var(--dstf-text-muted); }
|
|
492
|
+
/* A field's ⌖ (D149): muted at rest, the accent on a fill while its field
|
|
493
|
+
is ARMED — the next click on the plot sets it. Read off `aria-pressed`,
|
|
494
|
+
which the server renders. And the armed criterion's PLOT says so where
|
|
495
|
+
the click is made: its card carries `data-armed`, the plot an accent
|
|
496
|
+
outline, the line under it what the click sets. */
|
|
497
|
+
.dstf-arm { color: var(--dstf-text-muted); }
|
|
498
|
+
/* Close to its field's end: Quasar's 12 px before an appended control made
|
|
499
|
+
a points row, ten controls and eight of these, a line taller at 1280. */
|
|
500
|
+
.q-field__append:has(> .dstf-arm) { padding-left: 4px; }
|
|
501
|
+
.dstf-arm[aria-pressed="true"] {
|
|
502
|
+
color: var(--dstf-accent);
|
|
503
|
+
background: color-mix(in srgb, var(--dstf-accent) 18%, transparent);
|
|
504
|
+
}
|
|
505
|
+
.dstf-plot[data-armed] .dstf-plot-slot {
|
|
506
|
+
outline: 2px solid var(--dstf-accent); outline-offset: -2px;
|
|
507
|
+
}
|
|
508
|
+
.dstf-plot-armed { color: var(--dstf-accent); }
|
|
509
|
+
.dstf-sides-hinted > .q-field:first-child {
|
|
510
|
+
min-width: min(var(--dstf-hint-width), calc(100cqw - 180px));
|
|
511
|
+
}
|
|
512
|
+
@container (width < 420px) {
|
|
513
|
+
.dstf-sides { flex-wrap: wrap; }
|
|
514
|
+
.dstf-sides-hinted { flex: 1 1 100%; }
|
|
515
|
+
.dstf-sides-hinted > .q-field:first-child { min-width: 140px; flex: 1 1 140px; }
|
|
516
|
+
}
|
|
517
|
+
/* Focus (board 5A): "Showing 1 of N plots", the switcher naming it, and
|
|
518
|
+
the way back to all of them, on the accent tint so the page says at a
|
|
519
|
+
glance that what is below it is one plot of several. */
|
|
520
|
+
.dstf-focus-bar {
|
|
521
|
+
background: var(--dstf-selection); border: 1px solid var(--dstf-accent);
|
|
522
|
+
border-radius: 4px; padding: 4px 12px;
|
|
523
|
+
}
|
|
524
|
+
/* Eight rows of 42 px, and then the rows scroll (board 2A) — the
|
|
525
|
+
container this sits on holds the rows alone, never `Add row`. */
|
|
526
|
+
.dstf-table-scroll { max-height: 336px; overflow-y: auto; }
|
|
527
|
+
/* A table's row: its cells on one line where they fit and wrapped INSIDE
|
|
528
|
+
the row where they do not (a points row is ten controls) — the table
|
|
529
|
+
never scrolls sideways. Top-aligned: a cell that grows a line to say
|
|
530
|
+
"required" leaves the row's input lines level. */
|
|
531
|
+
.dstf-table-row { flex-wrap: wrap; align-items: flex-start; gap: 8px 12px; }
|
|
532
|
+
.dstf-table-row > .q-field, .dstf-table-row > .dstf-sides > .q-field {
|
|
533
|
+
width: 120px;
|
|
534
|
+
}
|
|
535
|
+
.dstf-table-row > .q-btn { margin-top: 6px; }
|
|
536
|
+
.dstf-wordmark { font-size: 20px; font-weight: 700; }
|
|
537
|
+
/* The pending drawer's preview (Q6 A): the entry as test_spec.json will
|
|
538
|
+
hold it, one line per row, never wrapped — a long line scrolls INSIDE
|
|
539
|
+
the diff, not the drawer. A flex item's min-width is its content's:
|
|
540
|
+
without the min-width 0 here and on the section and body columns a long
|
|
541
|
+
line widened the diff past the drawer, which clipped it, and nothing
|
|
542
|
+
scrolled. The lines sit in a box as wide as the longest of them, so an
|
|
543
|
+
added line's tint runs the whole scrolled width. */
|
|
544
|
+
.dstf-diff {
|
|
545
|
+
font-family: var(--dstf-mono); font-size: 12px; overflow-x: auto;
|
|
546
|
+
width: 100%; min-width: 0; max-width: 100%;
|
|
547
|
+
border: 1px solid var(--dstf-border-hairline); border-radius: 3px;
|
|
548
|
+
}
|
|
549
|
+
.dstf-diff-lines { width: max-content; min-width: 100%; }
|
|
550
|
+
.dstf-diff-line { display: flex; white-space: pre; line-height: 18px; padding: 0 6px; }
|
|
551
|
+
.dstf-diff-line > * { flex: none; }
|
|
552
|
+
.dstf-diff-add { background: var(--dstf-pass-tint); color: var(--dstf-pass-dark); }
|
|
553
|
+
.dstf-diff-del { background: var(--dstf-fail-tint); color: var(--dstf-fail-dark); }
|
|
554
|
+
.dstf-diff-gap, .dstf-diff-muted { color: var(--dstf-text-muted); }
|
|
555
|
+
.dstf-mono { font-family: var(--dstf-mono); }
|
|
556
|
+
.dstf-why { color: var(--dstf-text-muted); font-size: 12px; }
|
|
557
|
+
/* A card's title reads the expression its model wrote; the result-file
|
|
558
|
+
name — what a criterion, an override or a --filter names — rides beside
|
|
559
|
+
it, muted. Drawn only when the two differ. */
|
|
560
|
+
.dstf-varname { color: var(--dstf-text-muted); font-size: 12px; font-family: var(--dstf-mono); }
|
|
561
|
+
.dstf-progress { font-size: 12px; color: var(--dstf-text-secondary); }
|
|
562
|
+
.dstf-group {
|
|
563
|
+
font-size: 10px; text-transform: uppercase; letter-spacing: .03em;
|
|
564
|
+
color: var(--dstf-text-muted); padding: 12px 0 2px; overflow-wrap: anywhere;
|
|
565
|
+
}
|
|
566
|
+
/* A short, fixed title — the picker's sections, the Statistics card's —
|
|
567
|
+
never breaks (at 10 px "On this plot" measured 70 px in a 69.6 px box)
|
|
568
|
+
and spaces its words: uppercase at 10 px a space is 2.3 px, and "S P"
|
|
569
|
+
read "ON THISPLOT" in Chromium on one line. A drawer's package title
|
|
570
|
+
keeps `anywhere`. */
|
|
571
|
+
.dstf-pick-list .dstf-group, .dstf-stats .dstf-group {
|
|
572
|
+
white-space: nowrap; word-spacing: .2em;
|
|
573
|
+
}
|
|
574
|
+
/* The Statistics card (U3): a row per statistic, the numbers in fixed
|
|
575
|
+
right-aligned columns so Run and Baseline read down, the `+` or the
|
|
576
|
+
criterion's pill at the end; a changed row wears the report's warn
|
|
577
|
+
tint. */
|
|
578
|
+
.dstf-stats .dstf-group { padding-left: 8px; }
|
|
579
|
+
.dstf-stat-row {
|
|
580
|
+
width: 100%; min-height: 28px; padding: 0 8px;
|
|
581
|
+
border-top: 1px solid var(--dstf-border-hairline);
|
|
582
|
+
}
|
|
583
|
+
.dstf-stat-head { color: var(--dstf-text-muted); font-size: 12px; border-top: none; }
|
|
584
|
+
.dstf-stat-label {
|
|
585
|
+
flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis;
|
|
586
|
+
white-space: nowrap;
|
|
587
|
+
}
|
|
588
|
+
/* In px, not ch: the headings are not in the numbers' mono font, and a
|
|
589
|
+
`ch` column is as wide as its own font says. */
|
|
590
|
+
.dstf-stat-num { flex: none; width: 84px; text-align: right; }
|
|
591
|
+
.dstf-stat-delta { flex: none; width: 56px; text-align: right; }
|
|
592
|
+
.dstf-stat-end {
|
|
593
|
+
flex: none; width: 150px; display: flex; align-items: center;
|
|
594
|
+
justify-content: flex-end; gap: 6px;
|
|
595
|
+
}
|
|
596
|
+
.dstf-stat-changed { background: var(--dstf-warn-tint); }
|
|
597
|
+
/* Open, a changed row wears the selection as every open criteria row does:
|
|
598
|
+
the tint, declared later, hid which row the editor was under. */
|
|
599
|
+
.dstf-stats .dstf-crit-open { background: var(--dstf-selection); }
|
|
600
|
+
/* The editor under a Statistics row, or a tree row (U5), keeps inside the
|
|
601
|
+
card: `w-full` plus the editor's 16 px indent ran 16 px past its right
|
|
602
|
+
edge (B12). */
|
|
603
|
+
.dstf-stats .dstf-editor, .dstf-tree .dstf-editor { width: calc(100% - 16px); }
|
|
604
|
+
/* The Tests drawer's rows at a real library's lengths: the name takes what
|
|
605
|
+
the badge and the edits pill leave it and is cut with an ellipsis (the
|
|
606
|
+
whole id is its title), and they never shrink. A TRANSFORM id in Full,
|
|
607
|
+
or a 40-character short name, pushed the badge out of the drawer at
|
|
608
|
+
900 px, and a package label ran past it. */
|
|
609
|
+
.dstf-tests .dstf-name { flex: 1 1 auto; min-width: 0; }
|
|
610
|
+
.dstf-tests .dstf-name .q-btn__content > .block {
|
|
611
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
612
|
+
}
|
|
613
|
+
/* A Full name is cut at its start, where ids sharing a package prefix
|
|
614
|
+
differ least (`test_list._full_name`: the id is a <bdi> inside). */
|
|
615
|
+
.dstf-tests .dstf-name .dstf-cut-start { direction: rtl; }
|
|
616
|
+
.dstf-tests .dstf-badge, .dstf-tests .dstf-pill-named { flex: none; }
|
|
617
|
+
.dstf-row {
|
|
618
|
+
min-height: 32px; border-bottom: 1px solid var(--dstf-border-hairline);
|
|
619
|
+
background: var(--dstf-surface); transition: background .1s;
|
|
620
|
+
}
|
|
621
|
+
.dstf-row:hover { background: var(--dstf-surface-hover); }
|
|
622
|
+
.dstf-badge.q-badge {
|
|
623
|
+
color: #ffffff; font-size: 10.5px; font-weight: 700; text-transform: uppercase;
|
|
624
|
+
letter-spacing: .03em; border-radius: 3px;
|
|
625
|
+
}
|
|
626
|
+
/* The Test page's rail (3c): labelled buttons in 40 px. NiceGUI pads a
|
|
627
|
+
drawer by 1rem, which put the rail's buttons at 16-56 px of a 40 px
|
|
628
|
+
drawer and clipped "Tests" to "Tes" (the review's screenshot, and again
|
|
629
|
+
with 10 px labels): the drawer carries no padding. The rail and the
|
|
630
|
+
panel may give up the 1 px of the drawer's border rather than overflow
|
|
631
|
+
it. The open drawer's button sits on the selection tint. */
|
|
632
|
+
.nicegui-drawer.dstf-rail-drawer { padding: 0; gap: 0; }
|
|
633
|
+
.dstf-rail { width: 40px; min-width: 0; padding-top: 4px; }
|
|
634
|
+
.dstf-rail .q-btn { width: 100%; padding: 4px 0; min-width: 0; }
|
|
635
|
+
.dstf-rail-label { font-size: 10px; letter-spacing: 0; line-height: 1.2; }
|
|
636
|
+
.dstf-rail-on { background: var(--dstf-selection); color: var(--dstf-accent); }
|
|
637
|
+
/* The panel's width is the tab's (`TabState.drawer_width`, set inline by
|
|
638
|
+
`Rail`); its right edge is the resize grip (K5), 6 px of the drawer over
|
|
639
|
+
the panel's border. */
|
|
640
|
+
.dstf-drawer-panel { min-width: 0; flex: none; }
|
|
641
|
+
.dstf-rail-row { position: relative; }
|
|
642
|
+
.dstf-drawer-handle {
|
|
643
|
+
position: absolute; top: 0; right: 0; bottom: 0; width: 6px;
|
|
644
|
+
cursor: col-resize; touch-action: none; z-index: 1;
|
|
645
|
+
}
|
|
646
|
+
.dstf-drawer-handle:hover { background: var(--dstf-selection); }
|
|
647
|
+
/* Focused from the keyboard, the grip is the accent: ←/→ move it (K5). */
|
|
648
|
+
.dstf-drawer-handle:focus-visible { outline: none; background: var(--dstf-accent); }
|
|
649
|
+
/* The Variables drawer's rows (3c): "+ new plot" is there on the hovered
|
|
650
|
+
row only (the owner's ruling, Q6 A) — `visibility`, so the row does not
|
|
651
|
+
reflow as the mouse moves down the list — and on a row with focus in
|
|
652
|
+
it: a hidden button takes no focus, and Tab from a check skipped its
|
|
653
|
+
own. A family's members hang under it, indented; a family's swatch marks
|
|
654
|
+
an array, in the accent ramp (a plot picks a family's hue by its order
|
|
655
|
+
on that plot, not this). The target line is a tag on the accent tint
|
|
656
|
+
with the selected plot's ring inside it, as the owner's board drew it: a
|
|
657
|
+
bare ring beside the text read as an empty checkbox. */
|
|
658
|
+
.dstf-var-row { min-height: 28px; }
|
|
659
|
+
/* Two lines (2026-09-29): the label with the row's width on the first; the
|
|
660
|
+
muted name, "plot N" and "+ new plot" on the second, under the label
|
|
661
|
+
(past the check). A row with nothing but "+ new plot" is one line (K6). */
|
|
662
|
+
.dstf-var-line1 { min-height: 26px; }
|
|
663
|
+
.dstf-var-line2 { padding-left: 28px; min-height: 18px; font-size: 11px; }
|
|
664
|
+
.dstf-var-line2 .q-btn { min-height: 18px; }
|
|
665
|
+
.dstf-var-line2 .dstf-varname {
|
|
666
|
+
min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
667
|
+
}
|
|
668
|
+
.dstf-var-row:hover { background: var(--dstf-surface-hover); }
|
|
669
|
+
.dstf-var-row .dstf-var-new { visibility: hidden; }
|
|
670
|
+
.dstf-var-row:hover .dstf-var-new,
|
|
671
|
+
.dstf-var-row:focus-within .dstf-var-new { visibility: visible; }
|
|
672
|
+
.dstf-var-member { padding-left: 24px; }
|
|
673
|
+
.dstf-var-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
674
|
+
.dstf-var-where, .dstf-var-row .dstf-var-new { flex: none; white-space: nowrap; }
|
|
675
|
+
.dstf-var-ramp { display: inline-flex; flex: none; }
|
|
676
|
+
.dstf-var-ramp > span { width: 5px; height: 9px; }
|
|
677
|
+
.dstf-var-target {
|
|
678
|
+
cursor: pointer; font-size: 12px; padding: 4px 8px; border-radius: 3px;
|
|
679
|
+
background: var(--dstf-selection); color: var(--dstf-accent);
|
|
680
|
+
}
|
|
681
|
+
.dstf-var-ring {
|
|
682
|
+
width: 12px; height: 9px; border-radius: 2px; flex: none;
|
|
683
|
+
border: 2px solid var(--dstf-accent); box-sizing: content-box;
|
|
684
|
+
}
|
|
685
|
+
/* The add form's picker (U2): a list that scrolls inside the form row,
|
|
686
|
+
never a 200-row form; the chosen row on the selection tint, the
|
|
687
|
+
keyboard's cursor ringed; the Chosen line under it (K7). */
|
|
688
|
+
.dstf-pick-list { max-height: 240px; overflow-y: auto; }
|
|
689
|
+
.dstf-pick-row { cursor: pointer; min-height: 24px; padding: 0 4px; }
|
|
690
|
+
.dstf-pick-row:hover { background: var(--dstf-surface-hover); }
|
|
691
|
+
.dstf-pick-on { background: var(--dstf-selection); }
|
|
692
|
+
.dstf-pick-cursor { box-shadow: inset 0 0 0 2px var(--dstf-accent); }
|
|
693
|
+
.dstf-pick-chosen { color: var(--dstf-text-muted); padding: 2px 4px 0; }
|
|
694
|
+
/* The drawers' one control set (Q7): a menu's current choice carries its
|
|
695
|
+
check; a count pill is a status pill that is a button, ringed while it
|
|
696
|
+
filters. */
|
|
697
|
+
.dstf-menu-check { visibility: hidden; }
|
|
698
|
+
.dstf-menu-checked .dstf-menu-check { visibility: visible; }
|
|
699
|
+
.dstf-count-pill.q-btn { min-height: 20px; padding: 0 6px; font-size: 10.5px; }
|
|
700
|
+
.dstf-count-pill-on.q-btn { box-shadow: 0 0 0 2px var(--dstf-accent); }
|
|
701
|
+
.dstf-card {
|
|
702
|
+
border: 1px solid var(--dstf-border); border-radius: 4px; box-shadow: none;
|
|
703
|
+
background: var(--dstf-surface); padding: 16px;
|
|
704
|
+
}
|
|
705
|
+
/* The selected plot, the one the Variables drawer's checkboxes act on: two
|
|
706
|
+
classes and after `.dstf-card`, whose `box-shadow: none` otherwise wins
|
|
707
|
+
and the ring is classed but never painted (3c's LOOK, in Chromium). */
|
|
708
|
+
.dstf-card.dstf-plot-selected { box-shadow: inset 0 0 0 2px var(--dstf-accent); }
|
|
709
|
+
"""
|
|
710
|
+
+ _TINTS
|
|
711
|
+
+ _PILLS
|
|
712
|
+
+ _SWATCHES
|
|
713
|
+
# After the fills, so an outline wins over the colour it is drawn in.
|
|
714
|
+
+ ".dstf-swatch-off { background: transparent; }\n"
|
|
715
|
+
# Two classes: that alone beats the one-class status tints, whatever the
|
|
716
|
+
# order (as one class it tied with them and read the fail tint,
|
|
717
|
+
# rgb(255,235,238) — the 3c review). The order that matters is after
|
|
718
|
+
# `.dstf-row:hover`, which has the same specificity: last, the current
|
|
719
|
+
# row stays lit under the mouse.
|
|
720
|
+
+ ".dstf-row.dstf-row-current { background: var(--dstf-selection); }\n"
|
|
721
|
+
)
|