@liberation-data/desk 0.4.0 → 0.6.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/README.md +31 -1
- package/dist/core/desk.d.ts +13 -0
- package/dist/core/desk.d.ts.map +1 -1
- package/dist/core/desk.js +55 -1
- package/dist/core/desk.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/css/apps.css +57 -0
- package/dist/css/controls.css +975 -0
- package/dist/css/conversation.css +232 -0
- package/dist/css/dock.css +272 -0
- package/dist/css/menus.css +204 -0
- package/dist/css/overlays.css +237 -0
- package/dist/css/search.css +149 -0
- package/dist/css/setup.css +284 -0
- package/dist/css/tokens.css +90 -0
- package/dist/css/windows.css +321 -0
- package/dist/desk.css +1080 -1070
- package/dist/react/Desktop.d.ts.map +1 -1
- package/dist/react/Desktop.js +66 -17
- package/dist/react/Desktop.js.map +1 -1
- package/llms.txt +6 -0
- package/package.json +4 -3
- package/src/core/desk.ts +67 -1
- package/src/core/index.ts +1 -1
- package/src/css/apps.css +56 -0
- package/src/css/controls.css +974 -0
- package/src/css/conversation.css +231 -0
- package/src/css/dock.css +271 -0
- package/src/css/menus.css +203 -0
- package/src/css/overlays.css +236 -0
- package/src/css/search.css +148 -0
- package/src/css/setup.css +283 -0
- package/src/css/tokens.css +89 -0
- package/src/css/windows.css +320 -0
- package/src/react/Desktop.tsx +69 -20
- package/src/desk.css +0 -2820
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* desk — the stage and its windows. Every value reads a --desk-* token, so an
|
|
3
|
+
* app restyles the desk by redefining tokens, never by overriding selectors.
|
|
4
|
+
*/
|
|
5
|
+
:root {
|
|
6
|
+
--desk-ground: #eef0f4;
|
|
7
|
+
--desk-surface: #ffffff;
|
|
8
|
+
--desk-titlebar: #f6f7f9;
|
|
9
|
+
--desk-ink: #1b1f27;
|
|
10
|
+
--desk-muted: #667085;
|
|
11
|
+
--desk-rule: #dde1e8;
|
|
12
|
+
--desk-accent: #5b57f5;
|
|
13
|
+
--desk-close: #ef6b63;
|
|
14
|
+
--desk-mode: #34c47a;
|
|
15
|
+
--desk-info: #34c47a;
|
|
16
|
+
--desk-radius: 12px;
|
|
17
|
+
--desk-gap: 12px;
|
|
18
|
+
--desk-padding: 12px;
|
|
19
|
+
--desk-shadow: 0 1px 2px rgb(16 24 40 / 6%), 0 12px 32px rgb(16 24 40 / 10%);
|
|
20
|
+
--desk-shadow-floating: 0 2px 4px rgb(16 24 40 / 8%), 0 24px 60px rgb(16 24 40 / 22%);
|
|
21
|
+
--desk-font: system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
22
|
+
--desk-font-mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
23
|
+
|
|
24
|
+
/* The spacing scale (HIG.md §2). Nothing between these values. */
|
|
25
|
+
--desk-space-1: 4px;
|
|
26
|
+
--desk-space-2: 8px;
|
|
27
|
+
--desk-space-3: 12px;
|
|
28
|
+
--desk-space-4: 16px;
|
|
29
|
+
--desk-space-5: 24px;
|
|
30
|
+
--desk-space-6: 32px;
|
|
31
|
+
--desk-space-7: 48px;
|
|
32
|
+
|
|
33
|
+
/* State, kept apart from the accent. */
|
|
34
|
+
--desk-ok: #2f9e68;
|
|
35
|
+
--desk-warn: #b7791f;
|
|
36
|
+
--desk-bad: #c6483f;
|
|
37
|
+
|
|
38
|
+
--desk-control-height: 28px;
|
|
39
|
+
--desk-control-height-small: 22px;
|
|
40
|
+
--desk-control-radius: 7px;
|
|
41
|
+
--desk-field-bg: #ffffff;
|
|
42
|
+
--desk-on-accent: #ffffff;
|
|
43
|
+
--desk-ease: cubic-bezier(.32, .72, 0, 1);
|
|
44
|
+
--desk-dock-size: 48px;
|
|
45
|
+
--desk-dock-glass: rgb(255 255 255 / 72%);
|
|
46
|
+
--desk-dock-icon: linear-gradient(160deg, #ffffff, #eceff5);
|
|
47
|
+
--desk-badge: #ef4444;
|
|
48
|
+
--desk-badge-ink: #ffffff;
|
|
49
|
+
/* Room the stage leaves at the bottom for an overlay dock. */
|
|
50
|
+
--desk-inset-bottom: 0px;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@media (prefers-color-scheme: dark) {
|
|
54
|
+
:root:not([data-theme="light"]) {
|
|
55
|
+
--desk-ground: #07090f;
|
|
56
|
+
--desk-surface: #101828;
|
|
57
|
+
--desk-titlebar: #0b111d;
|
|
58
|
+
--desk-ink: #e6e6e6;
|
|
59
|
+
--desk-muted: #99a1af;
|
|
60
|
+
--desk-rule: #1e2939;
|
|
61
|
+
--desk-shadow: 0 12px 32px rgb(0 0 0 / 45%);
|
|
62
|
+
--desk-shadow-floating: 0 30px 80px rgb(0 0 0 / 70%);
|
|
63
|
+
--desk-dock-glass: rgb(11 18 32 / 62%);
|
|
64
|
+
--desk-dock-icon: linear-gradient(160deg, #1a2338, #0e1526);
|
|
65
|
+
--desk-ok: #3ecf8e;
|
|
66
|
+
--desk-info: #3ecf8e;
|
|
67
|
+
--desk-warn: #fbbf24;
|
|
68
|
+
--desk-bad: #f87171;
|
|
69
|
+
--desk-field-bg: rgb(0 0 0 / 35%);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
:root[data-theme="dark"] {
|
|
74
|
+
--desk-ground: #07090f;
|
|
75
|
+
--desk-surface: #101828;
|
|
76
|
+
--desk-titlebar: #0b111d;
|
|
77
|
+
--desk-ink: #e6e6e6;
|
|
78
|
+
--desk-muted: #99a1af;
|
|
79
|
+
--desk-rule: #1e2939;
|
|
80
|
+
--desk-shadow: 0 12px 32px rgb(0 0 0 / 45%);
|
|
81
|
+
--desk-shadow-floating: 0 30px 80px rgb(0 0 0 / 70%);
|
|
82
|
+
--desk-dock-glass: rgb(11 18 32 / 62%);
|
|
83
|
+
--desk-dock-icon: linear-gradient(160deg, #1a2338, #0e1526);
|
|
84
|
+
--desk-ok: #3ecf8e;
|
|
85
|
+
--desk-info: #3ecf8e;
|
|
86
|
+
--desk-warn: #fbbf24;
|
|
87
|
+
--desk-bad: #f87171;
|
|
88
|
+
--desk-field-bg: rgb(0 0 0 / 35%);
|
|
89
|
+
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
.desk-stage {
|
|
2
|
+
position: relative;
|
|
3
|
+
box-sizing: border-box;
|
|
4
|
+
height: 100%;
|
|
5
|
+
min-height: 0;
|
|
6
|
+
padding: var(--desk-padding);
|
|
7
|
+
padding-bottom: calc(var(--desk-padding) + var(--desk-inset-bottom));
|
|
8
|
+
display: grid;
|
|
9
|
+
grid-template: minmax(0, 1fr) / minmax(0, 1fr);
|
|
10
|
+
gap: var(--desk-gap);
|
|
11
|
+
overflow: hidden;
|
|
12
|
+
background: var(--desk-ground);
|
|
13
|
+
color: var(--desk-ink);
|
|
14
|
+
font-family: var(--desk-font);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.desk-window {
|
|
18
|
+
/* Filled windows fill the stage inside its padding, layered in the order they were used. */
|
|
19
|
+
position: absolute;
|
|
20
|
+
top: var(--desk-padding);
|
|
21
|
+
right: var(--desk-padding);
|
|
22
|
+
bottom: calc(var(--desk-padding) + var(--desk-inset-bottom));
|
|
23
|
+
left: var(--desk-padding);
|
|
24
|
+
box-sizing: border-box;
|
|
25
|
+
min-width: 0;
|
|
26
|
+
min-height: 0;
|
|
27
|
+
display: flex;
|
|
28
|
+
flex-direction: column;
|
|
29
|
+
background: var(--desk-surface);
|
|
30
|
+
border: 1px solid var(--desk-rule);
|
|
31
|
+
border-radius: var(--desk-radius);
|
|
32
|
+
box-shadow: var(--desk-shadow);
|
|
33
|
+
overflow: hidden;
|
|
34
|
+
transition: border-color .15s var(--desk-ease), box-shadow .15s var(--desk-ease);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.desk-window:focus {
|
|
38
|
+
/* The key window is already marked by its border; a ring on the whole window would be noise. */
|
|
39
|
+
outline: none;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.desk-window[data-focused] {
|
|
43
|
+
border-color: color-mix(in srgb, var(--desk-accent) 45%, var(--desk-rule));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.desk-window[data-hidden] {
|
|
47
|
+
display: none;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* One window at a time: it fills the stage, and the zoom control is not offered. */
|
|
51
|
+
.desk-stage[data-layout="fullscreen"] .desk-window {
|
|
52
|
+
position: static;
|
|
53
|
+
inset: auto;
|
|
54
|
+
grid-area: 1 / 1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.desk-window[data-mode="floating"] {
|
|
58
|
+
right: auto;
|
|
59
|
+
bottom: auto;
|
|
60
|
+
box-shadow: var(--desk-shadow-floating);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.desk-window[data-dragging] {
|
|
64
|
+
transition: none;
|
|
65
|
+
user-select: none;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.desk-titlebar {
|
|
69
|
+
flex: none;
|
|
70
|
+
display: flex;
|
|
71
|
+
align-items: center;
|
|
72
|
+
gap: 10px;
|
|
73
|
+
padding: 8px 12px;
|
|
74
|
+
background: var(--desk-titlebar);
|
|
75
|
+
border-bottom: 1px solid var(--desk-rule);
|
|
76
|
+
user-select: none;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.desk-titlebar[data-draggable] {
|
|
80
|
+
cursor: grab;
|
|
81
|
+
touch-action: none;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.desk-window[data-dragging] .desk-titlebar {
|
|
85
|
+
cursor: grabbing;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.desk-controls {
|
|
89
|
+
display: flex;
|
|
90
|
+
gap: 7px;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.desk-control {
|
|
94
|
+
width: 12px;
|
|
95
|
+
height: 12px;
|
|
96
|
+
padding: 0;
|
|
97
|
+
border: 0;
|
|
98
|
+
border-radius: 50%;
|
|
99
|
+
cursor: pointer;
|
|
100
|
+
background: var(--desk-rule);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.desk-window[data-focused] .desk-control[data-control="close"],
|
|
104
|
+
.desk-control[data-control="close"]:hover {
|
|
105
|
+
background: var(--desk-close);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.desk-window[data-focused] .desk-control[data-control="mode"],
|
|
109
|
+
.desk-control[data-control="mode"]:hover {
|
|
110
|
+
background: var(--desk-mode);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.desk-control:focus-visible {
|
|
114
|
+
outline: 2px solid var(--desk-accent);
|
|
115
|
+
outline-offset: 2px;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.desk-title {
|
|
119
|
+
margin: 0;
|
|
120
|
+
min-width: 0;
|
|
121
|
+
overflow: hidden;
|
|
122
|
+
text-overflow: ellipsis;
|
|
123
|
+
white-space: nowrap;
|
|
124
|
+
font-size: 13px;
|
|
125
|
+
font-weight: 600;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.desk-window:not([data-focused]) .desk-title {
|
|
129
|
+
color: var(--desk-muted);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.desk-window-note {
|
|
133
|
+
margin: 0 0 0 auto;
|
|
134
|
+
min-width: 0;
|
|
135
|
+
/* The title is what the window IS; the note is what it is looking at. In a narrow window the note
|
|
136
|
+
gives up its room first, and is cut before the title is. */
|
|
137
|
+
flex: 0 999 auto;
|
|
138
|
+
overflow: hidden;
|
|
139
|
+
text-overflow: ellipsis;
|
|
140
|
+
white-space: nowrap;
|
|
141
|
+
font-size: 11.5px;
|
|
142
|
+
color: var(--desk-muted);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* The note gives up the spare room; the (i) keeps its place at the end of the bar. */
|
|
146
|
+
.desk-window-note + .desk-window-info {
|
|
147
|
+
margin-left: var(--desk-space-2);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.desk-window-info {
|
|
151
|
+
margin-left: auto;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.desk-window-note ~ .desk-window-info {
|
|
155
|
+
margin-left: var(--desk-space-2);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.desk-window-info + .desk-window-actions,
|
|
159
|
+
.desk-window-note + .desk-window-actions {
|
|
160
|
+
margin-left: var(--desk-space-1);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.desk-window-actions {
|
|
164
|
+
margin-left: auto;
|
|
165
|
+
display: flex;
|
|
166
|
+
align-items: center;
|
|
167
|
+
gap: var(--desk-space-1);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.desk-body {
|
|
171
|
+
flex: 1;
|
|
172
|
+
min-height: 0;
|
|
173
|
+
overflow: auto;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.desk-window-status {
|
|
177
|
+
box-sizing: border-box;
|
|
178
|
+
height: 100%;
|
|
179
|
+
min-height: 120px;
|
|
180
|
+
display: flex;
|
|
181
|
+
flex-direction: column;
|
|
182
|
+
align-items: center;
|
|
183
|
+
justify-content: center;
|
|
184
|
+
gap: var(--desk-space-2);
|
|
185
|
+
padding: var(--desk-space-4);
|
|
186
|
+
text-align: center;
|
|
187
|
+
color: var(--desk-muted);
|
|
188
|
+
font-size: 13px;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.desk-window-status b {
|
|
192
|
+
color: var(--desk-ink);
|
|
193
|
+
font-weight: 600;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.desk-window-status-detail {
|
|
197
|
+
max-width: 40ch;
|
|
198
|
+
font-size: 12px;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/* Edges a window resizes from. Inside the window, so nothing clips them. */
|
|
202
|
+
.desk-edge {
|
|
203
|
+
position: absolute;
|
|
204
|
+
touch-action: none;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.desk-edge[data-edge="left"],
|
|
208
|
+
.desk-edge[data-edge="right"] {
|
|
209
|
+
top: 0;
|
|
210
|
+
bottom: 0;
|
|
211
|
+
width: 5px;
|
|
212
|
+
cursor: ew-resize;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.desk-edge[data-edge="left"] {
|
|
216
|
+
left: 0;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.desk-edge[data-edge="right"] {
|
|
220
|
+
right: 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.desk-edge[data-edge="bottom"] {
|
|
224
|
+
left: 0;
|
|
225
|
+
right: 0;
|
|
226
|
+
bottom: 0;
|
|
227
|
+
height: 5px;
|
|
228
|
+
cursor: ns-resize;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.desk-grip {
|
|
232
|
+
position: absolute;
|
|
233
|
+
right: 0;
|
|
234
|
+
bottom: 0;
|
|
235
|
+
width: 16px;
|
|
236
|
+
height: 16px;
|
|
237
|
+
cursor: nwse-resize;
|
|
238
|
+
touch-action: none;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/* Where a dragged window would land if it were dropped now. */
|
|
242
|
+
.desk-stage[data-snap]::after {
|
|
243
|
+
content: "";
|
|
244
|
+
position: absolute;
|
|
245
|
+
top: var(--desk-padding);
|
|
246
|
+
bottom: calc(var(--desk-padding) + var(--desk-inset-bottom));
|
|
247
|
+
width: calc(50% - var(--desk-padding) - var(--desk-gap) / 2);
|
|
248
|
+
border: 2px solid var(--desk-accent);
|
|
249
|
+
border-radius: var(--desk-radius);
|
|
250
|
+
background: color-mix(in srgb, var(--desk-accent) 14%, transparent);
|
|
251
|
+
pointer-events: none;
|
|
252
|
+
z-index: 5;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.desk-stage[data-snap="start"]::after {
|
|
256
|
+
left: var(--desk-padding);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.desk-stage[data-snap="end"]::after {
|
|
260
|
+
right: var(--desk-padding);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/* ── The inside of a window: controls that stay, content that scrolls ── */
|
|
264
|
+
|
|
265
|
+
.desk-pane {
|
|
266
|
+
display: flex;
|
|
267
|
+
flex-direction: column;
|
|
268
|
+
height: 100%;
|
|
269
|
+
min-height: 0;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.desk-pane-header,
|
|
273
|
+
.desk-pane-footer {
|
|
274
|
+
flex: none;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/* The only part that scrolls, so the toolbar never leaves with the content. */
|
|
278
|
+
.desk-pane-body {
|
|
279
|
+
flex: 1;
|
|
280
|
+
min-height: 0;
|
|
281
|
+
overflow: auto;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.desk-pane-body:focus-visible {
|
|
285
|
+
outline: 2px solid var(--desk-accent);
|
|
286
|
+
outline-offset: -2px;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/* A window whose whole content is a pane does not scroll itself: the pane's middle does. */
|
|
290
|
+
.desk-body:has(> .desk-pane) {
|
|
291
|
+
overflow: hidden;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.desk-toolbar {
|
|
295
|
+
display: flex;
|
|
296
|
+
align-items: center;
|
|
297
|
+
flex-wrap: wrap;
|
|
298
|
+
gap: var(--desk-space-2);
|
|
299
|
+
/* One centre line for everything in the row, whatever each control's own height is. */
|
|
300
|
+
min-height: calc(var(--desk-control-height) + var(--desk-space-2) * 2);
|
|
301
|
+
padding: var(--desk-space-2) var(--desk-space-3);
|
|
302
|
+
border-bottom: 1px solid var(--desk-rule);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.desk-toolbar > *,
|
|
306
|
+
.desk-toolbar-trailing > * {
|
|
307
|
+
flex: none;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.desk-toolbar .desk-grow {
|
|
311
|
+
flex: 1 1 140px;
|
|
312
|
+
min-width: 0;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.desk-toolbar-trailing {
|
|
316
|
+
margin-left: auto;
|
|
317
|
+
display: flex;
|
|
318
|
+
align-items: center;
|
|
319
|
+
gap: var(--desk-space-2);
|
|
320
|
+
}
|
package/src/react/Desktop.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useStat
|
|
|
2
2
|
import type { CSSProperties, PointerEvent as ReactPointerEvent, ReactNode } from 'react'
|
|
3
3
|
import { addCommandHandler, addDeskCommands, DeskCommands, STAGE_ATTRIBUTE, WINDOW_ATTRIBUTE, windowElement } from '../core/commands.js'
|
|
4
4
|
import { focusedId } from '../core/desk.js'
|
|
5
|
+
import type { Desk } from '../core/desk.js'
|
|
5
6
|
import type { DeskWindow, Frame, WindowId } from '../core/types.js'
|
|
6
7
|
import { arrangement } from './arrange.js'
|
|
7
8
|
import { InfoTip } from './infoTip.js'
|
|
@@ -94,6 +95,51 @@ export function Desktop({ renderWindow, title, note, info, actions, empty, loadi
|
|
|
94
95
|
desk.setStage(() => ({ width: stage.current?.clientWidth ?? 1024, height: stage.current?.clientHeight ?? 768 }))
|
|
95
96
|
}, [desk])
|
|
96
97
|
|
|
98
|
+
/*
|
|
99
|
+
* The desk changing size is not a layout decision, but it can undo one: unplug an external
|
|
100
|
+
* display and the windows you spread across it are past the edge of the laptop screen, where
|
|
101
|
+
* nothing can drag them back. They are brought in; plug the display back in and they go out
|
|
102
|
+
* again exactly as they were.
|
|
103
|
+
*
|
|
104
|
+
* Filled windows need none of this — they are whatever size the desk is — so a desk of only
|
|
105
|
+
* filled windows never commits anything here.
|
|
106
|
+
*/
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
const element = stage.current
|
|
109
|
+
if (!element || mode !== 'desktop') return undefined
|
|
110
|
+
let frame = 0
|
|
111
|
+
// Once per paint: dragging the browser window's edge fires this continuously.
|
|
112
|
+
const refit = () => {
|
|
113
|
+
cancelAnimationFrame(frame)
|
|
114
|
+
frame = requestAnimationFrame(() => {
|
|
115
|
+
const fit = desk.fitToStage()
|
|
116
|
+
/*
|
|
117
|
+
* One window that no longer fits is brought in and made smaller, and that is enough. Two or
|
|
118
|
+
* more means the layout itself is gone: each was clamped on its own, so windows that sat
|
|
119
|
+
* side by side now sit on top of each other, and the desk is left looking like one window.
|
|
120
|
+
* At that point it is laid out the way Arrange would — which is a layout for THIS screen,
|
|
121
|
+
* while the frames from the big one are remembered and come back with it.
|
|
122
|
+
*/
|
|
123
|
+
if (fit.squeezed.length > 1) arrangeNow(desk, element)
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
// The stage can change size without the window doing so — a sidebar opening beside it — so the
|
|
127
|
+
// element is watched where that can be watched, and the window is the fallback where it cannot.
|
|
128
|
+
if (typeof ResizeObserver === 'undefined') {
|
|
129
|
+
addEventListener('resize', refit)
|
|
130
|
+
return () => {
|
|
131
|
+
cancelAnimationFrame(frame)
|
|
132
|
+
removeEventListener('resize', refit)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const observer = new ResizeObserver(refit)
|
|
136
|
+
observer.observe(element)
|
|
137
|
+
return () => {
|
|
138
|
+
cancelAnimationFrame(frame)
|
|
139
|
+
observer.disconnect()
|
|
140
|
+
}
|
|
141
|
+
}, [desk, mode])
|
|
142
|
+
|
|
97
143
|
useEffect(() => (stage.current ? addDeskCommands(stage.current, desk) : undefined), [desk])
|
|
98
144
|
|
|
99
145
|
// Arrange lays every window out once, as free windows that can be moved again straight away.
|
|
@@ -103,26 +149,7 @@ export function Desktop({ renderWindow, title, note, info, actions, empty, loadi
|
|
|
103
149
|
return addCommandHandler(
|
|
104
150
|
element,
|
|
105
151
|
DeskCommands.arrange,
|
|
106
|
-
() =>
|
|
107
|
-
const { windows, stack } = desk.getState()
|
|
108
|
-
const [focused, previous] = [stack.at(-1), stack.at(-2)]
|
|
109
|
-
if (windows.length === 1) {
|
|
110
|
-
if (focused) desk.fill(focused)
|
|
111
|
-
return
|
|
112
|
-
}
|
|
113
|
-
const frames = arrangement(
|
|
114
|
-
windows.map(w => w.id),
|
|
115
|
-
focused ?? null,
|
|
116
|
-
previous ?? null,
|
|
117
|
-
arrangementArea(element),
|
|
118
|
-
{ minWidth: MIN_WIDTH, minHeight: MIN_HEIGHT },
|
|
119
|
-
)
|
|
120
|
-
desk.placeAll(frames)
|
|
121
|
-
// Windows that cascade come in front of the half they cascade over; the focused window stays in front of all.
|
|
122
|
-
if (previous) desk.focus(previous)
|
|
123
|
-
windows.forEach(w => w.id !== focused && w.id !== previous && desk.focus(w.id))
|
|
124
|
-
if (focused) desk.focus(focused)
|
|
125
|
-
},
|
|
152
|
+
() => arrangeNow(desk, element),
|
|
126
153
|
{
|
|
127
154
|
enabled: () => {
|
|
128
155
|
const { windows } = desk.getState()
|
|
@@ -206,6 +233,28 @@ interface WindowViewProps {
|
|
|
206
233
|
}
|
|
207
234
|
|
|
208
235
|
/** Moving by the title bar, or resizing from the corner or from the left, right or bottom edge. */
|
|
236
|
+
/** Lays every open window out at once: what Window → Arrange does, and what a shrunken desk needs. */
|
|
237
|
+
function arrangeNow(desk: Desk, element: HTMLElement) {
|
|
238
|
+
const { windows, stack } = desk.getState()
|
|
239
|
+
const [focused, previous] = [stack.at(-1), stack.at(-2)]
|
|
240
|
+
if (windows.length === 1) {
|
|
241
|
+
if (focused) desk.fill(focused)
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
const frames = arrangement(
|
|
245
|
+
windows.map(w => w.id),
|
|
246
|
+
focused ?? null,
|
|
247
|
+
previous ?? null,
|
|
248
|
+
arrangementArea(element),
|
|
249
|
+
{ minWidth: MIN_WIDTH, minHeight: MIN_HEIGHT },
|
|
250
|
+
)
|
|
251
|
+
desk.placeAll(frames)
|
|
252
|
+
// Windows that cascade come in front of the half they cascade over; the focused window stays in front of all.
|
|
253
|
+
if (previous) desk.focus(previous)
|
|
254
|
+
windows.forEach(w => w.id !== focused && w.id !== previous && desk.focus(w.id))
|
|
255
|
+
if (focused) desk.focus(focused)
|
|
256
|
+
}
|
|
257
|
+
|
|
209
258
|
type Gesture = 'move' | 'resize' | 'left' | 'right' | 'bottom'
|
|
210
259
|
|
|
211
260
|
/** The frame a gesture makes of `origin` after the pointer has travelled dx, dy. */
|