chamba 0.7.0 → 0.9.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 +34 -8
- 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 -210
- 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 -261
- 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 -36
- 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 +4 -4
- package/templates/Dockerfile +15 -22
- package/templates/claude-statusline.sh +5 -7
- package/templates/context/baseline.md +4 -2
- package/templates/context/web-pane-craft.md +1 -1
- package/templates/pane-apps/client/assets/api-BBDf_rzz.js +12 -0
- package/templates/pane-apps/client/assets/api-DNLbSBpK.css +1 -0
- package/templates/pane-apps/client/assets/bee-B7t97l5B.webp +0 -0
- package/templates/pane-apps/client/assets/buzz-CUUsbnXH.mp3 +0 -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-BkmyervY.js +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/home-3SeGzySM.css +1 -0
- package/templates/pane-apps/client/assets/home-Cu9xOd2i.js +1 -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/pop-D5ZANBC3.mp3 +0 -0
- package/templates/pane-apps/client/assets/python-CYQee8m4.json +4174 -0
- package/templates/pane-apps/client/assets/reviews-DMrGB-kG.js +153 -0
- package/templates/pane-apps/client/assets/reviews-nK_yRav9.css +1 -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-Cabb15F0.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/useNow-BHFKFraS.js +1 -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/home/index.html +16 -0
- package/templates/pane-apps/client/reviews/index.html +17 -0
- package/templates/pane-apps/client/specs/index.html +4 -2
- package/templates/pane-apps/server/home.mjs +12 -0
- package/templates/pane-apps/server/reviews.mjs +21 -0
- package/templates/pane-apps/server/specs.mjs +5 -1588
- package/templates/skills/chamba-statusline/SKILL.md +7 -19
- package/templates/skills/dx-review/SKILL.md +136 -0
- package/templates/skills/dx-review/references/acts.md +233 -0
- package/templates/skills/dx-spec/SKILL.md +230 -30
- package/templates/skills/dx-spec/references/principles-template.md +2 -0
- package/templates/skills/dx-spec/references/review-guide.md +6 -52
- package/templates/skills/dx-spec/references/spec-guide.md +25 -5
- package/templates/skills/dx-spec/references/stages.md +149 -60
- package/templates/skills/dx-spec-execute/SKILL.md +217 -38
- package/templates/startup.mjs +22 -16
- package/templates/tool-helper.sh +168 -0
- package/templates/webterm/README.md +82 -27
- package/templates/webterm/color.js +61 -0
- package/templates/webterm/config.js +55 -4
- package/templates/webterm/context/claude.md +0 -1
- package/templates/webterm/public/app/alerts.js +95 -14
- package/templates/webterm/public/app/anchored.js +81 -0
- package/templates/webterm/public/app/composer.js +37 -3
- package/templates/webterm/public/app/connection.js +23 -0
- package/templates/webterm/public/app/dom.js +3 -0
- package/templates/webterm/public/app/frames.js +52 -18
- package/templates/webterm/public/app/home-host.js +16 -0
- package/templates/webterm/public/app/main.js +24 -8
- 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 +169 -0
- package/templates/webterm/public/app/pane-shell.js +240 -135
- package/templates/webterm/public/app/pane.js +39 -24
- package/templates/webterm/public/app/reviews-host.js +19 -0
- package/templates/webterm/public/app/specs-host.js +12 -218
- package/templates/webterm/public/app/state.js +5 -0
- package/templates/webterm/public/app/tabs.js +6 -2
- package/templates/webterm/public/app/theme.js +75 -27
- package/templates/webterm/public/app/tool-host.js +318 -0
- package/templates/webterm/public/app/workspace-color.js +192 -0
- package/templates/webterm/public/index.html +22 -4
- package/templates/webterm/public/styles.css +195 -21
- package/templates/webterm/server.js +198 -150
- package/templates/webterm/tool-document.js +3 -2
- package/templates/webterm/tools/commands.js +61 -0
- package/templates/webterm/tools/home.js +58 -0
- package/templates/webterm/tools/index.js +26 -0
- package/templates/webterm/tools/mount.js +337 -0
- package/templates/webterm/tools/paths.js +34 -0
- package/templates/webterm/tools/reviews.js +137 -0
- package/templates/webterm/tools/specs.js +154 -0
- package/templates/webterm/typed-line.js +70 -7
- package/templates/context/context-usage.md +0 -1
- package/templates/context-usage.sh +0 -266
- package/templates/pane-apps/client/assets/specs-B1970L17.css +0 -1
- package/templates/pane-apps/client/assets/specs-cEee_SPn.js +0 -23
- package/templates/skills/dx-spec-config/SKILL.md +0 -313
- package/templates/skills/dx-spec-config/references/principles-template.md +0 -12
- package/templates/specs.sh +0 -106
- package/templates/webterm/specs.js +0 -358
|
@@ -19,18 +19,25 @@
|
|
|
19
19
|
//
|
|
20
20
|
// state.js what the server last told this window, and the URL's key
|
|
21
21
|
// dom.js the elements index.html ships with
|
|
22
|
-
//
|
|
22
|
+
// palette.js the eleven workspace colours, the six session slots, and the rules over them
|
|
23
|
+
// theme.js the colour this workspace is wearing, and the session colours that follow from it
|
|
23
24
|
// note.js the status pill over the terminal
|
|
24
25
|
// terminal.js the terminal, its size, and the keystrokes that go straight to the PTY
|
|
25
26
|
// tabs.js the session bar, its two animations, renaming, and reordering
|
|
27
|
+
// anchored.js a panel hung off the body under the button that opened it, and how it closes
|
|
26
28
|
// new-session.js the panel behind the chevron: which agent, and which directory
|
|
29
|
+
// workspace-color.js the colour this workspace wears: the button in the bar, and the panel of eleven
|
|
27
30
|
// alerts.js the browser tab (title and drawn favicon), and the chime
|
|
28
31
|
// cards.js the overlay cards
|
|
29
32
|
// connection.js the socket, the reconnect loop, and the curtain
|
|
30
33
|
// frames.js what to do with each frame the server sends
|
|
31
34
|
// pane-shell.js the pane itself: the tool tabs, the three widths, the drag and the spine
|
|
35
|
+
// pane-shape.js the shape a pane has beside one session, and the store that keeps one for each
|
|
32
36
|
// pane.js the Pages tool: the chips bar and the page
|
|
33
|
-
//
|
|
37
|
+
// tool-host.js a tool's frame, and the credentials the shell hands into it
|
|
38
|
+
// home-host.js the Home tool: which tool it is, and its panel
|
|
39
|
+
// specs-host.js the Specs tool, the same way
|
|
40
|
+
// reviews-host.js the Code Reviews tool, the same way again
|
|
34
41
|
// pane-frame.js a published page in a sandboxed frame, and the answer that comes back out of it
|
|
35
42
|
// pane-arrival.js which page an incoming list opens by itself, and when none of them does
|
|
36
43
|
// status-strip.js the attached session's model, context, quota meters and version, above the composer
|
|
@@ -45,9 +52,9 @@
|
|
|
45
52
|
// router talks to every feature and every feature answers back - so a module that ran another module's code at
|
|
46
53
|
// load time would be reaching into something half-built. Everything with an order to it happens here.
|
|
47
54
|
//
|
|
48
|
-
// Reading at load time follows from the same thing: a module may read from one that
|
|
49
|
-
// (dom.js, state.js, theme.js), since a
|
|
50
|
-
// one inside the cycle, whose values may not exist yet.
|
|
55
|
+
// Reading at load time follows from the same thing: a module may read from one that sits outside the cycle
|
|
56
|
+
// (dom.js, state.js, theme.js, and palette.js under it), since such a module is always finished by the time
|
|
57
|
+
// anything else runs, and never from one inside the cycle, whose values may not exist yet.
|
|
51
58
|
//
|
|
52
59
|
// Every module is imported here, including the ones this file has nothing to call. Several exist for what they
|
|
53
60
|
// do when they load - the microphone button, the chevron panel's Escape key, the workspace colour, the draft
|
|
@@ -58,22 +65,29 @@
|
|
|
58
65
|
import { refreshBrowserTab } from "./alerts.js";
|
|
59
66
|
import { installClipboard } from "./clipboard.js";
|
|
60
67
|
import { refreshComposer } from "./composer.js";
|
|
61
|
-
import { connect } from "./connection.js";
|
|
68
|
+
import { connect, LAST_SID_KEY } from "./connection.js";
|
|
69
|
+
import "./anchored.js";
|
|
62
70
|
import "./cards.js";
|
|
63
71
|
import "./dictation.js";
|
|
64
72
|
import "./dom.js";
|
|
73
|
+
import "./palette.js";
|
|
65
74
|
import "./drafts.js";
|
|
66
75
|
import "./frames.js";
|
|
76
|
+
import "./home-host.js";
|
|
67
77
|
import "./new-session.js";
|
|
68
78
|
import "./note.js";
|
|
69
79
|
import "./pane.js";
|
|
70
80
|
import "./pane-arrival.js";
|
|
81
|
+
import "./pane-shape.js";
|
|
71
82
|
import "./pane-frame.js";
|
|
83
|
+
import "./reviews-host.js";
|
|
72
84
|
import "./specs-host.js";
|
|
85
|
+
import "./tool-host.js";
|
|
73
86
|
import "./state.js";
|
|
74
87
|
import "./status-strip.js";
|
|
75
88
|
import "./strip-format.js";
|
|
76
89
|
import "./theme.js";
|
|
90
|
+
import "./workspace-color.js";
|
|
77
91
|
import { mountPane } from "./pane-shell.js";
|
|
78
92
|
import { renderBar } from "./tabs.js";
|
|
79
93
|
import { mountTerminal, term } from "./terminal.js";
|
|
@@ -83,8 +97,10 @@ import { mountTerminal, term } from "./terminal.js";
|
|
|
83
97
|
mountTerminal();
|
|
84
98
|
installClipboard();
|
|
85
99
|
refreshComposer();
|
|
86
|
-
// After every tool has registered, which is what makes the pane something to draw.
|
|
87
|
-
|
|
100
|
+
// After every tool has registered, which is what makes the pane something to draw. In the shape of the
|
|
101
|
+
// session this window was last on, which is the session the server is about to be asked for: the pane is drawn
|
|
102
|
+
// once, in what it will be, rather than at the defaults and then again a frame later.
|
|
103
|
+
mountPane(sessionStorage.getItem(LAST_SID_KEY));
|
|
88
104
|
renderBar();
|
|
89
105
|
// Before the first frame arrives, so the icon carries this workspace's frame from the moment the page loads.
|
|
90
106
|
refreshBrowserTab();
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
// Nothing typed here is trusted: the server resolves whatever path arrives and refuses what is not a directory in the
|
|
17
17
|
// workspace, and it refuses an agent it does not run.
|
|
18
18
|
|
|
19
|
+
import { createAnchoredPanel } from "./anchored.js";
|
|
19
20
|
import { sendFrame } from "./connection.js";
|
|
20
21
|
import { noteMsg } from "./note.js";
|
|
21
22
|
import { agents, defaultAgent, defaultCwd, KEY_QUERY } from "./state.js";
|
|
@@ -23,36 +24,17 @@ import { focusTerminal } from "./terminal.js";
|
|
|
23
24
|
|
|
24
25
|
const DIR_LIST_ID = "dirlist";
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
const panel = createAnchoredPanel("newtab-more");
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
if (!newPopEl) return;
|
|
30
|
-
newPopEl.remove();
|
|
31
|
-
newPopEl = null;
|
|
32
|
-
document.removeEventListener("pointerdown", onNewPopOutside, true);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Put an open panel back under its button after the bar was rebuilt. A frame arrives whenever anything in the
|
|
36
|
-
// container moves, and a panel that closed itself every time one did would be unusable; a button that is gone
|
|
37
|
-
// (the session limit) takes it with it. Clamped to the window, since the bar can be scrolled far to the right.
|
|
29
|
+
// Put an open panel back under its button after the bar was rebuilt. Called from renderBar().
|
|
38
30
|
export function repositionNewPop() {
|
|
39
|
-
|
|
40
|
-
const anchor = document.getElementById("newtab-more");
|
|
41
|
-
if (!anchor) {
|
|
42
|
-
closeNewPop();
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
const box = anchor.getBoundingClientRect();
|
|
46
|
-
const width = newPopEl.offsetWidth;
|
|
47
|
-
const left = Math.min(box.right - width, window.innerWidth - width - 8);
|
|
48
|
-
newPopEl.style.top = `${Math.round(box.bottom + 6)}px`;
|
|
49
|
-
newPopEl.style.left = `${Math.round(Math.max(8, left))}px`;
|
|
31
|
+
panel.reposition();
|
|
50
32
|
}
|
|
51
33
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
34
|
+
// Called when the socket drops. The panel hangs off the body, where the bar's offline rules cannot reach it,
|
|
35
|
+
// and nothing it offers can be answered with the socket down.
|
|
36
|
+
export function closeNewPop() {
|
|
37
|
+
panel.close();
|
|
56
38
|
}
|
|
57
39
|
|
|
58
40
|
// Fill the picker's suggestions from the container. Fetched when the panel opens rather than kept around, so
|
|
@@ -118,8 +100,8 @@ function popLabel(text) {
|
|
|
118
100
|
}
|
|
119
101
|
|
|
120
102
|
export function toggleNewPop() {
|
|
121
|
-
if (
|
|
122
|
-
|
|
103
|
+
if (panel.isOpen()) {
|
|
104
|
+
panel.close();
|
|
123
105
|
return;
|
|
124
106
|
}
|
|
125
107
|
const state = { agent: defaultAgent };
|
|
@@ -144,7 +126,7 @@ export function toggleNewPop() {
|
|
|
144
126
|
pop.append(popLabel("Directory"), field, list);
|
|
145
127
|
|
|
146
128
|
const start = () => {
|
|
147
|
-
|
|
129
|
+
panel.close();
|
|
148
130
|
sendFrame({ t: "new", cwd: field.value.trim(), agent: state.agent });
|
|
149
131
|
};
|
|
150
132
|
const row = document.createElement("div");
|
|
@@ -154,7 +136,7 @@ export function toggleNewPop() {
|
|
|
154
136
|
cancel.className = "btn";
|
|
155
137
|
cancel.textContent = "Cancel";
|
|
156
138
|
cancel.addEventListener("click", () => {
|
|
157
|
-
|
|
139
|
+
panel.close();
|
|
158
140
|
focusTerminal();
|
|
159
141
|
});
|
|
160
142
|
const go = document.createElement("button");
|
|
@@ -171,18 +153,8 @@ export function toggleNewPop() {
|
|
|
171
153
|
start();
|
|
172
154
|
});
|
|
173
155
|
|
|
174
|
-
|
|
175
|
-
newPopEl = pop;
|
|
176
|
-
repositionNewPop();
|
|
156
|
+
panel.open(pop);
|
|
177
157
|
field.focus();
|
|
178
158
|
field.select();
|
|
179
159
|
fillDirList(list);
|
|
180
|
-
// Capture, so a click on a tab closes this before that tab switches session.
|
|
181
|
-
document.addEventListener("pointerdown", onNewPopOutside, true);
|
|
182
160
|
}
|
|
183
|
-
|
|
184
|
-
document.addEventListener("keydown", (event) => {
|
|
185
|
-
if (event.key !== "Escape" || !newPopEl) return;
|
|
186
|
-
closeNewPop();
|
|
187
|
-
focusTerminal();
|
|
188
|
-
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// palette.js - the colours a workspace can wear, the slots a session rotates through, and the rules between them.
|
|
2
|
+
//
|
|
3
|
+
// Data and arithmetic only. Nothing here reads a browser global or does anything at load, which is what lets
|
|
4
|
+
// the tests import this file and check the rules by running them rather than by matching the source text.
|
|
5
|
+
// theme.js is what puts any of it on the page.
|
|
6
|
+
//
|
|
7
|
+
// The ids are the stored contract: a workspace remembers "azure" and never a hex, so a later version can move
|
|
8
|
+
// a shade without stranding a workspace on a value nobody chose. An id this version does not know means
|
|
9
|
+
// nothing is stored, and the workspace falls back to the colour the port gives it.
|
|
10
|
+
|
|
11
|
+
// Cyberpunk neons on a near-black bar, in hue order, and deliberately free of yellow, orange and red - those
|
|
12
|
+
// read as a warning here, and the bar has enough to say without one. Two rules hold over this list, and both
|
|
13
|
+
// are checked in tests/webterm-sessions.test.ts: every colour clears 3:1 against the interface's grounds, and
|
|
14
|
+
// no two are closer than 17.2 in CIE76, which is lime beside green, the closest pair this table already holds.
|
|
15
|
+
export const PALETTE = [
|
|
16
|
+
{ id: "lime", hex: "#7ae62f" },
|
|
17
|
+
{ id: "green", hex: "#3fe54f" },
|
|
18
|
+
{ id: "spring-green", hex: "#2fe58a" },
|
|
19
|
+
{ id: "turquoise", hex: "#1fe0cf" },
|
|
20
|
+
{ id: "azure", hex: "#29b6ff" },
|
|
21
|
+
{ id: "blue", hex: "#5c86ff" },
|
|
22
|
+
{ id: "indigo", hex: "#305efe" },
|
|
23
|
+
{ id: "neon-violet", hex: "#b45cff" },
|
|
24
|
+
{ id: "magenta", hex: "#f24bff" },
|
|
25
|
+
{ id: "orchid", hex: "#ff4ecb" },
|
|
26
|
+
{ id: "rose", hex: "#ff2e88" },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
// The six slots a session dot rotates through, written out in the order the interface has always had them.
|
|
30
|
+
// Restricting the hue-ordered list above to these six gives a different order - azure and magenta change
|
|
31
|
+
// places - so the order is a written fact here rather than a side effect of the table. Deriving it would move
|
|
32
|
+
// the automatic colour of every workspace whose port is congruent to 2 or 4 modulo 6, and shift the dot order
|
|
33
|
+
// of every session already open.
|
|
34
|
+
export const SESSION_IDS = ["spring-green", "turquoise", "magenta", "neon-violet", "azure", "rose"];
|
|
35
|
+
|
|
36
|
+
/** The hex for one id, or null when this version does not know the id. */
|
|
37
|
+
export function hexFor(id) {
|
|
38
|
+
return PALETTE.find((colour) => colour.id === id)?.hex ?? null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// The colour a workspace wears before anyone picks one, worked out over the six in their order and never over
|
|
42
|
+
// the eleven. Ports are handed out lowest-free-first per workspace, so neighbours land on different colours.
|
|
43
|
+
// The eleven are the set the user picks from, and they take no part in this: any other reading would move the
|
|
44
|
+
// colour of every workspace where nobody ever chose one.
|
|
45
|
+
export function automaticId(seed) {
|
|
46
|
+
return SESSION_IDS[seed % SESSION_IDS.length];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Every session slot except the one the workspace is wearing, so "never the workspace colour" is a property of
|
|
50
|
+
// the list the tabs index into rather than a rule someone has to remember. The pool is always five long: when
|
|
51
|
+
// the workspace wears a colour from outside the six, the slot the port would have chosen is the one held back,
|
|
52
|
+
// which leaves the registry's PALETTE_SIZE untouched and the dots where they were.
|
|
53
|
+
//
|
|
54
|
+
// `autoId` is the id automaticId() gave this window, and is always one of the six.
|
|
55
|
+
export function sessionColors(workspaceId, autoId) {
|
|
56
|
+
const taken = SESSION_IDS.includes(workspaceId) ? workspaceId : autoId;
|
|
57
|
+
return SESSION_IDS.filter((id) => id !== taken).map(hexFor);
|
|
58
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// pane-shape.js - what a pane's shape is, the rules that decide one, and the store that keeps one per session.
|
|
2
|
+
//
|
|
3
|
+
// The pane's own DOM-free module, so every rule here can be run rather than read as source: a browser entry
|
|
4
|
+
// written by an older version, a spoiled field, a session that ended, and the arithmetic of a drag. Nothing
|
|
5
|
+
// here holds an element or reaches another module, and each function is given whatever it works on, which is
|
|
6
|
+
// what lets a test hand it a plain object and a window width of its own choosing.
|
|
7
|
+
//
|
|
8
|
+
// The shape itself is one session's answer to how the pane looks beside it: how wide, whether it is put away,
|
|
9
|
+
// whether it has the whole row, which tool is open, how big a page is drawn, and which palette the
|
|
10
|
+
// reading is in. Nothing in it is about the work, so a shape that is lost costs a drag of the divider.
|
|
11
|
+
|
|
12
|
+
// One entry for the whole interface, holding a shape for each session. Named the way the interface names its
|
|
13
|
+
// own: webterm-color and webterm-drafts are its neighbours.
|
|
14
|
+
export const PANE_KEY = "webterm-pane";
|
|
15
|
+
|
|
16
|
+
// The tools there are to open, which a shape holds one of. Their ids and nothing else about them: what a tool
|
|
17
|
+
// is made of is the pane's business, and all this module asks is whether a stored id is one of them.
|
|
18
|
+
export const HOME = "home";
|
|
19
|
+
export const PAGES = "pages";
|
|
20
|
+
export const SPECS = "specs";
|
|
21
|
+
export const REVIEWS = "reviews";
|
|
22
|
+
const SHAPE_TOOLS = [HOME, PAGES, SPECS, REVIEWS];
|
|
23
|
+
// The tool a session opens on. Home, because it is the reading of the repository a session starts in, and
|
|
24
|
+
// because a stored shape naming another tool keeps that tool: this is what a session with no shape gets.
|
|
25
|
+
const DEFAULT_TOOL = HOME;
|
|
26
|
+
|
|
27
|
+
// Narrower than this and the pane is not a pane any more, so it becomes the spine instead. The mock's
|
|
28
|
+
// threshold, and the width the spine itself takes.
|
|
29
|
+
export const COLLAPSE_AT = 150;
|
|
30
|
+
// What the pane opens at, in a fraction of the window. Half is the default width itself rather than a pixel
|
|
31
|
+
// count, so a pane nobody has dragged stays at half through a window resize.
|
|
32
|
+
export const DEFAULT_FRACTION = 0.5;
|
|
33
|
+
// The most of the window a drag may give the pane.
|
|
34
|
+
const MAX_FRACTION = 0.7;
|
|
35
|
+
// How much further than that a drag has to go before it means the far end. Past the clamp the pane stops
|
|
36
|
+
// following the pointer, so without this the only way to the full width is the button, and a drag that keeps
|
|
37
|
+
// going reads as a request nothing answers. Wide enough that reaching the clamp is not the same as passing
|
|
38
|
+
// it, and the way back is the drag itself: anything short of this is a width again, which is the state the
|
|
39
|
+
// pane returns to when the pointer comes back.
|
|
40
|
+
const FULL_AT = 90;
|
|
41
|
+
// The least of the window the pane may come back into. Reopening is not the same as resizing: whatever width
|
|
42
|
+
// it was put away at, a pane the user has just asked for has to be wide enough to read a page in.
|
|
43
|
+
const MIN_OPEN_FRACTION = 1 / 3;
|
|
44
|
+
|
|
45
|
+
/** What a session nobody has shaped opens on. */
|
|
46
|
+
export function defaultShape() {
|
|
47
|
+
return { width: null, collapsed: false, full: false, tool: DEFAULT_TOOL, size: null, theme: null };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// --- How wide ----------------------------------------------------------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Where a drag of the divider leaves the pane: a width, put away, or the whole row.
|
|
54
|
+
*
|
|
55
|
+
* `wanted` is what the pointer asks for, and `before` is the width the pane had when the drag started. Both
|
|
56
|
+
* ends of the drag are the same rule read at a different threshold: past the near one the pane is put away
|
|
57
|
+
* and hands its old width back, rather than keeping the sliver the pointer passed through on the way there;
|
|
58
|
+
* past the far one it takes the row; and anywhere between them it is the width under the pointer, clamped.
|
|
59
|
+
*
|
|
60
|
+
* The clamp is what the pane is left holding, so coming back from the far end lands there rather than
|
|
61
|
+
* wherever the pointer had run to.
|
|
62
|
+
*/
|
|
63
|
+
export function draggedTo(wanted, windowWidth, before) {
|
|
64
|
+
const ceiling = windowWidth * MAX_FRACTION;
|
|
65
|
+
const full = wanted > ceiling + FULL_AT;
|
|
66
|
+
if (wanted < COLLAPSE_AT) return { width: before, collapsed: true, full };
|
|
67
|
+
return { width: Math.min(wanted, ceiling), collapsed: false, full };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The width a pane that is being brought back has to open into, which is the width it was put away at unless
|
|
72
|
+
* that would be a sliver.
|
|
73
|
+
*
|
|
74
|
+
* A pane dragged shut, or one a narrower window left narrow, has to come back as something a page fits in.
|
|
75
|
+
* A pane with no width of its own keeps none: the default fraction is wide enough by definition.
|
|
76
|
+
*/
|
|
77
|
+
export function reopenedFrom(shape, windowWidth) {
|
|
78
|
+
if (shape.width === null) return null;
|
|
79
|
+
return Math.max(shape.width, windowWidth * MIN_OPEN_FRACTION);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Whether the pane is at the width it opens at: no width of its own, no far end, and not the spine.
|
|
84
|
+
*
|
|
85
|
+
* What the half-width button is lit by, and therefore what a drag takes away and that button gives back.
|
|
86
|
+
*/
|
|
87
|
+
export function isDefaultWidth(shape) {
|
|
88
|
+
return shape.width === null && !shape.full && !shape.collapsed;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// --- The store ---------------------------------------------------------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
/** One stored shape, field by field: anything missing or malformed falls back to that field's default. */
|
|
94
|
+
export function adoptShape(stored) {
|
|
95
|
+
const shape = defaultShape();
|
|
96
|
+
if (stored === null || typeof stored !== "object") return shape;
|
|
97
|
+
if (typeof stored.width === "number" && stored.width >= COLLAPSE_AT) shape.width = stored.width;
|
|
98
|
+
if (typeof stored.collapsed === "boolean") shape.collapsed = stored.collapsed;
|
|
99
|
+
if (typeof stored.full === "boolean") shape.full = stored.full;
|
|
100
|
+
if (SHAPE_TOOLS.includes(stored.tool)) shape.tool = stored.tool;
|
|
101
|
+
// The multiplier is stored rather than its position, so a session that was left at a size this version no
|
|
102
|
+
// longer offers falls back to the default instead of landing between two steps.
|
|
103
|
+
if (typeof stored.size === "number" && Number.isFinite(stored.size)) shape.size = stored.size;
|
|
104
|
+
if (stored.theme === "dark" || stored.theme === "light") shape.theme = stored.theme;
|
|
105
|
+
return shape;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Every shape in the store, as it stands right now.
|
|
110
|
+
*
|
|
111
|
+
* An entry from the version that kept one shape for the whole window carries no `sessions`, and there is no
|
|
112
|
+
* session it could be meant for, so it is dropped rather than handed to the first session that asks. So is
|
|
113
|
+
* anything unreadable: a shape nobody can parse means the pane opens on its defaults, and never less.
|
|
114
|
+
*/
|
|
115
|
+
export function readShapes(storage) {
|
|
116
|
+
const shapes = new Map();
|
|
117
|
+
let held = null;
|
|
118
|
+
try {
|
|
119
|
+
held = JSON.parse(storage.getItem(PANE_KEY) ?? "{}")?.sessions ?? null;
|
|
120
|
+
} catch {
|
|
121
|
+
return shapes;
|
|
122
|
+
}
|
|
123
|
+
if (held === null || typeof held !== "object" || Array.isArray(held)) return shapes;
|
|
124
|
+
for (const [sid, shape] of Object.entries(held)) {
|
|
125
|
+
if (sid !== "") shapes.set(sid, adoptShape(shape));
|
|
126
|
+
}
|
|
127
|
+
return shapes;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Write the whole store. Storage that refuses only means the pane keeps its shape for this page's lifetime. */
|
|
131
|
+
export function writeShapes(storage, shapes) {
|
|
132
|
+
try {
|
|
133
|
+
storage.setItem(PANE_KEY, JSON.stringify({ sessions: Object.fromEntries(shapes) }));
|
|
134
|
+
} catch {
|
|
135
|
+
// Nothing to do about a disabled or full store, and nothing worth saying to the user about it.
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* One session's shape into the store, and every other session's left as it stands.
|
|
141
|
+
*
|
|
142
|
+
* Read, merge, write, because the store is shared by every window of one origin. A window that wrote its own
|
|
143
|
+
* whole map back would hand another session's pane to whatever that window last saw of it, which is how two
|
|
144
|
+
* windows quietly undo each other.
|
|
145
|
+
*/
|
|
146
|
+
export function mergeShape(storage, sid, shape) {
|
|
147
|
+
if (!sid) return;
|
|
148
|
+
const shapes = readShapes(storage);
|
|
149
|
+
shapes.set(sid, shape);
|
|
150
|
+
writeShapes(storage, shapes);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Drop the shape of every session that is no longer live, and say whether anything went.
|
|
155
|
+
*
|
|
156
|
+
* The store outlives the page, so a shape nobody drops here is kept for as long as the browser holds the
|
|
157
|
+
* origin. Called with the session list the server sends, the way the drafts and the composer history are.
|
|
158
|
+
*/
|
|
159
|
+
export function pruneShapes(storage, live) {
|
|
160
|
+
const shapes = readShapes(storage);
|
|
161
|
+
let removed = false;
|
|
162
|
+
for (const sid of [...shapes.keys()]) {
|
|
163
|
+
if (live.has(sid)) continue;
|
|
164
|
+
shapes.delete(sid);
|
|
165
|
+
removed = true;
|
|
166
|
+
}
|
|
167
|
+
if (removed) writeShapes(storage, shapes);
|
|
168
|
+
return removed;
|
|
169
|
+
}
|