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 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 = Number(Math.min(Math.max(ratio, min), max).toFixed(4));
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
 
@@ -1,35 +1,35 @@
1
1
  :root {
2
- --hl-background: oklch(0.09 0.02 264);
3
- --hl-foreground: oklch(0.97 0.01 255);
4
- --hl-primary: oklch(0.65 0.18 277);
5
- --hl-popover: oklch(0.12 0.02 264 / 0.78);
6
- --hl-border: oklch(0.28 0.05 266 / 0.55);
7
- --hl-card: oklch(0.13 0.02 264 / 0.62);
8
- --hl-secondary: oklch(0.22 0.03 265 / 0.55);
9
- --hl-muted-foreground: oklch(0.68 0.03 255);
10
- --hl-ring: oklch(0.65 0.18 277 / 0.55);
11
- --hl-accent: oklch(0.22 0.05 266 / 0.65);
12
- --hl-radius: 0.5rem;
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: oklch(0.68 0.03 255 / 0.65);
15
- --hl-drop-hint-border: color-mix(in oklch, oklch(0.65 0.18 277) 80%, transparent);
16
- --hl-drop-hint-bg: color-mix(in oklch, oklch(0.65 0.18 277) 12%, transparent);
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: oklch(0.96 0.01 264);
21
- --hl-foreground: oklch(0.13 0.02 264);
22
- --hl-primary: oklch(0.55 0.18 277);
23
- --hl-popover: oklch(0.98 0.01 264 / 0.84);
24
- --hl-border: oklch(0.82 0.02 264 / 0.65);
25
- --hl-card: oklch(1 0 0 / 0.72);
26
- --hl-secondary: oklch(0.97 0.01 264 / 0.65);
27
- --hl-muted-foreground: oklch(0.42 0.04 255);
28
- --hl-ring: oklch(0.55 0.18 277 / 0.55);
29
- --hl-accent: oklch(0.88 0.025 264 / 0.72);
30
- --hl-tab-inactive-color: oklch(0.42 0.04 255 / 0.65);
31
- --hl-drop-hint-border: color-mix(in oklch, oklch(0.55 0.18 277) 80%, transparent);
32
- --hl-drop-hint-bg: color-mix(in oklch, oklch(0.55 0.18 277) 12%, transparent);
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.25rem;
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);
@@ -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';
@@ -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.get(config.maximizedView)) {
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.get(id))
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.get(id))
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
- try {
163
- return parseSplitConfig(object, path);
164
- }
165
- catch {
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.2",
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": "^3.0.10",
44
- "@sveltejs/kit": "^2.70.3",
45
- "@sveltejs/package": "^2.5.8",
46
- "@sveltejs/vite-plugin-svelte": "^7.3.0",
47
- "@types/node": "^26.6.2",
48
- "oxfmt": "^0.68.0",
49
- "oxlint": "^1.83.0",
50
- "publint": "^0.3.24",
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.0"
58
+ "vite": "^8.3.2",
59
+ "vitest": "^5.0.3"
55
60
  },
56
61
  "peerDependencies": {
57
62
  "svelte": "^5.0.0"