// LayoutSplitter -- two panes with a divider the reader can move.
//
//
// ...
// ...
//
//
// The dragging lives in the reactive runtime behind data-wrn-splitter. Pointer
// moves fire far too often to route through a client function, and a state
// write made inside a pointermove callback is dropped, so the resolved size is
// held in the DOM as the --wrn-split custom property and these styles read it.
//
// This component previously declared resizeStart, resize and resizeEnd with no
// pointer handling whatsoever: a caller wired up @resize and received nothing,
// for ever, with no error.
//
// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
// and silently swallows the rule that follows it.
component LayoutSplitter {
outputs {
// Not named resize. An output named after a native DOM event never reaches
// a parent binding: the component emits it, but @resize on the tag is
// never invoked. sizeChange is unambiguous and does arrive.
sizeChange(payload: { size: number })
}
props {
color: string = "primary"
size: number = 50
orientation: string = "horizontal"
// Smallest share either pane may take, as a percentage. It also fixes the
// upper bound at 100 - minSize, so neither pane can be dragged away to
// nothing and left unrecoverable by pointer.
minSize: number = 15
step: number = 5
label: string = "Resize panels"
class: string = ""
}
functions {
// The runtime resolves the size and announces it; this turns that into the
// declared output so a parent @resize binding receives it.
client function reportResize(sourceEvent) {
var detail = sourceEvent ? sourceEvent.detail : null
if (!detail) {
return
}
output.sizeChange({ size: detail.size })
}
shared function isVertical() {
return orientation === "vertical"
}
shared function lowerBound() {
var value = Number(minSize)
if (!value || value < 0) {
return 15
}
return Math.min(45, value)
}
shared function upperBound() {
return 100 - lowerBound()
}
// Sizes arrive as HTML attributes, so a value outside the bounds is
// routine rather than exceptional. Clamp instead of rendering something
// the reader cannot undo.
shared function currentSize() {
var value = Number(size)
if (!value || value < 0) {
return 50
}
return Math.min(upperBound(), Math.max(lowerBound(), value))
}
shared function stepSize() {
var value = Number(step)
return value && value > 0 ? value : 5
}
}
view {
}
style {
.wire-splitter {
--splitter-accent: var(--wire-color-primary);
display: grid;
/* The first track follows the size the runtime resolves. */
grid-template-columns: var(--wrn-split, 50%) auto minmax(0, 1fr);
align-items: stretch;
width: 100%;
min-width: 0;
}
.wire-splitter[data-color="secondary"] {
--splitter-accent: var(--wire-color-secondary);
}
.wire-splitter[data-color="success"] {
--splitter-accent: var(--wire-color-success);
}
.wire-splitter[data-color="danger"] {
--splitter-accent: var(--wire-color-danger);
}
.wire-splitter[data-color="info"] {
--splitter-accent: var(--wire-color-info);
}
.wire-splitter[data-orientation="vertical"] {
grid-template-columns: minmax(0, 1fr);
grid-template-rows: var(--wrn-split, 50%) auto minmax(0, 1fr);
}
.wire-splitter__pane {
min-width: 0;
min-height: 0;
overflow: auto;
}
.wire-splitter__handle {
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
padding: 0 0.25rem;
border: 0;
background: transparent;
cursor: col-resize;
/* Without this the pointer drag selects the text in both panes. */
touch-action: none;
user-select: none;
}
.wire-splitter[data-orientation="vertical"] .wire-splitter__handle {
padding: 0.25rem 0;
cursor: row-resize;
}
.wire-splitter__grip {
display: block;
width: 2px;
height: 100%;
min-height: 1.5rem;
border-radius: 999px;
background: var(--wire-color-border);
transition: background 140ms ease;
}
.wire-splitter[data-orientation="vertical"] .wire-splitter__grip {
width: 100%;
min-width: 1.5rem;
height: 2px;
}
.wire-splitter__handle:hover .wire-splitter__grip,
.wire-splitter[data-wrn-splitter-dragging="true"] .wire-splitter__grip {
background: var(--splitter-accent);
}
.wire-splitter__handle:focus-visible {
outline: 2px solid var(--splitter-accent);
outline-offset: -2px;
border-radius: var(--wire-radius-sm);
}
/*
* Two panes side by side stop making sense on a phone. Stacking them keeps
* both readable, and the divider stops being draggable because the grid
* no longer has a second track to trade against.
*/
@media (max-width: 639px) {
.wire-splitter,
.wire-splitter[data-orientation="vertical"] {
grid-template-columns: minmax(0, 1fr);
grid-template-rows: auto auto auto;
}
.wire-splitter__handle {
cursor: default;
}
}
}
}