quilt-vanilla 0.1.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.
- package/LICENSE +21 -0
- package/README.md +58 -0
- package/dist/action-icons.d.ts +18 -0
- package/dist/action-icons.d.ts.map +1 -0
- package/dist/action-icons.js +36 -0
- package/dist/action-icons.js.map +1 -0
- package/dist/chrome-icons.d.ts +3 -0
- package/dist/chrome-icons.d.ts.map +1 -0
- package/dist/chrome-icons.js +32 -0
- package/dist/chrome-icons.js.map +1 -0
- package/dist/corners.d.ts +8 -0
- package/dist/corners.d.ts.map +1 -0
- package/dist/corners.js +205 -0
- package/dist/corners.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/lifetime.d.ts +10 -0
- package/dist/lifetime.d.ts.map +1 -0
- package/dist/lifetime.js +44 -0
- package/dist/lifetime.js.map +1 -0
- package/dist/menu.d.ts +10 -0
- package/dist/menu.d.ts.map +1 -0
- package/dist/menu.js +356 -0
- package/dist/menu.js.map +1 -0
- package/dist/panes.d.ts +13 -0
- package/dist/panes.d.ts.map +1 -0
- package/dist/panes.js +58 -0
- package/dist/panes.js.map +1 -0
- package/dist/picker.d.ts +6 -0
- package/dist/picker.d.ts.map +1 -0
- package/dist/picker.js +112 -0
- package/dist/picker.js.map +1 -0
- package/dist/registry.d.ts +25 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +39 -0
- package/dist/registry.js.map +1 -0
- package/dist/renderer.d.ts +3 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +570 -0
- package/dist/renderer.js.map +1 -0
- package/dist/resize.d.ts +7 -0
- package/dist/resize.d.ts.map +1 -0
- package/dist/resize.js +56 -0
- package/dist/resize.js.map +1 -0
- package/dist/shortcuts.d.ts +6 -0
- package/dist/shortcuts.d.ts.map +1 -0
- package/dist/shortcuts.js +64 -0
- package/dist/shortcuts.js.map +1 -0
- package/dist/styles.css +842 -0
- package/dist/tab-bar.d.ts +6 -0
- package/dist/tab-bar.d.ts.map +1 -0
- package/dist/tab-bar.js +336 -0
- package/dist/tab-bar.js.map +1 -0
- package/dist/tab-tooltip.d.ts +4 -0
- package/dist/tab-tooltip.d.ts.map +1 -0
- package/dist/tab-tooltip.js +95 -0
- package/dist/tab-tooltip.js.map +1 -0
- package/dist/tabs.d.ts +15 -0
- package/dist/tabs.d.ts.map +1 -0
- package/dist/tabs.js +126 -0
- package/dist/tabs.js.map +1 -0
- package/dist/theme.d.ts +206 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +166 -0
- package/dist/theme.js.map +1 -0
- package/dist/types.d.ts +66 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/windows.d.ts +23 -0
- package/dist/windows.d.ts.map +1 -0
- package/dist/windows.js +247 -0
- package/dist/windows.js.map +1 -0
- package/package.json +50 -0
- package/src/action-icons.ts +40 -0
- package/src/chrome-icons.ts +30 -0
- package/src/corners.ts +259 -0
- package/src/index.ts +27 -0
- package/src/lifetime.ts +46 -0
- package/src/menu.ts +432 -0
- package/src/panes.ts +76 -0
- package/src/picker.ts +114 -0
- package/src/registry.ts +46 -0
- package/src/renderer.ts +654 -0
- package/src/resize.ts +65 -0
- package/src/shortcuts.ts +90 -0
- package/src/styles.css +842 -0
- package/src/tab-bar.ts +382 -0
- package/src/tab-tooltip.ts +96 -0
- package/src/tabs.ts +129 -0
- package/src/theme.ts +211 -0
- package/src/types.ts +67 -0
- package/src/windows.ts +244 -0
package/src/theme.ts
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/** All values are CSS values. Theme state is separate from serialized layout structure. */
|
|
2
|
+
export interface LayoutTheme {
|
|
3
|
+
bg?: string;
|
|
4
|
+
panel?: string;
|
|
5
|
+
header?: string;
|
|
6
|
+
text?: string;
|
|
7
|
+
muted?: string;
|
|
8
|
+
line?: string;
|
|
9
|
+
accent?: string;
|
|
10
|
+
focus?: string;
|
|
11
|
+
radius?: string;
|
|
12
|
+
fontFamily?: string;
|
|
13
|
+
fontSize?: string;
|
|
14
|
+
/** Tab and menu icon size (default 16px). */
|
|
15
|
+
iconSize?: string;
|
|
16
|
+
/** Shared floating-tab inset and application content padding in CSS pixels (default 8px). */
|
|
17
|
+
panelPadding?: string;
|
|
18
|
+
headerHeight?: string;
|
|
19
|
+
/** Width of the vertical tab rail (default 32px). */
|
|
20
|
+
headerWidth?: string;
|
|
21
|
+
/** Resize gap in CSS pixels (default 4px). Explicit split gaps take precedence. */
|
|
22
|
+
resizeHandleWidth?: string;
|
|
23
|
+
/** Gap for disabled dividers in CSS pixels (default 0px). */
|
|
24
|
+
disabledResizeHandleWidth?: string;
|
|
25
|
+
/** Border color at disabled, gapless boundaries; defaults to line. Use transparent to hide. */
|
|
26
|
+
frozenPaneBorder?: string;
|
|
27
|
+
/** 8px target size. */
|
|
28
|
+
cornerHandleSize?: string;
|
|
29
|
+
/** 0px offset from pane edges. */
|
|
30
|
+
cornerHandleInset?: string;
|
|
31
|
+
/** muted color. */
|
|
32
|
+
cornerHandleColor?: string;
|
|
33
|
+
/** 0.35 idle opacity. */
|
|
34
|
+
cornerHandleOpacity?: string;
|
|
35
|
+
/** block; none disables corner targets. */
|
|
36
|
+
cornerHandleDisplay?: string;
|
|
37
|
+
/** 2px 0 0 2px bracket stroke. */
|
|
38
|
+
cornerHandleBorderWidth?: string;
|
|
39
|
+
/** 0px. */
|
|
40
|
+
cornerHandleRadius?: string;
|
|
41
|
+
/** transparent. */
|
|
42
|
+
cornerHandleFill?: string;
|
|
43
|
+
scrollbarThumb?: string;
|
|
44
|
+
scrollbarTrack?: string;
|
|
45
|
+
scrollbarSize?: string;
|
|
46
|
+
controlHeight?: string;
|
|
47
|
+
spacing?: string;
|
|
48
|
+
}
|
|
49
|
+
export const themeProperties = {
|
|
50
|
+
bg: '--layouts-bg',
|
|
51
|
+
panel: '--layouts-panel',
|
|
52
|
+
header: '--layouts-header',
|
|
53
|
+
text: '--layouts-text',
|
|
54
|
+
muted: '--layouts-muted',
|
|
55
|
+
line: '--layouts-line',
|
|
56
|
+
accent: '--layouts-accent',
|
|
57
|
+
focus: '--layouts-focus',
|
|
58
|
+
radius: '--layouts-radius',
|
|
59
|
+
fontFamily: '--layouts-font-family',
|
|
60
|
+
fontSize: '--layouts-font-size',
|
|
61
|
+
iconSize: '--layouts-icon-size',
|
|
62
|
+
panelPadding: '--layouts-panel-padding',
|
|
63
|
+
headerHeight: '--layouts-header-height',
|
|
64
|
+
headerWidth: '--layouts-header-width',
|
|
65
|
+
resizeHandleWidth: '--layouts-resize-handle-width',
|
|
66
|
+
disabledResizeHandleWidth: '--layouts-disabled-resize-handle-width',
|
|
67
|
+
frozenPaneBorder: '--layouts-frozen-pane-border',
|
|
68
|
+
cornerHandleSize: '--layouts-corner-handle-size',
|
|
69
|
+
cornerHandleInset: '--layouts-corner-handle-inset',
|
|
70
|
+
cornerHandleColor: '--layouts-corner-handle-color',
|
|
71
|
+
cornerHandleOpacity: '--layouts-corner-handle-opacity',
|
|
72
|
+
cornerHandleDisplay: '--layouts-corner-handle-display',
|
|
73
|
+
cornerHandleBorderWidth: '--layouts-corner-handle-border-width',
|
|
74
|
+
cornerHandleRadius: '--layouts-corner-handle-radius',
|
|
75
|
+
cornerHandleFill: '--layouts-corner-handle-fill',
|
|
76
|
+
scrollbarThumb: '--layouts-scrollbar-thumb',
|
|
77
|
+
scrollbarTrack: '--layouts-scrollbar-track',
|
|
78
|
+
scrollbarSize: '--layouts-scrollbar-size',
|
|
79
|
+
controlHeight: '--layouts-control-height',
|
|
80
|
+
spacing: '--layouts-spacing',
|
|
81
|
+
} as const;
|
|
82
|
+
/** Optional application presets: no framework, persistence, or global theme state. */
|
|
83
|
+
export const themeFamilies = {
|
|
84
|
+
neutral: {
|
|
85
|
+
dark: {
|
|
86
|
+
bg: '#0a0a0a',
|
|
87
|
+
panel: '#171717',
|
|
88
|
+
header: '#262626',
|
|
89
|
+
text: '#fafafa',
|
|
90
|
+
muted: '#a3a3a3',
|
|
91
|
+
line: '#525252',
|
|
92
|
+
accent: '#e5e5e5',
|
|
93
|
+
focus: '#e5e5e5',
|
|
94
|
+
},
|
|
95
|
+
light: {
|
|
96
|
+
bg: '#fafafa',
|
|
97
|
+
panel: '#ffffff',
|
|
98
|
+
header: '#f5f5f5',
|
|
99
|
+
text: '#171717',
|
|
100
|
+
muted: '#525252',
|
|
101
|
+
line: '#d4d4d4',
|
|
102
|
+
accent: '#262626',
|
|
103
|
+
focus: '#262626',
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
zinc: {
|
|
107
|
+
dark: {
|
|
108
|
+
bg: '#09090b',
|
|
109
|
+
panel: '#18181b',
|
|
110
|
+
header: '#27272a',
|
|
111
|
+
text: '#fafafa',
|
|
112
|
+
muted: '#a1a1aa',
|
|
113
|
+
line: '#52525b',
|
|
114
|
+
accent: '#d4d4d8',
|
|
115
|
+
focus: '#d4d4d8',
|
|
116
|
+
},
|
|
117
|
+
light: {
|
|
118
|
+
bg: '#fafafa',
|
|
119
|
+
panel: '#ffffff',
|
|
120
|
+
header: '#f4f4f5',
|
|
121
|
+
text: '#18181b',
|
|
122
|
+
muted: '#52525b',
|
|
123
|
+
line: '#d4d4d8',
|
|
124
|
+
accent: '#3f3f46',
|
|
125
|
+
focus: '#3f3f46',
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
stone: {
|
|
129
|
+
dark: {
|
|
130
|
+
bg: '#0c0a09',
|
|
131
|
+
panel: '#1c1917',
|
|
132
|
+
header: '#292524',
|
|
133
|
+
text: '#fafaf9',
|
|
134
|
+
muted: '#a8a29e',
|
|
135
|
+
line: '#57534e',
|
|
136
|
+
accent: '#d6d3d1',
|
|
137
|
+
focus: '#d6d3d1',
|
|
138
|
+
},
|
|
139
|
+
light: {
|
|
140
|
+
bg: '#fafaf9',
|
|
141
|
+
panel: '#ffffff',
|
|
142
|
+
header: '#f5f5f4',
|
|
143
|
+
text: '#1c1917',
|
|
144
|
+
muted: '#57534e',
|
|
145
|
+
line: '#d6d3d1',
|
|
146
|
+
accent: '#57534e',
|
|
147
|
+
focus: '#57534e',
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
mist: {
|
|
151
|
+
dark: {
|
|
152
|
+
bg: '#0b1114',
|
|
153
|
+
panel: '#141e23',
|
|
154
|
+
header: '#1e2c32',
|
|
155
|
+
text: '#edf4f7',
|
|
156
|
+
muted: '#9eb5bf',
|
|
157
|
+
line: '#425d69',
|
|
158
|
+
accent: '#8bc5d8',
|
|
159
|
+
focus: '#8bc5d8',
|
|
160
|
+
},
|
|
161
|
+
light: {
|
|
162
|
+
bg: '#f4f8fa',
|
|
163
|
+
panel: '#ffffff',
|
|
164
|
+
header: '#eaf1f4',
|
|
165
|
+
text: '#192c35',
|
|
166
|
+
muted: '#465f6b',
|
|
167
|
+
line: '#b8ccd5',
|
|
168
|
+
accent: '#28647b',
|
|
169
|
+
focus: '#28647b',
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
} as const satisfies Record<string, { dark: LayoutTheme; light: LayoutTheme }>;
|
|
173
|
+
export const themes = {
|
|
174
|
+
dark: {
|
|
175
|
+
bg: '#161a20',
|
|
176
|
+
panel: '#1d222a',
|
|
177
|
+
header: '#242a34',
|
|
178
|
+
text: '#e5e9f0',
|
|
179
|
+
muted: '#a2acba',
|
|
180
|
+
line: '#39414e',
|
|
181
|
+
accent: '#8bbaaa',
|
|
182
|
+
focus: '#b3decf',
|
|
183
|
+
},
|
|
184
|
+
light: {
|
|
185
|
+
bg: '#dce1e8',
|
|
186
|
+
panel: '#ffffff',
|
|
187
|
+
header: '#f0f3f7',
|
|
188
|
+
text: '#202936',
|
|
189
|
+
muted: '#586579',
|
|
190
|
+
line: '#c9d1dc',
|
|
191
|
+
accent: '#357a69',
|
|
192
|
+
focus: '#195fc1',
|
|
193
|
+
},
|
|
194
|
+
sage: {
|
|
195
|
+
bg: '#cbd3c7',
|
|
196
|
+
panel: '#f1f3ed',
|
|
197
|
+
header: '#e9ede4',
|
|
198
|
+
text: '#2d4133',
|
|
199
|
+
muted: '#72816e',
|
|
200
|
+
line: '#d0d8c9',
|
|
201
|
+
accent: '#6a957a',
|
|
202
|
+
focus: '#316c4b',
|
|
203
|
+
},
|
|
204
|
+
} as const satisfies Record<string, LayoutTheme>;
|
|
205
|
+
export function applyTheme(root: HTMLElement, theme: LayoutTheme) {
|
|
206
|
+
for (const key of Object.keys(themeProperties) as (keyof LayoutTheme)[]) {
|
|
207
|
+
const value = theme[key];
|
|
208
|
+
if (value === undefined) root.style.removeProperty(themeProperties[key]);
|
|
209
|
+
else root.style.setProperty(themeProperties[key], value);
|
|
210
|
+
}
|
|
211
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { TabRegistry } from './registry.js';
|
|
2
|
+
import type { LayoutTheme } from './theme.js';
|
|
3
|
+
import type { LayoutStore, Pane, WindowPlacement } from 'quilt-core';
|
|
4
|
+
export interface PaneContext {
|
|
5
|
+
element: HTMLElement;
|
|
6
|
+
document: Document;
|
|
7
|
+
window: Window;
|
|
8
|
+
pane: Pane;
|
|
9
|
+
/** Stable application-owned reference. The library does not serialize this value. */
|
|
10
|
+
state: unknown;
|
|
11
|
+
location: 'main' | 'popout';
|
|
12
|
+
}
|
|
13
|
+
export interface PaneView {
|
|
14
|
+
dispose(): void;
|
|
15
|
+
resize?(width: number, height: number): void;
|
|
16
|
+
update?(pane: Pane): void;
|
|
17
|
+
}
|
|
18
|
+
export type PaneRenderer = (context: PaneContext) => PaneView;
|
|
19
|
+
export interface TabBarStyle {
|
|
20
|
+
/** Left placement uses an icon-only rail; mode and shape apply in either placement. */
|
|
21
|
+
placement?: 'top' | 'left';
|
|
22
|
+
/** Automatic shows labels as width permits; compact always uses icons only. */
|
|
23
|
+
display?: 'automatic' | 'compact';
|
|
24
|
+
mode?: 'full' | 'tapered';
|
|
25
|
+
shape?: 'angle' | 'round' | 'scoop' | 'vertical' | 'rounded';
|
|
26
|
+
/** Anchored by default; floating bars overlay content with an 8px inset. */
|
|
27
|
+
attachment?: 'anchored' | 'floating';
|
|
28
|
+
fit?: 'full' | 'fit';
|
|
29
|
+
corners?: 'fitted' | 'rounded' | 'capsule';
|
|
30
|
+
/** Positive finite CSS pixels. Omit to match header height. */
|
|
31
|
+
taperWidth?: number;
|
|
32
|
+
}
|
|
33
|
+
export interface TabBarOptions extends TabBarStyle {
|
|
34
|
+
/** Partial overrides keyed by stable group ID; not serialized in Layout JSON. */
|
|
35
|
+
regions?: Record<string, TabBarStyle>;
|
|
36
|
+
}
|
|
37
|
+
export interface LayoutOptions {
|
|
38
|
+
tabBar?: TabBarOptions;
|
|
39
|
+
store: LayoutStore;
|
|
40
|
+
tabs?: TabRegistry;
|
|
41
|
+
theme?: LayoutTheme;
|
|
42
|
+
renderers: Record<string, PaneRenderer>;
|
|
43
|
+
/** Return a fresh decorative icon element. Unknown keys use a title initial. */
|
|
44
|
+
renderIcon?: (key: string, document: Document) => Element | undefined;
|
|
45
|
+
/** Opt-in conveniences; omitted and false disable all. */
|
|
46
|
+
shortcuts?:
|
|
47
|
+
| boolean
|
|
48
|
+
| {
|
|
49
|
+
maximize?: boolean;
|
|
50
|
+
middleClickClose?: boolean;
|
|
51
|
+
addTab?: boolean;
|
|
52
|
+
restoreClosedTab?: boolean;
|
|
53
|
+
};
|
|
54
|
+
getPaneState?: (paneId: string) => unknown;
|
|
55
|
+
onError?: (error: unknown) => void;
|
|
56
|
+
/** Copy additional app styles/assets into a same-origin companion document. */
|
|
57
|
+
prepareWindow?: (window: Window, pane: Pane) => void;
|
|
58
|
+
/** Override window creation for applications with a managed same-origin host. Must return synchronously. */
|
|
59
|
+
openWindow?: (pane: Pane, placement: WindowPlacement) => Window | null;
|
|
60
|
+
}
|
|
61
|
+
export interface MountedLayout {
|
|
62
|
+
setTabBar(options: TabBarOptions): void;
|
|
63
|
+
setTheme(theme: LayoutTheme): void;
|
|
64
|
+
popout(paneId: string, placement?: WindowPlacement): boolean;
|
|
65
|
+
returnPane(paneId: string): void;
|
|
66
|
+
dispose(): void;
|
|
67
|
+
}
|
package/src/windows.ts
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { themeProperties } from './theme.js';
|
|
2
|
+
import type { Pane, WindowPlacement } from 'quilt-core';
|
|
3
|
+
import type { LayoutOptions } from './types.js';
|
|
4
|
+
import { el, Scope } from './lifetime.js';
|
|
5
|
+
import { mountPane } from './panes.js';
|
|
6
|
+
import type { MountedPane } from './panes.js';
|
|
7
|
+
interface Companion {
|
|
8
|
+
window: Window;
|
|
9
|
+
mounted: MountedPane;
|
|
10
|
+
scope: Scope;
|
|
11
|
+
suppressReturn: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** The opener owns all state; child documents never load an independent app bundle. */
|
|
14
|
+
export class Windows {
|
|
15
|
+
private companions = new Map<string, Companion>();
|
|
16
|
+
private scope = new Scope();
|
|
17
|
+
private stopped = false;
|
|
18
|
+
readonly pending = new Map<string, WindowPlacement>();
|
|
19
|
+
constructor(
|
|
20
|
+
private doc: Document,
|
|
21
|
+
private options: LayoutOptions,
|
|
22
|
+
private error: (e: unknown) => void,
|
|
23
|
+
private themeRoot: HTMLElement,
|
|
24
|
+
) {
|
|
25
|
+
const interval = setInterval(() => this.check(), 300);
|
|
26
|
+
this.scope.add(() => clearInterval(interval));
|
|
27
|
+
this.scope.listen(doc.defaultView!, 'pagehide', () => this.dispose());
|
|
28
|
+
for (const p of options.store.getSnapshot().popouts)
|
|
29
|
+
this.pending.set(p.paneId, p.placement ?? {});
|
|
30
|
+
// Loading data never opens a browser window. Restore missing windows into the main layout.
|
|
31
|
+
this.restoreMissing();
|
|
32
|
+
this.scope.add(options.store.subscribe(() => this.reconcile()));
|
|
33
|
+
}
|
|
34
|
+
private restoreMissing() {
|
|
35
|
+
for (const entry of [...this.options.store.getSnapshot().popouts]) {
|
|
36
|
+
if (!this.companions.has(entry.paneId)) {
|
|
37
|
+
this.pending.set(entry.paneId, entry.placement ?? {});
|
|
38
|
+
this.options.store.returnPane(entry.paneId);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
private reconcile() {
|
|
43
|
+
if (this.stopped) return;
|
|
44
|
+
for (const [id, c] of this.companions)
|
|
45
|
+
if (!this.options.store.getSnapshot().popouts.some((p) => p.paneId === id))
|
|
46
|
+
this.closeCompanion(id, c);
|
|
47
|
+
for (const [id, c] of this.companions) {
|
|
48
|
+
const pane = this.options.store.getSnapshot().panes[id];
|
|
49
|
+
if (!pane || JSON.stringify(pane) === c.mounted.signature) continue;
|
|
50
|
+
try {
|
|
51
|
+
if (
|
|
52
|
+
pane.type !== c.mounted.pane.type ||
|
|
53
|
+
JSON.stringify(pane.params) !== JSON.stringify(c.mounted.pane.params)
|
|
54
|
+
) {
|
|
55
|
+
const replacement = mountPane(c.window.document, pane, 'popout', this.options);
|
|
56
|
+
c.mounted.element.replaceWith(replacement.element);
|
|
57
|
+
c.mounted.dispose();
|
|
58
|
+
c.mounted = replacement;
|
|
59
|
+
} else {
|
|
60
|
+
c.mounted.view.update?.(pane);
|
|
61
|
+
c.mounted.pane = pane;
|
|
62
|
+
c.mounted.signature = JSON.stringify(pane);
|
|
63
|
+
}
|
|
64
|
+
c.window.document.title = pane.title + ' — layouts';
|
|
65
|
+
const title = c.window.document.querySelector('.layouts-companion-bar strong');
|
|
66
|
+
if (title) title.textContent = pane.title;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
this.error(error);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
this.restoreMissing();
|
|
72
|
+
for (const id of this.pending.keys())
|
|
73
|
+
if (!this.options.store.getSnapshot().panes[id]) this.pending.delete(id);
|
|
74
|
+
}
|
|
75
|
+
private styles(target: Window, pane: Pane, scope: Scope) {
|
|
76
|
+
const source = this.doc,
|
|
77
|
+
dest = target.document;
|
|
78
|
+
dest.title = pane.title + ' — layouts';
|
|
79
|
+
const base = dest.createElement('base');
|
|
80
|
+
base.href = source.baseURI;
|
|
81
|
+
dest.head.append(base);
|
|
82
|
+
const holder = dest.createElement('meta');
|
|
83
|
+
holder.dataset.layoutsStyles = '';
|
|
84
|
+
dest.head.append(holder);
|
|
85
|
+
let copied: Element[] = [];
|
|
86
|
+
const copy = () => {
|
|
87
|
+
copied.forEach((n) => n.remove());
|
|
88
|
+
copied = Array.from(source.head.querySelectorAll('link[rel="stylesheet"], style')).map(
|
|
89
|
+
(n) => n.cloneNode(true) as Element,
|
|
90
|
+
);
|
|
91
|
+
copied.forEach((n) => dest.head.append(n));
|
|
92
|
+
dest.documentElement.className = source.documentElement.className;
|
|
93
|
+
dest.body.className = source.body.className;
|
|
94
|
+
for (const original of [source.documentElement, source.body]) {
|
|
95
|
+
const next = original === source.body ? dest.body : dest.documentElement;
|
|
96
|
+
for (const attribute of Array.from(next.attributes))
|
|
97
|
+
if (attribute.name === 'style' || attribute.name.startsWith('data-'))
|
|
98
|
+
next.removeAttribute(attribute.name);
|
|
99
|
+
for (const attribute of Array.from(original.attributes))
|
|
100
|
+
if (attribute.name === 'style' || attribute.name.startsWith('data-'))
|
|
101
|
+
next.setAttribute(attribute.name, attribute.value);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
copy();
|
|
105
|
+
const observer = new MutationObserver(copy);
|
|
106
|
+
observer.observe(source.head, { childList: true, subtree: true, characterData: true });
|
|
107
|
+
observer.observe(source.body, { attributes: true });
|
|
108
|
+
observer.observe(source.documentElement, { attributes: true });
|
|
109
|
+
scope.add(() => observer.disconnect());
|
|
110
|
+
}
|
|
111
|
+
open(id: string, placement: WindowPlacement = {}): boolean {
|
|
112
|
+
if (this.stopped) return false;
|
|
113
|
+
const existing = this.companions.get(id);
|
|
114
|
+
if (existing) {
|
|
115
|
+
existing.window.focus();
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
const pane = this.options.store.getSnapshot().panes[id];
|
|
119
|
+
if (!pane || !this.options.store.can(id, 'popout')) return false;
|
|
120
|
+
let child: Window | null = null,
|
|
121
|
+
mounted: MountedPane | undefined;
|
|
122
|
+
const scope = new Scope();
|
|
123
|
+
try {
|
|
124
|
+
const requested = { width: 640, height: 480, ...placement };
|
|
125
|
+
const features = [
|
|
126
|
+
'popup=yes',
|
|
127
|
+
...Object.entries(requested).map(([key, value]) => `${key}=${Math.round(value)}`),
|
|
128
|
+
].join(',');
|
|
129
|
+
child = this.options.openWindow
|
|
130
|
+
? this.options.openWindow(pane, requested)
|
|
131
|
+
: this.doc.defaultView!.open('about:blank', '', features);
|
|
132
|
+
if (!child)
|
|
133
|
+
throw new Error(
|
|
134
|
+
'The browser blocked the popout. Allow popups or try again from the pane menu.',
|
|
135
|
+
);
|
|
136
|
+
const dest = child.document;
|
|
137
|
+
dest.body.replaceChildren();
|
|
138
|
+
this.styles(child, pane, scope);
|
|
139
|
+
const shell = el(dest, 'div', 'layouts layouts-companion');
|
|
140
|
+
const bar = el(dest, 'header', 'layouts-companion-bar');
|
|
141
|
+
const button = el(dest, 'button', 'layouts-button', 'Return to layout');
|
|
142
|
+
button.type = 'button';
|
|
143
|
+
bar.append(el(dest, 'strong', '', pane.title), button);
|
|
144
|
+
shell.append(bar);
|
|
145
|
+
dest.body.append(shell);
|
|
146
|
+
const syncTheme = () => {
|
|
147
|
+
const computed = this.doc.defaultView!.getComputedStyle(this.themeRoot);
|
|
148
|
+
for (const property of Object.values(themeProperties)) {
|
|
149
|
+
const value = computed.getPropertyValue(property);
|
|
150
|
+
if (value.trim()) shell.style.setProperty(property, value);
|
|
151
|
+
else shell.style.removeProperty(property);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
syncTheme();
|
|
155
|
+
const themeObserver = new MutationObserver(syncTheme);
|
|
156
|
+
for (let node: HTMLElement | null = this.themeRoot; node; node = node.parentElement)
|
|
157
|
+
themeObserver.observe(node, { attributes: true });
|
|
158
|
+
themeObserver.observe(this.doc.head, { childList: true, subtree: true, characterData: true });
|
|
159
|
+
scope.add(() => themeObserver.disconnect());
|
|
160
|
+
this.options.prepareWindow?.(child, pane);
|
|
161
|
+
mounted = mountPane(dest, pane, 'popout', this.options);
|
|
162
|
+
shell.append(mounted.element);
|
|
163
|
+
const c: Companion = { window: child, mounted, scope, suppressReturn: false };
|
|
164
|
+
this.companions.set(id, c);
|
|
165
|
+
scope.listen(button, 'click', () => this.returnPane(id));
|
|
166
|
+
// Leave detection supplements closed-window polling; neither is a persistence guarantee.
|
|
167
|
+
scope.listen(child, 'pagehide', () => {
|
|
168
|
+
if (!c.suppressReturn && !this.stopped) queueMicrotask(() => this.returnPane(id));
|
|
169
|
+
});
|
|
170
|
+
this.options.store.popout(id, requested, { source: 'user' });
|
|
171
|
+
this.pending.delete(id);
|
|
172
|
+
child.focus();
|
|
173
|
+
return true;
|
|
174
|
+
} catch (error) {
|
|
175
|
+
this.companions.delete(id);
|
|
176
|
+
scope.dispose();
|
|
177
|
+
try {
|
|
178
|
+
mounted?.dispose();
|
|
179
|
+
} catch {
|
|
180
|
+
/* Finish rollback. */
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
child?.close();
|
|
184
|
+
} catch {
|
|
185
|
+
/* Browser may already have closed it. */
|
|
186
|
+
}
|
|
187
|
+
this.error(error);
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
returnPane(id: string) {
|
|
192
|
+
if (this.stopped) return;
|
|
193
|
+
try {
|
|
194
|
+
this.options.store.returnPane(id);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
this.error(error);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
private closeCompanion(id: string, c: Companion) {
|
|
200
|
+
c.suppressReturn = true;
|
|
201
|
+
this.companions.delete(id);
|
|
202
|
+
c.scope.dispose();
|
|
203
|
+
try {
|
|
204
|
+
c.mounted.dispose();
|
|
205
|
+
} catch (error) {
|
|
206
|
+
this.error(error);
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
c.window.close();
|
|
210
|
+
} catch {
|
|
211
|
+
/* A user may have navigated the companion away. */
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
private check() {
|
|
215
|
+
if (this.stopped) return;
|
|
216
|
+
for (const [id, c] of this.companions) {
|
|
217
|
+
try {
|
|
218
|
+
if (c.window.closed || c.window.document.defaultView === null) this.returnPane(id);
|
|
219
|
+
} catch {
|
|
220
|
+
this.returnPane(id);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
dispose() {
|
|
225
|
+
if (this.stopped) return;
|
|
226
|
+
this.stopped = true;
|
|
227
|
+
this.scope.dispose();
|
|
228
|
+
for (const [id, c] of this.companions) {
|
|
229
|
+
try {
|
|
230
|
+
c.window.document.body.replaceChildren(
|
|
231
|
+
el(
|
|
232
|
+
c.window.document,
|
|
233
|
+
'p',
|
|
234
|
+
'layouts-placeholder',
|
|
235
|
+
'Disconnected: the main layout session has ended.',
|
|
236
|
+
),
|
|
237
|
+
);
|
|
238
|
+
} catch {
|
|
239
|
+
/* Inaccessible or already closed. */
|
|
240
|
+
}
|
|
241
|
+
this.closeCompanion(id, c);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|