From b56cb8cba539b4c97bf7fb7e235750ba3ae1cce8 Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Sat, 8 Aug 2026 11:22:45 +0530 Subject: [PATCH] feat(ui): build LayoutSplitter and CustomScrollbar for real Both advertised behaviour they did not have. LayoutSplitter declared resizeStart, resize and resizeEnd with no pointer handling whatsoever, so a caller wired up @resize and received nothing, for ever, with no error, and its props were columns, gap and maxWidth copied from a grid scaffold. CustomScrollbar was the same shape with a scroll output. The splitter now resizes. Dragging lives in the reactive runtime behind data-wrn-splitter, because a pointermove fires far too often to route through a client function and a state write made in that callback is dropped; the resolved size is held on the container as a --wrn-split custom property and the component grids from it. The handle is a real separator: arrow keys step it, Home and End go to the bounds rather than to nothing, and it carries aria-valuenow, aria-valuemin and aria-valuemax. minSize fixes both bounds so neither pane can be dragged away and left unrecoverable. CustomScrollbar is CSS rather than script -- scrollbar-width and scrollbar-color with webkit rules for the engines that still need them -- and its fake scroll output is removed rather than left unimplemented, since a caller can listen for a plain scroll event. The test harness needed a fix too: mount did not bind the window CustomEvent, so the runtime built events from the host global and happy-dom listeners never matched them, which made anything dispatched look silently lost. Co-Authored-By: Claude Opus 5 --- docs/ui-visual-contract-0.8.json | 4 +- .../app/pages/components/custom-scrollbar.wrn | 79 +++---- .../app/pages/components/layout-splitter.wrn | 101 +++++---- .../component-showcase/app/pages/index.wrn | 4 +- .../component-showcase/app/pages/layout.wrn | 10 +- .../component-showcase/app/styles/global.css | 28 +++ .../scripts/showcase-profiles.mjs | 34 +++ .../component-showcase/showcase-manifest.json | 20 +- packages/csr/src/reactive-runtime.ts | 110 ++++++++++ packages/csr/test/reactive.test.ts | 84 +++++++ packages/ui/COMPONENTS.md | 10 +- packages/ui/component-reference.json | 89 ++++---- packages/ui/components/CustomScrollbar.wrn | 123 ++++++++++- packages/ui/components/LayoutSplitter.wrn | 207 +++++++++++++++++- packages/ui/test/ui.test.ts | 75 +++++++ 15 files changed, 794 insertions(+), 184 deletions(-) diff --git a/docs/ui-visual-contract-0.8.json b/docs/ui-visual-contract-0.8.json index 8adf52b8..61110915 100644 --- a/docs/ui-visual-contract-0.8.json +++ b/docs/ui-visual-contract-0.8.json @@ -29,7 +29,7 @@ "packages/ui/components/Container.wrn": "be8fce140043eced8b78c8dd14ff85ace4ed2d91671251641ef83e0fe4da8f2a", "packages/ui/components/ContextMenu.wrn": "2e011e9cb1c09a3a2344ed3fa29dcc74331cd39f838535d8aee4b249dceef0ce", "packages/ui/components/CopyMarkup.wrn": "8d57a7d72e02c126f855a181fc36e1b969edb90f683dce6b6ffe546d49d2b29b", - "packages/ui/components/CustomScrollbar.wrn": "4c1f7758b9cd47e20ecf922403e12b5b15b8127a2744383c6bd280ee20e5fdd1", + "packages/ui/components/CustomScrollbar.wrn": "fedb2ab06cc2cf9499c130590945ca6971a3edfb60407b6658ef7756e4be9823", "packages/ui/components/DataMap.wrn": "65552d73ecd1a148427dbb10d611f9352ca36bd490f595ab1e1a47e173445ba1", "packages/ui/components/DataTable.wrn": "5a76e50e8559c724aaf01ec954958ac640c99758d44f1eef8e6eb1cb6dc4f862", "packages/ui/components/DatePicker.wrn": "18b07e59c27c2720bf960e9c04ad70d9e422ddcf0817fede74e71899811bb387", @@ -53,7 +53,7 @@ "packages/ui/components/InputGroup.wrn": "0e23ea541a60e893c9d9e6f95113911ee459468d8d6105bf03aeb55395359123", "packages/ui/components/InputNumber.wrn": "a0aa4f4566bf54ffbef54c99648eab05b5f9870be6ea07639a4ecf3930afb631", "packages/ui/components/Kbd.wrn": "d51fa614b36f971f885410785b481c2e6900b2416e6e7c8868dfda1ebd5f3b4c", - "packages/ui/components/LayoutSplitter.wrn": "dac5a80acb76bf58dfd25233c68190c67d1aa1b3b0d4250a0957ea40912cf722", + "packages/ui/components/LayoutSplitter.wrn": "a86dc54b74aed6b05e770cd1802960d6309bba42fe80e899e8aaa8b1b7aa88e3", "packages/ui/components/LegendIndicator.wrn": "6a7607b27a17196f899132eb952203a1073e4077365a94935f485e700cbac665", "packages/ui/components/Link.wrn": "935ac24e8ebd067860dc9709ea9121710315ad6e02ce95cd5e982ae4d5978a02", "packages/ui/components/List.wrn": "02f960a8e82fa049aac477bac9c1d9371394c86d9c47513138f61bffc79dc7d8", diff --git a/examples/component-showcase/app/pages/components/custom-scrollbar.wrn b/examples/component-showcase/app/pages/components/custom-scrollbar.wrn index 21bca16a..9e095e91 100644 --- a/examples/component-showcase/app/pages/components/custom-scrollbar.wrn +++ b/examples/component-showcase/app/pages/components/custom-scrollbar.wrn @@ -1,11 +1,12 @@ // Generated by scripts/generate-showcase.mjs. Do not edit directly. page CustomScrollbarDetail { layout = "showcase" - state playground_size = ctx.url.searchParams.get("pg_size") ?? "default" state playground_color = ctx.url.searchParams.get("pg_color") ?? "primary" - state playground_columns = Number(ctx.url.searchParams.get("pg_columns") ?? "2") - state playground_gap = ctx.url.searchParams.get("pg_gap") ?? "md" - state playground_maxWidth = ctx.url.searchParams.get("pg_maxWidth") ?? "xl" + state playground_size = ctx.url.searchParams.get("pg_size") ?? "default" + state playground_axis = ctx.url.searchParams.get("pg_axis") ?? "vertical" + state playground_thickness = Number(ctx.url.searchParams.get("pg_thickness") ?? "8") + state playground_maxHeight = ctx.url.searchParams.get("pg_maxHeight") ?? "20rem" + state playground_radius = ctx.url.searchParams.get("pg_radius") ?? "999px" state playground_class = ctx.url.searchParams.get("pg_class") ?? "showcase-instance showcase-instance--1" seo { title = "Custom Scrollbar" @@ -20,26 +21,26 @@ page CustomScrollbarDetail { Layout component

