Skip to content

Commit a9da72b

Browse files
authored
Add MultiSplit and simplify code (#6)
1 parent bc33a56 commit a9da72b

File tree

14 files changed

+673
-374
lines changed

14 files changed

+673
-374
lines changed

.github/workflows/test.yml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,3 +142,41 @@ jobs:
142142
with:
143143
token: ${{ secrets.CODECOV_TOKEN }}
144144
slug: panel-extensions/panel-splitjs
145+
ui_test:
146+
name: ui:${{ matrix.environment }}:${{ matrix.os }}
147+
needs: [pre_commit, setup, pixi_lock]
148+
runs-on: ${{ matrix.os }}
149+
if: needs.setup.outputs.code_change == 'true'
150+
strategy:
151+
fail-fast: false
152+
matrix:
153+
os: ["ubuntu-latest", "macos-latest", "windows-latest"]
154+
environment: ["test-ui"]
155+
timeout-minutes: 60
156+
env:
157+
PANEL_LOG_LEVEL: info
158+
steps:
159+
- uses: holoviz-dev/holoviz_tasks/pixi_install@v0
160+
with:
161+
environments: ${{ matrix.environment }}
162+
install: false
163+
id: install
164+
- name: Test UI
165+
run: |
166+
# Create a .uicoveragerc file to set the concurrency library to greenlet
167+
# https://github.com/microsoft/playwright-python/issues/313
168+
echo "[run]\nconcurrency = greenlet" > .uicoveragerc
169+
FAIL="--screenshot only-on-failure --full-page-screenshot --output ui_screenshots --tracing retain-on-failure"
170+
pixi run -e ${{ matrix.environment }} test-ui $COV --cov-config=.uicoveragerc $FAIL
171+
- name: Upload UI Screenshots
172+
uses: actions/upload-artifact@v4
173+
if: always()
174+
with:
175+
name: ui_screenshots_${{ runner.os }}
176+
path: ./ui_screenshots
177+
if-no-files-found: ignore
178+
- name: Upload coverage reports to Codecov
179+
uses: codecov/codecov-action@v5
180+
with:
181+
token: ${{ secrets.CODECOV_TOKEN }}
182+
slug: panel-extensions/panel-splitjs

README.md

Lines changed: 114 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ A responsive, draggable split panel component for [Panel](https://panel.holoviz.
99

1010
## Features
1111

12-
- **Draggable divider** - Resize panels by dragging the divider between them
13-
- **Collapsible panels** - Toggle panels open/closed with optional buttons
12+
- **Draggable dividers** - Resize panels by dragging the divider between them
13+
- **Collapsible panels** - Collapse individual panels with toggle buttons
1414
- **Flexible orientation** - Support for both horizontal and vertical splits
15-
- **Minimum size constraints** - Enforce minimum panel sizes to prevent over-collapse
16-
- **Smooth animations** - Beautiful transitions when toggling panels
15+
- **Size constraints** - Enforce minimum and maximum panel sizes
16+
- **Snap behavior** - Smart snapping to minimum sizes for better UX
1717
- **Customizable sizes** - Control initial and expanded panel sizes
18-
- **Invertible layout** - Swap panel positions and button locations
18+
- **Multi-panel support** - Create layouts with 2+ panels using `MultiSplit`
1919

2020
## Installation
2121

@@ -43,7 +43,8 @@ pn.extension()
4343
split = Split(
4444
pn.pane.Markdown("## Left Panel\nContent here"),
4545
pn.pane.Markdown("## Right Panel\nMore content"),
46-
sizes=(50, 50), # Equal sizing
46+
sizes=(50, 50), # Equal sizing initially
47+
min_size=100, # Minimum 100px for each panel
4748
show_buttons=True
4849
)
4950

@@ -75,6 +76,7 @@ split = HSplit(
7576
left_panel,
7677
right_panel,
7778
sizes=(70, 30), # 70% left, 30% right
79+
min_size=200, # Minimum 200px for each panel
7880
show_buttons=True
7981
)
8082

@@ -96,7 +98,7 @@ split = VSplit(
9698
top_panel,
9799
bottom_panel,
98100
sizes=(60, 40),
99-
orientation="vertical"
101+
min_size=150
100102
)
101103

102104
split.servable()
@@ -110,72 +112,90 @@ from panel_splitjs import Split
110112

111113
pn.extension()
112114

113-
# Start with sidebar collapsed
115+
# Start with right panel collapsed
114116
split = Split(
115117
pn.pane.Markdown("## Main Content"),
116118
pn.pane.Markdown("## Collapsible Sidebar"),
117-
collapsed=True,
119+
collapsed=1, # 0 for first panel, 1 for second panel, None for not collapsed
118120
expanded_sizes=(65, 35), # When expanded, 65% main, 35% sidebar
119121
show_buttons=True,
120-
min_sizes=(200, 200) # Minimum 200px for each panel
122+
min_size=(200, 200) # Minimum 200px for each panel
121123
)
122124

123125
# Toggle collapse programmatically
124126
button = pn.widgets.Button(name="Toggle Sidebar")
125-
button.on_click(lambda e: setattr(split, 'collapsed', not split.collapsed))
127+
def toggle(event):
128+
split.collapsed = None if split.collapsed == 1 else 1
129+
button.on_click(toggle)
126130

127131
pn.Column(button, split).servable()
128132
```
129133

130-
### Inverted Layout
134+
### Multi-Panel Split
131135

132136
```python
133137
import panel as pn
134-
from panel_splitjs import Split
138+
from panel_splitjs import MultiSplit
135139

136140
pn.extension()
137141

138-
# Inverted: right panel collapses, button on right side
139-
split = Split(
140-
pn.pane.Markdown("## Secondary Panel"),
141-
pn.pane.Markdown("## Main Content"),
142-
invert=True, # Swap layout and button position
143-
collapsed=True,
144-
expanded_sizes=(35, 65),
145-
show_buttons=True
142+
# Create a layout with three panels
143+
multi = MultiSplit(
144+
pn.pane.Markdown("## Panel 1"),
145+
pn.pane.Markdown("## Panel 2"),
146+
pn.pane.Markdown("## Panel 3"),
147+
sizes=(30, 40, 30), # Three panels with custom sizing
148+
min_size=100, # Minimum 100px for each panel
149+
orientation="horizontal"
146150
)
147151

148-
split.servable()
152+
multi.servable()
149153
```
150154

151155
## API Reference
152156

153157
### Split
154158

155-
The main split panel component with full customization options.
159+
The main split panel component for creating two-panel layouts with collapsible functionality.
156160

157161
**Parameters:**
158162

159163
- `objects` (list): Two Panel components to display in the split panels
160-
- `collapsed` (bool, default=False): Whether the secondary panel is collapsed
161-
- `expanded_sizes` (tuple, default=(50, 50)): Percentage sizes when expanded (must sum to 100)
162-
- `invert` (bool, default=False): Swap panel positions and button locations (constant after init)
163-
- `min_sizes` (tuple, default=(0, 0)): Minimum sizes in pixels for each panel
164-
- `orientation` (str, default="horizontal"): Either "horizontal" or "vertical"
165-
- `show_buttons` (bool, default=False): Show collapse/expand toggle buttons
166-
- `sizes` (tuple, default=(100, 0)): Initial percentage sizes (must sum to 100)
164+
- `collapsed` (int | None, default=None): Which panel is collapsed - `0` for first panel, `1` for second panel, `None` for not collapsed
165+
- `expanded_sizes` (tuple, default=(50, 50)): Percentage sizes when both panels are expanded
166+
- `max_size` (int | tuple, default=None): Maximum sizes in pixels - single value applies to both panels, tuple for individual sizes
167+
- `min_size` (int | tuple, default=0): Minimum sizes in pixels - single value applies to both panels, tuple for individual sizes
168+
- `orientation` (str, default="horizontal"): Either `"horizontal"` or `"vertical"`
169+
- `show_buttons` (bool, default=True): Show collapse/expand toggle buttons on the divider
170+
- `sizes` (tuple, default=(50, 50)): Initial percentage sizes of the panels
171+
- `snap_size` (int, default=30): Snap to minimum size at this offset in pixels
172+
- `step_size` (int, default=1): Step size in pixels at which panel sizes can be changed
167173

168174
### HSplit
169175

170176
Horizontal split panel (convenience class).
171177

172-
Same parameters as `Split` but `orientation` is locked to "horizontal".
178+
Same parameters as `Split` but `orientation` is locked to `"horizontal"`.
173179

174180
### VSplit
175181

176182
Vertical split panel (convenience class).
177183

178-
Same parameters as `Split` but `orientation` is locked to "vertical".
184+
Same parameters as `Split` but `orientation` is locked to `"vertical"`.
185+
186+
### MultiSplit
187+
188+
Multi-panel split component for creating layouts with three or more panels.
189+
190+
**Parameters:**
191+
192+
- `objects` (list): List of Panel components to display (3 or more)
193+
- `max_size` (int | tuple, default=None): Maximum sizes in pixels - single value applies to all panels, tuple for individual sizes
194+
- `min_size` (int | tuple, default=100): Minimum sizes in pixels - single value applies to all panels, tuple for individual sizes
195+
- `orientation` (str, default="horizontal"): Either `"horizontal"` or `"vertical"`
196+
- `sizes` (tuple, default=None): Initial percentage sizes of the panels (length must match number of objects)
197+
- `snap_size` (int, default=30): Snap to minimum size at this offset in pixels
198+
- `step_size` (int, default=1): Step size in pixels at which panel sizes can be changed
179199

180200
## Common Use Cases
181201

@@ -193,10 +213,10 @@ output = pn.Column("# Output Area")
193213
split = Split(
194214
chat,
195215
output,
196-
collapsed=False,
216+
collapsed=None, # Both panels visible
197217
expanded_sizes=(50, 50),
198218
show_buttons=True,
199-
min_sizes=(300, 300)
219+
min_size=(300, 300) # Minimum 300px for each panel
200220
)
201221

202222
split.servable()
@@ -221,35 +241,90 @@ visualization = pn.pane.Markdown("## Main Visualization Area")
221241
split = Split(
222242
controls,
223243
visualization,
224-
collapsed=True,
244+
collapsed=0, # Start with controls collapsed
225245
expanded_sizes=(25, 75),
226246
show_buttons=True,
227-
min_sizes=(250, 400)
247+
min_size=(250, 400) # Minimum sizes for each panel
228248
)
229249

230250
split.servable()
231251
```
232252

233-
### Responsive Layout
253+
### Responsive Layout with Size Constraints
234254

235255
```python
236256
import panel as pn
237257
from panel_splitjs import Split
238258

239259
pn.extension()
240260

241-
# Automatically adjust to available space
242261
split = Split(
243262
pn.pane.Markdown("## Panel 1\nResponsive content"),
244263
pn.pane.Markdown("## Panel 2\nMore responsive content"),
245264
sizes=(50, 50),
246-
min_sizes=(200, 200), # Prevent panels from getting too small
265+
min_size=200, # Minimum 200px per panel
266+
max_size=800, # Maximum 800px per panel
267+
snap_size=50, # Snap to min size when within 50px
247268
show_buttons=True
248269
)
249270

250271
split.servable()
251272
```
252273

274+
### Complex Multi-Panel Layout
275+
276+
```python
277+
import panel as pn
278+
from panel_splitjs import MultiSplit
279+
280+
pn.extension()
281+
282+
# Create a four-panel layout
283+
sidebar = pn.Column("## Sidebar", pn.widgets.Select(options=["A", "B", "C"]))
284+
main = pn.pane.Markdown("## Main Content Area")
285+
detail = pn.pane.Markdown("## Detail Panel")
286+
console = pn.pane.Markdown("## Console Output")
287+
288+
multi = MultiSplit(
289+
sidebar,
290+
main,
291+
detail,
292+
console,
293+
sizes=(15, 40, 25, 20), # Custom sizing for each panel
294+
min_size=(150, 300, 200, 150), # Individual minimums
295+
orientation="horizontal"
296+
)
297+
298+
multi.servable()
299+
```
300+
301+
### Nested Splits
302+
303+
```python
304+
import panel as pn
305+
from panel_splitjs import HSplit, VSplit
306+
307+
pn.extension()
308+
309+
# Create a nested layout: horizontal split with vertical split on right
310+
left = pn.pane.Markdown("## Left Panel")
311+
312+
# Right side has a vertical split
313+
top_right = pn.pane.Markdown("## Top Right")
314+
bottom_right = pn.pane.Markdown("## Bottom Right")
315+
right = VSplit(top_right, bottom_right, sizes=(60, 40))
316+
317+
# Main horizontal split
318+
layout = HSplit(
319+
left,
320+
right,
321+
sizes=(30, 70),
322+
min_size=200
323+
)
324+
325+
layout.servable()
326+
```
327+
253328
## Development
254329

255330
This project is managed by [pixi](https://pixi.sh).

examples/apps/multi.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from panel_splitjs import MultiSplit
2+
from panel_material_ui import Paper
3+
4+
paper_opts = dict(elevation=3, margin=10, sizing_mode="stretch_both")
5+
6+
MultiSplit(
7+
objects=[
8+
Paper("Foo", **paper_opts),
9+
Paper("Bar", **paper_opts),
10+
Paper("Baz", **paper_opts),
11+
Paper("Qux", **paper_opts),
12+
Paper("Quux", **paper_opts),
13+
],
14+
sizes=(20, 30, 20, 10, 20),
15+
min_size=100,
16+
sizing_mode="stretch_width",
17+
height=400
18+
).servable()

examples/apps/simple.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33

44
pn.extension()
55

6-
Split(objects=['Foo', 'Bar'], height=500, width=500).servable()
7-
Split(objects=['Foo', 'Bar'], height=500, width=500, orientation="vertical").servable()
6+
Split('Foo', 'Bar', height=500, width=500).servable()
7+
8+
Split('Foo', 'Bar', height=500, width=500, orientation="vertical").servable()

pixi.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "panel-splitjs"
3-
channels = ["conda-forge", "pyviz/label/dev"]
3+
channels = ["pyviz/label/dev", "conda-forge"]
44
platforms = ["osx-arm64", "osx-64", "linux-64", "win-64"]
55

66
[tasks]
@@ -12,7 +12,7 @@ pre-commit-run = "pre-commit run -a"
1212

1313
[dependencies]
1414
python = ">=3.10"
15-
panel = ">=1.8.0"
15+
panel = ">=1.8.3a0"
1616
bokeh = ">=3.8.0"
1717
packaging = "*"
1818
watchfiles = "*"
@@ -73,7 +73,7 @@ _install = "pip install --no-deps --disable-pip-version-check -e ."
7373
_install-ui = 'playwright install chromium'
7474

7575
[feature.test-ui.tasks.test-ui]
76-
cmd = 'pytest ./tests/ui --ui --browser chromium -n logical --dist loadgroup --reruns 3 --reruns-delay 10'
76+
cmd = 'pytest ./tests/ --ui --browser chromium -n logical --dist loadgroup --reruns 3 --reruns-delay 10'
7777
depends-on = ["_install", "_install-ui"]
7878

7979
# =============================================

src/panel_splitjs/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
from .__version import __version__ # noqa
2-
from .base import HSplit, Split, VSplit
2+
from .base import HSplit, MultiSplit, Split, VSplit
33

4-
__all__ = ["HSplit", "Split", "VSplit"]
4+
__all__ = ["HSplit", "MultiSplit", "Split", "VSplit"]

0 commit comments

Comments
 (0)