horizon-layout 1.0.0

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.
@@ -0,0 +1,459 @@
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;
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);
17
+ }
18
+
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);
33
+ }
34
+
35
+ .horizon-layout {
36
+ width: 100%;
37
+ height: 100%;
38
+ overflow: hidden;
39
+ background: var(--hl-background);
40
+ font-family: system-ui, sans-serif;
41
+ font-size: 0.8125rem;
42
+ color: var(--hl-foreground);
43
+ }
44
+
45
+ .horizon-layout__content {
46
+ width: 100%;
47
+ height: 100%;
48
+ overflow: hidden;
49
+ }
50
+
51
+ .horizon-layout__content--maximized {
52
+ animation: hl-fade-in 80ms ease;
53
+ }
54
+
55
+ .horizon-layout-split {
56
+ position: relative;
57
+ width: 100%;
58
+ height: 100%;
59
+ display: flex;
60
+ }
61
+
62
+ .horizon-layout-split--vertical {
63
+ flex-direction: column;
64
+ }
65
+
66
+ .horizon-layout-split__pane {
67
+ min-width: 0;
68
+ min-height: 0;
69
+ overflow: hidden;
70
+ }
71
+
72
+ .horizon-layout-split__pane--horizontal {
73
+ height: 100%;
74
+ }
75
+
76
+ .horizon-layout-split__pane--vertical {
77
+ width: 100%;
78
+ }
79
+
80
+ .horizon-layout-split__resizer {
81
+ flex: 0 0 0px;
82
+ position: relative;
83
+ z-index: 1;
84
+ }
85
+
86
+ .horizon-layout-split__resizer--horizontal {
87
+ height: 100%;
88
+ }
89
+
90
+ .horizon-layout-split__resizer--vertical {
91
+ width: 100%;
92
+ }
93
+
94
+ .horizon-layout-split__resizer-handle {
95
+ position: absolute;
96
+ display: flex;
97
+ align-items: center;
98
+ justify-content: center;
99
+ z-index: 2;
100
+ touch-action: none;
101
+ outline: none;
102
+ }
103
+
104
+ .horizon-layout-split__resizer--horizontal .horizon-layout-split__resizer-handle {
105
+ cursor: col-resize;
106
+ top: 0;
107
+ bottom: 0;
108
+ left: -6px;
109
+ width: 12px;
110
+ }
111
+
112
+ .horizon-layout-split__resizer--vertical .horizon-layout-split__resizer-handle {
113
+ cursor: row-resize;
114
+ left: 0;
115
+ right: 0;
116
+ top: -6px;
117
+ height: 12px;
118
+ }
119
+
120
+ .horizon-layout-split__resizer-handle::before {
121
+ content: '';
122
+ position: absolute;
123
+ top: 50%;
124
+ left: 50%;
125
+ transform: translate(-50%, -50%);
126
+ border-radius: 100vmax;
127
+ background: var(--hl-primary);
128
+ opacity: 0;
129
+ transition: opacity 100ms ease;
130
+ }
131
+
132
+ .horizon-layout-split__resizer--horizontal .horizon-layout-split__resizer-handle::before {
133
+ width: 2px;
134
+ height: calc(100% - 16px);
135
+ }
136
+
137
+ .horizon-layout-split__resizer--vertical .horizon-layout-split__resizer-handle::before {
138
+ height: 2px;
139
+ width: calc(100% - 16px);
140
+ }
141
+
142
+ .horizon-layout-split__resizer-handle--hover::before,
143
+ .horizon-layout-split__resizer-handle--active::before {
144
+ opacity: 1;
145
+ }
146
+
147
+ .horizon-layout-split__resizer-handle:focus-visible::before {
148
+ opacity: 1;
149
+ background: var(--hl-ring);
150
+ }
151
+
152
+ .horizon-layout-split__ratio {
153
+ position: absolute;
154
+ padding: 2px 7px;
155
+ border-radius: 0.25rem;
156
+ background: var(--hl-popover);
157
+ border: 1px solid var(--hl-border);
158
+ color: var(--hl-primary);
159
+ font-size: 0.7rem;
160
+ font-variant-numeric: tabular-nums;
161
+ pointer-events: none;
162
+ opacity: 0;
163
+ transition: opacity 60ms linear;
164
+ white-space: nowrap;
165
+ z-index: 3;
166
+ backdrop-filter: blur(var(--hl-glass-blur));
167
+ }
168
+
169
+ .horizon-layout-split__ratio--horizontal {
170
+ top: 50%;
171
+ transform: translate(-50%, -50%);
172
+ }
173
+
174
+ .horizon-layout-split__ratio--vertical {
175
+ left: 50%;
176
+ transform: translate(-50%, -50%);
177
+ }
178
+
179
+ .horizon-layout-split__ratio--active {
180
+ opacity: 1;
181
+ }
182
+
183
+ .horizon-layout-tabgroup {
184
+ position: relative;
185
+ display: flex;
186
+ flex-direction: column;
187
+ width: 100%;
188
+ height: 100%;
189
+ min-width: 0;
190
+ min-height: 0;
191
+ overflow: hidden;
192
+ box-sizing: border-box;
193
+ border: 1px solid var(--hl-border);
194
+ background: var(--hl-card);
195
+ }
196
+
197
+ .horizon-layout-tabgroup__tab-bar {
198
+ position: relative;
199
+ z-index: 1;
200
+ display: flex;
201
+ align-items: stretch;
202
+ justify-content: space-between;
203
+ padding-right: 4px;
204
+ background: var(--hl-background);
205
+ border-bottom: 1px solid var(--hl-border);
206
+ min-height: 32px;
207
+ flex-shrink: 0;
208
+ }
209
+
210
+ .horizon-layout-tabgroup__tabs {
211
+ display: flex;
212
+ align-items: stretch;
213
+ flex: 1 1 auto;
214
+ min-width: 0;
215
+ overflow-x: auto;
216
+ overflow-y: hidden;
217
+ scrollbar-width: none;
218
+ outline: none;
219
+ }
220
+
221
+ .horizon-layout-tabgroup__tab {
222
+ position: relative;
223
+ display: inline-flex;
224
+ flex: 0 0 auto;
225
+ align-items: center;
226
+ gap: 0.35rem;
227
+ min-height: 32px;
228
+ max-width: 13rem;
229
+ padding: 0 0.65rem;
230
+ margin-top: 4px;
231
+ margin-bottom: 4px;
232
+ background: transparent;
233
+ border-radius: var(--hl-radius);
234
+ color: var(--hl-tab-inactive-color);
235
+ user-select: none;
236
+ touch-action: none;
237
+ outline: none;
238
+ transition:
239
+ color 60ms ease,
240
+ background 60ms ease;
241
+ }
242
+
243
+ .horizon-layout-tabgroup__tab:hover {
244
+ color: var(--hl-muted-foreground);
245
+ background: color-mix(in oklch, var(--hl-muted-foreground) 8%, transparent);
246
+ }
247
+
248
+ .horizon-layout-tabgroup__tab--active {
249
+ background: var(--hl-secondary);
250
+ color: var(--hl-foreground);
251
+ font-weight: 500;
252
+ }
253
+
254
+ .light .horizon-layout-tabgroup__tab--active {
255
+ background: color-mix(in oklch, var(--hl-border) 40%, transparent);
256
+ }
257
+
258
+ .light .horizon-layout-tabgroup__tab:hover {
259
+ background: color-mix(in oklch, var(--hl-border) 18%, transparent);
260
+ }
261
+
262
+ .horizon-layout-tabgroup__tab-active-fill {
263
+ position: absolute;
264
+ bottom: -1px;
265
+ left: 0;
266
+ right: 0;
267
+ height: 1px;
268
+ background: var(--hl-secondary);
269
+ pointer-events: none;
270
+ }
271
+
272
+ .horizon-layout-tabgroup__tabs:focus-visible .horizon-layout-tabgroup__tab--active {
273
+ outline: 2px solid var(--hl-ring);
274
+ outline-offset: -2px;
275
+ border-radius: 4px;
276
+ background: var(--hl-accent);
277
+ color: var(--hl-foreground);
278
+ }
279
+
280
+ .horizon-layout-tabgroup__tabs:focus-visible
281
+ .horizon-layout-tabgroup__tab--active
282
+ .horizon-layout-tabgroup__tab-active-fill {
283
+ background: var(--hl-accent);
284
+ }
285
+
286
+ .horizon-layout-tabgroup__tab-title {
287
+ flex: 1 1 auto;
288
+ min-width: 0;
289
+ white-space: nowrap;
290
+ overflow: hidden;
291
+ text-overflow: ellipsis;
292
+ font-size: 0.74rem;
293
+ letter-spacing: 0.01em;
294
+ }
295
+
296
+ .horizon-layout-tabgroup__tab-controls {
297
+ display: inline-flex;
298
+ align-items: center;
299
+ gap: 2px;
300
+ flex-shrink: 0;
301
+ }
302
+
303
+ .horizon-layout-tabgroup__tab-control {
304
+ display: inline-flex;
305
+ align-items: center;
306
+ justify-content: center;
307
+ }
308
+
309
+ .horizon-layout-tabgroup__tab-drop-hint {
310
+ flex: 0 0 2px;
311
+ align-self: stretch;
312
+ border-radius: 2px;
313
+ background: transparent;
314
+ transition: background 40ms ease;
315
+ pointer-events: none;
316
+ position: relative;
317
+ }
318
+
319
+ .horizon-layout-tabgroup__tab-drop-hint--hover {
320
+ background: var(--hl-primary);
321
+ }
322
+
323
+ .horizon-layout-tabgroup__tab-drop-zone {
324
+ flex: 0 0 0px;
325
+ align-self: stretch;
326
+ position: relative;
327
+ pointer-events: none;
328
+ z-index: 3;
329
+ }
330
+
331
+ .horizon-layout-tabgroup__tab-drop-zone::before {
332
+ content: '';
333
+ position: absolute;
334
+ top: 0;
335
+ bottom: 0;
336
+ left: -28px;
337
+ right: -28px;
338
+ pointer-events: all;
339
+ }
340
+
341
+ .horizon-layout-tabgroup__tab-drop-hint--end {
342
+ flex: 0 0 2px;
343
+ }
344
+
345
+ .horizon-layout-tabgroup__tab-drop-zone--end {
346
+ flex: 1 1 auto;
347
+ min-width: 48px;
348
+ }
349
+
350
+ .horizon-layout-tabgroup__tab-drop-zone--end::before {
351
+ inset: 0;
352
+ }
353
+
354
+ .horizon-layout-tabgroup__controls {
355
+ display: flex;
356
+ align-items: center;
357
+ gap: 2px;
358
+ flex-shrink: 0;
359
+ padding-left: 4px;
360
+ }
361
+
362
+ .horizon-layout-tabgroup__control {
363
+ display: inline-flex;
364
+ align-items: center;
365
+ justify-content: center;
366
+ }
367
+
368
+ .horizon-layout-tabgroup__body {
369
+ position: relative;
370
+ flex: 1;
371
+ min-height: 0;
372
+ overflow: hidden;
373
+ }
374
+
375
+ .horizon-layout-tabgroup__content {
376
+ width: 100%;
377
+ height: 100%;
378
+ overflow: auto;
379
+ background: var(--hl-card);
380
+ }
381
+
382
+ .horizon-layout-tabgroup__drop-zones {
383
+ position: absolute;
384
+ inset: 0;
385
+ pointer-events: none;
386
+ }
387
+
388
+ .horizon-layout-tabgroup__drop-zone {
389
+ position: absolute;
390
+ inset: 0;
391
+ pointer-events: all;
392
+ }
393
+
394
+ .horizon-layout-tabgroup__drop-zone--top {
395
+ clip-path: polygon(0% 0%, 100% 0%, 50% 50%);
396
+ }
397
+
398
+ .horizon-layout-tabgroup__drop-zone--right {
399
+ clip-path: polygon(100% 0%, 100% 100%, 50% 50%);
400
+ }
401
+
402
+ .horizon-layout-tabgroup__drop-zone--bottom {
403
+ clip-path: polygon(100% 100%, 0% 100%, 50% 50%);
404
+ }
405
+
406
+ .horizon-layout-tabgroup__drop-zone--left {
407
+ clip-path: polygon(0% 100%, 0% 0%, 50% 50%);
408
+ }
409
+
410
+ .horizon-layout-tabgroup__drop-hint {
411
+ position: absolute;
412
+ z-index: 2;
413
+ pointer-events: none;
414
+ border: 2px solid var(--hl-drop-hint-border);
415
+ border-radius: var(--hl-radius);
416
+ background: var(--hl-drop-hint-bg);
417
+ animation: hl-fade-in 40ms ease;
418
+ }
419
+
420
+ .horizon-layout-tabgroup__drop-hint--top {
421
+ inset: 2px 2px calc(50% + 2px) 2px;
422
+ }
423
+
424
+ .horizon-layout-tabgroup__drop-hint--right {
425
+ inset: 2px 2px 2px 50%;
426
+ }
427
+
428
+ .horizon-layout-tabgroup__drop-hint--bottom {
429
+ inset: 50% 2px 2px 2px;
430
+ }
431
+
432
+ .horizon-layout-tabgroup__drop-hint--left {
433
+ inset: 2px 50% 2px 2px;
434
+ }
435
+
436
+ .horizon-layout__popout {
437
+ width: 100vw;
438
+ height: 100vh;
439
+ display: flex;
440
+ flex-direction: column;
441
+ background: var(--hl-background);
442
+ color: var(--hl-foreground);
443
+ font-size: 0.8125rem;
444
+ }
445
+
446
+ .horizon-layout__popout .horizon-layout__content {
447
+ flex: 1;
448
+ min-height: 0;
449
+ overflow: hidden;
450
+ }
451
+
452
+ @keyframes hl-fade-in {
453
+ from {
454
+ opacity: 0;
455
+ }
456
+ to {
457
+ opacity: 1;
458
+ }
459
+ }
@@ -0,0 +1,3 @@
1
+ export { default as HorizonLayout } from './HorizonLayout.svelte';
2
+ export type * from './types.ts';
3
+ export * from './utils.ts';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { default as HorizonLayout } from './HorizonLayout.svelte';
2
+ export * from "./utils.js";
@@ -0,0 +1,8 @@
1
+ export type DropSide = 'top' | 'right' | 'bottom' | 'left';
2
+ export type DropTarget = {
3
+ side: DropSide;
4
+ tabIndex?: never;
5
+ } | {
6
+ tabIndex: number;
7
+ side?: never;
8
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import type { DropTarget } from './internal-types.ts';
2
+ import type { Modifier } from './types.ts';
3
+ export declare const DEFAULT_MIN_WIDTH_RATIO = 0.1;
4
+ export declare const DEFAULT_MIN_HEIGHT_RATIO = 0.2;
5
+ export declare function getModifier(event: KeyboardEvent): Modifier | null;
6
+ export declare function dropTargetType(target: DropTarget): 'side' | 'tab';
@@ -0,0 +1,25 @@
1
+ export const DEFAULT_MIN_WIDTH_RATIO = 0.1;
2
+ export const DEFAULT_MIN_HEIGHT_RATIO = 0.2;
3
+ /// returns the modifier (ctrl, alt, shift, or a combination of them)
4
+ /// associated with the keyboard event, or null if no modifier keys were pressed
5
+ export function getModifier(event) {
6
+ if (event.ctrlKey && event.altKey && event.shiftKey)
7
+ return 'ctrl+alt+shift';
8
+ if (event.altKey && event.shiftKey)
9
+ return 'alt+shift';
10
+ if (event.ctrlKey && event.shiftKey)
11
+ return 'ctrl+shift';
12
+ if (event.ctrlKey && event.altKey)
13
+ return 'ctrl+alt';
14
+ if (event.shiftKey)
15
+ return 'shift';
16
+ if (event.ctrlKey)
17
+ return 'ctrl';
18
+ if (event.altKey)
19
+ return 'alt';
20
+ return null;
21
+ }
22
+ /// returns 'side' if the target is a side drop target, or 'tab' if it's a tab drop target
23
+ export function dropTargetType(target) {
24
+ return target.side ? 'side' : 'tab';
25
+ }
@@ -0,0 +1,43 @@
1
+ import type { Snippet } from 'svelte';
2
+ export type Id<T extends string = string> = T extends '' ? never : T;
3
+ export type SplitDirection = 'horizontal' | 'vertical';
4
+ export interface View {
5
+ title: string;
6
+ snippet: Snippet;
7
+ tabControls?: Snippet<[Id]>[];
8
+ }
9
+ export interface SplitConfig {
10
+ direction: SplitDirection;
11
+ views: [NodeConfig, NodeConfig, ...NodeConfig[]];
12
+ splitPoints: [number, ...number[]];
13
+ }
14
+ export interface TabGroupConfig {
15
+ tabs: [Id, ...Id[]];
16
+ activeTabIndex: number;
17
+ }
18
+ export type NodeConfig = SplitConfig | TabGroupConfig;
19
+ export interface LayoutConfig {
20
+ root?: NodeConfig;
21
+ maximizedView?: Id;
22
+ popouts?: Id[];
23
+ }
24
+ export type Modifier = 'ctrl' | 'alt' | 'shift' | 'ctrl+alt' | 'ctrl+shift' | 'alt+shift' | 'ctrl+alt+shift';
25
+ export interface KeyboardShortcut {
26
+ modifier?: Modifier;
27
+ key: string;
28
+ }
29
+ export interface KeyboardControl<T> {
30
+ shortcuts: KeyboardShortcut[];
31
+ action: (data: T, event: KeyboardEvent) => void;
32
+ }
33
+ export interface KeyboardControls {
34
+ splitControls?: KeyboardControl<{
35
+ config: SplitConfig;
36
+ index: number;
37
+ }>[];
38
+ tabGroupControls?: KeyboardControl<TabGroupConfig>[];
39
+ }
40
+ export type ParentEntry = {
41
+ parent: SplitConfig;
42
+ index: number;
43
+ };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ import { SvelteMap } from 'svelte/reactivity';
2
+ import type { Id, LayoutConfig, NodeConfig, ParentEntry, TabGroupConfig } from './types.ts';
3
+ export declare function simplifyTabGroup(tabGroup: TabGroupConfig, nodeParentMap: SvelteMap<NodeConfig, ParentEntry>, config: LayoutConfig): void;
4
+ export declare function nodeConfigType(config: NodeConfig): 'split' | 'tabGroup';
5
+ export declare function buildNodeParentMap(root: NodeConfig): SvelteMap<NodeConfig, ParentEntry>;
6
+ export declare function areConfigsEquivalent(config1: LayoutConfig, config2: LayoutConfig): boolean;
7
+ export interface ValidateConfigOptions {
8
+ /** Minimum allowed pane width as a fraction of its split container. Default: DEFAULT_MIN_WIDTH_RATIO */
9
+ minWidthRatio?: number;
10
+ /** Minimum allowed pane height as a fraction of its split container. Default: DEFAULT_MIN_HEIGHT_RATIO */
11
+ minHeightRatio?: number;
12
+ }
13
+ export declare function validateConfig<V>(config: LayoutConfig, views: ReadonlyMap<Id, V>, options?: ValidateConfigOptions): void;
14
+ export declare function parseLayoutConfig(object: unknown): LayoutConfig;
15
+ export declare function cloneConfig(config: LayoutConfig): LayoutConfig;