├── .npmignore
├── tsconfig.json
├── .gitignore
├── tsconfig.build.json
├── vite.config.ts
├── LICENSE.md
├── package.json
├── docs
├── index.js
├── index.html
└── style.css
├── Readme.md
└── src
└── index.ts
/.npmignore:
--------------------------------------------------------------------------------
1 | examples/
2 | public/
3 | website/
4 | node_modules/
5 | src/
6 | tests/
7 | docs/
8 | testr/
9 |
10 | index.html
11 | test.js
12 | test.ts
13 | *.html
14 | *.css
15 |
16 | yarn.lock
17 | package-lock.json
18 |
19 | .gitignore
20 | .cache
21 | .vscode
22 | .babelrc
23 |
24 | rollup.config.js
25 | bs-config.js
26 | vite.config.ts
27 | tsconfig.json
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "module": "ESNext",
5 | "declaration": true,
6 | "declarationDir": "./dist/types",
7 | "outDir": "./dist",
8 | "strict": false,
9 | "moduleResolution": "node",
10 | "esModuleInterop": true,
11 | "skipLibCheck": true,
12 | "forceConsistentCasingInFileNames": true
13 | },
14 | "include": ["src"]
15 | }
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | public/
2 |
3 | # Logs
4 | logs
5 | *.log
6 | npm-debug.log*
7 | yarn-debug.log*
8 | yarn-error.log*
9 | pnpm-debug.log*
10 | lerna-debug.log*
11 | package-lock.json
12 |
13 | test.js
14 | test.ts
15 |
16 | node_modules
17 | dist
18 | dist-ssr
19 | *.local
20 | report-example-response.json
21 | # Editor directories and files
22 | .vscode/*
23 | !.vscode/extensions.json
24 | .idea
25 | .DS_Store
26 | *.suo
27 | *.ntvs*
28 | *.njsproj
29 | *.sln
30 | *.sw?
31 | *.zip
32 | *.rar
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "useDefineForClassFields": true,
5 | "module": "ESNext",
6 | "lib": [
7 | "ES2022",
8 | "DOM"
9 | ],
10 | "moduleResolution": "Node",
11 | "strict": false,
12 | "sourceMap": true,
13 | "resolveJsonModule": true,
14 | "esModuleInterop": true,
15 | "noEmit": true, // Vite handles emission
16 | "noImplicitAny": true,
17 | "noUnusedLocals": true,
18 | "noUnusedParameters": true,
19 | "allowSyntheticDefaultImports": true,
20 | "allowImportingTsExtensions": true
21 | },
22 | "include": [
23 | "src/**/*"
24 | ]
25 | }
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import dts from 'vite-plugin-dts';
3 | import path from 'node:path';
4 | import pkg from './package.json' assert { type: 'json' };
5 |
6 | export default defineConfig({
7 | plugins: [
8 | dts({
9 | compilerOptions: { removeComments: true },
10 | insertTypesEntry: true,
11 | tsconfigPath: './tsconfig.build.json'
12 | }),
13 | ],
14 | build: {
15 | lib: {
16 | entry: path.resolve(__dirname, 'src/index.ts'),
17 | name: 'SplitViews',
18 | fileName: (format) => format === 'es' ? 'index.js' : `index.${format}.js`,
19 | formats: ['es', 'umd'],
20 | },
21 | sourcemap: false,
22 | }
23 | });
24 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2020 - present Wutility
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "split-views",
3 | "version": "3.0.2",
4 | "description": "A lightweight, zero-dependency TypeScript library for creating resizable split views (panes) in web applications.",
5 | "private": false,
6 | "type": "module",
7 | "main": "./dist/index.js",
8 | "module": "./dist/index.js",
9 | "browser": "./dist/index.umd.js",
10 | "types": "./dist/types/index.d.ts",
11 | "files": [
12 | "dist"
13 | ],
14 | "scripts": {
15 | "build": "vite build",
16 | "dev": "vite",
17 | "preview": "vite preview",
18 | "clean": "rm -rf dist",
19 | "prepublishOnly": "npm run clean && npm run build"
20 | },
21 | "devDependencies": {
22 | "@types/node": "^24.3.0",
23 | "typescript": "^5.9.2",
24 | "vite": "^7.1.3",
25 | "vite-plugin-dts": "^4.5.4"
26 | },
27 | "keywords": [
28 | "resizer",
29 | "pane",
30 | "split",
31 | "view",
32 | "utility",
33 | "split panes",
34 | "resize panes",
35 | "split.js"
36 | ],
37 | "repository": {
38 | "type": "git",
39 | "url": "git+https://github.com/wutility/split-views.git"
40 | },
41 | "bugs": {
42 | "url": "https://github.com/wutility/split-views.git/issues"
43 | },
44 | "homepage": "https://github.com/wutility/split-views.git#readme",
45 | "license": "MIT"
46 | }
47 |
--------------------------------------------------------------------------------
/docs/index.js:
--------------------------------------------------------------------------------
1 | let defaultOptions = {
2 | direction: "horizontal",
3 | gutterSize: 5,
4 | minSize: 0,
5 | snapOffset: 0,
6 | columns: 3,
7 | }
8 |
9 | let sp = null
10 | const container = document.querySelector(".split-container")
11 | const form = document.querySelector(".config-form")
12 | const codeBlock = document.querySelector(".code-content")
13 |
14 | // Initialize split view
15 | function initializeSplitView() {
16 | const result = 100 / defaultOptions.columns
17 | const sizes = new Array(defaultOptions.columns).fill(result)
18 |
19 | container.innerHTML = ""
20 | sizes.forEach((s, i) => {
21 | container.innerHTML += `
22 |
23 |
Panel ${String.fromCharCode(65 + i)}
24 |
Resizable content area
25 |
26 |
`
27 | })
28 |
29 | sp = SplitViews({
30 | root: container,
31 | ...defaultOptions,
32 | sizes,
33 | onDragEnd: (newSizes) => {
34 | console.log(newSizes);
35 | updateCodeDisplay(newSizes)
36 | },
37 | })
38 |
39 | updateCodeDisplay(sizes)
40 | }
41 |
42 | function updateCodeDisplay(sizes = null) {
43 | const configCode = `// SplitViews Library Configuration
44 | import SplitViews from 'split-views';
45 |
46 | const splitView = new SplitViews({
47 | root: '.split-container',
48 | direction: '${defaultOptions.direction}',
49 | gutterSize: ${defaultOptions.gutterSize},
50 | minSize: ${defaultOptions.minSize},
51 | snapOffset: ${defaultOptions.snapOffset},
52 | sizes: [${sizes ? sizes.map((s) => s.toFixed(1)).join(", ") : new Array(defaultOptions.columns).fill((100 / defaultOptions.columns).toFixed(1)).join(", ")}],
53 | onDragEnd: (newSizes) => {
54 | console.log('Panel sizes updated:', newSizes);
55 | // Handle resize events in your application
56 | }
57 | });`
58 |
59 | codeBlock.textContent = configCode
60 | }
61 |
62 | // Handle form submission
63 | form.addEventListener("submit", (e) => {
64 | e.preventDefault()
65 |
66 | if (sp) sp.destroy()
67 |
68 | const formData = new FormData(e.target)
69 | const newOptions = {}
70 |
71 | for (const [key, value] of formData.entries()) {
72 | if (value !== "" && value !== null) {
73 | newOptions[key] = isNaN(value) ? value : Number(value)
74 | }
75 | }
76 |
77 | // Merge with current options, only updating provided values
78 | defaultOptions = { ...defaultOptions, ...newOptions }
79 |
80 | // Ensure columns is at least 1
81 | if (defaultOptions.columns < 1) {
82 | defaultOptions.columns = 2
83 | }
84 |
85 | initializeSplitView()
86 | })
87 |
88 | document.querySelector(".copy-code-btn")?.addEventListener("click", () => {
89 | navigator.clipboard.writeText(codeBlock.textContent).then(() => {
90 | const btn = document.querySelector(".copy-code-btn")
91 | const originalText = btn.textContent
92 | btn.textContent = "Copied!"
93 | setTimeout(() => {
94 | btn.textContent = originalText
95 | }, 2000)
96 | })
97 | })
98 |
99 | // Initialize on page load
100 | document.addEventListener("DOMContentLoaded", () => {
101 | initializeSplitView()
102 | })
103 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | SplitViews - Developer Library
8 |
10 |
11 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
31 |
32 |
33 |
34 |
35 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
Panel A
92 |
Resizable content area
93 |
94 |
95 |
96 |
97 |
Panel B
98 |
Another resizable area
99 |
100 |
101 |
102 |
103 |
107 |
108 |
109 |
// Configure your split view and see the code here
110 | const splitView = new SplitViews({
111 | // Configuration will update as you change settings
112 | });
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
125 |
126 |
127 |
128 |
129 |
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | # SplitViews
2 |
3 | A lightweight, framework-agnostic split-pane library for resizable layouts.\
4 | Zero dependencies. Modern **Pointer Events**. CSS-variable driven sizing.
5 |
6 | - ✨ **Modern**: Uses Pointer Events + `setPointerCapture` (no global listeners)
7 | - ⚡ **Performant**: Batched DOM writes, CSS variables for sizes, minimal
8 | reflows
9 | - 🧩 **Composable**: Works with nested splits, no framework required
10 | - ♿ **Accessible**: ARIA roles, orientations, keyboard focusable gutters
11 |
12 |
18 |
19 | 
20 |
21 |
22 |
23 | ### [Demo](https://wutility.github.io/split-views)
24 |
25 | ```js
26 | import SplitViews from "split-views";
27 | ```
28 |
29 | Or include it via jsDelivr CDN (UMD):
30 |
31 | ```html
32 |
33 |
34 | ```
35 |
36 | ## Quick Start
37 |
38 | HTML:
39 |
40 | ```html
41 |
42 |
Left Pane
43 |
Right Pane
44 |
45 | ```
46 |
47 | JavaScript:
48 |
49 | ```js
50 | import SplitViews from "splitviews";
51 |
52 | const split = SplitViews({
53 | root: "#editor",
54 | direction: "horizontal", // 'vertical' or 'horizontal' (default)
55 | gutterSize: 8,
56 | sizes: [30, 70], // percentages; defaults to equal split
57 | minSize: [120, 200], // px per pane or single px applied to all
58 | snapOffset: 8, // px tolerance before snapping to min
59 | onDrag: (sizes) => {
60 | console.log("resizing...", sizes);
61 | },
62 | onDragEnd: (sizes) => {
63 | console.log("final sizes:", sizes);
64 | },
65 | });
66 | ```
67 |
68 | ---
69 |
70 | ## Options
71 |
72 | | Option | Type | Default | Description |
73 | | ----------------- | ---------------------------- | ---------------- | --------------------------------------------------------- |
74 | | `root` | `HTMLElement \| string` | **required** | Container element or selector. Children become panes. |
75 | | `direction` | `'horizontal' \| 'vertical'` | `'horizontal'` | Split direction. |
76 | | `gutterSize` | `number` | `10` | Gutter thickness in px. |
77 | | `gutterClassName` | `string` | `'split-gutter'` | Class applied to each gutter. |
78 | | `minSize` | `number \| number[]` | `0` | Per-pane minimum size in px (array) or single px for all. |
79 | | `sizes` | `number[]` | equal split | Initial sizes in percentages. |
80 | | `snapOffset` | `number` | `0` | Extra px tolerance before snapping to min. |
81 | | `onDrag` | `(sizes:number[])=>void` | — | Called on every drag frame. |
82 | | `onDragEnd` | `(sizes:number[])=>void` | — | Called when dragging stops. |
83 |
84 | ---
85 |
86 | ## API
87 |
88 | - `destroy()`: Removes gutters and resets styles
89 | - `setSizes(sizes: number[])`: Programmatically set pane sizes
90 | - `getSizes(): number[]`: Get current pane sizes
91 |
92 | ---
93 |
94 | ## Styling
95 |
96 | The library sets:
97 |
98 | - `display: flex` and `flex-direction` on `root`
99 | - `flex-basis` per pane (via CSS variables)
100 | - Basic `cursor` and `touch-action` on gutters
101 |
102 | You control the look of gutters:
103 |
104 | ```css
105 | .split-root {
106 | contain: layout size style; /* hint for performance */
107 | }
108 |
109 | .split-gutter {
110 | background: transparent;
111 | position: relative;
112 | }
113 |
114 | /* Visible hairline */
115 | .split-gutter::before {
116 | content: "";
117 | position: absolute;
118 | inset: 0;
119 | background: rgba(0, 0, 0, 0.1);
120 | }
121 |
122 | .split-gutter:hover::before {
123 | background: rgba(0, 0, 0, 0.2);
124 | }
125 | ```
126 |
127 | CSS variables you can use:
128 |
129 | - `--pane--size`: applied as `flex-basis` for each pane
130 | - `--split-gutter-size`: gutter thickness
131 | - `--split-cursor`: cursor type (`col-resize` / `row-resize`)
132 |
133 | ---
134 |
135 | ## Accessibility
136 |
137 | Gutters automatically have:
138 |
139 | - `role="separator"`
140 | - `aria-orientation="vertical"` (for horizontal split) or `"horizontal"` (for
141 | vertical split)
142 | - `tabIndex="0"` so they can be focused
143 |
144 | ---
145 |
146 | ## Nested Splits
147 |
148 | You can nest multiple instances:
149 |
150 | ```js
151 | const outer = SplitViews({ root: "#root", direction: "horizontal" });
152 | const inner = SplitViews({
153 | root: '#root [data-split-pane="1"]',
154 | direction: "vertical",
155 | });
156 | ```
157 |
158 | Each instance manages only its direct children.
159 |
160 | ---
161 |
162 | ## Performance Notes
163 |
164 | - Uses `setPointerCapture` → no global listeners.
165 | - DOM writes batched with `requestAnimationFrame`.
166 | - CSS variables used for sizes → fast style recalculation.
167 | - Avoid heavy work in `onDrag`; debounce if needed.
168 | - Clean `destroy()` ensures no leaks.
169 |
170 | ---
171 |
172 | ## Browser Support
173 |
174 | - Chromium, Firefox, Safari (modern versions with **Pointer Events**).
175 | - No IE support (Pointer Events required).
176 |
177 | ---
178 |
179 | ## License
180 |
181 | MIT © 2025
182 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export interface SplitOptions {
2 | root: HTMLElement | string;
3 | direction?: 'horizontal' | 'vertical';
4 | gutterSize?: number;
5 | gutterClassName?: string;
6 | minSize?: number | number[];
7 | sizes?: number[];
8 | snapOffset?: number;
9 | onDrag?: (sizes: number[]) => void;
10 | onDragEnd?: (sizes: number[]) => void;
11 | }
12 |
13 | export default function SplitViews(opts: SplitOptions) {
14 | const toEl = (sel: HTMLElement | string): HTMLElement =>
15 | typeof sel === 'string' ? (document.querySelector(sel) as HTMLElement) : sel;
16 |
17 | const clamp = (n: number, min: number, max: number): number => Math.min(Math.max(n, min), max);
18 |
19 | const HORIZONTAL = 'horizontal' as const;
20 | const VERTICAL = 'vertical' as const;
21 |
22 | let root: HTMLElement;
23 | let panes: HTMLElement[];
24 |
25 | if (Array.isArray(opts.root)) {
26 | throw new Error('SplitViews: root must be HTMLElement | string');
27 | }
28 |
29 | root = toEl(opts.root);
30 | panes = Array.from(root.children) as HTMLElement[];
31 |
32 | const dir = opts.direction === VERTICAL ? VERTICAL : HORIZONTAL;
33 | const isH = dir === HORIZONTAL;
34 | const gutterSz = opts.gutterSize ?? 10;
35 | const snapOffset = opts.snapOffset ?? 0;
36 |
37 | const mins: ReadonlyArray = Array.isArray(opts.minSize)
38 | ? opts.minSize
39 | : Array(panes.length).fill(opts.minSize ?? 0);
40 |
41 | if (Array.isArray(opts.minSize) && panes.length !== mins.length) {
42 | throw new Error('SplitViews: minSize array length must equal pane count');
43 | }
44 |
45 | /* ---------- build gutters --------------------------------------------- */
46 | const gutters: HTMLElement[] = [];
47 | panes.forEach((pane, idx) => {
48 | pane.setAttribute('data-split-pane', String(idx));
49 | pane.style.flex = '0 0 auto';
50 | });
51 | for (let i = 1; i < panes.length; i++) {
52 | const g = document.createElement('div');
53 | g.className = opts.gutterClassName ?? 'split-gutter';
54 | g.style.cssText = `flex: 0 0 var(--split-gutter-size, ${gutterSz}px); cursor: var(--split-cursor, ${isH ? 'col-resize' : 'row-resize'}); touch-action: none;`;
55 | g.setAttribute('role', 'separator');
56 | g.setAttribute('aria-orientation', isH ? 'vertical' : 'horizontal');
57 | g.tabIndex = 0;
58 | g.dataset.gutterIndex = String(i - 1);
59 | root.insertBefore(g, panes[i]);
60 | gutters.push(g);
61 | }
62 |
63 | /* ---------- container -------------------------------------------------- */
64 | root.style.display = 'flex';
65 | root.style.flexDirection = isH ? 'row' : 'column';
66 | root.style.setProperty('--split-gutter-size', `${gutterSz}px`);
67 | root.style.setProperty('--split-cursor', isH ? 'col-resize' : 'row-resize');
68 |
69 | /* ---------- sizes (via CSS variables) --------------------------------- */
70 | let currentSizes: number[] = opts.sizes?.length ? [...opts.sizes] : Array(panes.length).fill(100 / panes.length);
71 |
72 | function applySizes(indices?: number[]): void {
73 | const totalPct = currentSizes.reduce((a, b) => a + b, 0);
74 | const safeSizes = totalPct === 0 ? Array(panes.length).fill(100 / panes.length) : currentSizes.map(s => (s / totalPct) * 100);
75 | const gutterTotal = gutters.length * gutterSz;
76 |
77 | const targets = indices ?? panes.map((_, i) => i);
78 |
79 | requestAnimationFrame(() => {
80 | targets.forEach(i => {
81 | const pct = safeSizes[i];
82 | const val = `calc(${pct}% - ${(pct / 100) * gutterTotal}px)`;
83 | root.style.setProperty(`--pane-${i}-size`, val);
84 | panes[i].style.flexBasis = `var(--pane-${i}-size)`;
85 | });
86 | });
87 | }
88 | applySizes();
89 |
90 | /* ---------- drag state ------------------------------------------------- */
91 | let activeGutter: HTMLElement | null = null;
92 | let prevIdx = -1;
93 | let nextIdx = -1;
94 | let startCoord = 0;
95 | let prevStartPct = 0;
96 | let nextStartPct = 0;
97 | let lastClientCoord = 0;
98 | let isTicking = false;
99 | let rootPx = 0;
100 |
101 | /* ---------- handlers (reused) ----------------------------------------- */
102 | const onMove = (ev: PointerEvent): void => {
103 | if (!activeGutter) return;
104 |
105 | lastClientCoord = isH ? ev.clientX : ev.clientY;
106 |
107 | if (!isTicking) {
108 | requestAnimationFrame(() => {
109 | if (!activeGutter) {
110 | isTicking = false;
111 | return;
112 | }
113 |
114 | const deltaPx = lastClientCoord - startCoord;
115 | const deltaPct = (deltaPx / rootPx) * 100;
116 |
117 | let prevPct = prevStartPct + deltaPct;
118 | let nextPct = nextStartPct - deltaPct;
119 |
120 | const prevMinPx = mins[prevIdx];
121 | const nextMinPx = mins[nextIdx];
122 | const prevMinPct = (prevMinPx / rootPx) * 100;
123 | const nextMinPct = (nextMinPx / rootPx) * 100;
124 |
125 | const prevSnap = ((prevMinPx + snapOffset) / rootPx) * 100;
126 | const nextSnap = ((nextMinPx + snapOffset) / rootPx) * 100;
127 |
128 | if (prevPct <= prevSnap) prevPct = prevMinPct;
129 | if (nextPct <= nextSnap) nextPct = nextMinPct;
130 |
131 | prevPct = clamp(prevPct, prevMinPct, 100 - nextMinPct);
132 | nextPct = clamp(nextPct, nextMinPct, 100 - prevMinPct);
133 |
134 | const total = prevPct + nextPct;
135 | if (total > 0) {
136 | prevPct = (prevPct / total) * (prevStartPct + nextStartPct);
137 | nextPct = (nextPct / total) * (prevStartPct + nextStartPct);
138 | }
139 |
140 | currentSizes[prevIdx] = prevPct;
141 | currentSizes[nextIdx] = nextPct;
142 |
143 | applySizes([prevIdx, nextIdx]);
144 | opts.onDrag?.([...currentSizes]);
145 | isTicking = false;
146 | });
147 | isTicking = true;
148 | }
149 | };
150 |
151 | const onUp = (ev: PointerEvent): void => {
152 | if (!activeGutter) return;
153 | activeGutter.releasePointerCapture(ev.pointerId);
154 | activeGutter.removeEventListener('pointermove', onMove);
155 | opts.onDragEnd?.([...currentSizes]);
156 | activeGutter = null;
157 | };
158 |
159 | function onDown(ev: PointerEvent, gutter: HTMLElement): void {
160 | ev.preventDefault();
161 | activeGutter = gutter;
162 | prevIdx = parseInt(gutter.dataset.gutterIndex!);
163 | nextIdx = prevIdx + 1;
164 |
165 | startCoord = isH ? ev.clientX : ev.clientY;
166 | prevStartPct = currentSizes[prevIdx];
167 | nextStartPct = currentSizes[nextIdx];
168 | rootPx = isH ? root.offsetWidth : root.offsetHeight;
169 |
170 | gutter.setPointerCapture(ev.pointerId);
171 | gutter.addEventListener('pointermove', onMove);
172 | gutter.addEventListener('pointerup', onUp, { once: true });
173 | }
174 |
175 | // Delegate pointerdown to root
176 | const onRootDown = (e: Event) => {
177 | const target = e.target as HTMLElement;
178 | if (gutters.includes(target)) {
179 | onDown(e as PointerEvent, target);
180 | }
181 | };
182 |
183 | root.addEventListener('pointerdown', onRootDown);
184 |
185 | /* ---------- public API ------------------------------------------------- */
186 | return {
187 | root,
188 | destroy(): void {
189 | gutters.forEach(g => g.remove());
190 | root.removeEventListener('pointerdown', onRootDown);
191 |
192 | root.style.removeProperty('display');
193 | root.style.removeProperty('flex-direction');
194 | root.style.removeProperty('--split-gutter-size');
195 | root.style.removeProperty('--split-cursor');
196 |
197 | panes.forEach((pane, i) => {
198 | pane.style.removeProperty('flex-basis');
199 | pane.style.removeProperty('flex');
200 | root.style.removeProperty(`--pane-${i}-size`);
201 | });
202 | },
203 | setSizes(sizes: number[]): void {
204 | if (sizes.length !== panes.length) return;
205 | currentSizes = [...sizes];
206 | applySizes();
207 | },
208 | getSizes: (): number[] => [...currentSizes]
209 | };
210 | }
--------------------------------------------------------------------------------
/docs/style.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 |
5 | /* [data-split-pane="0"] { flex-basis: var(--pane-0-size); } */
6 |
7 | body {
8 | margin: 0;
9 | padding: 0;
10 | font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
11 | background: #0d1117;
12 | color: #e6edf3;
13 | line-height: 1.6;
14 | }
15 |
16 | .app-container {
17 | min-height: 100vh;
18 | background: #0d1117;
19 | }
20 |
21 | /* New header design for developer library */
22 | .app-header {
23 | background: #161b22;
24 | border-bottom: 1px solid #30363d;
25 | padding: 1rem 0;
26 | }
27 |
28 | .header-content {
29 | max-width: 1400px;
30 | margin: 0 auto;
31 | padding: 0 2rem;
32 | display: flex;
33 | align-items: center;
34 | justify-content: space-between;
35 | }
36 |
37 | .brand {
38 | max-width: 500px;
39 | display: flex;
40 | flex-direction: column;
41 | gap: 1rem;
42 | }
43 |
44 | .logo {
45 | font-size: 1.75rem;
46 | font-weight: 700;
47 | margin: 0;
48 | color: #58a6ff;
49 | font-family: "JetBrains Mono", monospace;
50 | }
51 |
52 | .tagline {
53 | color: #8b949e;
54 | font-size: 0.875rem;
55 | font-weight: 500;
56 | }
57 |
58 | .header-actions {
59 | display: flex;
60 | align-items: center;
61 | gap: 1.5rem;
62 | }
63 |
64 | .header-link {
65 | color: #e6edf3;
66 | text-decoration: none;
67 | font-weight: 500;
68 | font-size: 0.875rem;
69 | padding: 0.5rem 0;
70 | border-bottom: 2px solid transparent;
71 | }
72 |
73 | .header-link:hover {
74 | color: #58a6ff;
75 | border-bottom-color: #58a6ff;
76 | }
77 |
78 | .install-btn {
79 | background: #21262d;
80 | border: 1px solid #30363d;
81 | color: #e6edf3;
82 | padding: 0.625rem 1rem;
83 | border-radius: 6px;
84 | font-family: "JetBrains Mono", monospace;
85 | font-size: 0.8125rem;
86 | font-weight: 500;
87 | cursor: pointer;
88 | }
89 |
90 | .install-btn:hover {
91 | background: #30363d;
92 | border-color: #58a6ff;
93 | }
94 |
95 | /* New main content layout without sidebar */
96 | .main-content {
97 | max-width: 1400px;
98 | margin: 0 auto;
99 | padding: 2rem;
100 | }
101 |
102 | .content-layout {
103 | display: grid;
104 | grid-template-columns: 350px 1fr;
105 | grid-template-rows: auto 1fr;
106 | gap: 2rem;
107 | min-height: calc(100vh - 120px);
108 | }
109 |
110 | .demo-section {
111 | grid-column: 2;
112 | grid-row: 1 / 3;
113 | }
114 |
115 | /* Redesigned control panel for developers */
116 | .control-panel {
117 | background: #161b22;
118 | border: 1px solid #30363d;
119 | border-radius: 8px;
120 | overflow: hidden;
121 | }
122 |
123 | .panel-header {
124 | padding: 1.25rem;
125 | border-bottom: 1px solid #30363d;
126 | display: flex;
127 | align-items: center;
128 | justify-content: space-between;
129 | background: #161b22;
130 | }
131 |
132 | .panel-title {
133 | font-size: 1rem;
134 | font-weight: 600;
135 | margin: 0;
136 | color: #e6edf3;
137 | }
138 |
139 | .panel-status {
140 | display: flex;
141 | align-items: center;
142 | gap: 0.5rem;
143 | }
144 |
145 | .status-dot {
146 | width: 8px;
147 | height: 8px;
148 | background: #3fb950;
149 | border-radius: 50%;
150 | }
151 |
152 | .status-text {
153 | color: #8b949e;
154 | font-size: 0.75rem;
155 | font-weight: 500;
156 | }
157 |
158 | .config-form {
159 | padding: 1.25rem;
160 | }
161 |
162 | .form-row {
163 | display: grid;
164 | grid-template-columns: 1fr 1fr;
165 | gap: 1rem;
166 | margin-bottom: 1rem;
167 | }
168 |
169 | .form-row:last-child {
170 | margin-bottom: 0;
171 | }
172 |
173 | .form-field {
174 | display: flex;
175 | flex-direction: column;
176 | }
177 |
178 | .field-label {
179 | font-size: 0.8125rem;
180 | font-weight: 500;
181 | color: #e6edf3;
182 | margin-bottom: 0.5rem;
183 | }
184 |
185 | .field-input,
186 | .field-select {
187 | background: #0d1117;
188 | border: 1px solid #30363d;
189 | border-radius: 6px;
190 | padding: 0.625rem 0.75rem;
191 | font-size: 0.8125rem;
192 | color: #e6edf3;
193 | font-family: "JetBrains Mono", monospace;
194 | }
195 |
196 | .field-input:focus,
197 | .field-select:focus {
198 | outline: none;
199 | border-color: #58a6ff;
200 | box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.1);
201 | }
202 |
203 | .field-input::placeholder {
204 | color: #6e7681;
205 | font-family: "JetBrains Mono", monospace;
206 | }
207 |
208 | .apply-btn {
209 | background: #238636;
210 | border: 1px solid #238636;
211 | color: #ffffff;
212 | padding: 0.625rem 1rem;
213 | border-radius: 6px;
214 | font-size: 0.8125rem;
215 | font-weight: 500;
216 | cursor: pointer;
217 | width: 100%;
218 | }
219 |
220 | .apply-btn:hover {
221 | background: #2ea043;
222 | border-color: #2ea043;
223 | }
224 |
225 | /* Enhanced demo section design */
226 | .demo-section {
227 | background: #161b22;
228 | border: 1px solid #30363d;
229 | border-radius: 8px;
230 | overflow: hidden;
231 | display: flex;
232 | flex-direction: column;
233 | }
234 |
235 | .demo-header {
236 | padding: 1.25rem;
237 | border-bottom: 1px solid #30363d;
238 | display: flex;
239 | align-items: center;
240 | justify-content: space-between;
241 | }
242 |
243 | .demo-title {
244 | font-size: 1rem;
245 | font-weight: 600;
246 | margin: 0;
247 | color: #e6edf3;
248 | }
249 |
250 | .demo-info {
251 | display: flex;
252 | align-items: center;
253 | gap: 0.5rem;
254 | }
255 |
256 | .info-text {
257 | color: #8b949e;
258 | font-size: 0.75rem;
259 | font-style: italic;
260 | }
261 |
262 | .split-gutter {background-color: #2ea043;}
263 |
264 | .split-container {
265 | margin-bottom: 1rem;
266 | border: 1px solid #30363d;
267 | overflow: hidden;
268 | background: #0d1117;
269 | height: 300px;
270 | }
271 |
272 | .pane {
273 | background: #161b22;
274 | overflow: auto;
275 | }
276 |
277 | .pane:hover {
278 | background: #21262d;
279 | border-color: #58a6ff;
280 | }
281 |
282 | .pane-content {
283 | height: 100%;
284 | display: flex;
285 | flex-direction: column;
286 | align-items: center;
287 | justify-content: center;
288 | text-align: center;
289 | border: 5px solid #30363d;
290 | }
291 |
292 | .pane-label {
293 | font-weight: 600;
294 | color: #58a6ff;
295 | font-size: 1.125rem;
296 | font-family: "JetBrains Mono", monospace;
297 | display: block;
298 | margin-bottom: 0.5rem;
299 | }
300 |
301 | .pane-desc {
302 | color: #8b949e;
303 | font-size: 0.8125rem;
304 | margin: 0;
305 | }
306 |
307 | .sp-gutter {
308 | background: #30363d;
309 | border-radius: 2px;
310 | }
311 |
312 | .sp-gutter:hover {
313 | background: #58a6ff;
314 | }
315 |
316 | /* Enhanced code section with developer-friendly styling */
317 | .code-section {
318 | background: #161b22;
319 | border: 1px solid #30363d;
320 | border-radius: 8px;
321 | overflow: hidden;
322 | }
323 |
324 | .code-header {
325 | padding: 1.25rem;
326 | border-bottom: 1px solid #30363d;
327 | display: flex;
328 | align-items: center;
329 | justify-content: space-between;
330 | background: #161b22;
331 | }
332 |
333 | .code-title {
334 | font-size: 1rem;
335 | font-weight: 600;
336 | margin: 0;
337 | color: #e6edf3;
338 | }
339 |
340 | .copy-code-btn {
341 | background: #21262d;
342 | border: 1px solid #30363d;
343 | color: #e6edf3;
344 | padding: 0.5rem 0.75rem;
345 | border-radius: 4px;
346 | font-size: 0.75rem;
347 | font-weight: 500;
348 | cursor: pointer;
349 | }
350 |
351 | .copy-code-btn:hover {
352 | background: #30363d;
353 | border-color: #58a6ff;
354 | }
355 |
356 | .code-wrapper {
357 | background: #0d1117;
358 | overflow-x: auto;
359 | }
360 |
361 | .code-block {
362 | margin: 0;
363 | padding: 1.25rem;
364 | background: transparent;
365 | }
366 |
367 | .code-content {
368 | color: #e6edf3;
369 | font-family: "JetBrains Mono", monospace;
370 | font-size: 0.8125rem;
371 | line-height: 1.6;
372 | white-space: pre-wrap;
373 | }
374 |
375 | /* Responsive design for mobile devices */
376 | @media (max-width: 1024px) {
377 | .content-layout {
378 | grid-template-columns: 1fr;
379 | grid-template-rows: auto auto auto;
380 | gap: 1.5rem;
381 | }
382 |
383 | .demo-section {
384 | grid-column: 1;
385 | grid-row: 2;
386 | }
387 |
388 | .header-content {
389 | padding: 0 1rem;
390 | }
391 |
392 | .main-content {
393 | padding: 1.5rem 1rem;
394 | }
395 | }
396 |
397 | @media (max-width: 768px) {
398 | .header-content {
399 | flex-direction: column;
400 | gap: 1rem;
401 | text-align: center;
402 | }
403 |
404 | .header-actions {
405 | flex-wrap: wrap;
406 | justify-content: center;
407 | }
408 |
409 | .form-row {
410 | grid-template-columns: 1fr;
411 | }
412 |
413 | .brand {
414 | flex-direction: column;
415 | gap: 0.5rem;
416 | }
417 |
418 | .tagline {
419 | font-size: 0.75rem;
420 | }
421 | }
--------------------------------------------------------------------------------