panel-splitjs 0.0.1a0__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.
- panel_splitjs/__init__.py +4 -0
- panel_splitjs/__version.py +46 -0
- panel_splitjs/_version.py +34 -0
- panel_splitjs/base.py +139 -0
- panel_splitjs/dist/css/arrow_down.svg +3 -0
- panel_splitjs/dist/css/arrow_left.svg +3 -0
- panel_splitjs/dist/css/arrow_right.svg +3 -0
- panel_splitjs/dist/css/arrow_up.svg +3 -0
- panel_splitjs/dist/css/handle.svg +23 -0
- panel_splitjs/dist/css/handle_vertical.svg +23 -0
- panel_splitjs/dist/css/splitjs.css +239 -0
- panel_splitjs/dist/panel-splitjs.bundle.js +1 -0
- panel_splitjs/models/splitjs.js +215 -0
- panel_splitjs-0.0.1a0.dist-info/METADATA +325 -0
- panel_splitjs-0.0.1a0.dist-info/RECORD +16 -0
- panel_splitjs-0.0.1a0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Define the package version.
|
|
2
|
+
|
|
3
|
+
Called __version.py as setuptools_scm will create a _version.py
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import pathlib
|
|
7
|
+
|
|
8
|
+
PACKAGE = "panel_splitjs"
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
# For performance reasons on imports, avoid importing setuptools_scm
|
|
12
|
+
# if not in a .git folder
|
|
13
|
+
if (pathlib.Path(__file__).parent.parent.parent / ".git").is_dir():
|
|
14
|
+
# If setuptools_scm is installed (e.g. in a development environment with
|
|
15
|
+
# an editable install), then use it to determine the version dynamically.
|
|
16
|
+
from setuptools_scm import get_version
|
|
17
|
+
|
|
18
|
+
# This will fail with LookupError if the package is not installed in
|
|
19
|
+
# editable mode or if Git is not installed.
|
|
20
|
+
__version__ = get_version(
|
|
21
|
+
root="../..", relative_to=__file__, version_scheme="post-release"
|
|
22
|
+
)
|
|
23
|
+
else:
|
|
24
|
+
raise FileNotFoundError
|
|
25
|
+
except (ImportError, LookupError, FileNotFoundError):
|
|
26
|
+
# As a fallback, use the version that is hard-coded in the file.
|
|
27
|
+
try:
|
|
28
|
+
# __version__ was added in _version in setuptools-scm 7.0.0, we rely on
|
|
29
|
+
# the hopefully stable version variable.
|
|
30
|
+
from ._version import version as __version__ # type: ignore
|
|
31
|
+
except (ModuleNotFoundError, ImportError):
|
|
32
|
+
# Either _version doesn't exist (ModuleNotFoundError) or version isn't
|
|
33
|
+
# in _version (ImportError). ModuleNotFoundError is a subclass of
|
|
34
|
+
# ImportError, let's be explicit anyway.
|
|
35
|
+
|
|
36
|
+
# Try something else:
|
|
37
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
__version__ = version(PACKAGE)
|
|
41
|
+
except PackageNotFoundError:
|
|
42
|
+
# The user is probably trying to run this without having installed
|
|
43
|
+
# the package.
|
|
44
|
+
__version__ = "0.0.0+unknown"
|
|
45
|
+
|
|
46
|
+
__all__ = ("__version__",)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '0.0.1a0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 0, 1, 'a0')
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
panel_splitjs/base.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import param
|
|
4
|
+
from bokeh.embed.bundle import extension_dirs
|
|
5
|
+
from panel.custom import Children, JSComponent
|
|
6
|
+
from panel.layout import Spacer
|
|
7
|
+
from panel.layout.base import ListLike
|
|
8
|
+
|
|
9
|
+
BASE_PATH = Path(__file__).parent
|
|
10
|
+
DIST_PATH = BASE_PATH / 'dist'
|
|
11
|
+
|
|
12
|
+
extension_dirs['panel-splitjs'] = DIST_PATH
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SplitChildren(Children):
|
|
16
|
+
"""A Children parameter that only allows at most two items."""
|
|
17
|
+
|
|
18
|
+
def _transform_value(self, val):
|
|
19
|
+
if val is param.parameterized.Undefined:
|
|
20
|
+
return [Spacer(), Spacer()]
|
|
21
|
+
if any(v is None for v in val):
|
|
22
|
+
val[:] = [Spacer() if v is None else v for v in val]
|
|
23
|
+
if len(val) == 1:
|
|
24
|
+
val.append(Spacer())
|
|
25
|
+
if len(val) == 0:
|
|
26
|
+
val.extend([Spacer(), Spacer()])
|
|
27
|
+
val = super()._transform_value(val)
|
|
28
|
+
return val
|
|
29
|
+
|
|
30
|
+
def _validate(self, val):
|
|
31
|
+
super()._validate(val)
|
|
32
|
+
if len(val) <= 2:
|
|
33
|
+
return
|
|
34
|
+
if self.owner is None:
|
|
35
|
+
objtype = ""
|
|
36
|
+
elif isinstance(self.owner, param.Parameterized):
|
|
37
|
+
objtype = self.owner.__class__.__name__
|
|
38
|
+
else:
|
|
39
|
+
objtype = self.owner.__name__
|
|
40
|
+
raise ValueError(f"{objtype} component must have at most two children.")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Split(JSComponent, ListLike):
|
|
44
|
+
"""
|
|
45
|
+
Split is a component for creating a responsive split panel layout.
|
|
46
|
+
|
|
47
|
+
This component uses split.js to create a draggable split layout with two panels.
|
|
48
|
+
|
|
49
|
+
Key features include:
|
|
50
|
+
- Collapsible panels with toggle button
|
|
51
|
+
- Minimum size constraints for each panel
|
|
52
|
+
- Invertible layout to support different UI configurations
|
|
53
|
+
- Responsive sizing with automatic adjustments
|
|
54
|
+
- Animation for better user experience
|
|
55
|
+
|
|
56
|
+
The component is ideal for creating application layouts with a main content area
|
|
57
|
+
and a secondary panel that can be toggled (like a chat interface with output display).
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
collapsed = param.Boolean(default=False, doc="""
|
|
61
|
+
Whether the secondary panel is collapsed.
|
|
62
|
+
When True, only one panel is visible (determined by invert).
|
|
63
|
+
When False, both panels are visible according to expanded_sizes.""")
|
|
64
|
+
|
|
65
|
+
expanded_sizes = param.NumericTuple(default=(50, 50), length=2, doc="""
|
|
66
|
+
The sizes of the two panels when expanded (as percentages).
|
|
67
|
+
Default is (50, 50) which means the left panel takes up 35% of the space
|
|
68
|
+
and the right panel takes up 65% when expanded.
|
|
69
|
+
When invert=True, these percentages are automatically swapped.""")
|
|
70
|
+
|
|
71
|
+
invert = param.Boolean(default=False, constant=True, doc="""
|
|
72
|
+
Whether to invert the layout, changing the toggle button side and panel styles.""")
|
|
73
|
+
|
|
74
|
+
min_sizes = param.NumericTuple(default=(0, 0), length=2, doc="""
|
|
75
|
+
The minimum sizes of the two panels (in pixels).
|
|
76
|
+
Default is (0, 0) which allows both panels to fully collapse.
|
|
77
|
+
Set to (300, 0) or similar values if you want to enforce minimum widths during dragging.
|
|
78
|
+
When invert=True, these values are automatically swapped.""")
|
|
79
|
+
|
|
80
|
+
objects = SplitChildren(doc="""
|
|
81
|
+
The component to place in the left panel.
|
|
82
|
+
When invert=True, this will appear on the right side.""")
|
|
83
|
+
|
|
84
|
+
orientation = param.Selector(default="horizontal", objects=["horizontal", "vertical"], doc="""
|
|
85
|
+
The orientation of the split panel. Default is horizontal.""")
|
|
86
|
+
|
|
87
|
+
show_buttons = param.Boolean(default=False, doc="""
|
|
88
|
+
Whether to show the toggle buttons on the divider.
|
|
89
|
+
When False, the buttons are hidden and panels can only be resized by dragging.""")
|
|
90
|
+
|
|
91
|
+
sizes = param.NumericTuple(default=(100, 0), length=2, doc="""
|
|
92
|
+
The initial sizes of the two panels (as percentages).
|
|
93
|
+
Default is (100, 0) which means the left panel takes up all the space
|
|
94
|
+
and the right panel is not visible.""")
|
|
95
|
+
|
|
96
|
+
_bundle = DIST_PATH / "panel-splitjs.bundle.js"
|
|
97
|
+
_esm = Path(__file__).parent / "models" / "splitjs.js"
|
|
98
|
+
|
|
99
|
+
_stylesheets = [DIST_PATH / "css" / "splitjs.css"]
|
|
100
|
+
|
|
101
|
+
def __init__(self, *objects, **params):
|
|
102
|
+
if objects:
|
|
103
|
+
params["objects"] = list(objects)
|
|
104
|
+
super().__init__(**params)
|
|
105
|
+
if self.invert:
|
|
106
|
+
# Swap min_sizes when inverted
|
|
107
|
+
left_min, right_min = self.min_sizes
|
|
108
|
+
self.min_sizes = (right_min, left_min)
|
|
109
|
+
|
|
110
|
+
# Swap expanded_sizes when inverted
|
|
111
|
+
left_exp, right_exp = self.expanded_sizes
|
|
112
|
+
self.expanded_sizes = (right_exp, left_exp)
|
|
113
|
+
|
|
114
|
+
@param.depends("collapsed", watch=True)
|
|
115
|
+
def _send_collapsed_update(self):
|
|
116
|
+
"""Send message to JS when collapsed state changes in Python"""
|
|
117
|
+
self._send_msg({"type": "update_collapsed", "collapsed": self.collapsed})
|
|
118
|
+
|
|
119
|
+
def _handle_msg(self, msg):
|
|
120
|
+
"""Handle messages from JS"""
|
|
121
|
+
if 'collapsed' in msg:
|
|
122
|
+
collapsed = msg['collapsed']
|
|
123
|
+
with param.discard_events(self):
|
|
124
|
+
# Important to discard so when user drags the panel, it doesn't
|
|
125
|
+
# expand to the expanded sizes
|
|
126
|
+
self.collapsed = collapsed
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class HSplit(Split):
|
|
130
|
+
|
|
131
|
+
orientation = param.Selector(default="horizontal", objects=["horizontal"], readonly=True)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class VSplit(Split):
|
|
135
|
+
|
|
136
|
+
orientation = param.Selector(default="vertical", objects=["vertical"], readonly=True)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
__all__ = ["HSplit", "Split", "VSplit"]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<svg
|
|
2
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
3
|
+
width="10"
|
|
4
|
+
height="24"
|
|
5
|
+
viewBox="0 0 10 24"
|
|
6
|
+
fill="currentColor"
|
|
7
|
+
aria-hidden="true"
|
|
8
|
+
>
|
|
9
|
+
<rect x="2.5" y="2" width="2" height="2" rx="0" />
|
|
10
|
+
<rect x="2.5" y="5" width="2" height="2" rx="0" />
|
|
11
|
+
<rect x="2.5" y="8" width="2" height="2" rx="0" />
|
|
12
|
+
<rect x="2.5" y="11" width="2" height="2" rx="0" />
|
|
13
|
+
<rect x="2.5" y="14" width="2" height="2" rx="0" />
|
|
14
|
+
<rect x="2.5" y="17" width="2" height="2" rx="0" />
|
|
15
|
+
<rect x="2.5" y="20" width="2" height="2" rx="0" />
|
|
16
|
+
<rect x="5.5" y="2" width="2" height="2" rx="0" />
|
|
17
|
+
<rect x="5.5" y="5" width="2" height="2" rx="0" />
|
|
18
|
+
<rect x="5.5" y="8" width="2" height="2" rx="0" />
|
|
19
|
+
<rect x="5.5" y="11" width="2" height="2" rx="0" />
|
|
20
|
+
<rect x="5.5" y="14" width="2" height="2" rx="0" />
|
|
21
|
+
<rect x="5.5" y="17" width="2" height="2" rx="0" />
|
|
22
|
+
<rect x="5.5" y="20" width="2" height="2" rx="0" />
|
|
23
|
+
</svg>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<svg
|
|
2
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
3
|
+
width="24"
|
|
4
|
+
height="10"
|
|
5
|
+
viewBox="0 0 24 10"
|
|
6
|
+
fill="currentColor"
|
|
7
|
+
aria-hidden="true"
|
|
8
|
+
>
|
|
9
|
+
<rect x="2" y="2.5" width="2" height="2" />
|
|
10
|
+
<rect x="5" y="2.5" width="2" height="2" />
|
|
11
|
+
<rect x="8" y="2.5" width="2" height="2" />
|
|
12
|
+
<rect x="11" y="2.5" width="2" height="2" />
|
|
13
|
+
<rect x="14" y="2.5" width="2" height="2" />
|
|
14
|
+
<rect x="17" y="2.5" width="2" height="2" />
|
|
15
|
+
<rect x="20" y="2.5" width="2" height="2" />
|
|
16
|
+
<rect x="2" y="5.5" width="2" height="2" />
|
|
17
|
+
<rect x="5" y="5.5" width="2" height="2" />
|
|
18
|
+
<rect x="8" y="5.5" width="2" height="2" />
|
|
19
|
+
<rect x="11" y="5.5" width="2" height="2" />
|
|
20
|
+
<rect x="14" y="5.5" width="2" height="2" />
|
|
21
|
+
<rect x="17" y="5.5" width="2" height="2" />
|
|
22
|
+
<rect x="20" y="5.5" width="2" height="2" />
|
|
23
|
+
</svg>
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
.split {
|
|
2
|
+
display: flex;
|
|
3
|
+
height: 100%;
|
|
4
|
+
width: 100%;
|
|
5
|
+
overflow: clip; /* Clip overflow to prevent scrollbars; inner content will have their own */
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.split.horizontal {
|
|
9
|
+
flex-direction: row;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.split.vertical {
|
|
13
|
+
flex-direction: column;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/* Style for initial load to prevent FOUC */
|
|
17
|
+
.split.loading {
|
|
18
|
+
visibility: hidden;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/* Max width for comfortable reading */
|
|
22
|
+
.left-panel-content {
|
|
23
|
+
max-width: clamp(450px, 95vw, 1200px);
|
|
24
|
+
margin: 0px auto;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* Split panel styles */
|
|
28
|
+
.split > div {
|
|
29
|
+
position: relative;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.split > div:nth-child(2) {
|
|
33
|
+
overflow: visible;
|
|
34
|
+
position: relative;
|
|
35
|
+
width: 100%;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* Content wrapper styles */
|
|
39
|
+
.content-wrapper {
|
|
40
|
+
width: 100%;
|
|
41
|
+
height: 100%;
|
|
42
|
+
display: contents;
|
|
43
|
+
box-sizing: border-box;
|
|
44
|
+
padding-inline: 0.5rem;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* Toggle button basic styles */
|
|
48
|
+
.toggle-button-left, .toggle-button-right, .toggle-button-up, .toggle-button-down {
|
|
49
|
+
position: absolute;
|
|
50
|
+
width: 24px;
|
|
51
|
+
height: 24px;
|
|
52
|
+
display: flex;
|
|
53
|
+
align-items: center;
|
|
54
|
+
justify-content: center;
|
|
55
|
+
cursor: pointer;
|
|
56
|
+
z-index: 100; /* High z-index to ensure always on top */
|
|
57
|
+
opacity: 0.5;
|
|
58
|
+
transition: opacity 0.2s;
|
|
59
|
+
border-radius: 4px;
|
|
60
|
+
padding: 2px;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* Left button (<) - positioned on left side of divider */
|
|
64
|
+
.toggle-button-left {
|
|
65
|
+
left: -34px; /* 24px width + 10px spacing from divider */
|
|
66
|
+
top: 50%;
|
|
67
|
+
transform: translateY(-50%);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* Right button (>) - positioned on right side of divider */
|
|
71
|
+
.toggle-button-right {
|
|
72
|
+
left: 2px;
|
|
73
|
+
top: 50%;
|
|
74
|
+
transform: translateY(-50%);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* Up button (^) - positioned above divider */
|
|
78
|
+
.toggle-button-up {
|
|
79
|
+
top: -34px; /* 24px height + 10px spacing from divider */
|
|
80
|
+
left: 50%;
|
|
81
|
+
transform: translateX(-50%);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* Down button (v) - positioned below divider */
|
|
85
|
+
.toggle-button-down {
|
|
86
|
+
top: 2px;
|
|
87
|
+
left: 50%;
|
|
88
|
+
transform: translateX(-50%);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* Ensure buttons stay visible even when panels are collapsed */
|
|
92
|
+
.split > div:first-child {
|
|
93
|
+
min-width: 0 !important; /* Override any minimum width */
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.split > div:nth-child(2) {
|
|
97
|
+
overflow: visible !important; /* Ensure buttons remain visible */
|
|
98
|
+
position: relative !important;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* Collapsed state */
|
|
102
|
+
.collapsed-content {
|
|
103
|
+
display: none;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* Gutter styles */
|
|
107
|
+
.gutter {
|
|
108
|
+
background-color: var(--panel-background-color);
|
|
109
|
+
background-repeat: no-repeat;
|
|
110
|
+
background-position: 50%;
|
|
111
|
+
transition: background-color 0.2s;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.gutter:hover {
|
|
115
|
+
background-color: var(--panel-surface-color);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.gutter.gutter-horizontal {
|
|
119
|
+
cursor: col-resize;
|
|
120
|
+
position: relative;
|
|
121
|
+
width: 10px;
|
|
122
|
+
z-index: 1;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.gutter.gutter-horizontal {
|
|
126
|
+
cursor: row-resize;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.gutter::after {
|
|
130
|
+
content: "";
|
|
131
|
+
position: absolute;
|
|
132
|
+
top: 50%;
|
|
133
|
+
left: 50%;
|
|
134
|
+
transform: translate(-50%, -50%);
|
|
135
|
+
-webkit-mask-image: url("handle.svg");
|
|
136
|
+
mask-image: url("handle.svg");
|
|
137
|
+
-webkit-mask-repeat: no-repeat;
|
|
138
|
+
mask-repeat: no-repeat;
|
|
139
|
+
background-color: var(--panel-border-color);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.gutter.gutter-horizontal::after {
|
|
143
|
+
width: 10px;
|
|
144
|
+
height: 24px;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.gutter.gutter-vertical::after {
|
|
148
|
+
width: 24px;
|
|
149
|
+
height: 10px;
|
|
150
|
+
-webkit-mask-image: url("handle_vertical.svg");
|
|
151
|
+
mask-image: url("handle_vertical.svg");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.split > div:nth-child(2) > div:not(.toggle-button-left):not(.toggle-button-right):not(.toggle-button-up):not(.toggle-button-down) {
|
|
155
|
+
width: 100%;
|
|
156
|
+
height: 100%;
|
|
157
|
+
overflow: auto;
|
|
158
|
+
padding-top: 36px; /* Space for the toggle buttons */
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* Toggle button base styles */
|
|
162
|
+
.toggle-button-left,
|
|
163
|
+
.toggle-button-right,
|
|
164
|
+
.toggle-button-up,
|
|
165
|
+
.toggle-button-down {
|
|
166
|
+
width: 24px;
|
|
167
|
+
height: 24px;
|
|
168
|
+
background-color: var(--panel-on-background-color);
|
|
169
|
+
-webkit-mask-repeat: no-repeat;
|
|
170
|
+
mask-repeat: no-repeat;
|
|
171
|
+
-webkit-mask-position: center;
|
|
172
|
+
mask-position: center;
|
|
173
|
+
-webkit-mask-size: 16px;
|
|
174
|
+
mask-size: 16px;
|
|
175
|
+
opacity: 0.5;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.toggle-button-left:hover, .toggle-button-right:hover, .toggle-button-up:hover, .toggle-button-down:hover {
|
|
179
|
+
opacity: 1;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/* Left arrow button */
|
|
183
|
+
.toggle-button-left {
|
|
184
|
+
-webkit-mask-image: url("arrow_left.svg");
|
|
185
|
+
mask-image: url("arrow_left.svg");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* Right arrow button */
|
|
189
|
+
.toggle-button-right {
|
|
190
|
+
-webkit-mask-image: url("arrow_right.svg");
|
|
191
|
+
mask-image: url("arrow_right.svg");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/* Up arrow button */
|
|
195
|
+
.toggle-button-up {
|
|
196
|
+
-webkit-mask-image: url("arrow_up.svg");
|
|
197
|
+
mask-image: url("arrow_up.svg");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* Down arrow button */
|
|
201
|
+
.toggle-button-down {
|
|
202
|
+
-webkit-mask-image: url("arrow_down.svg");
|
|
203
|
+
mask-image: url("arrow_down.svg");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/* Animation for toggle icon */
|
|
207
|
+
@keyframes jumpLeftRight {
|
|
208
|
+
0%, 100% { transform: translateY(-50%); }
|
|
209
|
+
25% { transform: translate(-4px, -50%); }
|
|
210
|
+
50% { transform: translateY(-50%); }
|
|
211
|
+
75% { transform: translate(4px, -50%); }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
@keyframes jumpUpDown {
|
|
215
|
+
0%, 100% { transform: translateX(-50%); }
|
|
216
|
+
25% { transform: translate(-50%, -4px); }
|
|
217
|
+
50% { transform: translateX(-50%); }
|
|
218
|
+
75% { transform: translate(-50%, 4px); }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.toggle-button-left.animated, .toggle-button-right.animated {
|
|
222
|
+
animation-name: jumpLeftRight;
|
|
223
|
+
animation-duration: 0.5s;
|
|
224
|
+
animation-timing-function: ease;
|
|
225
|
+
animation-iteration-count: 3;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.toggle-button-up.animated, .toggle-button-down.animated {
|
|
229
|
+
animation-name: jumpUpDown;
|
|
230
|
+
animation-duration: 0.5s;
|
|
231
|
+
animation-timing-function: ease;
|
|
232
|
+
animation-iteration-count: 3;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/* List navigation styles */
|
|
236
|
+
ul.nav.flex-column {
|
|
237
|
+
padding-inline-start: 0;
|
|
238
|
+
margin: 0;
|
|
239
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var he=Object.defineProperty;var Se=(a,i)=>{for(var s in i)he(a,s,{get:i[s],enumerable:!0})};var M={};Se(M,{render:()=>xe});var h=typeof window<"u"?window:null,J=h===null,G=J?void 0:h.document,S="addEventListener",z="removeEventListener",$="getBoundingClientRect",j="_a",y="_b",_="_c",q="horizontal",w=function(){return!1},ze=J?"calc":["","-webkit-","-moz-","-o-"].filter(function(a){var i=G.createElement("div");return i.style.cssText="width:"+a+"calc(9px)",!!i.style.length}).shift()+"calc",ie=function(a){return typeof a=="string"||a instanceof String},ae=function(a){if(ie(a)){var i=G.querySelector(a);if(!i)throw new Error("Selector "+a+" did not match a DOM element");return i}return a},p=function(a,i,s){var l=a[i];return l!==void 0?l:s},T=function(a,i,s,l){if(i){if(l==="end")return 0;if(l==="center")return a/2}else if(s){if(l==="start")return 0;if(l==="center")return a/2}return a},ye=function(a,i){var s=G.createElement("div");return s.className="gutter gutter-"+i,s},we=function(a,i,s){var l={};return ie(i)?l[a]=i:l[a]=ze+"("+i+"% - "+s+"px)",l},be=function(a,i){var s;return s={},s[a]=i+"px",s},Ee=function(a,i){if(i===void 0&&(i={}),J)return{};var s=a,l,b,E,B,C,o;Array.from&&(s=Array.from(s));var V=ae(s[0]),x=V.parentNode,L=getComputedStyle?getComputedStyle(x):null,O=L?L.flexDirection:null,D=p(i,"sizes")||s.map(function(){return 100/s.length}),F=p(i,"minSize",100),d=Array.isArray(F)?F:s.map(function(){return F}),u=p(i,"maxSize",1/0),v=Array.isArray(u)?u:s.map(function(){return u}),m=p(i,"expandToMin",!1),U=p(i,"gutterSize",10),H=p(i,"gutterAlign","center"),X=p(i,"snapOffset",30),le=Array.isArray(X)?X:s.map(function(){return X}),Y=p(i,"dragInterval",1),k=p(i,"direction",q),Z=p(i,"cursor",k===q?"col-resize":"row-resize"),oe=p(i,"gutter",ye),K=p(i,"elementStyle",we),ce=p(i,"gutterStyle",be);k===q?(l="width",b="clientX",E="left",B="right",C="clientWidth"):k==="vertical"&&(l="height",b="clientY",E="top",B="bottom",C="clientHeight");function P(n,e,t,r){var f=K(l,e,t,r);Object.keys(f).forEach(function(c){n.style[c]=f[c]})}function ue(n,e,t){var r=ce(l,e,t);Object.keys(r).forEach(function(f){n.style[f]=r[f]})}function I(){return o.map(function(n){return n.size})}function Q(n){return"touches"in n?n.touches[0][b]:n[b]}function ee(n){var e=o[this.a],t=o[this.b],r=e.size+t.size;e.size=n/this.size*r,t.size=r-n/this.size*r,P(e.element,e.size,this[y],e.i),P(t.element,t.size,this[_],t.i)}function fe(n){var e,t=o[this.a],r=o[this.b];this.dragging&&(e=Q(n)-this.start+(this[y]-this.dragOffset),Y>1&&(e=Math.round(e/Y)*Y),e<=t.minSize+t.snapOffset+this[y]?e=t.minSize+this[y]:e>=this.size-(r.minSize+r.snapOffset+this[_])&&(e=this.size-(r.minSize+this[_])),e>=t.maxSize-t.snapOffset+this[y]?e=t.maxSize+this[y]:e<=this.size-(r.maxSize-r.snapOffset+this[_])&&(e=this.size-(r.maxSize+this[_])),ee.call(this,e),p(i,"onDrag",w)(I()))}function te(){var n=o[this.a].element,e=o[this.b].element,t=n[$](),r=e[$]();this.size=t[l]+r[l]+this[y]+this[_],this.start=t[E],this.end=t[B]}function de(n){if(!getComputedStyle)return null;var e=getComputedStyle(n);if(!e)return null;var t=n[C];return t===0?null:(k===q?t-=parseFloat(e.paddingLeft)+parseFloat(e.paddingRight):t-=parseFloat(e.paddingTop)+parseFloat(e.paddingBottom),t)}function ne(n){var e=de(x);if(e===null||d.reduce(function(c,g){return c+g},0)>e)return n;var t=0,r=[],f=n.map(function(c,g){var N=e*c/100,R=T(U,g===0,g===n.length-1,H),W=d[g]+R;return N<W?(t+=W-N,r.push(0),W):(r.push(N-W),N)});return t===0?n:f.map(function(c,g){var N=c;if(t>0&&r[g]-t>0){var R=Math.min(t,r[g]-t);t-=R,N=c-R}return N/e*100})}function ve(){var n=this,e=o[n.a].element,t=o[n.b].element;n.dragging&&p(i,"onDragEnd",w)(I()),n.dragging=!1,h[z]("mouseup",n.stop),h[z]("touchend",n.stop),h[z]("touchcancel",n.stop),h[z]("mousemove",n.move),h[z]("touchmove",n.move),n.stop=null,n.move=null,e[z]("selectstart",w),e[z]("dragstart",w),t[z]("selectstart",w),t[z]("dragstart",w),e.style.userSelect="",e.style.webkitUserSelect="",e.style.MozUserSelect="",e.style.pointerEvents="",t.style.userSelect="",t.style.webkitUserSelect="",t.style.MozUserSelect="",t.style.pointerEvents="",n.gutter.style.cursor="",n.parent.style.cursor="",G.body.style.cursor=""}function pe(n){if(!("button"in n&&n.button!==0)){var e=this,t=o[e.a].element,r=o[e.b].element;e.dragging||p(i,"onDragStart",w)(I()),n.preventDefault(),e.dragging=!0,e.move=fe.bind(e),e.stop=ve.bind(e),h[S]("mouseup",e.stop),h[S]("touchend",e.stop),h[S]("touchcancel",e.stop),h[S]("mousemove",e.move),h[S]("touchmove",e.move),t[S]("selectstart",w),t[S]("dragstart",w),r[S]("selectstart",w),r[S]("dragstart",w),t.style.userSelect="none",t.style.webkitUserSelect="none",t.style.MozUserSelect="none",t.style.pointerEvents="none",r.style.userSelect="none",r.style.webkitUserSelect="none",r.style.MozUserSelect="none",r.style.pointerEvents="none",e.gutter.style.cursor=Z,e.parent.style.cursor=Z,G.body.style.cursor=Z,te.call(e),e.dragOffset=Q(n)-e.end}}D=ne(D);var A=[];o=s.map(function(n,e){var t={element:ae(n),size:D[e],minSize:d[e],maxSize:v[e],snapOffset:le[e],i:e},r;if(e>0&&(r={a:e-1,b:e,dragging:!1,direction:k,parent:x},r[y]=T(U,e-1===0,!1,H),r[_]=T(U,!1,e===s.length-1,H),O==="row-reverse"||O==="column-reverse")){var f=r.a;r.a=r.b,r.b=f}if(e>0){var c=oe(e,k,t.element);ue(c,U,e),r[j]=pe.bind(r),c[S]("mousedown",r[j]),c[S]("touchstart",r[j]),x.insertBefore(c,t.element),r.gutter=c}return P(t.element,t.size,T(U,e===0,e===s.length-1,H),e),e>0&&A.push(r),t});function re(n){var e=n.i===A.length,t=e?A[n.i-1]:A[n.i];te.call(t);var r=e?t.size-n.minSize-t[_]:n.minSize+t[y];ee.call(t,r)}o.forEach(function(n){var e=n.element[$]()[l];e<n.minSize&&(m?re(n):n.minSize=e)});function me(n){var e=ne(n);e.forEach(function(t,r){if(r>0){var f=A[r-1],c=o[f.a],g=o[f.b];c.size=e[r-1],g.size=t,P(c.element,c.size,f[y],c.i),P(g.element,g.size,f[_],g.i)}})}function ge(n,e){A.forEach(function(t){if(e!==!0?t.parent.removeChild(t.gutter):(t.gutter[z]("mousedown",t[j]),t.gutter[z]("touchstart",t[j])),n!==!0){var r=K(l,t.a.size,t[y]);Object.keys(r).forEach(function(f){o[t.a].element.style[f]="",o[t.b].element.style[f]=""})}})}return{setSizes:me,getSizes:I,collapse:function(e){re(o[e])},destroy:ge,parent:x,pairs:A}},se=Ee;function xe({model:a,view:i}){let s=document.createElement("div");s.className=`split ${a.orientation}`,s.classList.add("loading");let l=document.createElement("div"),b=document.createElement("div");s.append(l,b);let E=document.createElement("div");if(E.classList.add("content-wrapper"),a.show_buttons){let d=document.createElement("div"),u=document.createElement("div");a.orientation==="horizontal"?(d.className="toggle-button-left",u.className="toggle-button-right"):(d.className="toggle-button-up",u.className="toggle-button-down"),b.appendChild(d),b.appendChild(u),d.addEventListener("click",()=>{C++,o=0;let v;C===1?v=[50,50]:(v=[0,100],C=0),x.setSizes(v);let m=v[1]<=5;a.send_msg({collapsed:m}),a.collapsed=m,L(m,v),i.invalidate_layout()}),u.addEventListener("click",()=>{o++,C=0;let v;o===1?v=[50,50]:(v=[100,0],o=0),x.setSizes(v);let m=v[1]<=5;a.send_msg({collapsed:m}),a.collapsed=m,L(m,v),i.invalidate_layout()})}let B=a.collapsed?[100,0]:a.expanded_sizes,C=0,o=0;function V(){C=0,o=0}let x=se([l,b],{sizes:B,minSize:[0,0],gutterSize:8,direction:a.orientation,onDragEnd:d=>{i.invalidate_layout();let u=d[1]<=5,v=d[0]<=5,m=u;a.collapsed!==m&&(a.send_msg({collapsed:m}),a.collapsed=m),L(m,d),V()}});function L(d,u=null){let v=u?u[0]<=5:!1;(u?u[1]<=5:!1)?E.className="collapsed-content":E.className="content-wrapper",v?O.className="collapsed-content":O.className="content-wrapper"}a.on("msg:custom",d=>{if(d.type==="update_collapsed"){let u=d.collapsed;a.collapsed=u,u?x.setSizes([100,0]):x.setSizes(a.expanded_sizes),L(u),i.invalidate_layout()}}),a.on("after_layout",()=>{setTimeout(()=>{s.classList.remove("loading"),a.show_buttons&&!window._toggleAnimationShown&&(leftArrowButton.classList.add("animated"),rightArrowButton.classList.add("animated"),setTimeout(()=>{leftArrowButton.classList.remove("animated"),rightArrowButton.classList.remove("animated"),window._toggleAnimationShown=!0},1500)),window.dispatchEvent(new Event("resize"))},100)});let O=document.createElement("div");O.classList.add("left-content-wrapper"),a.collapsed&&(E.className="collapsed-content"),O.classList.add("left-panel-content");let[D,F]=a.get_child("objects");return O.append(D),l.append(O),E.append(F),b.append(E),s}var Le={HSplit:M,Split:M,VSplit:M};export{Le as default};
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import Split from "https://esm.sh/split.js@1.6.5"
|
|
2
|
+
|
|
3
|
+
export function render({ model, view }) {
|
|
4
|
+
const splitDiv = document.createElement("div")
|
|
5
|
+
splitDiv.className = `split ${model.orientation}`
|
|
6
|
+
splitDiv.classList.add("loading")
|
|
7
|
+
|
|
8
|
+
const split0 = document.createElement("div")
|
|
9
|
+
const split1 = document.createElement("div")
|
|
10
|
+
splitDiv.append(split0, split1)
|
|
11
|
+
|
|
12
|
+
// Create content wrapper for right panel
|
|
13
|
+
const contentWrapper = document.createElement("div")
|
|
14
|
+
contentWrapper.classList.add("content-wrapper")
|
|
15
|
+
|
|
16
|
+
if (model.show_buttons) {
|
|
17
|
+
// Create toggle icons for both sides of the divider
|
|
18
|
+
const leftArrowButton = document.createElement("div") // < button
|
|
19
|
+
const rightArrowButton = document.createElement("div") // > button
|
|
20
|
+
|
|
21
|
+
// Set button icons based on orientation
|
|
22
|
+
if (model.orientation === "horizontal") {
|
|
23
|
+
leftArrowButton.className = "toggle-button-left"
|
|
24
|
+
rightArrowButton.className = "toggle-button-right"
|
|
25
|
+
} else {
|
|
26
|
+
leftArrowButton.className = "toggle-button-up"
|
|
27
|
+
rightArrowButton.className = "toggle-button-down"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Add both buttons to the right panel (split1) so they're positioned relative to the divider
|
|
31
|
+
split1.appendChild(leftArrowButton)
|
|
32
|
+
split1.appendChild(rightArrowButton)
|
|
33
|
+
|
|
34
|
+
// Left/Up arrow button event listener - two-step toggle
|
|
35
|
+
leftArrowButton.addEventListener("click", () => {
|
|
36
|
+
leftClickCount++
|
|
37
|
+
rightClickCount = 0 // Reset other button's count
|
|
38
|
+
|
|
39
|
+
let newSizes
|
|
40
|
+
|
|
41
|
+
if (leftClickCount === 1) {
|
|
42
|
+
// First tap: make sizes 50, 50
|
|
43
|
+
newSizes = [50, 50]
|
|
44
|
+
} else {
|
|
45
|
+
// Second tap (or more): make sizes 0, 100
|
|
46
|
+
newSizes = [0, 100]
|
|
47
|
+
leftClickCount = 0 // Reset after second tap
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
splitInstance.setSizes(newSizes)
|
|
51
|
+
|
|
52
|
+
// Update collapsed state based on new sizes
|
|
53
|
+
const newCollapsedState = newSizes[1] <= 5
|
|
54
|
+
model.send_msg({ collapsed: newCollapsedState })
|
|
55
|
+
model.collapsed = newCollapsedState
|
|
56
|
+
|
|
57
|
+
updateUIForCollapsedState(newCollapsedState, newSizes)
|
|
58
|
+
view.invalidate_layout()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// Right/Down arrow button event listener - two-step toggle
|
|
62
|
+
rightArrowButton.addEventListener("click", () => {
|
|
63
|
+
rightClickCount++
|
|
64
|
+
leftClickCount = 0 // Reset other button's count
|
|
65
|
+
|
|
66
|
+
let newSizes
|
|
67
|
+
|
|
68
|
+
if (rightClickCount === 1) {
|
|
69
|
+
// First tap: make sizes 50, 50
|
|
70
|
+
newSizes = [50, 50]
|
|
71
|
+
} else {
|
|
72
|
+
// Second tap (or more): make sizes 100, 0
|
|
73
|
+
newSizes = [100, 0]
|
|
74
|
+
rightClickCount = 0 // Reset after second tap
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
splitInstance.setSizes(newSizes)
|
|
78
|
+
|
|
79
|
+
// Update collapsed state based on new sizes
|
|
80
|
+
const newCollapsedState = newSizes[1] <= 5
|
|
81
|
+
model.send_msg({ collapsed: newCollapsedState })
|
|
82
|
+
model.collapsed = newCollapsedState
|
|
83
|
+
|
|
84
|
+
updateUIForCollapsedState(newCollapsedState, newSizes)
|
|
85
|
+
view.invalidate_layout()
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Determine initial state - collapsed means right panel is hidden
|
|
90
|
+
const initSizes = model.collapsed ? [100, 0] : model.expanded_sizes
|
|
91
|
+
|
|
92
|
+
// Track click counts for toggle behavior
|
|
93
|
+
let leftClickCount = 0 // For < button
|
|
94
|
+
let rightClickCount = 0 // For > button
|
|
95
|
+
|
|
96
|
+
// Function to reset click counts when sizes change via dragging
|
|
97
|
+
function resetClickCounts() {
|
|
98
|
+
leftClickCount = 0
|
|
99
|
+
rightClickCount = 0
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Use minSize of 0 to allow full collapse via buttons
|
|
103
|
+
const splitInstance = Split([split0, split1], {
|
|
104
|
+
sizes: initSizes,
|
|
105
|
+
minSize: [0, 0], // Allow full collapse for both panels
|
|
106
|
+
gutterSize: 8, // Match the 8px width in CSS
|
|
107
|
+
direction: model.orientation,
|
|
108
|
+
onDragEnd: (sizes) => {
|
|
109
|
+
view.invalidate_layout()
|
|
110
|
+
|
|
111
|
+
// Determine the new collapsed state based on panel sizes
|
|
112
|
+
const rightPanelCollapsed = sizes[1] <= 5
|
|
113
|
+
const leftPanelCollapsed = sizes[0] <= 5
|
|
114
|
+
|
|
115
|
+
// The model's collapsed state represents whether the right panel is collapsed
|
|
116
|
+
const newCollapsedState = rightPanelCollapsed
|
|
117
|
+
|
|
118
|
+
if (model.collapsed !== newCollapsedState) {
|
|
119
|
+
// Send message to Python about collapsed state change
|
|
120
|
+
model.send_msg({ collapsed: newCollapsedState })
|
|
121
|
+
model.collapsed = newCollapsedState
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Update UI based on current sizes
|
|
125
|
+
updateUIForCollapsedState(newCollapsedState, sizes)
|
|
126
|
+
|
|
127
|
+
// Reset click counts when user drags the splitter
|
|
128
|
+
resetClickCounts()
|
|
129
|
+
},
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
// Function to update UI elements based on collapsed state
|
|
133
|
+
function updateUIForCollapsedState(isCollapsed, sizes = null) {
|
|
134
|
+
// Determine current panel state
|
|
135
|
+
const leftPanelHidden = sizes ? sizes[0] <= 5 : false
|
|
136
|
+
const rightPanelHidden = sizes ? sizes[1] <= 5 : false
|
|
137
|
+
|
|
138
|
+
// Update content visibility
|
|
139
|
+
if (rightPanelHidden) {
|
|
140
|
+
contentWrapper.className = "collapsed-content"
|
|
141
|
+
} else {
|
|
142
|
+
contentWrapper.className = "content-wrapper"
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (leftPanelHidden) {
|
|
146
|
+
leftContentWrapper.className = "collapsed-content"
|
|
147
|
+
} else {
|
|
148
|
+
leftContentWrapper.className = "content-wrapper"
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Listen for collapsed state changes from Python
|
|
153
|
+
model.on("msg:custom", (event) => {
|
|
154
|
+
if (event.type === "update_collapsed") {
|
|
155
|
+
const newCollapsedState = event.collapsed
|
|
156
|
+
|
|
157
|
+
model.collapsed = newCollapsedState
|
|
158
|
+
|
|
159
|
+
// Update split sizes based on new collapsed state
|
|
160
|
+
if (newCollapsedState) {
|
|
161
|
+
// Collapse right panel (show only left)
|
|
162
|
+
splitInstance.setSizes([100, 0])
|
|
163
|
+
} else {
|
|
164
|
+
// Expand to show both panels
|
|
165
|
+
splitInstance.setSizes(model.expanded_sizes)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
updateUIForCollapsedState(newCollapsedState)
|
|
169
|
+
view.invalidate_layout()
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
model.on("after_layout", () => {
|
|
174
|
+
setTimeout(() => {
|
|
175
|
+
splitDiv.classList.remove("loading")
|
|
176
|
+
|
|
177
|
+
// Only add animation on initial load
|
|
178
|
+
if (model.show_buttons && !window._toggleAnimationShown) {
|
|
179
|
+
// Add animation on first load only
|
|
180
|
+
leftArrowButton.classList.add("animated")
|
|
181
|
+
rightArrowButton.classList.add("animated")
|
|
182
|
+
|
|
183
|
+
// Remove animation after it completes and set flag
|
|
184
|
+
setTimeout(() => {
|
|
185
|
+
leftArrowButton.classList.remove("animated")
|
|
186
|
+
rightArrowButton.classList.remove("animated")
|
|
187
|
+
window._toggleAnimationShown = true
|
|
188
|
+
}, 1500)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
window.dispatchEvent(new Event("resize"))
|
|
192
|
+
}, 100)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
// Create a centered content wrapper for the left panel
|
|
196
|
+
const leftContentWrapper = document.createElement("div")
|
|
197
|
+
leftContentWrapper.classList.add("left-content-wrapper")
|
|
198
|
+
|
|
199
|
+
// Set initial display based on collapsed state
|
|
200
|
+
if (model.collapsed) {
|
|
201
|
+
contentWrapper.className = "collapsed-content"
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Apply left-panel-content class to the left panel
|
|
205
|
+
leftContentWrapper.classList.add("left-panel-content")
|
|
206
|
+
|
|
207
|
+
// Append children to the appropriate containers
|
|
208
|
+
const [left, right] = model.get_child("objects")
|
|
209
|
+
leftContentWrapper.append(left)
|
|
210
|
+
split0.append(leftContentWrapper)
|
|
211
|
+
contentWrapper.append(right)
|
|
212
|
+
split1.append(contentWrapper)
|
|
213
|
+
|
|
214
|
+
return splitDiv
|
|
215
|
+
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: panel-splitjs
|
|
3
|
+
Version: 0.0.1a0
|
|
4
|
+
Summary: Provides split.js components for Panel.
|
|
5
|
+
Project-URL: Homepage, https://github.com/panel-extensions/panel-splitjs
|
|
6
|
+
Project-URL: Source, https://github.com/panel-extensions/panel-splitjs
|
|
7
|
+
Author-email: Philipp Rudiger <philipp.jfr@gmail.com>
|
|
8
|
+
Maintainer-email: Philipp Rudiger <philipp.jfr@gmail.com>
|
|
9
|
+
License: BSD
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
14
|
+
Classifier: Natural Language :: English
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: packaging
|
|
25
|
+
Requires-Dist: panel>=1.8.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: mkdocs-material; extra == 'dev'
|
|
28
|
+
Requires-Dist: mkdocstrings[python]; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-asyncio; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest-rerunfailures; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest-xdist; extra == 'dev'
|
|
33
|
+
Requires-Dist: watchfiles; extra == 'dev'
|
|
34
|
+
Provides-Extra: mypy
|
|
35
|
+
Requires-Dist: mypy; extra == 'mypy'
|
|
36
|
+
Requires-Dist: types-requests; extra == 'mypy'
|
|
37
|
+
Requires-Dist: typing-extensions; extra == 'mypy'
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
# panel-splitjs
|
|
41
|
+
|
|
42
|
+
[](https://github.com/panel-extensions/panel-splitjs/actions/workflows/ci.yml)
|
|
43
|
+
[](https://prefix.dev/channels/conda-forge/packages/panel-splitjs)
|
|
44
|
+
[](https://pypi.org/project/panel-splitjs)
|
|
45
|
+
[](https://pypi.org/project/panel-splitjs)
|
|
46
|
+
|
|
47
|
+
A responsive, draggable split panel component for [Panel](https://panel.holoviz.org) applications, powered by [split.js](https://split.js.org/).
|
|
48
|
+
|
|
49
|
+
## Features
|
|
50
|
+
|
|
51
|
+
- **Draggable divider** - Resize panels by dragging the divider between them
|
|
52
|
+
- **Collapsible panels** - Toggle panels open/closed with optional buttons
|
|
53
|
+
- **Flexible orientation** - Support for both horizontal and vertical splits
|
|
54
|
+
- **Minimum size constraints** - Enforce minimum panel sizes to prevent over-collapse
|
|
55
|
+
- **Smooth animations** - Beautiful transitions when toggling panels
|
|
56
|
+
- **Customizable sizes** - Control initial and expanded panel sizes
|
|
57
|
+
- **Invertible layout** - Swap panel positions and button locations
|
|
58
|
+
|
|
59
|
+
## Installation
|
|
60
|
+
|
|
61
|
+
Install via pip:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install panel-splitjs
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Or via conda:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
conda install -c conda-forge panel-splitjs
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Quick Start
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
import panel as pn
|
|
77
|
+
from panel_splitjs import Split
|
|
78
|
+
|
|
79
|
+
pn.extension()
|
|
80
|
+
|
|
81
|
+
# Create a simple split layout
|
|
82
|
+
split = Split(
|
|
83
|
+
pn.pane.Markdown("## Left Panel\nContent here"),
|
|
84
|
+
pn.pane.Markdown("## Right Panel\nMore content"),
|
|
85
|
+
sizes=(50, 50), # Equal sizing
|
|
86
|
+
show_buttons=True
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
split.servable()
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Usage Examples
|
|
93
|
+
|
|
94
|
+
### Basic Horizontal Split
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
import panel as pn
|
|
98
|
+
from panel_splitjs import HSplit
|
|
99
|
+
|
|
100
|
+
pn.extension()
|
|
101
|
+
|
|
102
|
+
left_panel = pn.Column(
|
|
103
|
+
"# Main Content",
|
|
104
|
+
pn.widgets.TextInput(name="Input"),
|
|
105
|
+
pn.pane.Markdown("This is the main content area.")
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
right_panel = pn.Column(
|
|
109
|
+
"# Sidebar",
|
|
110
|
+
pn.widgets.Select(name="Options", options=["A", "B", "C"]),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
split = HSplit(
|
|
114
|
+
left_panel,
|
|
115
|
+
right_panel,
|
|
116
|
+
sizes=(70, 30), # 70% left, 30% right
|
|
117
|
+
show_buttons=True
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
split.servable()
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Vertical Split
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
import panel as pn
|
|
127
|
+
from panel_splitjs import VSplit
|
|
128
|
+
|
|
129
|
+
pn.extension()
|
|
130
|
+
|
|
131
|
+
top_panel = pn.pane.Markdown("## Top Section\nHeader content")
|
|
132
|
+
bottom_panel = pn.pane.Markdown("## Bottom Section\nFooter content")
|
|
133
|
+
|
|
134
|
+
split = VSplit(
|
|
135
|
+
top_panel,
|
|
136
|
+
bottom_panel,
|
|
137
|
+
sizes=(60, 40),
|
|
138
|
+
orientation="vertical"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
split.servable()
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Collapsible Sidebar
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
import panel as pn
|
|
148
|
+
from panel_splitjs import Split
|
|
149
|
+
|
|
150
|
+
pn.extension()
|
|
151
|
+
|
|
152
|
+
# Start with sidebar collapsed
|
|
153
|
+
split = Split(
|
|
154
|
+
pn.pane.Markdown("## Main Content"),
|
|
155
|
+
pn.pane.Markdown("## Collapsible Sidebar"),
|
|
156
|
+
collapsed=True,
|
|
157
|
+
expanded_sizes=(65, 35), # When expanded, 65% main, 35% sidebar
|
|
158
|
+
show_buttons=True,
|
|
159
|
+
min_sizes=(200, 200) # Minimum 200px for each panel
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Toggle collapse programmatically
|
|
163
|
+
button = pn.widgets.Button(name="Toggle Sidebar")
|
|
164
|
+
button.on_click(lambda e: setattr(split, 'collapsed', not split.collapsed))
|
|
165
|
+
|
|
166
|
+
pn.Column(button, split).servable()
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Inverted Layout
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
import panel as pn
|
|
173
|
+
from panel_splitjs import Split
|
|
174
|
+
|
|
175
|
+
pn.extension()
|
|
176
|
+
|
|
177
|
+
# Inverted: right panel collapses, button on right side
|
|
178
|
+
split = Split(
|
|
179
|
+
pn.pane.Markdown("## Secondary Panel"),
|
|
180
|
+
pn.pane.Markdown("## Main Content"),
|
|
181
|
+
invert=True, # Swap layout and button position
|
|
182
|
+
collapsed=True,
|
|
183
|
+
expanded_sizes=(35, 65),
|
|
184
|
+
show_buttons=True
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
split.servable()
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## API Reference
|
|
191
|
+
|
|
192
|
+
### Split
|
|
193
|
+
|
|
194
|
+
The main split panel component with full customization options.
|
|
195
|
+
|
|
196
|
+
**Parameters:**
|
|
197
|
+
|
|
198
|
+
- `objects` (list): Two Panel components to display in the split panels
|
|
199
|
+
- `collapsed` (bool, default=False): Whether the secondary panel is collapsed
|
|
200
|
+
- `expanded_sizes` (tuple, default=(50, 50)): Percentage sizes when expanded (must sum to 100)
|
|
201
|
+
- `invert` (bool, default=False): Swap panel positions and button locations (constant after init)
|
|
202
|
+
- `min_sizes` (tuple, default=(0, 0)): Minimum sizes in pixels for each panel
|
|
203
|
+
- `orientation` (str, default="horizontal"): Either "horizontal" or "vertical"
|
|
204
|
+
- `show_buttons` (bool, default=False): Show collapse/expand toggle buttons
|
|
205
|
+
- `sizes` (tuple, default=(100, 0)): Initial percentage sizes (must sum to 100)
|
|
206
|
+
|
|
207
|
+
### HSplit
|
|
208
|
+
|
|
209
|
+
Horizontal split panel (convenience class).
|
|
210
|
+
|
|
211
|
+
Same parameters as `Split` but `orientation` is locked to "horizontal".
|
|
212
|
+
|
|
213
|
+
### VSplit
|
|
214
|
+
|
|
215
|
+
Vertical split panel (convenience class).
|
|
216
|
+
|
|
217
|
+
Same parameters as `Split` but `orientation` is locked to "vertical".
|
|
218
|
+
|
|
219
|
+
## Common Use Cases
|
|
220
|
+
|
|
221
|
+
### Chat Interface with Output
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
import panel as pn
|
|
225
|
+
from panel_splitjs import Split
|
|
226
|
+
|
|
227
|
+
pn.extension()
|
|
228
|
+
|
|
229
|
+
chat = pn.chat.ChatInterface()
|
|
230
|
+
output = pn.Column("# Output Area")
|
|
231
|
+
|
|
232
|
+
split = Split(
|
|
233
|
+
chat,
|
|
234
|
+
output,
|
|
235
|
+
collapsed=False,
|
|
236
|
+
expanded_sizes=(50, 50),
|
|
237
|
+
show_buttons=True,
|
|
238
|
+
min_sizes=(300, 300)
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
split.servable()
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Dashboard with Collapsible Controls
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
import panel as pn
|
|
248
|
+
from panel_splitjs import Split
|
|
249
|
+
|
|
250
|
+
pn.extension()
|
|
251
|
+
|
|
252
|
+
controls = pn.Column(
|
|
253
|
+
pn.widgets.Select(name="Dataset", options=["A", "B", "C"]),
|
|
254
|
+
pn.widgets.IntSlider(name="Threshold", start=0, end=100),
|
|
255
|
+
pn.widgets.Button(name="Update")
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
visualization = pn.pane.Markdown("## Main Visualization Area")
|
|
259
|
+
|
|
260
|
+
split = Split(
|
|
261
|
+
controls,
|
|
262
|
+
visualization,
|
|
263
|
+
collapsed=True,
|
|
264
|
+
expanded_sizes=(25, 75),
|
|
265
|
+
show_buttons=True,
|
|
266
|
+
min_sizes=(250, 400)
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
split.servable()
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Responsive Layout
|
|
273
|
+
|
|
274
|
+
```python
|
|
275
|
+
import panel as pn
|
|
276
|
+
from panel_splitjs import Split
|
|
277
|
+
|
|
278
|
+
pn.extension()
|
|
279
|
+
|
|
280
|
+
# Automatically adjust to available space
|
|
281
|
+
split = Split(
|
|
282
|
+
pn.pane.Markdown("## Panel 1\nResponsive content"),
|
|
283
|
+
pn.pane.Markdown("## Panel 2\nMore responsive content"),
|
|
284
|
+
sizes=(50, 50),
|
|
285
|
+
min_sizes=(200, 200), # Prevent panels from getting too small
|
|
286
|
+
show_buttons=True
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
split.servable()
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## Development
|
|
293
|
+
|
|
294
|
+
This project is managed by [pixi](https://pixi.sh).
|
|
295
|
+
|
|
296
|
+
### Setup
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
git clone https://github.com/panel-extensions/panel-splitjs
|
|
300
|
+
cd panel-splitjs
|
|
301
|
+
|
|
302
|
+
pixi run pre-commit-install
|
|
303
|
+
pixi run postinstall
|
|
304
|
+
pixi run test
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Building
|
|
308
|
+
|
|
309
|
+
```bash
|
|
310
|
+
pixi run build
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Testing
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
pixi run test
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## Contributing
|
|
320
|
+
|
|
321
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
322
|
+
|
|
323
|
+
## License
|
|
324
|
+
|
|
325
|
+
See LICENSE file for details.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
panel_splitjs/__init__.py,sha256=3o1ZsgXufccivyXhnMunBBNQZliCwQY4GW9XKQ9gRDM,124
|
|
2
|
+
panel_splitjs/__version.py,sha256=rpIYpR7RLjD5NBNasrT3f60C0qTL-nnHBifOofA_qyo,1778
|
|
3
|
+
panel_splitjs/_version.py,sha256=N6jqqryygxntTpQZELt2H0LAGZ4wKgVPTWGAhPzx98U,712
|
|
4
|
+
panel_splitjs/base.py,sha256=OcdprAQyY--hTgiMCUVOFw2RsuknlS46o3DUv89lO9E,5337
|
|
5
|
+
panel_splitjs/dist/panel-splitjs.bundle.js,sha256=tcWSuP8-WKzPVq1qsV_FnnFBq_eGB32AqnKu3cwIXpc,8466
|
|
6
|
+
panel_splitjs/dist/css/arrow_down.svg,sha256=pPSAQIH-UkJf1HbWJhAapaT-rROqn6JVPQldOT3Al0I,214
|
|
7
|
+
panel_splitjs/dist/css/arrow_left.svg,sha256=XZ1Qf7CzKTij-Fa4Up51Gbu_WnBcScK7Qx-tOprvEzE,215
|
|
8
|
+
panel_splitjs/dist/css/arrow_right.svg,sha256=_-3m5dLhPwH9caIEk8XLp3Z8-xQHHvTBny6AcyFVRb4,214
|
|
9
|
+
panel_splitjs/dist/css/arrow_up.svg,sha256=FoVQYz0kh1SAf7EJAs2ZI-ZuuQjcPzR3uM4uViZ87Qs,215
|
|
10
|
+
panel_splitjs/dist/css/handle.svg,sha256=tEQAE3lNBVzPigcp9Z0SQZCW0bSzfECYIvpl19NOd0E,899
|
|
11
|
+
panel_splitjs/dist/css/handle_vertical.svg,sha256=2QZdZzNiLaJp93Ot4tJBhfGjF07EiMfN-Hq3Uf5Z_BI,801
|
|
12
|
+
panel_splitjs/dist/css/splitjs.css,sha256=CIo6tHTT0_NpYjoJFxtCB60uKBUBEFYtvyphDsQpviM,5257
|
|
13
|
+
panel_splitjs/models/splitjs.js,sha256=kDkD-CTnPhRmsE1y6MCIMr31y90LS2r2J3EkFbDUZu0,6965
|
|
14
|
+
panel_splitjs-0.0.1a0.dist-info/METADATA,sha256=9TkauebpnY0rouEEtKJq0sb1-Bt9VtJfLrYGPaEBkr0,8178
|
|
15
|
+
panel_splitjs-0.0.1a0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
16
|
+
panel_splitjs-0.0.1a0.dist-info/RECORD,,
|