diff --git a/main.js b/main.js
index 6bab309..a41980a 100644
--- a/main.js
+++ b/main.js
@@ -124,4 +124,126 @@ globalThis.displayListEditor = {
},
};
+const explorer = document.querySelector(".explorer");
+const paneSplitter = document.getElementById("pane-splitter");
+const verticalLayout = window.matchMedia("(max-width: 980px)");
+const defaultPaneRatio = 0.5;
+const minimumPaneRatio = 0.2;
+const minimumPaneSize = 240;
+let paneRatio = defaultPaneRatio;
+let isResizingPanes = false;
+
+function isVerticalLayout() {
+ return verticalLayout.matches;
+}
+
+function paneRatioBounds() {
+ const vertical = isVerticalLayout();
+ const explorerSize = vertical ? explorer.clientHeight : explorer.clientWidth;
+ const splitterSize = vertical ? paneSplitter.offsetHeight : paneSplitter.offsetWidth;
+ const availableSize = Math.max(1, explorerSize - splitterSize);
+ const lowerBound = Math.max(
+ minimumPaneRatio,
+ Math.min(0.5, minimumPaneSize / availableSize),
+ );
+ return { lower: lowerBound, upper: 1 - lowerBound };
+}
+
+function setPaneRatio(nextRatio) {
+ const { lower, upper } = paneRatioBounds();
+ paneRatio = Math.min(upper, Math.max(lower, nextRatio));
+ explorer.style.setProperty("--source-pane-share", `${paneRatio}fr`);
+ explorer.style.setProperty("--detail-pane-share", `${1 - paneRatio}fr`);
+
+ const percentage = Math.round(paneRatio * 100);
+ paneSplitter.setAttribute("aria-valuemin", String(Math.round(lower * 100)));
+ paneSplitter.setAttribute("aria-valuemax", String(Math.round(upper * 100)));
+ paneSplitter.setAttribute("aria-valuenow", String(percentage));
+ paneSplitter.setAttribute("aria-valuetext", `Source pane ${percentage}%`);
+}
+
+function setPaneRatioFromPointer(event) {
+ const vertical = isVerticalLayout();
+ const explorerRect = explorer.getBoundingClientRect();
+ const splitterSize = vertical ? paneSplitter.offsetHeight : paneSplitter.offsetWidth;
+ const availableSize = Math.max(
+ 1,
+ (vertical ? explorerRect.height : explorerRect.width) - splitterSize,
+ );
+ const pointerPosition = vertical
+ ? event.clientY - explorerRect.top
+ : event.clientX - explorerRect.left;
+ setPaneRatio((pointerPosition - splitterSize / 2) / availableSize);
+}
+
+paneSplitter.addEventListener("pointerdown", (event) => {
+ if (event.button !== 0) {
+ return;
+ }
+
+ isResizingPanes = true;
+ paneSplitter.setPointerCapture(event.pointerId);
+ paneSplitter.classList.add("is-dragging");
+ document.body.classList.add("is-resizing-panes");
+ setPaneRatioFromPointer(event);
+ event.preventDefault();
+});
+
+paneSplitter.addEventListener("pointermove", (event) => {
+ if (isResizingPanes && paneSplitter.hasPointerCapture(event.pointerId)) {
+ setPaneRatioFromPointer(event);
+ }
+});
+
+function finishPaneResize(event) {
+ if (!isResizingPanes) {
+ return;
+ }
+
+ isResizingPanes = false;
+ if (paneSplitter.hasPointerCapture(event.pointerId)) {
+ paneSplitter.releasePointerCapture(event.pointerId);
+ }
+ paneSplitter.classList.remove("is-dragging");
+ document.body.classList.remove("is-resizing-panes");
+}
+
+paneSplitter.addEventListener("pointerup", finishPaneResize);
+paneSplitter.addEventListener("pointercancel", finishPaneResize);
+paneSplitter.addEventListener("lostpointercapture", finishPaneResize);
+paneSplitter.addEventListener("dblclick", () => setPaneRatio(defaultPaneRatio));
+
+paneSplitter.addEventListener("keydown", (event) => {
+ const vertical = isVerticalLayout();
+ const decreaseKey = vertical ? "ArrowUp" : "ArrowLeft";
+ const increaseKey = vertical ? "ArrowDown" : "ArrowRight";
+ const step = event.shiftKey ? 0.1 : 0.02;
+
+ if (event.key === decreaseKey) {
+ setPaneRatio(paneRatio - step);
+ } else if (event.key === increaseKey) {
+ setPaneRatio(paneRatio + step);
+ } else if (event.key === "Home") {
+ setPaneRatio(paneRatioBounds().lower);
+ } else if (event.key === "End") {
+ setPaneRatio(paneRatioBounds().upper);
+ } else {
+ return;
+ }
+
+ event.preventDefault();
+});
+
+function updateSplitterOrientation() {
+ paneSplitter.setAttribute(
+ "aria-orientation",
+ isVerticalLayout() ? "horizontal" : "vertical",
+ );
+ setPaneRatio(paneRatio);
+}
+
+verticalLayout.addEventListener("change", updateSplitterOrientation);
+window.addEventListener("resize", () => setPaneRatio(paneRatio));
+updateSplitterOrientation();
+
await init();
diff --git a/styles.css b/styles.css
index 1b41470..8478252 100644
--- a/styles.css
+++ b/styles.css
@@ -211,10 +211,16 @@ h1 {
}
.explorer {
+ --source-pane-share: 1fr;
+ --detail-pane-share: 1fr;
+
height: max(640px, calc(100svh - 224px));
max-height: 920px;
display: grid;
- grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
+ grid-template-columns:
+ minmax(0, var(--source-pane-share))
+ 9px
+ minmax(0, var(--detail-pane-share));
border: 1px solid var(--line-strong);
border-radius: 14px;
overflow: hidden;
@@ -231,7 +237,47 @@ h1 {
}
.source-pane {
- border-right: 1px solid var(--line-strong);
+ border-right: 0;
+}
+
+.pane-splitter {
+ min-width: 0;
+ position: relative;
+ z-index: 3;
+ border: 0;
+ padding: 0;
+ cursor: col-resize;
+ touch-action: none;
+ background: #f4f6f8;
+}
+
+.pane-splitter::before {
+ width: 3px;
+ height: 34px;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ border-radius: 999px;
+ background: var(--line-strong);
+ content: "";
+ transform: translate(-50%, -50%);
+ transition: background 120ms ease, box-shadow 120ms ease;
+}
+
+.pane-splitter:hover::before,
+.pane-splitter:focus-visible::before,
+.pane-splitter.is-dragging::before {
+ background: var(--blue);
+ box-shadow: 0 0 0 3px rgb(36 107 253 / 14%);
+}
+
+.pane-splitter:focus-visible {
+ outline-offset: -2px;
+}
+
+body.is-resizing-panes {
+ cursor: col-resize;
+ user-select: none;
}
.pane-header,
@@ -728,19 +774,36 @@ footer {
}
.explorer {
- height: auto;
+ height: max(1180px, calc(100svh - 120px));
max-height: none;
grid-template-columns: 1fr;
+ grid-template-rows:
+ minmax(0, var(--source-pane-share))
+ 9px
+ minmax(0, var(--detail-pane-share));
}
.source-pane {
- height: 560px;
+ height: auto;
border-right: 0;
- border-bottom: 1px solid var(--line-strong);
+ border-bottom: 0;
}
.detail-pane {
- min-height: 620px;
+ min-height: 0;
+ }
+
+ .pane-splitter {
+ cursor: row-resize;
+ }
+
+ .pane-splitter::before {
+ width: 34px;
+ height: 3px;
+ }
+
+ body.is-resizing-panes {
+ cursor: row-resize;
}
}