chamba 0.6.1 → 0.8.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 +53 -14
- package/README.md +38 -11
- package/dist/commands/advanced.js +7 -278
- package/dist/commands/dev.js +11 -612
- package/dist/commands/doctor.js +1 -29
- package/dist/commands/menu.js +1 -80
- package/dist/commands/onboard.js +6 -230
- package/dist/commands/settings.js +15 -349
- package/dist/lib/agent-commands.js +2 -0
- package/dist/lib/agent-context.js +6 -184
- package/dist/lib/browser.js +1 -40
- package/dist/lib/chamba-yaml.js +10 -191
- package/dist/lib/constants.js +1 -125
- package/dist/lib/dockerfile-builder.js +41 -260
- package/dist/lib/env.js +2 -78
- package/dist/lib/git-env.js +1 -21
- package/dist/lib/global-config.js +4 -66
- package/dist/lib/pnpm-store.js +1 -19
- package/dist/lib/ports.js +1 -210
- package/dist/lib/safe-rm.js +1 -26
- package/dist/lib/sessions.js +2 -34
- package/dist/lib/shadows.js +1 -176
- package/dist/lib/skills.js +2 -0
- package/dist/lib/templates.js +1 -0
- package/dist/lib/webterm.js +1 -305
- package/dist/lib/workspace-identity.js +4 -260
- package/package.json +6 -6
- package/templates/Dockerfile +24 -12
- package/templates/claude-statusline.sh +5 -7
- package/templates/context/baseline.md +2 -0
- package/templates/pane-apps/client/assets/c-BNa-DeUl.json +3552 -0
- package/templates/pane-apps/client/assets/cpp-B-T4NRoH.json +20239 -0
- package/templates/pane-apps/client/assets/csharp-BbxHChhA.json +6010 -0
- package/templates/pane-apps/client/assets/css-CWCxuwmz.json +1860 -0
- package/templates/pane-apps/client/assets/diff-B-2N89g8.json +154 -0
- package/templates/pane-apps/client/assets/docker-DWTvYW26.json +103 -0
- package/templates/pane-apps/client/assets/files-D0nJgFAA.js +12 -0
- package/templates/pane-apps/client/assets/files-h0K1vZyQ.css +1 -0
- package/templates/pane-apps/client/assets/go-B2hyYeNg.json +3156 -0
- package/templates/pane-apps/client/assets/graphql-Z6Id2aNO.json +1292 -0
- package/templates/pane-apps/client/assets/html-BtzvlIfu.json +2627 -0
- package/templates/pane-apps/client/assets/ini-Bn3Twap1.json +108 -0
- package/templates/pane-apps/client/assets/java-C7t2dwLO.json +1885 -0
- package/templates/pane-apps/client/assets/javascript-DuC2v0C0.json +5996 -0
- package/templates/pane-apps/client/assets/json-CwdkWeV5.json +206 -0
- package/templates/pane-apps/client/assets/jsonc-MYWDbTU3.json +206 -0
- package/templates/pane-apps/client/assets/jsx-Cn8EZuIC.json +5996 -0
- package/templates/pane-apps/client/assets/kotlin-CFNOz1gw.json +580 -0
- package/templates/pane-apps/client/assets/make-BVnlFCcl.json +629 -0
- package/templates/pane-apps/client/assets/markdown-B4Xk2WM_.json +3263 -0
- package/templates/pane-apps/client/assets/php-CAQzQmmI.json +4188 -0
- package/templates/pane-apps/client/assets/python-CYQee8m4.json +4174 -0
- package/templates/pane-apps/client/assets/reviews-BsoGZ35r.css +1 -0
- package/templates/pane-apps/client/assets/reviews-iW0Mbn4e.js +153 -0
- package/templates/pane-apps/client/assets/ruby-D7R58tus.json +3114 -0
- package/templates/pane-apps/client/assets/rust-BFBbFiei.json +1103 -0
- package/templates/pane-apps/client/assets/scss-TQRhBkPG.json +1874 -0
- package/templates/pane-apps/client/assets/shellscript-DieAkC4W.json +2375 -0
- package/templates/pane-apps/client/assets/specs-D9z3neR-.js +13 -0
- package/templates/pane-apps/client/assets/sql-Bxd030LV.json +634 -0
- package/templates/pane-apps/client/assets/toml-ChrKGzsV.json +430 -0
- package/templates/pane-apps/client/assets/tsx-D2e90MFw.json +5996 -0
- package/templates/pane-apps/client/assets/typescript-DonXQ_n1.json +5747 -0
- package/templates/pane-apps/client/assets/xml-B17r2RXF.json +382 -0
- package/templates/pane-apps/client/assets/yaml-CKn4h1zy.json +627 -0
- package/templates/pane-apps/client/reviews/index.html +15 -0
- package/templates/pane-apps/client/specs/index.html +14 -0
- package/templates/pane-apps/server/reviews.mjs +20 -0
- package/templates/pane-apps/server/specs.mjs +5 -0
- package/templates/skills/chamba-statusline/SKILL.md +7 -19
- package/templates/skills/dx-review/SKILL.md +123 -0
- package/templates/skills/dx-review/references/acts.md +162 -0
- package/templates/skills/dx-spec/SKILL.md +561 -0
- package/templates/skills/dx-spec/references/imagination-guide.md +140 -0
- package/templates/skills/dx-spec/references/principles-template.md +2 -0
- package/templates/skills/dx-spec/references/review-guide.md +127 -0
- package/templates/skills/dx-spec/references/spec-guide.md +145 -0
- package/templates/skills/dx-spec/references/stages.md +484 -0
- package/templates/skills/dx-spec-execute/SKILL.md +437 -0
- package/templates/startup.mjs +10 -4
- package/templates/tool-helper.sh +166 -0
- package/templates/webterm/README.md +79 -9
- package/templates/webterm/color.js +61 -0
- package/templates/webterm/config.js +74 -0
- package/templates/webterm/context/claude.md +0 -1
- package/templates/webterm/public/app/alerts.js +4 -4
- package/templates/webterm/public/app/anchored.js +81 -0
- package/templates/webterm/public/app/composer.js +4 -1
- package/templates/webterm/public/app/connection.js +9 -0
- package/templates/webterm/public/app/dom.js +15 -5
- package/templates/webterm/public/app/frames.js +51 -10
- package/templates/webterm/public/app/main.js +26 -6
- package/templates/webterm/public/app/new-session.js +13 -41
- package/templates/webterm/public/app/palette.js +58 -0
- package/templates/webterm/public/app/pane-shape.js +167 -0
- package/templates/webterm/public/app/pane-shell.js +380 -0
- package/templates/webterm/public/app/pane.js +77 -193
- package/templates/webterm/public/app/reviews-host.js +15 -0
- package/templates/webterm/public/app/specs-host.js +16 -0
- package/templates/webterm/public/app/state.js +5 -0
- package/templates/webterm/public/app/tabs.js +6 -2
- package/templates/webterm/public/app/terminal.js +8 -0
- package/templates/webterm/public/app/theme.js +75 -27
- package/templates/webterm/public/app/tool-host.js +302 -0
- package/templates/webterm/public/app/workspace-color.js +192 -0
- package/templates/webterm/public/index.html +68 -30
- package/templates/webterm/public/styles.css +270 -34
- package/templates/webterm/server.js +321 -0
- package/templates/webterm/tool-document.js +67 -0
- package/templates/webterm/tools/commands.js +61 -0
- package/templates/webterm/tools/index.js +25 -0
- package/templates/webterm/tools/mount.js +337 -0
- package/templates/webterm/tools/paths.js +34 -0
- package/templates/webterm/tools/reviews.js +133 -0
- package/templates/webterm/tools/specs.js +154 -0
- package/templates/webterm/typed-line.js +148 -0
- package/templates/context/context-usage.md +0 -1
- package/templates/context-usage.sh +0 -266
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
// pane-shell.js - the pane itself: which tool is open in it, and how wide it is.
|
|
2
|
+
//
|
|
3
|
+
// The pane is furniture. What sits in it are tools - Pages (pane.js), Specs (specs-host.js) and Code Reviews
|
|
4
|
+
// (reviews-host.js) - and the line between them is the whole point of this module: a tool draws inside its
|
|
5
|
+
// own panel and nothing else, while the tab bar that says which tool is open, and the controls that say how
|
|
6
|
+
// wide the pane is, are drawn out here. A tool can therefore never move the pane, and never lie about which
|
|
7
|
+
// tool the user is looking at.
|
|
8
|
+
//
|
|
9
|
+
// Width has three states, and only one of them is a width. Between the two ends the divider drags anything,
|
|
10
|
+
// clamped as it always was. Past the near end the pane is put away and becomes the spine. Past the far end it
|
|
11
|
+
// takes the whole row and the terminal is squeezed out - which is a state rather than a wider drag, so the
|
|
12
|
+
// drag's own clamps stay where they are. The grip is reachable in every one of them, and dragging it is
|
|
13
|
+
// always the way back.
|
|
14
|
+
//
|
|
15
|
+
// What the pane is belongs to the session it is beside - how wide, which tool, how big a page is drawn, and
|
|
16
|
+
// the reading theme - so switching sessions gives each one the pane it was left with, and a reload gives them
|
|
17
|
+
// all back. What is in a tool belongs to the session too, and the tool keeps that itself.
|
|
18
|
+
//
|
|
19
|
+
// This module imports dom.js and pane-shape.js and nothing else, deliberately: the tools import it and
|
|
20
|
+
// register themselves as they load, so it has to be finished before any of them runs. Both of those are
|
|
21
|
+
// leaves - pane-shape.js is the shape, its rules and its store, and it holds no element and reaches no
|
|
22
|
+
// other module. Every rule about a shape lives there rather than here, so it can be run in a test; what is
|
|
23
|
+
// left here is the pane as a thing on the screen.
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
grip,
|
|
27
|
+
pane,
|
|
28
|
+
panebar,
|
|
29
|
+
paneCollapse,
|
|
30
|
+
paneFull,
|
|
31
|
+
paneHalf,
|
|
32
|
+
spine,
|
|
33
|
+
spineBadge,
|
|
34
|
+
toolPages,
|
|
35
|
+
toolReviews,
|
|
36
|
+
toolSpecs,
|
|
37
|
+
toolTabs,
|
|
38
|
+
} from "./dom.js";
|
|
39
|
+
import {
|
|
40
|
+
DEFAULT_FRACTION,
|
|
41
|
+
defaultShape,
|
|
42
|
+
draggedTo,
|
|
43
|
+
isDefaultWidth,
|
|
44
|
+
mergeShape,
|
|
45
|
+
PAGES,
|
|
46
|
+
pruneShapes,
|
|
47
|
+
REVIEWS,
|
|
48
|
+
readShapes,
|
|
49
|
+
reopenedFrom,
|
|
50
|
+
SPECS,
|
|
51
|
+
} from "./pane-shape.js";
|
|
52
|
+
|
|
53
|
+
// The tools' ids come from the shape, which holds one of them. Re-exported here because this module is the
|
|
54
|
+
// pane, and a tool that asks whether it is open is asking the pane rather than the store.
|
|
55
|
+
export { PAGES, REVIEWS, SPECS };
|
|
56
|
+
|
|
57
|
+
// The tools, in the order their tabs are drawn. A tool is a tab, a panel, and a render function it hands over
|
|
58
|
+
// when it loads; the pane knows nothing else about any of them.
|
|
59
|
+
const TOOLS = [
|
|
60
|
+
{ id: PAGES, label: "Pages", panel: toolPages },
|
|
61
|
+
{ id: SPECS, label: "Specs", panel: toolSpecs },
|
|
62
|
+
{ id: REVIEWS, label: "Code Reviews", panel: toolReviews },
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
// The one shape this window is drawing, and the session it belongs to. One and not a map of them: the store
|
|
66
|
+
// is shared by every window of this origin, so a shape held here from a session this window left is a copy
|
|
67
|
+
// that has had time to go stale. It is read again on every move to another session instead.
|
|
68
|
+
let sid = null;
|
|
69
|
+
let current = defaultShape();
|
|
70
|
+
// Whether a frame from the server named that session. The id this window opens with comes from its own
|
|
71
|
+
// storage and is the session it is about to ask for, which is enough to draw in and not enough to write
|
|
72
|
+
// under: the server may hand this window a different one, or none.
|
|
73
|
+
let told = false;
|
|
74
|
+
|
|
75
|
+
// id -> what that tool has to say for itself on its tab: how many things are waiting in it, and whether it
|
|
76
|
+
// has anything at all. A tool nobody has heard from yet is empty and waiting on nothing.
|
|
77
|
+
const EMPTY_MARK = { unread: 0, empty: true, counted: true };
|
|
78
|
+
const marks = new Map();
|
|
79
|
+
// id -> the tool's own render function, handed over as it loads.
|
|
80
|
+
const renderers = new Map();
|
|
81
|
+
|
|
82
|
+
// --- What every session remembers --------------------------------------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
// Kept in the store, under the session this window is driving and no other. A window that has not been told
|
|
85
|
+
// its session yet writes nothing: the id it opened with may turn out to be another window's session, and the
|
|
86
|
+
// shape of that session is not this window's to set.
|
|
87
|
+
function remember() {
|
|
88
|
+
if (!told) return;
|
|
89
|
+
mergeShape(localStorage, sid, current);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// The shape of one session, off the store. A window with no session draws the defaults and stores nothing,
|
|
93
|
+
// because there is nobody to store it under.
|
|
94
|
+
function shapeOf(next) {
|
|
95
|
+
if (next === null) return defaultShape();
|
|
96
|
+
return readShapes(localStorage).get(next) ?? defaultShape();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Draw everything the shape decides: the pane's own chrome, and the tool open in it, which has a bar and a
|
|
100
|
+
// document to lay out at whatever width the pane now has.
|
|
101
|
+
function redraw() {
|
|
102
|
+
renderShell();
|
|
103
|
+
renderers.get(current.tool)?.();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// The shape just changed, so keep it under this session and draw it. Every act on the pane ends here.
|
|
107
|
+
function reshape() {
|
|
108
|
+
remember();
|
|
109
|
+
redraw();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The window is beside that session now, so the pane is drawn in that session's own shape.
|
|
114
|
+
*
|
|
115
|
+
* The shape is read again here rather than kept from the load, so a window coming back to a session sees
|
|
116
|
+
* what it was left at, whichever window left it there. Called before the tools are told, because what they
|
|
117
|
+
* lay out depends on how wide the pane now is and on which of them is open in it.
|
|
118
|
+
*/
|
|
119
|
+
export function applySessionShape(next) {
|
|
120
|
+
const held = next ?? null;
|
|
121
|
+
// The first frame is worth acting on even where it names the session this window opened with: it is what
|
|
122
|
+
// makes the shape this window's to write, and the store may have moved since the page loaded.
|
|
123
|
+
if (held === sid && told) return;
|
|
124
|
+
// A drag in flight belongs to the session that is leaving. Ending it stores the width under that session
|
|
125
|
+
// rather than carrying the pointer into the next one's shape.
|
|
126
|
+
if (dragging) endDrag();
|
|
127
|
+
sid = held;
|
|
128
|
+
told = true;
|
|
129
|
+
current = shapeOf(held);
|
|
130
|
+
redraw();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** A session that left the bar takes its pane shape with it, the way its unsent message and its page list do. */
|
|
134
|
+
export function prunePaneShapes(live) {
|
|
135
|
+
pruneShapes(localStorage, live);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** How big a page was last drawn in this session, or null when nobody has said. */
|
|
139
|
+
export function storedPageSize() {
|
|
140
|
+
return current.size;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The Pages tool's zoom, kept with the rest of the session's shape so one key has one writer. */
|
|
144
|
+
export function rememberPageSize(size) {
|
|
145
|
+
current.size = size;
|
|
146
|
+
remember();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The reading theme this session was left on, or null for the system preference. */
|
|
150
|
+
export function storedPaneTheme() {
|
|
151
|
+
return current.theme;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The Specs tool's reading theme, kept with the rest of the session's shape.
|
|
156
|
+
*
|
|
157
|
+
* The tool's frame has an opaque origin and therefore no storage of its own, so the pane holds the choice
|
|
158
|
+
* and hands it back with the session. Anything but the two palettes means the system preference.
|
|
159
|
+
*/
|
|
160
|
+
export function rememberPaneTheme(theme) {
|
|
161
|
+
current.theme = theme === "dark" || theme === "light" ? theme : null;
|
|
162
|
+
remember();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// --- The tools ---------------------------------------------------------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* A tool says how to draw its panel. Called as the tool's module loads, which is why this module imports
|
|
169
|
+
* nothing that could still be half-built at that point.
|
|
170
|
+
*/
|
|
171
|
+
export function registerTool(id, render) {
|
|
172
|
+
renderers.set(id, render);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Whether the pane is open on that tool right now. */
|
|
176
|
+
export function isToolOpen(id) {
|
|
177
|
+
return current.tool === id;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* What a tool has to say on its tab: `unread` is what is waiting for the user in it, and `empty` is whether
|
|
182
|
+
* it has anything to show at all - which is what an untouched pane reads to decide whether to stay out of
|
|
183
|
+
* the way.
|
|
184
|
+
*
|
|
185
|
+
* `counted` is whether that number is worth showing. A tool that asks the user things counts them, because
|
|
186
|
+
* two questions waiting is a different day from one. A tool that only receives counts nothing the user
|
|
187
|
+
* would act on differently, so its tab says that something arrived and leaves the number out of it.
|
|
188
|
+
*/
|
|
189
|
+
export function noteTool(id, { unread = 0, empty = false, counted = true } = {}) {
|
|
190
|
+
marks.set(id, { unread, empty, counted });
|
|
191
|
+
renderShell();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Open a tool, drawing its panel. A tool the user chose is the tool this session comes back on. */
|
|
195
|
+
export function showTool(id) {
|
|
196
|
+
if (!TOOLS.some((tool) => tool.id === id)) return;
|
|
197
|
+
current.tool = id;
|
|
198
|
+
reshape();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function drawTabs() {
|
|
202
|
+
const tabs = TOOLS.map((tool) => {
|
|
203
|
+
const button = document.createElement("button");
|
|
204
|
+
button.type = "button";
|
|
205
|
+
button.className = tool.id === current.tool ? "tool-tab on" : "tool-tab";
|
|
206
|
+
button.dataset.tool = tool.id;
|
|
207
|
+
button.append(tool.label);
|
|
208
|
+
// What is waiting in a tool nobody is looking at. The one thing a tab says beyond its name, so a tool
|
|
209
|
+
// never has to take the screen to be noticed.
|
|
210
|
+
const mark = marks.get(tool.id) ?? EMPTY_MARK;
|
|
211
|
+
if (mark.unread > 0) {
|
|
212
|
+
const badge = document.createElement("span");
|
|
213
|
+
badge.className = mark.counted ? "tool-mark" : "tool-mark dot";
|
|
214
|
+
if (mark.counted) badge.textContent = String(mark.unread);
|
|
215
|
+
else badge.setAttribute("aria-label", "something new");
|
|
216
|
+
button.append(badge);
|
|
217
|
+
}
|
|
218
|
+
button.addEventListener("click", () => showTool(tool.id));
|
|
219
|
+
return button;
|
|
220
|
+
});
|
|
221
|
+
toolTabs.replaceChildren(...tabs);
|
|
222
|
+
for (const tool of TOOLS) tool.panel.hidden = tool.id !== current.tool;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// --- How wide ----------------------------------------------------------------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
// How wide, put away, or the whole row. Its own function because a drag is the one thing that changes the
|
|
228
|
+
// pane without changing anything in it: sixty of these a second are cheap, and sixty full redraws are not.
|
|
229
|
+
function applyGeometry() {
|
|
230
|
+
const { width, full, collapsed } = current;
|
|
231
|
+
pane.classList.toggle("collapsed", collapsed);
|
|
232
|
+
// Only one of the two can be true: a pane that is put away has no width to be full of.
|
|
233
|
+
pane.classList.toggle("full", full && !collapsed);
|
|
234
|
+
// The width goes into a custom property rather than straight onto `width`, so the stylesheet keeps the
|
|
235
|
+
// last word: the collapsed spine, the full row and the narrow-screen layout are rules, and an inline
|
|
236
|
+
// width would beat all three.
|
|
237
|
+
pane.style.setProperty("--pane-width", width === null ? `${DEFAULT_FRACTION * 100}%` : `${width}px`);
|
|
238
|
+
paneFull.classList.toggle("on", full && !collapsed);
|
|
239
|
+
paneHalf.classList.toggle("on", isDefaultWidth(current));
|
|
240
|
+
const back = full && !collapsed;
|
|
241
|
+
paneFull.title = back ? "Give the terminal its room back" : "Full width";
|
|
242
|
+
paneFull.setAttribute("aria-label", paneFull.title);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Draw the pane's own chrome: the tabs, the width, and what the spine says while the pane is away. */
|
|
246
|
+
export function renderShell() {
|
|
247
|
+
const mark = marks.get(current.tool) ?? EMPTY_MARK;
|
|
248
|
+
pane.hidden = false;
|
|
249
|
+
// There is no width worth choosing for a tool with nothing in it.
|
|
250
|
+
grip.hidden = mark.empty && current.collapsed;
|
|
251
|
+
|
|
252
|
+
applyGeometry();
|
|
253
|
+
drawTabs();
|
|
254
|
+
|
|
255
|
+
// The spine stands for the whole pane, so it carries what every tool is waiting on, not just the open one.
|
|
256
|
+
// It counts what the tools count, and falls back to the bare dot when all that arrived was uncounted:
|
|
257
|
+
// a number here has to mean the same thing it means on a tab.
|
|
258
|
+
let waiting = 0;
|
|
259
|
+
let arrived = false;
|
|
260
|
+
for (const entry of marks.values()) {
|
|
261
|
+
if (entry.unread <= 0) continue;
|
|
262
|
+
if (entry.counted) waiting += entry.unread;
|
|
263
|
+
else arrived = true;
|
|
264
|
+
}
|
|
265
|
+
spineBadge.textContent = waiting > 0 ? String(waiting) : "";
|
|
266
|
+
spineBadge.classList.toggle("dot", waiting === 0 && arrived);
|
|
267
|
+
spineBadge.hidden = waiting === 0 && !arrived;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Draw the pane for the first time, on the tool this window was left on. Called from the entry rather than
|
|
272
|
+
* here, because the tools register as they load and there is no pane to draw until they all have.
|
|
273
|
+
*/
|
|
274
|
+
export function mountPane(startSid = null) {
|
|
275
|
+
sid = startSid ?? null;
|
|
276
|
+
current = shapeOf(sid);
|
|
277
|
+
redraw();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Whether the pane is put away in this session, which is whether it is drawn as the spine. */
|
|
281
|
+
export function isPaneCollapsed() {
|
|
282
|
+
return current.collapsed;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Away, and back. Two ways in and one way out, because the pane is put away far more often than it is
|
|
286
|
+
// resized: the button in the bar is the obvious one, and dragging the grip past the threshold is the other.
|
|
287
|
+
export function setPaneCollapsed(next) {
|
|
288
|
+
current.collapsed = next;
|
|
289
|
+
if (current.collapsed) {
|
|
290
|
+
// Coming back to a pane that fills the window, with the terminal nowhere, is not what putting one
|
|
291
|
+
// away asks for. The width it had is remembered; the far end is not.
|
|
292
|
+
current.full = false;
|
|
293
|
+
} else {
|
|
294
|
+
current.width = reopenedFrom(current, window.innerWidth);
|
|
295
|
+
}
|
|
296
|
+
reshape();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Back to the width the pane opens at. Not a width of its own: the pane is left with none, and the
|
|
300
|
+
// stylesheet draws the default fraction, so a window resized afterwards leaves the pane at half rather
|
|
301
|
+
// than at the pixels half happened to be when the button was pressed.
|
|
302
|
+
function setHalf() {
|
|
303
|
+
current.width = null;
|
|
304
|
+
current.full = false;
|
|
305
|
+
current.collapsed = false;
|
|
306
|
+
reshape();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// The far end, and back. A press either way, and a drag of the grip is the other way back.
|
|
310
|
+
function setFull(next) {
|
|
311
|
+
current.full = next;
|
|
312
|
+
if (current.full) current.collapsed = false;
|
|
313
|
+
reshape();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
paneFull.addEventListener("click", () => setFull(!current.full));
|
|
317
|
+
paneHalf.addEventListener("click", setHalf);
|
|
318
|
+
paneCollapse.addEventListener("click", () => setPaneCollapsed(true));
|
|
319
|
+
spine.addEventListener("click", () => setPaneCollapsed(false));
|
|
320
|
+
|
|
321
|
+
// --- Saying that something arrived -------------------------------------------------------------------------------------------------------
|
|
322
|
+
|
|
323
|
+
// One pulse of the pane's edge, in the workspace colour. ARRIVAL_MS must match the keyframes in styles.css.
|
|
324
|
+
const ARRIVAL_MS = 1_000;
|
|
325
|
+
let arrivalTimer = null;
|
|
326
|
+
|
|
327
|
+
// The class comes off on a timer rather than on animationend, because with a reduced-motion preference there
|
|
328
|
+
// is no animation to end - the stylesheet holds the highlight instead of pulsing it, and this is what makes
|
|
329
|
+
// the two last the same moment.
|
|
330
|
+
export function flashPane() {
|
|
331
|
+
clearTimeout(arrivalTimer);
|
|
332
|
+
// A pulse still running when the next page lands restarts rather than stacking: off, force the browser to
|
|
333
|
+
// notice, on again. Reading a layout property is what makes the restart real.
|
|
334
|
+
pane.classList.remove("arriving");
|
|
335
|
+
void pane.offsetWidth;
|
|
336
|
+
pane.classList.add("arriving");
|
|
337
|
+
arrivalTimer = setTimeout(() => pane.classList.remove("arriving"), ARRIVAL_MS);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// --- The divider -------------------------------------------------------------------------------------------------------------------------
|
|
341
|
+
|
|
342
|
+
// Dragging sets a pixel width until it stops making sense, and below the threshold the pane becomes the
|
|
343
|
+
// spine rather than a sliver. Pointer capture keeps the drag alive over the terminal and the frame alike -
|
|
344
|
+
// a page is a document with its own event loop, and without capture the pointer would be lost in it.
|
|
345
|
+
let dragging = false;
|
|
346
|
+
// The width the pane had when the drag started. Dragging it shut is a request to put it away, not a request
|
|
347
|
+
// for a 150px pane, so crossing the threshold hands its old width back rather than keeping the sliver the
|
|
348
|
+
// pointer passed through on the way there.
|
|
349
|
+
let widthBeforeDrag = null;
|
|
350
|
+
|
|
351
|
+
grip.addEventListener("pointerdown", (event) => {
|
|
352
|
+
dragging = true;
|
|
353
|
+
widthBeforeDrag = current.width;
|
|
354
|
+
document.body.classList.add("dragging");
|
|
355
|
+
grip.setPointerCapture(event.pointerId);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
grip.addEventListener("pointermove", (event) => {
|
|
359
|
+
if (!dragging) return;
|
|
360
|
+
const wanted = Math.max(0, window.innerWidth - event.clientX - grip.offsetWidth / 2);
|
|
361
|
+
// A drag is a width, so it is the way into every state the pane has and the way out of each of them.
|
|
362
|
+
// Where each of those states begins is pane-shape.js's rule; what is left here is the pointer.
|
|
363
|
+
Object.assign(current, draggedTo(wanted, window.innerWidth, widthBeforeDrag));
|
|
364
|
+
applyGeometry();
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
function endDrag() {
|
|
368
|
+
if (!dragging) return;
|
|
369
|
+
dragging = false;
|
|
370
|
+
document.body.classList.remove("dragging");
|
|
371
|
+
// The shape changed under the pointer, and only the geometry followed it. A drag that crossed either
|
|
372
|
+
// threshold also changed which of the three states the pane is in, which the tool has to hear about.
|
|
373
|
+
reshape();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
grip.addEventListener("pointerup", endDrag);
|
|
377
|
+
grip.addEventListener("pointercancel", endDrag);
|
|
378
|
+
|
|
379
|
+
// The bar is chrome: a click on it must not leave the terminal without focus for no reason.
|
|
380
|
+
panebar.addEventListener("mousedown", (event) => event.preventDefault());
|