Custom Scrollbar

Theme-aware, responsive custom scrollbar component.

-
6 props1 slots1 outputsTheme readyResponsive
+
7 props1 slots0 outputsTheme readyResponsive
-
+
Interactive playground

Configure Custom Scrollbar

Change any prop and inspect the server-rendered component immediately.

Live preview
-
+

Line 1. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.

Line 2. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.

Line 3. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.

Line 4. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.

Line 5. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.

Line 6. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.

Line 7. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.

Line 8. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.

Line 9. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.

Line 10. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.

Line 11. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.

Line 12. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.

Component code
<CustomScrollbar />
-
Event outputInteract with the preview to inspect the typed payload.
+
-
Component props6 controls
-
+
Component props7 controls
+
@@ -48,72 +49,54 @@ page CustomScrollbarDetail {
Live examples

Designed for real product surfaces

Compare configurations and resize the browser to check responsive behavior.

-
Recommended

Production default

Balanced spacing, hierarchy, and content for the most common product workflow.

+
Recommended

Themed scrolling region

Scrollbar appearance is CSS rather than script: scrollbar-width and scrollbar-color are the standard properties, with webkit rules for the engines that still need them. The thumb follows the component colour.

01
Live preview
-
+

Line 1. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.

Line 2. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.

Line 3. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.

Line 4. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.

Line 5. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.

Line 6. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.

Line 7. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.

Line 8. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.

Line 9. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.

Line 10. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.

Line 11. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.

Line 12. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.

-
+
Component usage.wrn
-
<CustomScrollbar />
+
<CustomScrollbar
+  maxHeight="14rem">
+  <div class="showcase-scroll-demo"><p>Line 1. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 2. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 3. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 4. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 5. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 6. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 7. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 8. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 9. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 10. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 11. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p><p>Line 12. A scrolling region only shows its scrollbar when the content actually overflows, so this demo ships enough of it.</p></div>
+</CustomScrollbar>
-
Dense UI

Compact application

A tighter variation for dashboards, side panels, tables, and operational interfaces.

+
Advanced

Horizontal, thicker track

A horizontal region with a heavier track. thickness is clamped, since it arrives as an attribute and a scrollbar wider than its content helps nobody.

02
Live preview
-
+
Column 1Column 2Column 3Column 4Column 5Column 6Column 7Column 8Column 9Column 10Column 11Column 12Column 13Column 14
-
+
Component usage.wrn
<CustomScrollbar
+  color="info"
   size="sm"
-/>
-
-
-
-
-
-
Extended

Rich configuration

A more expressive variation using additional data, stronger emphasis, and optional states.

- 03 -
-
-
-
Live preview
- -
-
-
-
Component usage.wrn
-
<CustomScrollbar
-  size="lg"
-/>
+ axis="horizontal" + thickness="14" + maxHeight="8rem" + radius="4px"> + <div class="showcase-scroll-demo showcase-scroll-demo--wide"><span>Column 1</span><span>Column 2</span><span>Column 3</span><span>Column 4</span><span>Column 5</span><span>Column 6</span><span>Column 7</span><span>Column 8</span><span>Column 9</span><span>Column 10</span><span>Column 11</span><span>Column 12</span><span>Column 13</span><span>Column 14</span></div> +</CustomScrollbar>
-
Component outputs

Receive every typed component output

Use declarative output handlers in .wrn files or register a direct output handler from JavaScript. The canonical API exposes payload and does not require event.detail.

@scrollFires when the component emits the scroll event.
Declarative handlers.wrn
<CustomScrollbar
-  @scroll='console.log(payload)'
-/>
Direct output handlers.js
import { registerOutputHandler } from "@wrnexus/csr/outputs"
-
-const component = document.querySelector("[data-ui-component=\"CustomScrollbar\"], [data-component=\"CustomScrollbar\"]")
-
-if (component) registerOutputHandler(component, "scroll", (payload) => {
-  console.log("scroll", payload)
-})
-
Component API

Props and configuration

All content and behavior shown above is supplied through these props and slots.

PropTypeDefaultRequired
sizestring"default"No
colorstring"primary"No
columnsnumber2No
gapstring"md"No
maxWidthstring"xl"No
classstring""No
+ +
Component API

Props and configuration

All content and behavior shown above is supplied through these props and slots.

PropTypeDefaultRequired
colorstring"primary"No
sizestring"default"No
axisstring"vertical"No
thicknessnumber8No
maxHeightstring"20rem"No
radiusstring"999px"No
classstring""No