panel-splitjs 0.0.1a0__py3-none-any.whl → 0.1.0__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 CHANGED
@@ -1,4 +1,4 @@
1
1
  from .__version import __version__ # noqa
2
- from .base import HSplit, Split, VSplit
2
+ from .base import HSplit, MultiSplit, Split, VSplit
3
3
 
4
- __all__ = ["HSplit", "Split", "VSplit"]
4
+ __all__ = ["HSplit", "MultiSplit", "Split", "VSplit"]
panel_splitjs/_version.py CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.0.1a0'
32
- __version_tuple__ = version_tuple = (0, 0, 1, 'a0')
31
+ __version__ = version = '0.1.0'
32
+ __version_tuple__ = version_tuple = (0, 1, 0)
33
33
 
34
34
  __commit_id__ = commit_id = None
panel_splitjs/base.py CHANGED
@@ -3,7 +3,6 @@ from pathlib import Path
3
3
  import param
4
4
  from bokeh.embed.bundle import extension_dirs
5
5
  from panel.custom import Children, JSComponent
6
- from panel.layout import Spacer
7
6
  from panel.layout.base import ListLike
8
7
 
9
8
  BASE_PATH = Path(__file__).parent
@@ -12,35 +11,61 @@ DIST_PATH = BASE_PATH / 'dist'
12
11
  extension_dirs['panel-splitjs'] = DIST_PATH
13
12
 
14
13
 
15
- class SplitChildren(Children):
16
- """A Children parameter that only allows at most two items."""
14
+ class Size(param.Parameter):
17
15
 
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
16
+ __slots__ = ['length']
17
+
18
+ def __init__(self, default=None, length=None, **params):
19
+ super().__init__(default=default, **params)
20
+ self.length = length
29
21
 
30
22
  def _validate(self, val):
31
23
  super()._validate(val)
32
- if len(val) <= 2:
24
+ if val is None:
33
25
  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.")
26
+ if self.length is not None and isinstance(val, tuple) and len(val) != self.length:
27
+ raise ValueError(f"Size parameter {self.name!r} must have length {self.length}")
28
+ if not (isinstance(val, (int, float)) or (isinstance(val, tuple) and all(isinstance(v, (int, float)) for v in val))):
29
+ raise ValueError(f"Size parameter {self.name!r} only takes int or float values")
30
+
31
+
32
+ class SplitBase(JSComponent, ListLike):
33
+
34
+ max_size = Size(default=None, doc="""
35
+ The maximum sizes of the panels (in pixels) either as a single value or a tuple.""")
36
+
37
+ min_size = Size(default=None, doc="""
38
+ The minimum sizes of the panels (in pixels) either as a single value or a tuple.""")
39
+
40
+ objects = Children(doc="""
41
+ The list of child objects that make up the layout.""")
42
+
43
+ orientation = param.Selector(default="horizontal", objects=["horizontal", "vertical"], doc="""
44
+ The orientation of the split panel. Default is horizontal.""")
45
+
46
+ sizes = param.NumericTuple(default=None, length=0, doc="""
47
+ The sizes of the panels (as percentages) on initialization. The value is automatically
48
+ synced to the sizes of the panels in the frontend.""")
41
49
 
50
+ step_size = param.Integer(default=1, doc="""
51
+ The step size (in pixels) at which the size of the panels can be changed.""")
42
52
 
43
- class Split(JSComponent, ListLike):
53
+ snap_size = param.Integer(default=30, doc="""
54
+ Snap to minimum size at this offset in pixels.""")
55
+
56
+ _bundle = DIST_PATH / "panel-splitjs.bundle.js"
57
+ _stylesheets = [DIST_PATH / "css" / "splitjs.css"]
58
+
59
+ __abstract = True
60
+
61
+ def _process_property_change(self, props):
62
+ props = super()._process_property_change(props)
63
+ if 'sizes' in props:
64
+ props['sizes'] = tuple(props['sizes'])
65
+ return props
66
+
67
+
68
+ class Split(SplitBase):
44
69
  """
45
70
  Split is a component for creating a responsive split panel layout.
46
71
 
@@ -57,83 +82,82 @@ class Split(JSComponent, ListLike):
57
82
  and a secondary panel that can be toggled (like a chat interface with output display).
58
83
  """
59
84
 
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.""")
85
+ collapsed = param.Integer(default=None, doc="""
86
+ Whether the first or second panel is collapsed. 0 for first panel, 1 for second panel, None for not collapsed.""")
64
87
 
65
88
  expanded_sizes = param.NumericTuple(default=(50, 50), length=2, doc="""
66
89
  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.
90
+ Default is (50, 50) .
69
91
  When invert=True, these percentages are automatically swapped.""")
70
92
 
71
- invert = param.Boolean(default=False, constant=True, doc="""
72
- Whether to invert the layout, changing the toggle button side and panel styles.""")
93
+ max_size = Size(default=None, length=2, doc="""
94
+ The maximum sizes of the panels (in pixels) either as a single value or a tuple of two values.""")
73
95
 
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.""")
96
+ min_size = Size(default=0, length=2, doc="""
97
+ The minimum sizes of the panels (in pixels) either as a single value or a tuple of two values.""")
79
98
 
