figpack 0.2.17__py3-none-any.whl → 0.2.18__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.
Potentially problematic release.
This version of figpack might be problematic. Click here for more details.
- figpack/__init__.py +1 -1
- figpack/cli.py +74 -0
- figpack/core/_bundle_utils.py +29 -0
- figpack/core/_file_handler.py +192 -0
- figpack/core/_server_manager.py +42 -6
- figpack/core/_show_view.py +1 -1
- figpack/core/_view_figure.py +43 -12
- figpack/extensions.py +356 -0
- figpack/figpack-figure-dist/assets/{index-DHWczh-Q.css → index-BJUFDPIM.css} +1 -1
- figpack/figpack-figure-dist/assets/index-nBpxgXXT.js +91 -0
- figpack/figpack-figure-dist/index.html +2 -2
- {figpack-0.2.17.dist-info → figpack-0.2.18.dist-info}/METADATA +1 -1
- {figpack-0.2.17.dist-info → figpack-0.2.18.dist-info}/RECORD +17 -15
- figpack/figpack-figure-dist/assets/index-DBwmtEpB.js +0 -91
- {figpack-0.2.17.dist-info → figpack-0.2.18.dist-info}/WHEEL +0 -0
- {figpack-0.2.17.dist-info → figpack-0.2.18.dist-info}/entry_points.txt +0 -0
- {figpack-0.2.17.dist-info → figpack-0.2.18.dist-info}/licenses/LICENSE +0 -0
- {figpack-0.2.17.dist-info → figpack-0.2.18.dist-info}/top_level.txt +0 -0
figpack/extensions.py
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Extension management functionality for figpack
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from typing import Dict, List, Optional, Tuple
|
|
10
|
+
from urllib.parse import urljoin
|
|
11
|
+
|
|
12
|
+
import requests
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Default wheel repository URL
|
|
16
|
+
DEFAULT_WHEEL_REPO_URL = "https://flatironinstitute.github.io/figpack/wheels/"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def parse_wheel_filename(filename: str) -> Optional[Dict[str, str]]:
|
|
20
|
+
"""
|
|
21
|
+
Parse a wheel filename to extract package information.
|
|
22
|
+
|
|
23
|
+
Wheel filename format: {distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl
|
|
24
|
+
"""
|
|
25
|
+
if not filename.endswith(".whl"):
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
# Remove .whl extension
|
|
29
|
+
basename = filename[:-4]
|
|
30
|
+
|
|
31
|
+
# Split on hyphens, but be careful about package names with hyphens
|
|
32
|
+
parts = basename.split("-")
|
|
33
|
+
|
|
34
|
+
if len(parts) < 5:
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
# The last 3 parts are always python_tag, abi_tag, platform_tag
|
|
38
|
+
python_tag = parts[-3]
|
|
39
|
+
abi_tag = parts[-2]
|
|
40
|
+
platform_tag = parts[-1]
|
|
41
|
+
|
|
42
|
+
# Everything before the last 3 parts, split between name and version
|
|
43
|
+
name_version_parts = parts[:-3]
|
|
44
|
+
|
|
45
|
+
# Find where version starts (first part that looks like a version number)
|
|
46
|
+
version_start_idx = 1 # Default to second part
|
|
47
|
+
for i, part in enumerate(name_version_parts[1:], 1):
|
|
48
|
+
if re.match(r"^\d+", part): # Starts with a digit
|
|
49
|
+
version_start_idx = i
|
|
50
|
+
break
|
|
51
|
+
|
|
52
|
+
name = "-".join(name_version_parts[:version_start_idx])
|
|
53
|
+
version = "-".join(name_version_parts[version_start_idx:])
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
"name": name,
|
|
57
|
+
"version": version,
|
|
58
|
+
"python_tag": python_tag,
|
|
59
|
+
"abi_tag": abi_tag,
|
|
60
|
+
"platform_tag": platform_tag,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ExtensionManager:
|
|
65
|
+
"""Manages figpack extension packages"""
|
|
66
|
+
|
|
67
|
+
def __init__(self, wheel_repo_url: str = DEFAULT_WHEEL_REPO_URL):
|
|
68
|
+
self.wheel_repo_url = wheel_repo_url.rstrip("/") + "/"
|
|
69
|
+
self._available_cache = None
|
|
70
|
+
|
|
71
|
+
def get_installed_packages(self) -> Dict[str, str]:
|
|
72
|
+
"""Get currently installed figpack extension packages and their versions"""
|
|
73
|
+
try:
|
|
74
|
+
result = subprocess.run(
|
|
75
|
+
[sys.executable, "-m", "pip", "list", "--format=json"],
|
|
76
|
+
capture_output=True,
|
|
77
|
+
text=True,
|
|
78
|
+
check=True,
|
|
79
|
+
)
|
|
80
|
+
installed_packages = json.loads(result.stdout)
|
|
81
|
+
|
|
82
|
+
# Get available extensions to filter against
|
|
83
|
+
available_extensions = self.get_available_packages()
|
|
84
|
+
|
|
85
|
+
# Filter for figpack extensions
|
|
86
|
+
figpack_extensions = {}
|
|
87
|
+
for pkg in installed_packages:
|
|
88
|
+
pkg_name = pkg["name"]
|
|
89
|
+
if pkg_name in available_extensions:
|
|
90
|
+
figpack_extensions[pkg_name] = pkg["version"]
|
|
91
|
+
|
|
92
|
+
return figpack_extensions
|
|
93
|
+
except (subprocess.CalledProcessError, json.JSONDecodeError) as e:
|
|
94
|
+
print(f"Warning: Could not get installed packages: {e}")
|
|
95
|
+
return {}
|
|
96
|
+
|
|
97
|
+
def get_available_packages(self) -> Dict[str, Dict[str, str]]:
|
|
98
|
+
"""
|
|
99
|
+
Get available packages from the wheel repository.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Dict mapping package names to their info (version, description, etc.)
|
|
103
|
+
"""
|
|
104
|
+
if self._available_cache is not None:
|
|
105
|
+
return self._available_cache
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
# Try the simple index first as it's easier to parse
|
|
109
|
+
packages = self._get_packages_from_simple_index()
|
|
110
|
+
|
|
111
|
+
# If that fails, try the main index.html
|
|
112
|
+
if not packages:
|
|
113
|
+
packages = self._get_packages_from_main_index()
|
|
114
|
+
|
|
115
|
+
self._available_cache = packages
|
|
116
|
+
return packages
|
|
117
|
+
|
|
118
|
+
except requests.RequestException as e:
|
|
119
|
+
print(f"Warning: Could not fetch available packages: {e}")
|
|
120
|
+
return {}
|
|
121
|
+
|
|
122
|
+
def _get_packages_from_simple_index(self) -> Dict[str, Dict[str, str]]:
|
|
123
|
+
"""Get packages from simple.html using regex parsing"""
|
|
124
|
+
try:
|
|
125
|
+
simple_url = urljoin(self.wheel_repo_url, "simple.html")
|
|
126
|
+
response = requests.get(simple_url, timeout=10)
|
|
127
|
+
response.raise_for_status()
|
|
128
|
+
|
|
129
|
+
packages = {}
|
|
130
|
+
|
|
131
|
+
# Parse wheel filenames from simple index using regex
|
|
132
|
+
# Look for href attributes containing .whl files
|
|
133
|
+
wheel_pattern = re.compile(r'href="([^"]*\.whl)"')
|
|
134
|
+
matches = wheel_pattern.findall(response.text)
|
|
135
|
+
|
|
136
|
+
for wheel_filename in matches:
|
|
137
|
+
wheel_info = parse_wheel_filename(wheel_filename)
|
|
138
|
+
if wheel_info and wheel_info["name"].startswith("figpack_"):
|
|
139
|
+
pkg_name = wheel_info["name"]
|
|
140
|
+
|
|
141
|
+
if pkg_name not in packages:
|
|
142
|
+
packages[pkg_name] = {
|
|
143
|
+
"version": wheel_info["version"],
|
|
144
|
+
"description": self._get_package_description(pkg_name),
|
|
145
|
+
}
|
|
146
|
+
else:
|
|
147
|
+
current_version = packages[pkg_name]["version"]
|
|
148
|
+
new_version = wheel_info["version"]
|
|
149
|
+
if self._is_newer_version(new_version, current_version):
|
|
150
|
+
packages[pkg_name]["version"] = new_version
|
|
151
|
+
|
|
152
|
+
return packages
|
|
153
|
+
|
|
154
|
+
except requests.RequestException:
|
|
155
|
+
return {}
|
|
156
|
+
|
|
157
|
+
def _get_packages_from_main_index(self) -> Dict[str, Dict[str, str]]:
|
|
158
|
+
"""Get packages from main index.html using regex parsing"""
|
|
159
|
+
try:
|
|
160
|
+
index_url = urljoin(self.wheel_repo_url, "index.html")
|
|
161
|
+
response = requests.get(index_url, timeout=10)
|
|
162
|
+
response.raise_for_status()
|
|
163
|
+
|
|
164
|
+
packages = {}
|
|
165
|
+
|
|
166
|
+
# Parse wheel filenames from main index using regex
|
|
167
|
+
# Look for links to .whl files
|
|
168
|
+
wheel_pattern = re.compile(r'<a[^>]+href="([^"]*\.whl)"[^>]*>')
|
|
169
|
+
matches = wheel_pattern.findall(response.text)
|
|
170
|
+
|
|
171
|
+
for wheel_filename in matches:
|
|
172
|
+
wheel_info = parse_wheel_filename(wheel_filename)
|
|
173
|
+
if wheel_info and wheel_info["name"].startswith("figpack_"):
|
|
174
|
+
pkg_name = wheel_info["name"]
|
|
175
|
+
|
|
176
|
+
if pkg_name not in packages:
|
|
177
|
+
packages[pkg_name] = {
|
|
178
|
+
"version": wheel_info["version"],
|
|
179
|
+
"description": self._get_package_description(pkg_name),
|
|
180
|
+
}
|
|
181
|
+
else:
|
|
182
|
+
current_version = packages[pkg_name]["version"]
|
|
183
|
+
new_version = wheel_info["version"]
|
|
184
|
+
if self._is_newer_version(new_version, current_version):
|
|
185
|
+
packages[pkg_name]["version"] = new_version
|
|
186
|
+
|
|
187
|
+
return packages
|
|
188
|
+
|
|
189
|
+
except requests.RequestException:
|
|
190
|
+
return {}
|
|
191
|
+
|
|
192
|
+
def _get_package_description(self, package_name: str) -> str:
|
|
193
|
+
"""Get a description for a package based on its name"""
|
|
194
|
+
descriptions = {
|
|
195
|
+
"figpack_3d": "3D visualization extension using Three.js",
|
|
196
|
+
"figpack_force_graph": "Force-directed graph visualization extension",
|
|
197
|
+
"figpack_franklab": "Frank Lab specific neuroscience visualization tools",
|
|
198
|
+
"figpack_spike_sorting": "Spike sorting specific visualization tools",
|
|
199
|
+
}
|
|
200
|
+
return descriptions.get(package_name, "Figpack extension package")
|
|
201
|
+
|
|
202
|
+
def _is_newer_version(self, version1: str, version2: str) -> bool:
|
|
203
|
+
"""Simple version comparison - returns True if version1 > version2"""
|
|
204
|
+
try:
|
|
205
|
+
v1_parts = [int(x) for x in version1.split(".")]
|
|
206
|
+
v2_parts = [int(x) for x in version2.split(".")]
|
|
207
|
+
|
|
208
|
+
# Pad shorter version with zeros
|
|
209
|
+
max_len = max(len(v1_parts), len(v2_parts))
|
|
210
|
+
v1_parts.extend([0] * (max_len - len(v1_parts)))
|
|
211
|
+
v2_parts.extend([0] * (max_len - len(v2_parts)))
|
|
212
|
+
|
|
213
|
+
return v1_parts > v2_parts
|
|
214
|
+
except ValueError:
|
|
215
|
+
# If we can't parse versions, just do string comparison
|
|
216
|
+
return version1 > version2
|
|
217
|
+
|
|
218
|
+
def list_extensions(self) -> None:
|
|
219
|
+
"""List all available extensions with their status"""
|
|
220
|
+
print("Available figpack extensions:")
|
|
221
|
+
print()
|
|
222
|
+
|
|
223
|
+
installed = self.get_installed_packages()
|
|
224
|
+
available = self.get_available_packages()
|
|
225
|
+
|
|
226
|
+
if not available:
|
|
227
|
+
print("No extensions found in the wheel repository.")
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
for ext_name, ext_info in available.items():
|
|
231
|
+
if ext_name in installed:
|
|
232
|
+
status = f"✓ {ext_name} (installed: {installed[ext_name]}, latest: {ext_info['version']})"
|
|
233
|
+
else:
|
|
234
|
+
status = f"✗ {ext_name} (not installed, latest: {ext_info['version']})"
|
|
235
|
+
|
|
236
|
+
print(f"{status} - {ext_info['description']}")
|
|
237
|
+
|
|
238
|
+
def install_extensions(
|
|
239
|
+
self, extensions: List[str], upgrade: bool = False, install_all: bool = False
|
|
240
|
+
) -> bool:
|
|
241
|
+
"""Install or upgrade extensions"""
|
|
242
|
+
available = self.get_available_packages()
|
|
243
|
+
|
|
244
|
+
if install_all:
|
|
245
|
+
extensions = list(available.keys())
|
|
246
|
+
|
|
247
|
+
if not extensions:
|
|
248
|
+
print("No extensions specified")
|
|
249
|
+
return False
|
|
250
|
+
|
|
251
|
+
# Validate extension names
|
|
252
|
+
invalid_extensions = [ext for ext in extensions if ext not in available]
|
|
253
|
+
if invalid_extensions:
|
|
254
|
+
print(f"Error: Unknown extensions: {', '.join(invalid_extensions)}")
|
|
255
|
+
print(f"Available extensions: {', '.join(available.keys())}")
|
|
256
|
+
return False
|
|
257
|
+
|
|
258
|
+
success = True
|
|
259
|
+
for extension in extensions:
|
|
260
|
+
if not self._install_single_extension(extension, upgrade):
|
|
261
|
+
success = False
|
|
262
|
+
|
|
263
|
+
return success
|
|
264
|
+
|
|
265
|
+
def _install_single_extension(self, extension: str, upgrade: bool = False) -> bool:
|
|
266
|
+
"""Install a single extension"""
|
|
267
|
+
cmd = [
|
|
268
|
+
sys.executable,
|
|
269
|
+
"-m",
|
|
270
|
+
"pip",
|
|
271
|
+
"install",
|
|
272
|
+
"--find-links",
|
|
273
|
+
self.wheel_repo_url,
|
|
274
|
+
extension,
|
|
275
|
+
]
|
|
276
|
+
|
|
277
|
+
if upgrade:
|
|
278
|
+
cmd.append("--upgrade")
|
|
279
|
+
|
|
280
|
+
try:
|
|
281
|
+
print(f"Installing {extension}...")
|
|
282
|
+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
283
|
+
|
|
284
|
+
# Check if it was actually installed or already satisfied
|
|
285
|
+
if "successfully installed" in result.stdout.lower():
|
|
286
|
+
print(f"✓ Successfully installed {extension}")
|
|
287
|
+
elif "already satisfied" in result.stdout.lower():
|
|
288
|
+
print(f"✓ {extension} is already installed")
|
|
289
|
+
else:
|
|
290
|
+
print(f"✓ {extension} installation completed")
|
|
291
|
+
|
|
292
|
+
return True
|
|
293
|
+
|
|
294
|
+
except subprocess.CalledProcessError as e:
|
|
295
|
+
print(f"✗ Failed to install {extension}")
|
|
296
|
+
if e.stderr:
|
|
297
|
+
print(f"Error: {e.stderr.strip()}")
|
|
298
|
+
return False
|
|
299
|
+
|
|
300
|
+
def uninstall_extensions(self, extensions: List[str]) -> bool:
|
|
301
|
+
"""Uninstall extensions"""
|
|
302
|
+
if not extensions:
|
|
303
|
+
print("No extensions specified")
|
|
304
|
+
return False
|
|
305
|
+
|
|
306
|
+
available = self.get_available_packages()
|
|
307
|
+
|
|
308
|
+
# Validate extension names
|
|
309
|
+
invalid_extensions = [ext for ext in extensions if ext not in available]
|
|
310
|
+
if invalid_extensions:
|
|
311
|
+
print(f"Error: Unknown extensions: {', '.join(invalid_extensions)}")
|
|
312
|
+
return False
|
|
313
|
+
|
|
314
|
+
# Check which extensions are actually installed
|
|
315
|
+
installed = self.get_installed_packages()
|
|
316
|
+
not_installed = [ext for ext in extensions if ext not in installed]
|
|
317
|
+
|
|
318
|
+
if not_installed:
|
|
319
|
+
print(
|
|
320
|
+
f"Warning: These extensions are not installed: {', '.join(not_installed)}"
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
to_uninstall = [ext for ext in extensions if ext in installed]
|
|
324
|
+
if not to_uninstall:
|
|
325
|
+
print("No installed extensions to uninstall")
|
|
326
|
+
return True
|
|
327
|
+
|
|
328
|
+
# Confirm uninstallation
|
|
329
|
+
print(f"This will uninstall: {', '.join(to_uninstall)}")
|
|
330
|
+
response = input("Continue? [y/N]: ").strip().lower()
|
|
331
|
+
if response not in ["y", "yes"]:
|
|
332
|
+
print("Uninstallation cancelled")
|
|
333
|
+
return False
|
|
334
|
+
|
|
335
|
+
success = True
|
|
336
|
+
for extension in to_uninstall:
|
|
337
|
+
if not self._uninstall_single_extension(extension):
|
|
338
|
+
success = False
|
|
339
|
+
|
|
340
|
+
return success
|
|
341
|
+
|
|
342
|
+
def _uninstall_single_extension(self, extension: str) -> bool:
|
|
343
|
+
"""Uninstall a single extension"""
|
|
344
|
+
cmd = [sys.executable, "-m", "pip", "uninstall", "-y", extension]
|
|
345
|
+
|
|
346
|
+
try:
|
|
347
|
+
print(f"Uninstalling {extension}...")
|
|
348
|
+
subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
349
|
+
print(f"✓ Successfully uninstalled {extension}")
|
|
350
|
+
return True
|
|
351
|
+
|
|
352
|
+
except subprocess.CalledProcessError as e:
|
|
353
|
+
print(f"✗ Failed to uninstall {extension}")
|
|
354
|
+
if e.stderr:
|
|
355
|
+
print(f"Error: {e.stderr.strip()}")
|
|
356
|
+
return False
|
|
@@ -1 +1 @@
|
|
|
1
|
-
.unitEntryBase{padding-right:13px;padding-top:10px}.unselectedUnitEntry{border:1px transparent solid;font-weight:400;color:#000;background-color:#fff}.selectedUnitEntry{border:1px solid blue;font-weight:700;color:#000;background-color:#b5d1ff}.unitIdsColumnLabelStyle{min-width:200px;font-weight:700;padding:7px 5px}.unitLabelsStyle{padding-right:3px;color:#333}.plotUnitLabel{text-align:left;margin-left:20px;margin-top:5px;font-weight:700}.plotWrapperButtonParent{display:inline-block}.plotWrapperStyleButton{padding-top:50px}.plotUnselectedStyle{border:3px solid rgb(202,224,231)}.plotSelectedStyle{border:3px solid #9999ff;background-color:#fff}.plotUnselectableStyle{border:3px solid transparent}.plotCurrentStyle{border:3px solid blue;background-color:#fff}.plotLabelStyle{font-weight:700;text-align:center;overflow:hidden;white-space:nowrap}:root{font-family:system-ui,Avenir,Helvetica,Arial,sans-serif;line-height:1.5;font-weight:400;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}a{font-weight:500;color:#646cff;text-decoration:inherit}a:hover{color:#535bf2}body{margin:0;overflow:hidden}h1{font-size:3.2em;line-height:1.1}button{border-radius:8px;border:1px solid transparent;padding:.6em 1.2em;font-size:1em;font-weight:500;font-family:inherit;background-color:#1a1a1a;cursor:pointer;transition:border-color .25s}button:hover{border-color:#646cff}button:focus,button:focus-visible{outline:4px auto -webkit-focus-ring-color}.status-bar{position:fixed;bottom:0;left:0;right:0;height:30px;background-color:#2a2a2a;color:#fff;display:flex;align-items:center;padding:0 12px;font-size:12px;border-top:1px solid #444;z-index:1000}.status-bar.warning{background-color:#ff9800;color:#000}.status-bar.error{background-color:#f44336;color:#fff}.status-bar.expired{background-color:#d32f2f;color:#fff}.manage-button{background-color:#444;color:#fff;border:none;padding:4px 8px;border-radius:4px;cursor:pointer;font-size:11px;height:22px;margin-left:12px;transition:background-color .2s}.manage-button:hover{background-color:#666}.about-button{background-color:#444;color:#fff;border:none;padding:4px 8px;border-radius:4px;cursor:pointer;font-size:11px;height:22px;margin-left:12px;transition:background-color .2s}.about-button:hover{background-color:#666}.about-dialog-overlay{position:fixed;inset:0;background-color:#00000080;display:flex;justify-content:center;align-items:center;z-index:2000}.about-dialog{background-color:#2a2a2a;color:#fff;border-radius:8px;max-width:600px;max-height:80vh;width:90%;box-shadow:0 4px 20px #0000004d;overflow:hidden}.about-dialog-header{display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid #444}.about-dialog-header h2{margin:0;font-size:18px;font-weight:600}.about-dialog-close{background:none;border:none;color:#fff;font-size:24px;cursor:pointer;padding:0;width:30px;height:30px;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:background-color .2s}.about-dialog-close:hover{background-color:#444}.about-dialog-content{padding:20px;overflow-y:auto;max-height:calc(80vh - 80px)}.about-dialog-description{line-height:1.6}.about-dialog-description h1,.about-dialog-description h2,.about-dialog-description h3{margin-top:0;margin-bottom:12px}.about-dialog-description p{margin-bottom:12px}.about-dialog-description code{background-color:#444;padding:2px 4px;border-radius:3px;font-family:Courier New,monospace;font-size:.9em}.about-dialog-description pre{background-color:#444;padding:12px;border-radius:4px;overflow-x:auto;margin-bottom:12px}.about-dialog-description ul,.about-dialog-description ol{margin-bottom:12px;padding-left:20px}.about-dialog-description blockquote{border-left:4px solid #666;padding-left:16px;margin:12px 0;font-style:italic}@media (prefers-color-scheme: light){:root{color:#213547;background-color:#fff}a:hover{color:#747bff}button{background-color:#f9f9f9}.status-bar{background-color:#f5f5f5;color:#333;border-top:1px solid #ddd}.status-bar.warning{background-color:#fff3cd;color:#856404;border-top:1px solid #ffeaa7}.status-bar.error,.status-bar.expired{background-color:#f8d7da;color:#721c24;border-top:1px solid #f5c6cb}.manage-button{background-color:#e0e0e0;color:#333;border:1px solid #ccc}.manage-button:hover{background-color:#d0d0d0}.about-button{background-color:#e0e0e0;color:#333;border:1px solid #ccc}.about-button:hover{background-color:#d0d0d0}.about-dialog{background-color:#fff;color:#333;box-shadow:0 4px 20px #00000026}.about-dialog-header{border-bottom:1px solid #ddd}.about-dialog-close{color:#333}.about-dialog-close:hover{background-color:#f0f0f0}.about-dialog-description code,.about-dialog-description pre{background-color:#f5f5f5;color:#333}.about-dialog-description blockquote{border-left:4px solid #ccc}}
|
|
1
|
+
.unitEntryBase{padding-right:13px;padding-top:10px}.unselectedUnitEntry{border:1px transparent solid;font-weight:400;color:#000;background-color:#fff}.selectedUnitEntry{border:1px solid blue;font-weight:700;color:#000;background-color:#b5d1ff}.unitIdsColumnLabelStyle{min-width:200px;font-weight:700;padding:7px 5px}.unitLabelsStyle{padding-right:3px;color:#333}.plotUnitLabel{text-align:left;margin-left:20px;margin-top:5px;font-weight:700}.plotWrapperButtonParent{display:inline-block}.plotWrapperStyleButton{padding-top:50px}.plotUnselectedStyle{border:3px solid rgb(202,224,231)}.plotSelectedStyle{border:3px solid #9999ff;background-color:#fff}.plotUnselectableStyle{border:3px solid transparent}.plotCurrentStyle{border:3px solid blue;background-color:#fff}.plotLabelStyle{font-weight:700;text-align:center;overflow:hidden;white-space:nowrap}:root{font-family:system-ui,Avenir,Helvetica,Arial,sans-serif;line-height:1.5;font-weight:400;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}a{font-weight:500;color:#646cff;text-decoration:inherit}a:hover{color:#535bf2}body{margin:0;overflow:hidden}h1{font-size:3.2em;line-height:1.1}button{border-radius:8px;border:1px solid transparent;padding:.6em 1.2em;font-size:1em;font-weight:500;font-family:inherit;background-color:#1a1a1a;cursor:pointer;transition:border-color .25s}button:hover{border-color:#646cff}button:focus,button:focus-visible{outline:4px auto -webkit-focus-ring-color}.status-bar{position:fixed;bottom:0;left:0;right:0;height:30px;background-color:#2a2a2a;color:#fff;display:flex;align-items:center;padding:0 12px;font-size:12px;border-top:1px solid #444;z-index:1000}.status-bar.warning{background-color:#ff9800;color:#000}.status-bar.error{background-color:#f44336;color:#fff}.status-bar.expired{background-color:#d32f2f;color:#fff}.manage-button{background-color:#444;color:#fff;border:none;padding:4px 8px;border-radius:4px;cursor:pointer;font-size:11px;height:22px;margin-left:12px;transition:background-color .2s}.manage-button:hover{background-color:#666}.about-button{background-color:#444;color:#fff;border:none;padding:4px 8px;border-radius:4px;cursor:pointer;font-size:11px;height:22px;margin-left:12px;transition:background-color .2s}.about-button:hover{background-color:#666}.about-dialog-overlay{position:fixed;inset:0;background-color:#00000080;display:flex;justify-content:center;align-items:center;z-index:2000}.about-dialog{background-color:#2a2a2a;color:#fff;border-radius:8px;max-width:600px;max-height:80vh;width:90%;box-shadow:0 4px 20px #0000004d;overflow:hidden}.about-dialog-header{display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid #444}.about-dialog-header h2{margin:0;font-size:18px;font-weight:600}.about-dialog-close{background:none;border:none;color:#fff;font-size:24px;cursor:pointer;padding:0;width:30px;height:30px;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:background-color .2s}.about-dialog-close:hover{background-color:#444}.about-dialog-content{padding:20px;overflow-y:auto;max-height:calc(80vh - 80px)}.about-dialog-description{line-height:1.6}.about-dialog-description h1,.about-dialog-description h2,.about-dialog-description h3{margin-top:0;margin-bottom:12px}.about-dialog-description p{margin-bottom:12px}.about-dialog-description code{background-color:#444;padding:2px 4px;border-radius:3px;font-family:Courier New,monospace;font-size:.9em}.about-dialog-description pre{background-color:#444;padding:12px;border-radius:4px;overflow-x:auto;margin-bottom:12px}.about-dialog-description ul,.about-dialog-description ol{margin-bottom:12px;padding-left:20px}.about-dialog-description blockquote{border-left:4px solid #666;padding-left:16px;margin:12px 0;font-style:italic}.manage-edits-view{display:flex;align-items:center;margin-left:12px}.edits-notification{color:#ff9800;font-weight:500;margin-right:8px}.save-changes-button{background-color:#ff9800;color:#000;border:none;padding:4px 8px;border-radius:4px;cursor:pointer;font-size:11px;height:22px;transition:background-color .2s}.save-changes-button:hover{background-color:#f57c00}.dialog-overlay{position:fixed;inset:0;background-color:#00000080;display:flex;justify-content:center;align-items:center;z-index:2000}.dialog-content{background-color:#2a2a2a;color:#fff;border-radius:8px;max-width:500px;width:90%;box-shadow:0 4px 20px #0000004d;overflow:hidden}.dialog-header{display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid #444}.dialog-header h3{margin:0;font-size:18px;font-weight:600}.dialog-close{background:none;border:none;color:#fff;font-size:24px;cursor:pointer;padding:0;width:30px;height:30px;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:background-color .2s}.dialog-close:hover{background-color:#444}.dialog-body{padding:20px}.dialog-body p{margin:0 0 12px;line-height:1.5}.dialog-footer{padding:16px 20px;border-top:1px solid #444;display:flex;justify-content:flex-end}.dialog-button{background-color:#444;color:#fff;border:none;padding:8px 16px;border-radius:4px;cursor:pointer;font-size:14px;transition:background-color .2s}.dialog-button:hover{background-color:#666}.dialog-fullscreen{position:fixed;inset:0;background-color:#2a2a2a;color:#fff;z-index:2000;display:flex;flex-direction:column}.dialog-content-fullscreen{display:flex;flex-direction:column;height:100%;overflow:hidden}.dialog-content-fullscreen .dialog-header{flex-shrink:0}.dialog-content-fullscreen .dialog-body{flex:1;overflow-y:auto;padding:20px}.dialog-content-fullscreen .dialog-footer{flex-shrink:0}@media (prefers-color-scheme: light){:root{color:#213547;background-color:#fff}a:hover{color:#747bff}button{background-color:#f9f9f9}.status-bar{background-color:#f5f5f5;color:#333;border-top:1px solid #ddd}.status-bar.warning{background-color:#fff3cd;color:#856404;border-top:1px solid #ffeaa7}.status-bar.error,.status-bar.expired{background-color:#f8d7da;color:#721c24;border-top:1px solid #f5c6cb}.manage-button{background-color:#e0e0e0;color:#333;border:1px solid #ccc}.manage-button:hover{background-color:#d0d0d0}.about-button{background-color:#e0e0e0;color:#333;border:1px solid #ccc}.about-button:hover{background-color:#d0d0d0}.about-dialog{background-color:#fff;color:#333;box-shadow:0 4px 20px #00000026}.about-dialog-header{border-bottom:1px solid #ddd}.about-dialog-close{color:#333}.about-dialog-close:hover{background-color:#f0f0f0}.about-dialog-description code,.about-dialog-description pre{background-color:#f5f5f5;color:#333}.about-dialog-description blockquote{border-left:4px solid #ccc}.edits-notification{color:#e65100}.save-changes-button{background-color:#ff9800;color:#fff;border:1px solid #f57c00}.save-changes-button:hover{background-color:#f57c00}.dialog-content{background-color:#fff;color:#333;box-shadow:0 4px 20px #00000026}.dialog-header{border-bottom:1px solid #ddd}.dialog-close{color:#333}.dialog-close:hover{background-color:#f0f0f0}.dialog-footer{border-top:1px solid #ddd}.dialog-button{background-color:#e0e0e0;color:#333;border:1px solid #ccc}.dialog-button:hover{background-color:#d0d0d0}.dialog-fullscreen{background-color:#fff;color:#333}}
|