// 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; } } } }