80
- objects = SplitChildren(doc="""
99
+ objects = Children(doc="""
81
100
  The component to place in the left panel.
82
101
  When invert=True, this will appear on the right side.""")
83
102
 
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="""
103
+ show_buttons = param.Boolean(default=True, doc="""
88
104
  Whether to show the toggle buttons on the divider.
89
105
  When False, the buttons are hidden and panels can only be resized by dragging.""")
90
106
 
91
- sizes = param.NumericTuple(default=(100, 0), length=2, doc="""
107
+ sizes = param.NumericTuple(default=(50, 50), length=2, doc="""
92
108
  The initial sizes of the two panels (as percentages).
93
- Default is (100, 0) which means the left panel takes up all the space
109
+ Default is (50, 50) which means the left panel takes up 50% of the space
94
110
  and the right panel is not visible.""")
95
111
 
96
- _bundle = DIST_PATH / "panel-splitjs.bundle.js"
97
- _esm = Path(__file__).parent / "models" / "splitjs.js"
98
-
99
- _stylesheets = [DIST_PATH / "css" / "splitjs.css"]
112
+ _esm = Path(__file__).parent / "models" / "split.js"
100
113
 
101
114
  def __init__(self, *objects, **params):
102
115
  if objects:
103
116
  params["objects"] = list(objects)
117
+ if "objects" in params:
118
+ objects = params["objects"]
119
+ if len(objects) > 2:
120
+ raise ValueError("Split component must have at most two children.")
104
121
  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
122
 
128
123
 
129
124
  class HSplit(Split):
125
+ """
126
+ HSplit is a component for creating a responsive horizontal split panel layout.
127
+ """
130
128
 
131
129
  orientation = param.Selector(default="horizontal", objects=["horizontal"], readonly=True)
132
130
 
133
131
 
134
132
  class VSplit(Split):
133
+ """
134
+ VSplit is a component for creating a responsive vertical split panel layout.
135
+ """
135
136
 
136
137
  orientation = param.Selector(default="vertical", objects=["vertical"], readonly=True)
137
138
 
138
139
 
139
- __all__ = ["HSplit", "Split", "VSplit"]
140
+ class MultiSplit(SplitBase):
141
+ """
142
+ MultiSplit is a component for creating a responsive multi-split panel layout.
143
+ """
144
+
145
+ min_size = Size(default=100, length=None, doc="""
146
+ The minimum sizes of the panels (in pixels) either as a single value or a tuple.""")
147
+
148
+ _esm = Path(__file__).parent / "models" / "multi_split.js"
149
+
150
+ def __init__(self, *objects, **params):
151
+ if objects:
152
+ params["objects"] = list(objects)
153
+ if "objects" in params:
154
+ objects = params["objects"]
155
+ self.param.sizes.length = len(objects)
156
+ super().__init__(**params)
157
+
158
+ @param.depends("objects", watch=True)
159
+ def _update_sizes(self):
160
+ self.param.sizes.length = len(self.objects)
161
+
162
+
163
+ __all__ = ["HSplit", "MultiSplit", "Split", "VSplit"]
@@ -2,7 +2,7 @@
2
2
  display: flex;
3
3
  height: 100%;
4
4
  width: 100%;
5
- overflow: clip; /* Clip overflow to prevent scrollbars; inner content will have their own */
5
+ overflow: clip;
6
6
  }
7
7
 
8
8
  .split.horizontal {
@@ -29,12 +29,26 @@
29
29
  position: relative;
30
30
  }
31
31
 
32
- .split > div:nth-child(2) {
32
+ .split.single-split > div:nth-child(1) {
33
+ overflow: clip;
34
+ }
35
+
36
+ .split.single-split > div:nth-child(2) {
33
37
  overflow: visible;
34
38
  position: relative;
35
39
  width: 100%;
36
40
  }
37
41
 
42
+ /* Ensure buttons stay visible even when panels are collapsed */
43
+ .split.single-split > div:first-child {
44
+ min-width: 0 !important; /* Override any minimum width */
45
+ }
46
+
47
+ .split.single-split > div:nth-child(2) {
48
+ overflow: visible !important; /* Ensure buttons remain visible */
49
+ position: relative !important;
50
+ }
51
+
38
52
  /* Content wrapper styles */
39
53
  .content-wrapper {
40
54
  width: 100%;
@@ -88,16 +102,6 @@
88
102
  transform: translateX(-50%);
89
103
  }
90
104
 
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
105
  /* Collapsed state */
102
106
  .collapsed-content {
103
107
  display: none;
@@ -1 +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};
1
+ var ye=Object.defineProperty;var ce=(t,i)=>{for(var a in i)ye(t,a,{get:i[a],enumerable:!0})};var I={};ce(I,{render:()=>Oe});var _=typeof window<"u"?window:null,ee=_===null,Z=ee?void 0:_.document,w="addEventListener",E="removeEventListener",T="getBoundingClientRect",H="_a",x="_b",D="_c",Y="horizontal",O=function(){return!1},_e=ee?"calc":["","-webkit-","-moz-","-o-"].filter(function(t){var i=Z.createElement("div");return i.style.cssText="width:"+t+"calc(9px)",!!i.style.length}).shift()+"calc",ue=function(t){return typeof t=="string"||t instanceof String},oe=function(t){if(ue(t)){var i=Z.querySelector(t);if(!i)throw new Error("Selector "+t+" did not match a DOM element");return i}return t},d=function(t,i,a){var c=t[i];return c!==void 0?c:a},J=function(t,i,a,c){if(i){if(c==="end")return 0;if(c==="center")return t/2}else if(a){if(c==="start")return 0;if(c==="center")return t/2}return t},be=function(t,i){var a=Z.createElement("div");return a.className="gutter gutter-"+i,a},we=function(t,i,a){var c={};return ue(i)?c[t]=i:c[t]=_e+"("+i+"% - "+a+"px)",c},Ee=function(t,i){var a;return a={},a[t]=i+"px",a},xe=function(t,i){if(i===void 0&&(i={}),ee)return{};var a=t,c,z,p,S,g,l;Array.from&&(a=Array.from(a));var y=oe(a[0]),b=y.parentNode,$=getComputedStyle?getComputedStyle(b):null,f=$?$.flexDirection:null,M=d(i,"sizes")||a.map(function(){return 100/a.length}),B=d(i,"minSize",100),C=Array.isArray(B)?B:a.map(function(){return B}),N=d(i,"maxSize",1/0),q=Array.isArray(N)?N:a.map(function(){return N}),o=d(i,"expandToMin",!1),m=d(i,"gutterSize",10),F=d(i,"gutterAlign","center"),P=d(i,"snapOffset",30),j=Array.isArray(P)?P:a.map(function(){return P}),L=d(i,"dragInterval",1),U=d(i,"direction",Y),Q=d(i,"cursor",U===Y?"col-resize":"row-resize"),fe=d(i,"gutter",be),ne=d(i,"elementStyle",we),ve=d(i,"gutterStyle",Ee);U===Y?(c="width",z="clientX",p="left",S="right",g="clientWidth"):U==="vertical"&&(c="height",z="clientY",p="top",S="bottom",g="clientHeight");function R(r,e,n,s){var v=ne(c,e,n,s);Object.keys(v).forEach(function(u){r.style[u]=v[u]})}function pe(r,e,n){var s=ve(c,e,n);Object.keys(s).forEach(function(v){r.style[v]=s[v]})}function V(){return l.map(function(r){return r.size})}function re(r){return"touches"in r?r.touches[0][z]:r[z]}function se(r){var e=l[this.a],n=l[this.b],s=e.size+n.size;e.size=r/this.size*s,n.size=s-r/this.size*s,R(e.element,e.size,this[x],e.i),R(n.element,n.size,this[D],n.i)}function de(r){var e,n=l[this.a],s=l[this.b];this.dragging&&(e=re(r)-this.start+(this[x]-this.dragOffset),L>1&&(e=Math.round(e/L)*L),e<=n.minSize+n.snapOffset+this[x]?e=n.minSize+this[x]:e>=this.size-(s.minSize+s.snapOffset+this[D])&&(e=this.size-(s.minSize+this[D])),e>=n.maxSize-n.snapOffset+this[x]?e=n.maxSize+this[x]:e<=this.size-(s.maxSize-s.snapOffset+this[D])&&(e=this.size-(s.maxSize+this[D])),se.call(this,e),d(i,"onDrag",O)(V()))}function ie(){var r=l[this.a].element,e=l[this.b].element,n=r[T](),s=e[T]();this.size=n[c]+s[c]+this[x]+this[D],this.start=n[p],this.end=n[S]}function ge(r){if(!getComputedStyle)return null;var e=getComputedStyle(r);if(!e)return null;var n=r[g];return n===0?null:(U===Y?n-=parseFloat(e.paddingLeft)+parseFloat(e.paddingRight):n-=parseFloat(e.paddingTop)+parseFloat(e.paddingBottom),n)}function ae(r){var e=ge(b);if(e===null||C.reduce(function(u,h){return u+h},0)>e)return r;var n=0,s=[],v=r.map(function(u,h){var A=e*u/100,W=J(m,h===0,h===r.length-1,F),X=C[h]+W;return A<X?(n+=X-A,s.push(0),X):(s.push(A-X),A)});return n===0?r:v.map(function(u,h){var A=u;if(n>0&&s[h]-n>0){var W=Math.min(n,s[h]-n);n-=W,A=u-W}return A/e*100})}function ze(){var r=this,e=l[r.a].element,n=l[r.b].element;r.dragging&&d(i,"onDragEnd",O)(V()),r.dragging=!1,_[E]("mouseup",r.stop),_[E]("touchend",r.stop),_[E]("touchcancel",r.stop),_[E]("mousemove",r.move),_[E]("touchmove",r.move),r.stop=null,r.move=null,e[E]("selectstart",O),e[E]("dragstart",O),n[E]("selectstart",O),n[E]("dragstart",O),e.style.userSelect="",e.style.webkitUserSelect="",e.style.MozUserSelect="",e.style.pointerEvents="",n.style.userSelect="",n.style.webkitUserSelect="",n.style.MozUserSelect="",n.style.pointerEvents="",r.gutter.style.cursor="",r.parent.style.cursor="",Z.body.style.cursor=""}function me(r){if(!("button"in r&&r.button!==0)){var e=this,n=l[e.a].element,s=l[e.b].element;e.dragging||d(i,"onDragStart",O)(V()),r.preventDefault(),e.dragging=!0,e.move=de.bind(e),e.stop=ze.bind(e),_[w]("mouseup",e.stop),_[w]("touchend",e.stop),_[w]("touchcancel",e.stop),_[w]("mousemove",e.move),_[w]("touchmove",e.move),n[w]("selectstart",O),n[w]("dragstart",O),s[w]("selectstart",O),s[w]("dragstart",O),n.style.userSelect="none",n.style.webkitUserSelect="none",n.style.MozUserSelect="none",n.style.pointerEvents="none",s.style.userSelect="none",s.style.webkitUserSelect="none",s.style.MozUserSelect="none",s.style.pointerEvents="none",e.gutter.style.cursor=Q,e.parent.style.cursor=Q,Z.body.style.cursor=Q,ie.call(e),e.dragOffset=re(r)-e.end}}M=ae(M);var k=[];l=a.map(function(r,e){var n={element:oe(r),size:M[e],minSize:C[e],maxSize:q[e],snapOffset:j[e],i:e},s;if(e>0&&(s={a:e-1,b:e,dragging:!1,direction:U,parent:b},s[x]=J(m,e-1===0,!1,F),s[D]=J(m,!1,e===a.length-1,F),f==="row-reverse"||f==="column-reverse")){var v=s.a;s.a=s.b,s.b=v}if(e>0){var u=fe(e,U,n.element);pe(u,m,e),s[H]=me.bind(s),u[w]("mousedown",s[H]),u[w]("touchstart",s[H]),b.insertBefore(u,n.element),s.gutter=u}return R(n.element,n.size,J(m,e===0,e===a.length-1,F),e),e>0&&k.push(s),n});function le(r){var e=r.i===k.length,n=e?k[r.i-1]:k[r.i];ie.call(n);var s=e?n.size-r.minSize-n[D]:r.minSize+n[x];se.call(n,s)}l.forEach(function(r){var e=r.element[T]()[c];e<r.minSize&&(o?le(r):r.minSize=e)});function he(r){var e=ae(r);e.forEach(function(n,s){if(s>0){var v=k[s-1],u=l[v.a],h=l[v.b];u.size=e[s-1],h.size=n,R(u.element,u.size,v[x],u.i),R(h.element,h.size,v[D],h.i)}})}function Se(r,e){k.forEach(function(n){if(e!==!0?n.parent.removeChild(n.gutter):(n.gutter[E]("mousedown",n[H]),n.gutter[E]("touchstart",n[H])),r!==!0){var s=ne(c,n.a.size,n[x]);Object.keys(s).forEach(function(v){l[n.a].element.style[v]="",l[n.b].element.style[v]=""})}})}return{setSizes:he,getSizes:V,collapse:function(e){le(l[e])},destroy:Se,parent:b,pairs:k}},K=xe;var G=5;function Oe({model:t,el:i}){let a=document.createElement("div");a.className=`split single-split ${t.orientation}`,a.classList.add("loading");let c=document.createElement("div");c.className="split-panel";let z=document.createElement("div");z.className="split-panel",a.append(c,z);let p=document.createElement("div"),S=document.createElement("div");if(p.className=t.collapsed===0?"collapsed-content":"content-wrapper",S.className=t.collapsed===1?"collapsed-content":"content-wrapper",t.objects!=null&&t.objects.length==2){let[o,m]=t.get_child("objects");p.append(o),S.append(m)}c.append(p),z.append(S);let g,l,y=0,b=0;function $(){y=b=0}t.show_buttons&&(g=document.createElement("div"),l=document.createElement("div"),t.orientation==="horizontal"?(g.className="toggle-button-left",l.className="toggle-button-right"):(g.className="toggle-button-up",l.className="toggle-button-down"),z.append(g,l),g.addEventListener("click",()=>{y++,b=0;let o;y===1&&t.sizes[1]<t.expanded_sizes[1]?(o=t.expanded_sizes,f=null):(f=0,o=[0,100],y=0),t.collapsed=f,N(o,!0)}),l.addEventListener("click",()=>{b++,y=0;let o;b===1&&t.sizes[0]<t.expanded_sizes[0]?(o=t.expanded_sizes,f=null):(f=1,o=[100,0],b=0),t.collapsed=f,N(o,!0)})),i.append(a);let f=t.collapsed,M=t.sizes,B=f?[100,0]:t.sizes,C=K([c,z],{sizes:B,minSize:t.min_size,maxSize:t.max_size||+"Infinity",dragInterval:t.step_size,snapOffset:t.snap_size,gutterSize:8,direction:t.orientation,onDrag:o=>{let m=o[0]<=G?0:o[1]<=G?1:null;f!==m&&(f=m,N(o))},onDragEnd:o=>{f=o[0]<=G?0:o[1]<=G?1:null,t.collapsed=f,N(o,!0),$()}});function N(o=null,m=!1){let F=o?o[0]<=G:!1,P=o?o[1]<=G:!1,[j,L]=o;P?(S.className="collapsed-content",[j,L]=[100,0]):S.className="content-wrapper",F?(p.className="collapsed-content",[j,L]=[0,100]):p.className="content-wrapper",m&&(C.setSizes([j,L]),o=[j,L],t.sizes=[j,L],window.dispatchEvent(new Event("resize")))}t.on("sizes",()=>{M!==t.sizes&&(M=t.sizes,N(M,!0))}),t.on("collapsed",()=>{if(f===t.collapsed)return;f=t.collapsed;let o=f===0?[0,100]:f===1?[100,0]:t.expanded_sizes;N(o,!0)});let q=!1;t.on("after_layout",()=>{q||(q=!0,t.show_buttons&&(g.classList.add("animated"),l.classList.add("animated"),setTimeout(()=>{g.classList.remove("animated"),l.classList.remove("animated")},1500)),window.dispatchEvent(new Event("resize")),a.classList.remove("loading"))}),t.on("remove",()=>C.destroy())}var te={};ce(te,{render:()=>Ne});function Ne({model:t,el:i}){let a=document.createElement("div");a.className=`split multi-split ${t.orientation}`,a.classList.add("loading");let c=t.objects?t.get_child("objects"):[],z=[];for(let l=0;l<c.length;l++){let y=document.createElement("div");y.className="split-panel",a.append(y),z.push(y),y.append(c[l])}i.append(a);let p=t.sizes,S=K(z,{sizes:p,minSize:t.min_size||0,maxSize:t.max_size||+"Infinity",dragInterval:t.step_size||1,snapOffset:t.snap_size||30,gutterSize:8,direction:t.orientation,onDragEnd:l=>{p=l,this.model.sizes=p}});t.on("sizes",()=>{p!==t.sizes&&(p=t.sizes,S.setSizes(p))});let g=!1;t.on("after_layout",()=>{g||(g=!0,a.classList.remove("loading"))}),t.on("remove",()=>S.destroy())}var ke={HSplit:I,MultiSplit:te,Split:I,VSplit:I};export{ke as default};
@@ -0,0 +1,52 @@
1
+ import Split from "https://esm.sh/split.js@1.6.5"
2
+
3
+ export function render({ model, el }) {
4
+ const split_div = document.createElement("div")
5
+ split_div.className = `split multi-split ${model.orientation}`
6
+ split_div.classList.add("loading")
7
+
8
+ const objects = model.objects ? model.get_child("objects") : []
9
+ const split_items = []
10
+ for (let i = 0; i < objects.length; i++) {
11
+ const split_item = document.createElement("div")
12
+ split_item.className = "split-panel"
13
+ split_div.append(split_item)
14
+ split_items.push(split_item)
15
+ split_item.append(objects[i])
16
+ }
17
+
18
+ el.append(split_div)
19
+
20
+ let sizes = model.sizes
21
+ const split = Split(split_items, {
22
+ sizes: sizes,
23
+ minSize: model.min_size || 0,
24
+ maxSize: model.max_size || Number("Infinity"),
25
+ dragInterval: model.step_size || 1,
26
+ snapOffset: model.snap_size || 30,
27
+ gutterSize: 8,
28
+ direction: model.orientation,
29
+ onDragEnd: (new_sizes) => {
30
+ sizes = new_sizes
31
+ this.model.sizes = sizes
32
+ }
33
+ })
34
+
35
+ model.on("sizes", () => {
36
+ if (sizes === model.sizes) {
37
+ return
38
+ }
39
+ sizes = model.sizes
40
+ split.setSizes(sizes)
41
+ })
42
+
43
+ let initialized = false
44
+ model.on("after_layout", () => {
45
+ if (!initialized) {
46
+ initialized = true
47
+ split_div.classList.remove("loading")
48
+ }
49
+ })
50
+
51
+ model.on("remove", () => split.destroy())
52
+ }
@@ -0,0 +1,176 @@
1
+ import Split from "https://esm.sh/split.js@1.6.5"
2
+
3
+ const COLLAPSED_SIZE = 5
4
+
5
+ export function render({ model, el }) {
6
+ const split_div = document.createElement("div")
7
+ split_div.className = `split single-split ${model.orientation}`
8
+ split_div.classList.add("loading")
9
+
10
+ const split0 = document.createElement("div")
11
+ split0.className = "split-panel"
12
+ const split1 = document.createElement("div")
13
+ split1.className = "split-panel"
14
+ split_div.append(split0, split1)
15
+
16
+ const left_content_wrapper = document.createElement("div")
17
+ const right_content_wrapper = document.createElement("div")
18
+ left_content_wrapper.className = model.collapsed === 0 ? "collapsed-content" : "content-wrapper"
19
+ right_content_wrapper.className = model.collapsed === 1 ? "collapsed-content" : "content-wrapper"
20
+
21
+ if (model.objects != null && model.objects.length == 2) {
22
+ const [left, right] = model.get_child("objects")
23
+ left_content_wrapper.append(left)
24
+ right_content_wrapper.append(right)
25
+ }
26
+ split0.append(left_content_wrapper)
27
+ split1.append(right_content_wrapper)
28
+
29
+ let left_arrow_button, right_arrow_button
30
+ let left_click_count = 0
31
+ let right_click_count = 0
32
+ function reset_click_counts() {
33
+ left_click_count = right_click_count = 0
34
+ }
35
+ if (model.show_buttons) {
36
+ left_arrow_button = document.createElement("div")
37
+ right_arrow_button = document.createElement("div")
38
+ if (model.orientation === "horizontal") {
39
+ left_arrow_button.className = "toggle-button-left"
40
+ right_arrow_button.className = "toggle-button-right"
41
+ } else {
42
+ left_arrow_button.className = "toggle-button-up"
43
+ right_arrow_button.className = "toggle-button-down"
44
+ }
45
+ split1.append(left_arrow_button, right_arrow_button)
46
+
47
+ left_arrow_button.addEventListener("click", () => {
48
+ left_click_count++
49
+ right_click_count = 0
50
+
51
+ let new_sizes
52
+ if (left_click_count === 1 && model.sizes[1] < model.expanded_sizes[1]) {
53
+ new_sizes = model.expanded_sizes
54
+ is_collapsed = null
55
+ } else {
56
+ is_collapsed = 0
57
+ new_sizes = [0, 100]
58
+ left_click_count = 0
59
+ }
60
+ model.collapsed = is_collapsed
61
+ sync_ui(new_sizes, true)
62
+ })
63
+
64
+ right_arrow_button.addEventListener("click", () => {
65
+ right_click_count++
66
+ left_click_count = 0
67
+
68
+ let new_sizes
69
+ if (right_click_count === 1 && model.sizes[0] < model.expanded_sizes[0]) {
70
+ new_sizes = model.expanded_sizes
71
+ is_collapsed = null
72
+ } else {
73
+ is_collapsed = 1
74
+ new_sizes = [100, 0]
75
+ right_click_count = 0
76
+ }
77
+ model.collapsed = is_collapsed
78
+ sync_ui(new_sizes, true)
79
+ })
80
+ }
81
+
82
+ el.append(split_div)
83
+
84
+ let is_collapsed = model.collapsed
85
+ let sizes = model.sizes
86
+ const init_sizes = is_collapsed ? [100, 0] : model.sizes
87
+ const split_instance = Split([split0, split1], {
88
+ sizes: init_sizes,
89
+ minSize: model.min_size,
90
+ maxSize: model.max_size || Number("Infinity"),
91
+ dragInterval: model.step_size,
92
+ snapOffset: model.snap_size,
93
+ gutterSize: 8,
94
+ direction: model.orientation,
95
+ onDrag: (sizes) => {
96
+ const new_collapsed_state = sizes[0] <= COLLAPSED_SIZE ? 0 : (sizes[1] <= COLLAPSED_SIZE ? 1 : null)
97
+ if (is_collapsed !== new_collapsed_state) {
98
+ is_collapsed = new_collapsed_state
99
+ sync_ui(sizes)
100
+ }
101
+ },
102
+ onDragEnd: (sizes) => {
103
+ const new_collapsed_state = sizes[0] <= COLLAPSED_SIZE ? 0 : (sizes[1] <= COLLAPSED_SIZE ? 1 : null)
104
+ is_collapsed = new_collapsed_state
105
+ model.collapsed = is_collapsed
106
+ sync_ui(sizes, true)
107
+ reset_click_counts()
108
+ },
109
+ })
110
+
111
+ function sync_ui(sizes = null, resize = false) {
112
+ const left_panel_hidden = sizes ? sizes[0] <= COLLAPSED_SIZE : false
113
+ const right_panel_hidden = sizes ? sizes[1] <= COLLAPSED_SIZE : false
114
+
115
+ let [ls, rs] = sizes
116
+ if (right_panel_hidden) {
117
+ right_content_wrapper.className = "collapsed-content";
118
+ [ls, rs] = [100, 0]
119
+ } else {
120
+ right_content_wrapper.className = "content-wrapper"
121
+ }
122
+
123
+ if (left_panel_hidden) {
124
+ left_content_wrapper.className = "collapsed-content";
125
+ [ls, rs] = [0, 100]
126
+ } else {
127
+ left_content_wrapper.className = "content-wrapper"
128
+ }
129
+ if (resize) {
130
+ split_instance.setSizes([ls, rs])
131
+ sizes = [ls, rs]
132
+ model.sizes = [ls, rs]
133
+ window.dispatchEvent(new Event('resize'))
134
+ }
135
+ }
136
+
137
+ model.on("sizes", () => {
138
+ if (sizes === model.sizes) {
139
+ return
140
+ }
141
+ sizes = model.sizes
142
+ sync_ui(sizes, true)
143
+ })
144
+
145
+ model.on("collapsed", () => {
146
+ if (is_collapsed === model.collapsed) {
147
+ return
148
+ }
149
+ is_collapsed = model.collapsed
150
+ const new_sizes = is_collapsed === 0 ? [0, 100] : (is_collapsed === 1 ? [100, 0] : model.expanded_sizes)
151
+ sync_ui(new_sizes, true)
152
+ })
153
+
154
+ let initialized = false
155
+ model.on("after_layout", () => {
156
+ if (initialized) {
157
+ return
158
+ }
159
+ initialized = true
160
+ if (model.show_buttons) {
161
+ // Add animation on first load only
162
+ left_arrow_button.classList.add("animated")
163
+ right_arrow_button.classList.add("animated")
164
+
165
+ // Remove animation after it completes
166
+ setTimeout(() => {
167
+ left_arrow_button.classList.remove("animated")
168
+ right_arrow_button.classList.remove("animated")
169
+ }, 1500)
170
+ }
171
+ window.dispatchEvent(new Event('resize'))
172
+ split_div.classList.remove("loading")
173
+ })
174
+
175
+ model.on("remove", () => split_instance.destroy())
176
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: panel-splitjs
3
- Version: 0.0.1a0
3
+ Version: 0.1.0
4
4
  Summary: Provides split.js components for Panel.
5
5
  Project-URL: Homepage, https://github.com/panel-extensions/panel-splitjs
6
6
  Project-URL: Source, https://github.com/panel-extensions/panel-splitjs
@@ -48,13 +48,13 @@ A responsive, draggable split panel component for [Panel](https://panel.holoviz.
48
48
 
49
49
  ## Features
50
50
 
51
- - **Draggable divider** - Resize panels by dragging the divider between them
52
- - **Collapsible panels** - Toggle panels open/closed with optional buttons
51
+ - **Draggable dividers** - Resize panels by dragging the divider between them
52
+ - **Collapsible panels** - Collapse individual panels with toggle buttons
53
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
54
+ - **Size constraints** - Enforce minimum and maximum panel sizes
55
+ - **Snap behavior** - Smart snapping to minimum sizes for better UX
56
56
  - **Customizable sizes** - Control initial and expanded panel sizes
57
- - **Invertible layout** - Swap panel positions and button locations
57
+ - **Multi-panel support** - Create layouts with 2+ panels using `MultiSplit`
58
58
 
59
59
  ## Installation
60
60
 
@@ -82,7 +82,8 @@ pn.extension()
82
82
  split = Split(
83
83
  pn.pane.Markdown("## Left Panel\nContent here"),
84
84
  pn.pane.Markdown("## Right Panel\nMore content"),
85
- sizes=(50, 50), # Equal sizing
85
+ sizes=(50, 50), # Equal sizing initially
86
+ min_size=100, # Minimum 100px for each panel
86
87
  show_buttons=True
87
88
  )
88
89
 
@@ -114,6 +115,7 @@ split = HSplit(
114
115
  left_panel,
115
116
  right_panel,
116
117
  sizes=(70, 30), # 70% left, 30% right
118
+ min_size=200, # Minimum 200px for each panel
117
119
  show_buttons=True
118
120
  )
119
121
 
@@ -135,7 +137,7 @@ split = VSplit(
135
137
  top_panel,
136
138
  bottom_panel,
137
139
  sizes=(60, 40),
138
- orientation="vertical"
140
+ min_size=150
139
141
  )
140
142
 
141
143
  split.servable()
@@ -149,72 +151,90 @@ from panel_splitjs import Split
149
151
 
150
152
  pn.extension()
151
153
 
152
- # Start with sidebar collapsed
154
+ # Start with right panel collapsed
153
155
  split = Split(
154
156
  pn.pane.Markdown("## Main Content"),
155
157
  pn.pane.Markdown("## Collapsible Sidebar"),
156
- collapsed=True,
158
+ collapsed=1, # 0 for first panel, 1 for second panel, None for not collapsed
157
159
  expanded_sizes=(65, 35), # When expanded, 65% main, 35% sidebar
158
160
  show_buttons=True,
159
- min_sizes=(200, 200) # Minimum 200px for each panel
161
+ min_size=(200, 200) # Minimum 200px for each panel
160
162
  )
161
163
 
162
164
  # Toggle collapse programmatically
163
165
  button = pn.widgets.Button(name="Toggle Sidebar")
164
- button.on_click(lambda e: setattr(split, 'collapsed', not split.collapsed))
166
+ def toggle(event):
167
+ split.collapsed = None if split.collapsed == 1 else 1
168
+ button.on_click(toggle)
165
169
 
166
170
  pn.Column(button, split).servable()
167
171
  ```
168
172
 
169
- ### Inverted Layout
173
+ ### Multi-Panel Split
170
174
 
171
175
  ```python
172
176
  import panel as pn
173
- from panel_splitjs import Split
177
+ from panel_splitjs import MultiSplit
174
178
 
175
179
  pn.extension()
176
180
 
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
181
+ # Create a layout with three panels
182
+ multi = MultiSplit(
183
+ pn.pane.Markdown("## Panel 1"),
184
+ pn.pane.Markdown("## Panel 2"),
185
+ pn.pane.Markdown("## Panel 3"),
186
+ sizes=(30, 40, 30), # Three panels with custom sizing
187
+ min_size=100, # Minimum 100px for each panel
188
+ orientation="horizontal"
185
189
  )
186
190
 
187
- split.servable()
191
+ multi.servable()
188
192
  ```
189
193
 
190
194
  ## API Reference
191
195
 
192
196
  ### Split
193
197
 
194
- The main split panel component with full customization options.
198
+ The main split panel component for creating two-panel layouts with collapsible functionality.
195
199
 
196
200
  **Parameters:**
197
201
 
198
202
  - `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)
203
+ - `collapsed` (int | None, default=None): Which panel is collapsed - `0` for first panel, `1` for second panel, `None` for not collapsed
204
+ - `expanded_sizes` (tuple, default=(50, 50)): Percentage sizes when both panels are expanded
205
+ - `max_size` (int | tuple, default=None): Maximum sizes in pixels - single value applies to both panels, tuple for individual sizes
206
+ - `min_size` (int | tuple, default=0): Minimum sizes in pixels - single value applies to both panels, tuple for individual sizes
207
+ - `orientation` (str, default="horizontal"): Either `"horizontal"` or `"vertical"`
208
+ - `show_buttons` (bool, default=True): Show collapse/expand toggle buttons on the divider
209
+ - `sizes` (tuple, default=(50, 50)): Initial percentage sizes of the panels
210
+ - `snap_size` (int, default=30): Snap to minimum size at this offset in pixels
211
+ - `step_size` (int, default=1): Step size in pixels at which panel sizes can be changed
206
212
 
207
213
  ### HSplit
208
214
 
209
215
  Horizontal split panel (convenience class).
210
216
 
211
- Same parameters as `Split` but `orientation` is locked to "horizontal".
217
+ Same parameters as `Split` but `orientation` is locked to `"horizontal"`.
212
218
 
213
219
  ### VSplit
214
220
 
215
221
  Vertical split panel (convenience class).
216
222
 
217
- Same parameters as `Split` but `orientation` is locked to "vertical".
223
+ Same parameters as `Split` but `orientation` is locked to `"vertical"`.
224
+
225
+ ### MultiSplit
226
+
227
+ Multi-panel split component for creating layouts with three or more panels.
228
+
229
+ **Parameters:**
230
+
231
+ - `objects` (list): List of Panel components to display (3 or more)
232
+ - `max_size` (int | tuple, default=None): Maximum sizes in pixels - single value applies to all panels, tuple for individual sizes
233
+ - `min_size` (int | tuple, default=100): Minimum sizes in pixels - single value applies to all panels, tuple for individual sizes
234
+ - `orientation` (str, default="horizontal"): Either `"horizontal"` or `"vertical"`
235
+ - `sizes` (tuple, default=None): Initial percentage sizes of the panels (length must match number of objects)
236
+ - `snap_size` (int, default=30): Snap to minimum size at this offset in pixels
237
+ - `step_size` (int, default=1): Step size in pixels at which panel sizes can be changed
218
238
 
219
239
  ## Common Use Cases
220
240
 
@@ -232,10 +252,10 @@ output = pn.Column("# Output Area")
232
252
  split = Split(
233
253
  chat,
234
254
  output,
235
- collapsed=False,
255
+ collapsed=None, # Both panels visible
236
256
  expanded_sizes=(50, 50),
237
257
  show_buttons=True,
238
- min_sizes=(300, 300)
258
+ min_size=(300, 300) # Minimum 300px for each panel
239
259
  )
240
260
 
241
261
  split.servable()
@@ -260,16 +280,16 @@ visualization = pn.pane.Markdown("## Main Visualization Area")
260
280
  split = Split(
261
281
  controls,
262
282
  visualization,
263
- collapsed=True,
283
+ collapsed=0, # Start with controls collapsed
264
284
  expanded_sizes=(25, 75),
265
285
  show_buttons=True,
266
- min_sizes=(250, 400)
286
+ min_size=(250, 400) # Minimum sizes for each panel
267
287
  )
268
288
 
269
289
  split.servable()
270
290
  ```
271
291
 
272
- ### Responsive Layout
292
+ ### Responsive Layout with Size Constraints
273
293
 
274
294
  ```python
275
295
  import panel as pn
@@ -277,18 +297,73 @@ from panel_splitjs import Split
277
297
 
278
298
  pn.extension()
279
299
 
280
- # Automatically adjust to available space
281
300
  split = Split(
282
301
  pn.pane.Markdown("## Panel 1\nResponsive content"),
283
302
  pn.pane.Markdown("## Panel 2\nMore responsive content"),
284
303
  sizes=(50, 50),
285
- min_sizes=(200, 200), # Prevent panels from getting too small
304
+ min_size=200, # Minimum 200px per panel
305
+ max_size=800, # Maximum 800px per panel
306
+ snap_size=50, # Snap to min size when within 50px
286
307
  show_buttons=True
287
308
  )
288
309
 
289
310
  split.servable()
290
311
  ```
291
312
 
313
+ ### Complex Multi-Panel Layout
314
+
315
+ ```python
316
+ import panel as pn
317
+ from panel_splitjs import MultiSplit
318
+
319
+ pn.extension()
320
+
321
+ # Create a four-panel layout
322
+ sidebar = pn.Column("## Sidebar", pn.widgets.Select(options=["A", "B", "C"]))
323
+ main = pn.pane.Markdown("## Main Content Area")
324
+ detail = pn.pane.Markdown("## Detail Panel")
325
+ console = pn.pane.Markdown("## Console Output")
326
+
327
+ multi = MultiSplit(
328
+ sidebar,
329
+ main,
330
+ detail,
331
+ console,
332
+ sizes=(15, 40, 25, 20), # Custom sizing for each panel
333
+ min_size=(150, 300, 200, 150), # Individual minimums
334
+ orientation="horizontal"
335
+ )
336
+
337
+ multi.servable()
338
+ ```
339
+
340
+ ### Nested Splits
341
+
342
+ ```python
343
+ import panel as pn
344
+ from panel_splitjs import HSplit, VSplit
345
+
346
+ pn.extension()
347
+
348
+ # Create a nested layout: horizontal split with vertical split on right
349
+ left = pn.pane.Markdown("## Left Panel")
350
+
351
+ # Right side has a vertical split
352
+ top_right = pn.pane.Markdown("## Top Right")
353
+ bottom_right = pn.pane.Markdown("## Bottom Right")
354
+ right = VSplit(top_right, bottom_right, sizes=(60, 40))
355
+
356
+ # Main horizontal split
357
+ layout = HSplit(
358
+ left,
359
+ right,
360
+ sizes=(30, 70),
361
+ min_size=200
362
+ )
363
+
364
+ layout.servable()
365
+ ```
366
+
292
367
  ## Development
293
368
 
294
369
  This project is managed by [pixi](https://pixi.sh).
@@ -0,0 +1,17 @@
1
+ panel_splitjs/__init__.py,sha256=BKEeHeKAv90kV3GybL6FH8RCwFZM4B6gffr7T8242Qk,150
2
+ panel_splitjs/__version.py,sha256=rpIYpR7RLjD5NBNasrT3f60C0qTL-nnHBifOofA_qyo,1778
3
+ panel_splitjs/_version.py,sha256=5jwwVncvCiTnhOedfkzzxmxsggwmTBORdFL_4wq0ZeY,704
4
+ panel_splitjs/base.py,sha256=x3ynR2CaKUQxhQOGvKyje7zaR8UDppvOygQG89stzTA,5994
5
+ panel_splitjs/dist/panel-splitjs.bundle.js,sha256=rsJvlKUnpPwOQ6q0Axqve8eLesHk1Y8GjMxbIkwjkag,9380
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=L80iIafpH9Ii5qy0duO-KNroG6VaP7AzQImZR3d7zgI,5281
13
+ panel_splitjs/models/multi_split.js,sha256=U9TQcCK5Ho0GElYaMCOzwEosnBv40vN7XOLzsGzlISk,1348
14
+ panel_splitjs/models/split.js,sha256=zJefB8ivxMi-j7oKgTc9wF5K3CIxSKommQPfYL0tpR8,5470
15
+ panel_splitjs-0.1.0.dist-info/METADATA,sha256=FMJQQJtzyj-5RTFnmLQlivB1weEzww7P3QcmSJ_ZnAg,10877
16
+ panel_splitjs-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
17
+ panel_splitjs-0.1.0.dist-info/RECORD,,
@@ -1,215 +0,0 @@
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
- }
@@ -1,16 +0,0 @@
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,,