horizon-layout 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/dist/HorizonLayout.svelte +2 -2
- package/dist/horizon-layout.css +28 -28
- package/dist/internal-utils.d.ts +1 -0
- package/dist/internal-utils.js +5 -0
- package/dist/utils.js +10 -12
- package/package.json +15 -10
package/README.md
CHANGED
|
@@ -1,9 +1,30 @@
|
|
|
1
1
|
# HorizonLayout
|
|
2
2
|
|
|
3
|
+
<div align="center">
|
|
4
|
+
<a href="https://www.npmjs.com/package/horizon-layout">
|
|
5
|
+
<img src="https://img.shields.io/npm/v/horizon-layout.svg" alt="npm version" />
|
|
6
|
+
</a>
|
|
7
|
+
<a href="https://www.npmjs.com/package/horizon-layout">
|
|
8
|
+
<img src="https://img.shields.io/npm/l/horizon-layout.svg" alt="npm license" />
|
|
9
|
+
</a>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
3
12
|
### A headless, fully keyboard-accessible docking layout for Svelte 5. Users can drag tabs between panes, resize splits, and pop views out into separate browser windows, all described by a plain serialisable config object that you bind to and persist however you like.
|
|
4
13
|
|
|
5
14
|
---
|
|
6
15
|
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
Install the package from npm:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npm install horizon-layout
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Requires Svelte 5 as a peer dependency.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
7
28
|
## Quick start
|
|
8
29
|
|
|
9
30
|
```svelte
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
validateConfig
|
|
23
23
|
} from './utils.js';
|
|
24
24
|
import { DEFAULT_MIN_WIDTH_RATIO, DEFAULT_MIN_HEIGHT_RATIO } from './internal-utils.js';
|
|
25
|
-
import { dropTargetType } from './internal-utils.js';
|
|
25
|
+
import { dropTargetType, roundSplitPoint } from './internal-utils.js';
|
|
26
26
|
import type { DropTarget } from './internal-types.js';
|
|
27
27
|
|
|
28
28
|
interface Popout {
|
|
@@ -364,7 +364,7 @@
|
|
|
364
364
|
const minRatio = split.direction === 'horizontal' ? minWidthRatio : minHeightRatio;
|
|
365
365
|
const min = (split.splitPoints[index - 1] ?? 0) + minRatio;
|
|
366
366
|
const max = (split.splitPoints[index + 1] ?? 1) - minRatio;
|
|
367
|
-
const clamped =
|
|
367
|
+
const clamped = roundSplitPoint(Math.min(Math.max(ratio, min), max));
|
|
368
368
|
if (clamped !== split.splitPoints[index]) split.splitPoints[index] = clamped;
|
|
369
369
|
}
|
|
370
370
|
|
package/dist/horizon-layout.css
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
1
|
:root {
|
|
2
|
-
--hl-background:
|
|
3
|
-
--hl-foreground:
|
|
4
|
-
--hl-primary:
|
|
5
|
-
--hl-popover:
|
|
6
|
-
--hl-border:
|
|
7
|
-
--hl-card:
|
|
8
|
-
--hl-secondary:
|
|
9
|
-
--hl-muted-foreground:
|
|
10
|
-
--hl-ring:
|
|
11
|
-
--hl-accent:
|
|
12
|
-
--hl-radius: 0.
|
|
2
|
+
--hl-background: #111624;
|
|
3
|
+
--hl-foreground: #f1f5f9;
|
|
4
|
+
--hl-primary: #598dff;
|
|
5
|
+
--hl-popover: #171e32;
|
|
6
|
+
--hl-border: #212c47;
|
|
7
|
+
--hl-card: #141a2b;
|
|
8
|
+
--hl-secondary: #192138;
|
|
9
|
+
--hl-muted-foreground: #8192b0;
|
|
10
|
+
--hl-ring: #598dff;
|
|
11
|
+
--hl-accent: #1e2944;
|
|
12
|
+
--hl-radius: 0.375rem;
|
|
13
13
|
--hl-glass-blur: 14px;
|
|
14
|
-
--hl-tab-inactive-color:
|
|
15
|
-
--hl-drop-hint-border: color-mix(in oklch,
|
|
16
|
-
--hl-drop-hint-bg: color-mix(in oklch,
|
|
14
|
+
--hl-tab-inactive-color: color-mix(in oklch, #8192b0 88%, transparent);
|
|
15
|
+
--hl-drop-hint-border: color-mix(in oklch, #598dff 80%, transparent);
|
|
16
|
+
--hl-drop-hint-bg: color-mix(in oklch, #598dff 12%, transparent);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
.light {
|
|
20
|
-
--hl-background:
|
|
21
|
-
--hl-foreground:
|
|
22
|
-
--hl-primary:
|
|
23
|
-
--hl-popover:
|
|
24
|
-
--hl-border: oklch(0.
|
|
25
|
-
--hl-card:
|
|
26
|
-
--hl-secondary:
|
|
27
|
-
--hl-muted-foreground:
|
|
28
|
-
--hl-ring:
|
|
29
|
-
--hl-accent:
|
|
30
|
-
--hl-tab-inactive-color:
|
|
31
|
-
--hl-drop-hint-border: color-mix(in oklch,
|
|
32
|
-
--hl-drop-hint-bg: color-mix(in oklch,
|
|
20
|
+
--hl-background: #f8fafc;
|
|
21
|
+
--hl-foreground: #0f172a;
|
|
22
|
+
--hl-primary: #265de9;
|
|
23
|
+
--hl-popover: #ffffff;
|
|
24
|
+
--hl-border: oklch(0.9288 0.0126 255.5);
|
|
25
|
+
--hl-card: #ffffff;
|
|
26
|
+
--hl-secondary: #f1f5f9;
|
|
27
|
+
--hl-muted-foreground: #59687f;
|
|
28
|
+
--hl-ring: #265de9;
|
|
29
|
+
--hl-accent: #e2e8f0;
|
|
30
|
+
--hl-tab-inactive-color: color-mix(in oklch, #59687f 93%, transparent);
|
|
31
|
+
--hl-drop-hint-border: color-mix(in oklch, #265de9 80%, transparent);
|
|
32
|
+
--hl-drop-hint-bg: color-mix(in oklch, #265de9 12%, transparent);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
.horizon-layout {
|
|
@@ -152,7 +152,7 @@
|
|
|
152
152
|
.horizon-layout-split__ratio {
|
|
153
153
|
position: absolute;
|
|
154
154
|
padding: 2px 7px;
|
|
155
|
-
border-radius: 0.
|
|
155
|
+
border-radius: 0.125rem;
|
|
156
156
|
background: var(--hl-popover);
|
|
157
157
|
border: 1px solid var(--hl-border);
|
|
158
158
|
color: var(--hl-primary);
|
package/dist/internal-utils.d.ts
CHANGED
|
@@ -2,5 +2,6 @@ import type { DropTarget } from './internal-types.ts';
|
|
|
2
2
|
import type { Modifier } from './types.ts';
|
|
3
3
|
export declare const DEFAULT_MIN_WIDTH_RATIO = 0.1;
|
|
4
4
|
export declare const DEFAULT_MIN_HEIGHT_RATIO = 0.2;
|
|
5
|
+
export declare function roundSplitPoint(ratio: number): number;
|
|
5
6
|
export declare function getModifier(event: KeyboardEvent): Modifier | null;
|
|
6
7
|
export declare function dropTargetType(target: DropTarget): 'side' | 'tab';
|
package/dist/internal-utils.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
export const DEFAULT_MIN_WIDTH_RATIO = 0.1;
|
|
2
2
|
export const DEFAULT_MIN_HEIGHT_RATIO = 0.2;
|
|
3
|
+
/// rounds a split point to the precision it is stored with, so that values
|
|
4
|
+
/// computed with floating-point arithmetic can be compared reliably
|
|
5
|
+
export function roundSplitPoint(ratio) {
|
|
6
|
+
return Number(ratio.toFixed(4));
|
|
7
|
+
}
|
|
3
8
|
/// returns the modifier (ctrl, alt, shift, or a combination of them)
|
|
4
9
|
/// associated with the keyboard event, or null if no modifier keys were pressed
|
|
5
10
|
export function getModifier(event) {
|
package/dist/utils.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SvelteMap } from 'svelte/reactivity';
|
|
2
|
-
import { DEFAULT_MIN_WIDTH_RATIO, DEFAULT_MIN_HEIGHT_RATIO } from "./internal-utils.js";
|
|
2
|
+
import { DEFAULT_MIN_WIDTH_RATIO, DEFAULT_MIN_HEIGHT_RATIO, roundSplitPoint } from "./internal-utils.js";
|
|
3
3
|
/// Remove a tab group that has become empty, then collapse any resulting
|
|
4
4
|
/// single-child splits up the tree.
|
|
5
5
|
export function simplifyTabGroup(tabGroup, nodeParentMap, config) {
|
|
@@ -115,11 +115,11 @@ export function areConfigsEquivalent(config1, config2) {
|
|
|
115
115
|
/// split-point constraints are checked with the same thresholds.
|
|
116
116
|
export function validateConfig(config, views, options = {}) {
|
|
117
117
|
const { minWidthRatio = DEFAULT_MIN_WIDTH_RATIO, minHeightRatio = DEFAULT_MIN_HEIGHT_RATIO } = options;
|
|
118
|
-
if (config.maximizedView && !views.
|
|
118
|
+
if (config.maximizedView && !views.has(config.maximizedView)) {
|
|
119
119
|
throw new Error(`unknown maximizedView id "${config.maximizedView}"`);
|
|
120
120
|
}
|
|
121
121
|
for (const id of config.popouts ?? []) {
|
|
122
|
-
if (!views.
|
|
122
|
+
if (!views.has(id))
|
|
123
123
|
throw new Error(`unknown popout id "${id}"`);
|
|
124
124
|
}
|
|
125
125
|
if (!config.root)
|
|
@@ -130,7 +130,7 @@ export function validateConfig(config, views, options = {}) {
|
|
|
130
130
|
throw new Error('activeTabIndex out of range');
|
|
131
131
|
}
|
|
132
132
|
for (const id of tabGroup.tabs) {
|
|
133
|
-
if (!views.
|
|
133
|
+
if (!views.has(id))
|
|
134
134
|
throw new Error(`unknown tab id "${id}"`);
|
|
135
135
|
if (seenIds.has(id))
|
|
136
136
|
throw new Error(`duplicate tab id "${id}"`);
|
|
@@ -143,9 +143,9 @@ export function validateConfig(config, views, options = {}) {
|
|
|
143
143
|
}
|
|
144
144
|
const minRatio = split.direction === 'horizontal' ? minWidthRatio : minHeightRatio;
|
|
145
145
|
for (let i = 0; i < split.splitPoints.length; i++) {
|
|
146
|
-
const point = split.splitPoints[i];
|
|
146
|
+
const point = roundSplitPoint(split.splitPoints[i]);
|
|
147
147
|
const prev = split.splitPoints[i - 1] ?? 0;
|
|
148
|
-
if (point < prev + minRatio || point > 1 - minRatio) {
|
|
148
|
+
if (point < roundSplitPoint(prev + minRatio) || point > roundSplitPoint(1 - minRatio)) {
|
|
149
149
|
throw new Error('splitPoints violate minRatio constraints');
|
|
150
150
|
}
|
|
151
151
|
}
|
|
@@ -159,12 +159,10 @@ export function validateConfig(config, views, options = {}) {
|
|
|
159
159
|
validateNode(config.root);
|
|
160
160
|
}
|
|
161
161
|
function parseNodeConfig(object, path) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
return parseTabGroupConfig(object, path);
|
|
167
|
-
}
|
|
162
|
+
if (typeof object !== 'object' || object === null)
|
|
163
|
+
throw new Error(`${path}: expected an object`);
|
|
164
|
+
const isSplit = 'direction' in object || 'views' in object || 'splitPoints' in object;
|
|
165
|
+
return isSplit ? parseSplitConfig(object, path) : parseTabGroupConfig(object, path);
|
|
168
166
|
}
|
|
169
167
|
function parseSplitConfig(object, path) {
|
|
170
168
|
if (typeof object !== 'object' || object === null)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "horizon-layout",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"keywords": [
|
|
5
5
|
"svelte"
|
|
6
6
|
],
|
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
],
|
|
22
22
|
"types": "./dist/index.d.ts",
|
|
23
23
|
"svelte": "./dist/index.js",
|
|
24
|
+
"imports": {
|
|
25
|
+
"#lib/*": "./src/lib/*"
|
|
26
|
+
},
|
|
24
27
|
"exports": {
|
|
25
28
|
".": {
|
|
26
29
|
"types": "./dist/index.d.ts",
|
|
@@ -37,21 +40,23 @@
|
|
|
37
40
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
38
41
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
39
42
|
"lint": "oxlint . && oxfmt --check .",
|
|
43
|
+
"test": "vitest run",
|
|
40
44
|
"format": "oxfmt ."
|
|
41
45
|
},
|
|
42
46
|
"devDependencies": {
|
|
43
|
-
"@sveltejs/adapter-static": "^
|
|
44
|
-
"@sveltejs/kit": "^
|
|
45
|
-
"@sveltejs/package": "^
|
|
46
|
-
"@sveltejs/vite-plugin-svelte": "^7.3.
|
|
47
|
-
"@types/node": "^26.6.
|
|
48
|
-
"oxfmt": "^0.
|
|
49
|
-
"oxlint": "^1.
|
|
50
|
-
"publint": "^0.3.
|
|
47
|
+
"@sveltejs/adapter-static": "^4.0.0",
|
|
48
|
+
"@sveltejs/kit": "^3.0.0",
|
|
49
|
+
"@sveltejs/package": "^3.0.0",
|
|
50
|
+
"@sveltejs/vite-plugin-svelte": "^7.3.1",
|
|
51
|
+
"@types/node": "^26.6.4",
|
|
52
|
+
"oxfmt": "^0.71.0",
|
|
53
|
+
"oxlint": "^1.86.0",
|
|
54
|
+
"publint": "^0.3.25",
|
|
51
55
|
"svelte": "^5.57.1",
|
|
52
56
|
"svelte-check": "^4.7.6",
|
|
53
57
|
"typescript": "^6.0.3",
|
|
54
|
-
"vite": "^8.3.
|
|
58
|
+
"vite": "^8.3.2",
|
|
59
|
+
"vitest": "^5.0.3"
|
|
55
60
|
},
|
|
56
61
|
"peerDependencies": {
|
|
57
62
|
"svelte": "^5.0.0"
|