diff --git a/src/panel_splitjs/base.py b/src/panel_splitjs/base.py index 5f56156..f340bbe 100644 --- a/src/panel_splitjs/base.py +++ b/src/panel_splitjs/base.py @@ -39,6 +39,9 @@ def _validate(self, val): class SplitBase(JSComponent, ListLike): + gutter_size = param.Integer(default=8, doc=""" + Width of the gutter element.""") + max_size = Size(default=None, doc=""" The maximum sizes of the panels (in pixels) either as a single value or a tuple.""") @@ -109,7 +112,7 @@ class Split(SplitBase): The component to place in the left panel. When invert=True, this will appear on the right side.""") - show_buttons = param.Boolean(default=True, doc=""" + show_buttons = param.Boolean(default=False, doc=""" Whether to show the toggle buttons on the divider. When False, the buttons are hidden and panels can only be resized by dragging.""") diff --git a/src/panel_splitjs/dist/css/splitjs.css b/src/panel_splitjs/dist/css/splitjs.css index 6e1cbd5..80b7a63 100644 --- a/src/panel_splitjs/dist/css/splitjs.css +++ b/src/panel_splitjs/dist/css/splitjs.css @@ -13,38 +13,27 @@ flex-direction: column; } +/* Padding for panel edges unless collapsed */ +.single-split.horizontal.expand-buttons > div:nth-child(1) { padding-right: 15px; } +.single-split.horizontal > div:nth-child(1):has(.collapsed-content) { padding-right: 0; } +.single-split.horizontal.expand-buttons > div:nth-child(3) { padding-left: 15px; } +.single-split.horizontal > div:nth-child(3):has(.collapsed-content) { padding-left: 0; } + +.single-split.vertical.expand-buttons > div:nth-child(1) { padding-bottom: 15px; } +.single-split.vertical > div:nth-child(1):has(.collapsed-content) { padding-bottom: 0; } +.single-split.vertical.expand-buttons > div:nth-child(3) { padding-top: 15px; } +.single-split.vertical > div:nth-child(3):has(.collapsed-content) { padding-top: 0; } + /* Style for initial load to prevent FOUC */ .loading { visibility: hidden; } -/* Max width for comfortable reading */ -.left-panel-content { - max-width: clamp(450px, 95vw, 1200px); - margin: 0px auto; -} - -/* Split panel styles */ +/* Split panel styles (combine .split > div rules) */ .split > div { position: relative; } -.split.single-split > div:nth-child(1) { - overflow: clip; -} - -.split.single-split > div:nth-child(2) { - overflow: visible; - position: relative; - width: 100%; -} - - -.split.single-split > div:nth-child(2) { - overflow: visible !important; /* Ensure buttons remain visible */ - position: relative !important; -} - /* Content wrapper styles */ .content-wrapper { width: 100%; @@ -54,7 +43,7 @@ padding-inline: 0.5rem; } -/* Toggle button basic styles */ +/* Toggle button base styles (deduplicated, combine base and arrow masks) */ .toggle-button-left, .toggle-button-right, .toggle-button-up, .toggle-button-down { position: absolute; width: 24px; @@ -68,34 +57,47 @@ transition: opacity 0.2s; border-radius: 4px; padding: 2px; + + background-color: var(--panel-on-background-color); + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: 16px; + mask-size: 16px; } -/* Left button (<) - positioned on left side of divider */ .toggle-button-left { - left: -34px; /* 24px width + 10px spacing from divider */ + left: -28px; top: 50%; transform: translateY(-50%); + -webkit-mask-image: url("arrow_left.svg"); + mask-image: url("arrow_left.svg"); } - -/* Right button (>) - positioned on right side of divider */ .toggle-button-right { - left: 2px; + left: -4px; top: 50%; transform: translateY(-50%); + -webkit-mask-image: url("arrow_right.svg"); + mask-image: url("arrow_right.svg"); } - -/* Up button (^) - positioned above divider */ .toggle-button-up { - top: -34px; /* 24px height + 10px spacing from divider */ + top: -28px; left: 50%; transform: translateX(-50%); + -webkit-mask-image: url("arrow_up.svg"); + mask-image: url("arrow_up.svg"); } - -/* Down button (v) - positioned below divider */ .toggle-button-down { - top: 2px; + top: -4px; left: 50%; transform: translateX(-50%); + -webkit-mask-image: url("arrow_down.svg"); + mask-image: url("arrow_down.svg"); +} + +.toggle-button-left:hover, .toggle-button-right:hover, .toggle-button-up:hover, .toggle-button-down:hover { + opacity: 1; } /* Collapsed state */ @@ -109,23 +111,19 @@ background-repeat: no-repeat; background-position: 50%; transition: background-color 0.2s; + z-index: 999; } - .gutter:hover { background-color: var(--panel-surface-color); } - .gutter.gutter-horizontal { cursor: col-resize; position: relative; width: 10px; - z-index: 1; } - .gutter.gutter-vertical { cursor: row-resize; } - .gutter::after { content: ""; position: absolute; @@ -138,13 +136,11 @@ mask-repeat: no-repeat; background-color: var(--panel-border-color); } - .gutter.gutter-horizontal::after { width: 10px; height: 24px; cursor: col-resize; } - .gutter.gutter-vertical::after { width: 24px; height: 10px; @@ -152,19 +148,18 @@ mask-image: url("handle_vertical.svg"); cursor: row-resize; } - .gutter > .divider { --gap: 40px; --thickness: 1px; - --color: var(--border-color); + --color: var(--panel-border-color); background: var(--color); position: absolute; } - -/* Horizontal variant */ +/* Horizontal variant for divider */ .gutter-vertical > .divider { top: 50%; - width: 100%; + left: 2.5%; + width: 95%; height: var(--thickness); background: linear-gradient(to right, @@ -175,11 +170,11 @@ var(--color) calc(50% + var(--gap) / 2), var(--color) 100%); } - -/* Vertical variant */ +/* Vertical variant for divider */ .gutter-horizontal > .divider { left: 50%; - height: 100%; + top: 2.5%; + height: 95%; width: var(--thickness); background: linear-gradient(to bottom, @@ -191,73 +186,25 @@ var(--color) 100%); } -/* Toggle button base styles */ -.toggle-button-left, -.toggle-button-right, -.toggle-button-up, -.toggle-button-down { - width: 24px; - height: 24px; - background-color: var(--panel-on-background-color); - -webkit-mask-repeat: no-repeat; - mask-repeat: no-repeat; - -webkit-mask-position: center; - mask-position: center; - -webkit-mask-size: 16px; - mask-size: 16px; - opacity: 0.5; -} - -.toggle-button-left:hover, .toggle-button-right:hover, .toggle-button-up:hover, .toggle-button-down:hover { - opacity: 1; -} - -/* Left arrow button */ -.toggle-button-left { - -webkit-mask-image: url("arrow_left.svg"); - mask-image: url("arrow_left.svg"); -} - -/* Right arrow button */ -.toggle-button-right { - -webkit-mask-image: url("arrow_right.svg"); - mask-image: url("arrow_right.svg"); -} - -/* Up arrow button */ -.toggle-button-up { - -webkit-mask-image: url("arrow_up.svg"); - mask-image: url("arrow_up.svg"); -} - -/* Down arrow button */ -.toggle-button-down { - -webkit-mask-image: url("arrow_down.svg"); - mask-image: url("arrow_down.svg"); -} - -/* Animation for toggle icon */ +/* Toggle button animation */ @keyframes jumpLeftRight { 0%, 100% { transform: translateY(-50%); } 25% { transform: translate(-4px, -50%); } 50% { transform: translateY(-50%); } 75% { transform: translate(4px, -50%); } } - @keyframes jumpUpDown { 0%, 100% { transform: translateX(-50%); } 25% { transform: translate(-50%, -4px); } 50% { transform: translateX(-50%); } 75% { transform: translate(-50%, 4px); } } - .toggle-button-left.animated, .toggle-button-right.animated { animation-name: jumpLeftRight; animation-duration: 0.5s; animation-timing-function: ease; animation-iteration-count: 3; } - .toggle-button-up.animated, .toggle-button-down.animated { animation-name: jumpUpDown; animation-duration: 0.5s; diff --git a/src/panel_splitjs/models/multi_split.js b/src/panel_splitjs/models/multi_split.js index 0562f88..79bb272 100644 --- a/src/panel_splitjs/models/multi_split.js +++ b/src/panel_splitjs/models/multi_split.js @@ -3,6 +3,7 @@ import Split from "https://esm.sh/split.js@1.6.5" export function render({ model, el }) { const split_div = document.createElement("div") split_div.className = `split multi-split ${model.orientation}` + split_div.style.visibility = "hidden" split_div.classList.add("loading") let split = null @@ -61,7 +62,7 @@ export function render({ model, el }) { maxSize: model.max_size || Number("Infinity"), dragInterval: model.step_size || 1, snapOffset: model.snap_size || 30, - gutterSize: 8, + gutterSize: model.gutter_size, gutter: (index, direction) => { const gutter = document.createElement('div') gutter.className = `gutter gutter-${direction}` @@ -94,6 +95,7 @@ export function render({ model, el }) { model.on("after_layout", () => { if (!initialized) { initialized = true + split_div.style.visibility = "" split_div.classList.remove("loading") } }) diff --git a/src/panel_splitjs/models/split.js b/src/panel_splitjs/models/split.js index 64e2b52..ae6a4a0 100644 --- a/src/panel_splitjs/models/split.js +++ b/src/panel_splitjs/models/split.js @@ -4,15 +4,19 @@ const COLLAPSED_SIZE = 5 export function render({ model, el }) { const split_div = document.createElement("div") - split_div.className = `split single-split ${model.orientation}` + split_div.className = `split single-split ${model.orientation} ` + split_div.style.visibility = "hidden" split_div.classList.add("loading") + if (model.show_buttons) { + split_div.classList.add("expand-buttons") + } const [left_min, right_min] = Array.isArray(model.min_size) ? model.min_size : [model.min_size, model.min_size] if (model.orientation === "horizontal") { - split_div.style.minWidth = `${left_min + right_min + 8}px` + split_div.style.minWidth = `${left_min + right_min + model.gutter_size}px` } else { - split_div.style.minHeight = `${left_min + right_min + 8}px` + split_div.style.minHeight = `${left_min + right_min + model.gutter_size}px` } const split0 = document.createElement("div") @@ -116,7 +120,7 @@ export function render({ model, el }) { maxSize: model.max_size || Number("Infinity"), dragInterval: model.step_size, snapOffset: model.snap_size, - gutterSize: 8, + gutterSize: model.gutter_size, gutter: (index, direction) => { const gutter = document.createElement('div') gutter.className = `gutter gutter-${direction}` @@ -204,6 +208,7 @@ export function render({ model, el }) { }, 1500) } window.dispatchEvent(new Event('resize')) + split_div.style.visibility = "" split_div.classList.remove("loading") }) diff --git a/tests/test_ui.py b/tests/test_ui.py index 7cd222b..9b22f90 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -157,7 +157,7 @@ def test_split_sizes_programmatically(page): @pytest.mark.parametrize('orientation', ['horizontal', 'vertical']) def test_split_click_toggle_button(page, orientation): kwargs = {'width': 400} if orientation == 'horizontal' else {'height': 400} - split = Split(Button(name='Left'), Button(name='Right'), orientation=orientation, **kwargs) + split = Split(Button(name='Left'), Button(name='Right'), orientation=orientation, show_buttons=True, **kwargs) serve_component(page, split) btn1, btn2 = ("left", "right") if orientation == "horizontal" else ("up", "down")