@crowdedkingdoms/crowdyjs 12.0.0 → 12.2.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/MIGRATION.md +45 -0
- package/README.md +70 -1
- package/dist/crowdy-studio/agent-dom-shell.d.ts +29 -1
- package/dist/crowdy-studio/agent-dom-shell.d.ts.map +1 -1
- package/dist/crowdy-studio/agent-dom-shell.js +108 -44
- package/dist/crowdy-studio/dom-shell.d.ts +46 -3
- package/dist/crowdy-studio/dom-shell.d.ts.map +1 -1
- package/dist/crowdy-studio/dom-shell.js +551 -125
- package/dist/crowdy-studio/embed/dock.d.ts +56 -0
- package/dist/crowdy-studio/embed/dock.d.ts.map +1 -0
- package/dist/crowdy-studio/embed/dock.js +231 -0
- package/dist/crowdy-studio/embed/embed-styles.d.ts +12 -0
- package/dist/crowdy-studio/embed/embed-styles.d.ts.map +1 -0
- package/dist/crowdy-studio/embed/embed-styles.js +549 -0
- package/dist/crowdy-studio/embed/hud-layer.d.ts +40 -0
- package/dist/crowdy-studio/embed/hud-layer.d.ts.map +1 -0
- package/dist/crowdy-studio/embed/hud-layer.js +100 -0
- package/dist/crowdy-studio/embed/index.d.ts +5 -0
- package/dist/crowdy-studio/embed/index.d.ts.map +1 -0
- package/dist/crowdy-studio/embed/index.js +4 -0
- package/dist/crowdy-studio/embed/panel.d.ts +162 -0
- package/dist/crowdy-studio/embed/panel.d.ts.map +1 -0
- package/dist/crowdy-studio/embed/panel.js +642 -0
- package/dist/crowdy-studio/index.d.ts +3 -0
- package/dist/crowdy-studio/index.d.ts.map +1 -1
- package/dist/crowdy-studio/index.js +3 -0
- package/dist/crowdy-studio/layout.d.ts +50 -0
- package/dist/crowdy-studio/layout.d.ts.map +1 -0
- package/dist/crowdy-studio/layout.js +155 -0
- package/dist/crowdy-studio/mount.d.ts.map +1 -1
- package/dist/crowdy-studio/mount.js +3 -1
- package/dist/crowdy-studio/splitter.d.ts +33 -0
- package/dist/crowdy-studio/splitter.d.ts.map +1 -0
- package/dist/crowdy-studio/splitter.js +133 -0
- package/dist/crowdy-studio/styles.d.ts +1 -1
- package/dist/crowdy-studio/styles.d.ts.map +1 -1
- package/dist/crowdy-studio/styles.js +128 -13
- package/dist/domains/gameModel.d.ts +68 -1
- package/dist/domains/gameModel.d.ts.map +1 -1
- package/dist/domains/gameModel.js +88 -1
- package/dist/generated/graphql.d.ts +132 -15
- package/dist/generated/graphql.d.ts.map +1 -1
- package/dist/generated/graphql.js +12 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/kit/npcs.d.ts.map +1 -1
- package/dist/kit/social.d.ts.map +1 -1
- package/dist/player-host/control-banner.d.ts +40 -0
- package/dist/player-host/control-banner.d.ts.map +1 -0
- package/dist/player-host/control-banner.js +252 -0
- package/dist/player-host/control-gate.d.ts +77 -0
- package/dist/player-host/control-gate.d.ts.map +1 -0
- package/dist/player-host/control-gate.js +197 -0
- package/dist/player-host/index.d.ts +2 -0
- package/dist/player-host/index.d.ts.map +1 -1
- package/dist/player-host/index.js +2 -0
- package/package.json +6 -1
|
@@ -1,37 +1,69 @@
|
|
|
1
1
|
import { CrowdyStudioAgentDomShell, } from './agent-dom-shell.js';
|
|
2
|
+
import { StudioLayoutController, studioPaneSizeRange, } from './layout.js';
|
|
3
|
+
import { createPaneSplitter } from './splitter.js';
|
|
2
4
|
import { projectTargets, } from './models.js';
|
|
3
5
|
import { CROWDY_STUDIO_STYLES } from './styles.js';
|
|
4
|
-
|
|
6
|
+
const FAILURE_PHASES = new Set([
|
|
7
|
+
'COMPILE_FAILED',
|
|
8
|
+
'ERROR',
|
|
9
|
+
'PARTIAL_FAILURE',
|
|
10
|
+
]);
|
|
11
|
+
/**
|
|
12
|
+
* Modular DOM shell; editor implementations mount into `editorHost`.
|
|
13
|
+
*
|
|
14
|
+
* Layout is user directed: the editor is always visible while the explorer,
|
|
15
|
+
* project settings, bottom panel, and agent dock are toggled from the
|
|
16
|
+
* activity rail, resized with accessible splitters, and persisted through
|
|
17
|
+
* {@link StudioLayoutController}.
|
|
18
|
+
*/
|
|
5
19
|
export class CrowdyStudioDomShell {
|
|
6
20
|
constructor(host, controller, agentController, agentOptions = {}) {
|
|
7
21
|
this.controller = controller;
|
|
8
22
|
this.panelButtons = new Map();
|
|
9
23
|
this.panels = new Map();
|
|
24
|
+
this.railButtons = new Map();
|
|
25
|
+
this.splitters = new Map();
|
|
10
26
|
this.activePanel = 'problems';
|
|
27
|
+
this.explorerForm = null;
|
|
28
|
+
this.lastState = null;
|
|
29
|
+
this.lastPhase = 'IDLE';
|
|
11
30
|
this.disposed = false;
|
|
12
31
|
const style = document.createElement('style');
|
|
13
32
|
style.textContent = CROWDY_STUDIO_STYLES;
|
|
14
33
|
this.root = element('div', 'ck-crowdy-studio');
|
|
15
34
|
this.root.append(style);
|
|
35
|
+
this.layout = new StudioLayoutController();
|
|
36
|
+
// ----- Top bar -----------------------------------------------------
|
|
16
37
|
const toolbar = element('div', 'ck-crowdy-studio-toolbar');
|
|
17
38
|
const projectTools = element('div', 'ck-crowdy-studio-project-tools');
|
|
18
|
-
this.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
39
|
+
this.projectMenu = this.createMenu('No projects yet', {
|
|
40
|
+
buttonClass: 'ck-crowdy-studio-project-button',
|
|
41
|
+
ariaLabel: 'Current project',
|
|
42
|
+
});
|
|
43
|
+
this.projectMenuList = this.projectMenu.menu;
|
|
44
|
+
this.saveMenu = this.createMenu('Saved', {
|
|
45
|
+
buttonClass: 'ck-crowdy-studio-save',
|
|
46
|
+
ariaLabel: 'Save status',
|
|
47
|
+
});
|
|
48
|
+
this.saveMessageText = element('p', 'ck-crowdy-studio-save-message');
|
|
22
49
|
this.saveAction = button('Retry save');
|
|
23
50
|
this.remoteAction = button('Use cloud version');
|
|
24
|
-
this.
|
|
25
|
-
this.
|
|
26
|
-
|
|
51
|
+
this.saveActions = element('div', 'ck-crowdy-studio-save-actions');
|
|
52
|
+
this.saveActions.append(this.saveAction, this.remoteAction);
|
|
53
|
+
this.saveMenu.menu.append(this.saveMessageText, this.saveActions);
|
|
54
|
+
projectTools.append(this.projectMenu.wrap, this.saveMenu.wrap);
|
|
27
55
|
const runtimeTools = element('div', 'ck-crowdy-studio-runtime-tools');
|
|
28
56
|
this.testAction = button('Test draft');
|
|
29
|
-
this.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
57
|
+
this.testAction.className = 'ck-crowdy-studio-primary';
|
|
58
|
+
this.runMenu = this.createMenu('Run ▾', {
|
|
59
|
+
ariaLabel: 'More run actions',
|
|
60
|
+
});
|
|
61
|
+
this.deployAction = menuItem('Deploy live');
|
|
62
|
+
this.stopAction = menuItem('Stop project');
|
|
63
|
+
this.runMenu.menu.append(this.deployAction, this.stopAction);
|
|
64
|
+
runtimeTools.append(this.testAction, this.runMenu.wrap);
|
|
34
65
|
toolbar.append(projectTools, runtimeTools);
|
|
66
|
+
// ----- New-project popover -----------------------------------------
|
|
35
67
|
this.newForm = document.createElement('form');
|
|
36
68
|
this.newForm.className = 'ck-crowdy-studio-new';
|
|
37
69
|
this.newForm.dataset.open = 'false';
|
|
@@ -57,13 +89,34 @@ export class CrowdyStudioDomShell {
|
|
|
57
89
|
const create = button('Create project', 'submit');
|
|
58
90
|
const cancel = button('Cancel');
|
|
59
91
|
this.newForm.append(this.newName, this.newKind, create, cancel);
|
|
60
|
-
|
|
92
|
+
// ----- Workspace: rail + panes + editor -----------------------------
|
|
93
|
+
this.workspace = element('div', 'ck-crowdy-studio-workspace');
|
|
94
|
+
const rail = element('nav', 'ck-crowdy-studio-rail');
|
|
95
|
+
rail.setAttribute('aria-label', 'Studio panes');
|
|
96
|
+
rail.append(this.railButton('explorer', 'Files', 'Show or hide project files'), this.railButton('settings', 'Settings', 'Show or hide project settings'), this.railButton('bottom', 'Console', 'Show or hide the console panel'));
|
|
61
97
|
this.explorer = element('aside', 'ck-crowdy-studio-explorer');
|
|
62
|
-
|
|
98
|
+
this.explorer.setAttribute('aria-label', 'Project files');
|
|
99
|
+
this.editorColumn = element('section', 'ck-crowdy-studio-editor-column');
|
|
63
100
|
this.tabs = element('div', 'ck-crowdy-studio-tabs');
|
|
64
101
|
this.editorHost = element('div', 'ck-crowdy-studio-editor');
|
|
65
|
-
|
|
66
|
-
const
|
|
102
|
+
this.bottom = element('section', 'ck-crowdy-studio-bottom');
|
|
103
|
+
const panelTabs = element('div', 'ck-crowdy-studio-panel-tabs');
|
|
104
|
+
this.problemsPanel = this.createPanel('problems', 'Problems', panelTabs);
|
|
105
|
+
this.buildPanel = this.createPanel('build', 'Build', panelTabs);
|
|
106
|
+
this.logsPanel = this.createPanel('logs', 'Logs', panelTabs);
|
|
107
|
+
this.runsPanel = this.createPanel('runs', 'Runs', panelTabs);
|
|
108
|
+
this.invokePanel = this.createPanel('invoke', 'Invoke', panelTabs);
|
|
109
|
+
const hidePanel = button('×');
|
|
110
|
+
hidePanel.className = 'ck-crowdy-studio-panel-hide';
|
|
111
|
+
hidePanel.setAttribute('aria-label', 'Hide the console panel');
|
|
112
|
+
hidePanel.addEventListener('click', () => this.layout.setVisible('bottom', false));
|
|
113
|
+
panelTabs.append(hidePanel);
|
|
114
|
+
const panelBody = element('div', 'ck-crowdy-studio-panel-body');
|
|
115
|
+
for (const panel of this.panels.values())
|
|
116
|
+
panelBody.append(panel);
|
|
117
|
+
this.bottom.append(panelTabs, panelBody);
|
|
118
|
+
this.editorColumn.append(this.tabs, this.editorHost, this.paneSplitter('bottom', 'horizontal', 'after', 'Resize the console panel'), this.bottom);
|
|
119
|
+
this.settings = element('aside', 'ck-crowdy-studio-settings');
|
|
67
120
|
const settingsTitle = element('h3');
|
|
68
121
|
settingsTitle.textContent = 'Project settings';
|
|
69
122
|
this.settingsName = input('Project name');
|
|
@@ -75,43 +128,38 @@ export class CrowdyStudioDomShell {
|
|
|
75
128
|
['OPTIONAL', 'Optional companion'],
|
|
76
129
|
['REQUIRED', 'Require client companion'],
|
|
77
130
|
]);
|
|
78
|
-
settings.append(settingsTitle, labeled('Name', this.settingsName), labeled('Description', this.settingsDescription), labeled('Server module', this.serverModuleName), labeled('Client module', this.clientModuleName), labeled('Pairing', this.pairing));
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
this.
|
|
83
|
-
this.
|
|
84
|
-
this.
|
|
85
|
-
this.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
this.invokeExport = input('Export name');
|
|
92
|
-
this.invokeExport.value = 'invoke';
|
|
93
|
-
this.invokeParams = document.createElement('textarea');
|
|
94
|
-
this.invokeParams.placeholder = '{"example": true}';
|
|
95
|
-
this.invokeParams.setAttribute('aria-label', 'Invoke JSON parameters');
|
|
96
|
-
const invokeButton = button('Invoke server export');
|
|
97
|
-
this.invokeResult = document.createElement('pre');
|
|
98
|
-
const invokeControls = element('div', 'ck-crowdy-studio-invoke');
|
|
99
|
-
invokeControls.append(this.invokeExport, this.invokeParams, invokeButton);
|
|
100
|
-
this.invokePanel.append(invokeControls, this.invokeResult);
|
|
101
|
-
this.root.append(toolbar, this.newForm, main, bottom);
|
|
102
|
-
host.appendChild(this.root);
|
|
103
|
-
this.agentShell = agentController
|
|
104
|
-
? new CrowdyStudioAgentDomShell(this.root, agentController, agentOptions)
|
|
105
|
-
: null;
|
|
106
|
-
newButton.addEventListener('click', () => {
|
|
107
|
-
this.newForm.dataset.open =
|
|
108
|
-
this.newForm.dataset.open === 'true' ? 'false' : 'true';
|
|
109
|
-
if (this.newForm.dataset.open === 'true')
|
|
110
|
-
this.newName.focus();
|
|
111
|
-
});
|
|
112
|
-
cancel.addEventListener('click', () => {
|
|
113
|
-
this.newForm.dataset.open = 'false';
|
|
131
|
+
this.settings.append(settingsTitle, labeled('Name', this.settingsName), labeled('Description', this.settingsDescription), labeled('Server module', this.serverModuleName), labeled('Client module', this.clientModuleName), labeled('Pairing', this.pairing));
|
|
132
|
+
this.workspace.append(rail, this.explorer, this.paneSplitter('explorer', 'vertical', 'before', 'Resize the file explorer'), this.editorColumn, this.paneSplitter('settings', 'vertical', 'after', 'Resize project settings'), this.settings);
|
|
133
|
+
// ----- Status bar ----------------------------------------------------
|
|
134
|
+
const statusbar = element('footer', 'ck-crowdy-studio-statusbar');
|
|
135
|
+
this.saveStatus = element('span', 'ck-crowdy-studio-status');
|
|
136
|
+
this.runtimeStatus = document.createElement('button');
|
|
137
|
+
this.runtimeStatus.type = 'button';
|
|
138
|
+
this.runtimeStatus.className =
|
|
139
|
+
'ck-crowdy-studio-status ck-crowdy-studio-statusbar-runtime';
|
|
140
|
+
this.runtimeStatus.setAttribute('aria-label', 'Runtime status; opens the console panel');
|
|
141
|
+
this.runtimeStatus.addEventListener('click', () => {
|
|
142
|
+
this.layout.setVisible('bottom', true);
|
|
143
|
+
this.activatePanel(this.activePanel);
|
|
114
144
|
});
|
|
145
|
+
this.budgetStatus = element('span', 'ck-crowdy-studio-status');
|
|
146
|
+
statusbar.append(this.saveStatus, this.runtimeStatus, this.budgetStatus);
|
|
147
|
+
// The popover is absolutely positioned against the toolbar so it opens
|
|
148
|
+
// right under the project menu button.
|
|
149
|
+
toolbar.append(this.newForm);
|
|
150
|
+
this.root.append(toolbar, this.workspace, statusbar);
|
|
151
|
+
host.appendChild(this.root);
|
|
152
|
+
// ----- Agent dock ----------------------------------------------------
|
|
153
|
+
if (agentController) {
|
|
154
|
+
rail.append(this.railButton('agent', 'Agent', 'Show or hide the Crowdy Agent dock'));
|
|
155
|
+
this.workspace.append(this.paneSplitter('agent', 'vertical', 'after', 'Resize the agent dock'));
|
|
156
|
+
this.agentShell = new CrowdyStudioAgentDomShell(this.workspace, agentController, { ...agentOptions, layout: agentOptions.layout ?? this.layout });
|
|
157
|
+
this.root.dataset.agent = 'true';
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
this.agentShell = null;
|
|
161
|
+
}
|
|
162
|
+
// ----- Wiring ---------------------------------------------------------
|
|
115
163
|
this.newForm.addEventListener('submit', (event) => {
|
|
116
164
|
event.preventDefault();
|
|
117
165
|
void this.run(async () => {
|
|
@@ -123,18 +171,32 @@ export class CrowdyStudioDomShell {
|
|
|
123
171
|
this.newForm.dataset.open = 'false';
|
|
124
172
|
});
|
|
125
173
|
});
|
|
126
|
-
|
|
127
|
-
|
|
174
|
+
cancel.addEventListener('click', () => {
|
|
175
|
+
this.newForm.dataset.open = 'false';
|
|
176
|
+
});
|
|
177
|
+
this.testAction.addEventListener('click', () => {
|
|
178
|
+
this.revealPanel('build');
|
|
179
|
+
void this.run(() => this.controller.testDraft());
|
|
180
|
+
});
|
|
181
|
+
this.deployAction.addEventListener('click', () => {
|
|
182
|
+
this.runMenu.close();
|
|
183
|
+
this.revealPanel('build');
|
|
184
|
+
void this.run(() => this.controller.deployLive());
|
|
185
|
+
});
|
|
186
|
+
this.stopAction.addEventListener('click', () => {
|
|
187
|
+
this.runMenu.close();
|
|
188
|
+
void this.run(() => this.controller.stopProject());
|
|
128
189
|
});
|
|
129
|
-
this.testAction.addEventListener('click', () => void this.run(() => this.controller.testDraft()));
|
|
130
|
-
this.deployAction.addEventListener('click', () => void this.run(() => this.controller.deployLive()));
|
|
131
|
-
stop.addEventListener('click', () => void this.run(() => this.controller.stopProject()));
|
|
132
190
|
this.saveAction.addEventListener('click', () => {
|
|
191
|
+
this.saveMenu.close();
|
|
133
192
|
void this.run(() => this.controller.getState().saveState === 'CONFLICT'
|
|
134
193
|
? this.controller.overwriteConflict()
|
|
135
194
|
: this.controller.retrySave());
|
|
136
195
|
});
|
|
137
|
-
this.remoteAction.addEventListener('click', () =>
|
|
196
|
+
this.remoteAction.addEventListener('click', () => {
|
|
197
|
+
this.saveMenu.close();
|
|
198
|
+
void this.run(() => this.controller.acceptRemoteConflict());
|
|
199
|
+
});
|
|
138
200
|
this.settingsName.addEventListener('change', () => this.controller.updateSettings({ name: this.settingsName.value }));
|
|
139
201
|
this.settingsDescription.addEventListener('change', () => this.controller.updateSettings({
|
|
140
202
|
description: this.settingsDescription.value,
|
|
@@ -146,15 +208,28 @@ export class CrowdyStudioDomShell {
|
|
|
146
208
|
clientModuleName: this.clientModuleName.value,
|
|
147
209
|
}));
|
|
148
210
|
this.pairing.addEventListener('change', () => this.controller.setPairingPreference(this.pairing.value));
|
|
211
|
+
this.invokeExport = input('Export name');
|
|
212
|
+
this.invokeExport.value = 'invoke';
|
|
213
|
+
this.invokeParams = document.createElement('textarea');
|
|
214
|
+
this.invokeParams.placeholder = '{"example": true}';
|
|
215
|
+
this.invokeParams.setAttribute('aria-label', 'Invoke JSON parameters');
|
|
216
|
+
const invokeButton = button('Invoke server export');
|
|
217
|
+
this.invokeResult = document.createElement('pre');
|
|
218
|
+
const invokeControls = element('div', 'ck-crowdy-studio-invoke');
|
|
219
|
+
invokeControls.append(this.invokeExport, this.invokeParams, invokeButton);
|
|
220
|
+
this.invokePanel.append(invokeControls, this.invokeResult);
|
|
149
221
|
invokeButton.addEventListener('click', () => {
|
|
150
222
|
void this.run(() => this.controller.invoke(this.invokeExport.value, this.invokeParams.value));
|
|
151
223
|
});
|
|
152
224
|
this.controller.setSurfaceVisible('usage', true);
|
|
225
|
+
this.unsubscribeLayout = this.layout.subscribe((state) => this.applyLayout(state));
|
|
226
|
+
this.applyLayout(this.layout.getState());
|
|
153
227
|
this.activatePanel('problems');
|
|
154
228
|
}
|
|
155
229
|
render(state) {
|
|
156
230
|
if (this.disposed)
|
|
157
231
|
return;
|
|
232
|
+
this.lastState = state;
|
|
158
233
|
this.renderProjects(state);
|
|
159
234
|
this.renderSaveState(state);
|
|
160
235
|
this.renderExplorer(state);
|
|
@@ -172,14 +247,13 @@ export class CrowdyStudioDomShell {
|
|
|
172
247
|
this.invokeResult.textContent = state.invokeResult
|
|
173
248
|
? formatInvokeResult(state.invokeResult)
|
|
174
249
|
: '';
|
|
175
|
-
this.runtimeStatus.textContent =
|
|
176
|
-
state.runtime.phase,
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
.filter(Boolean)
|
|
181
|
-
.join(' · ');
|
|
250
|
+
this.runtimeStatus.textContent =
|
|
251
|
+
[state.runtime.phase, state.runtime.target, state.runtime.message]
|
|
252
|
+
.filter(Boolean)
|
|
253
|
+
.join(' · ') || 'IDLE';
|
|
254
|
+
this.runtimeStatus.dataset.phase = state.runtime.phase;
|
|
182
255
|
this.budgetStatus.textContent = budgetText(state);
|
|
256
|
+
this.autoRevealFailures(state);
|
|
183
257
|
}
|
|
184
258
|
dispose() {
|
|
185
259
|
if (this.disposed)
|
|
@@ -188,9 +262,174 @@ export class CrowdyStudioDomShell {
|
|
|
188
262
|
this.controller.setSurfaceVisible('usage', false);
|
|
189
263
|
this.controller.setSurfaceVisible('logs', false);
|
|
190
264
|
this.controller.setSurfaceVisible('runs', false);
|
|
265
|
+
this.unsubscribeLayout();
|
|
266
|
+
for (const splitter of this.splitters.values())
|
|
267
|
+
splitter.dispose();
|
|
268
|
+
this.projectMenu.dispose();
|
|
269
|
+
this.saveMenu.dispose();
|
|
270
|
+
this.runMenu.dispose();
|
|
191
271
|
this.agentShell?.dispose();
|
|
192
272
|
this.root.remove();
|
|
193
273
|
}
|
|
274
|
+
// ----- Layout ----------------------------------------------------------
|
|
275
|
+
railButton(pane, label, description) {
|
|
276
|
+
const control = button(label);
|
|
277
|
+
control.className = 'ck-crowdy-studio-rail-button';
|
|
278
|
+
control.dataset.pane = pane;
|
|
279
|
+
control.title = description;
|
|
280
|
+
control.setAttribute('aria-pressed', 'false');
|
|
281
|
+
control.addEventListener('click', () => {
|
|
282
|
+
const wasVisible = this.layout.isVisible(pane);
|
|
283
|
+
this.layout.toggle(pane);
|
|
284
|
+
if (!wasVisible)
|
|
285
|
+
this.collapseOverlappingPanes(pane);
|
|
286
|
+
});
|
|
287
|
+
this.railButtons.set(pane, control);
|
|
288
|
+
return control;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Narrow widths turn side panes into overlays (styles.ts container
|
|
292
|
+
* queries), where several open panes would stack invisibly on top of each
|
|
293
|
+
* other. Enforce the "single pane" rule there: opening one side pane
|
|
294
|
+
* closes the others that overlay at the current width.
|
|
295
|
+
*/
|
|
296
|
+
collapseOverlappingPanes(opened) {
|
|
297
|
+
if (opened === 'bottom')
|
|
298
|
+
return;
|
|
299
|
+
const width = this.root.clientWidth;
|
|
300
|
+
if (!width)
|
|
301
|
+
return;
|
|
302
|
+
const overlaying = [];
|
|
303
|
+
if (width <= 900)
|
|
304
|
+
overlaying.push('settings');
|
|
305
|
+
if (width <= 760)
|
|
306
|
+
overlaying.push('agent');
|
|
307
|
+
if (width <= 620)
|
|
308
|
+
overlaying.push('explorer');
|
|
309
|
+
if (!overlaying.includes(opened))
|
|
310
|
+
return;
|
|
311
|
+
for (const pane of overlaying) {
|
|
312
|
+
if (pane !== opened && this.layout.isVisible(pane)) {
|
|
313
|
+
this.layout.setVisible(pane, false);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
paneSplitter(pane, orientation, position, label) {
|
|
318
|
+
const splitter = createPaneSplitter({
|
|
319
|
+
orientation,
|
|
320
|
+
pane: position,
|
|
321
|
+
label,
|
|
322
|
+
range: () => studioPaneSizeRange(pane),
|
|
323
|
+
getSize: () => this.layout.paneSize(pane),
|
|
324
|
+
setSize: (size, commit) => this.layout.setSize(pane, size, commit),
|
|
325
|
+
});
|
|
326
|
+
this.splitters.set(pane, splitter);
|
|
327
|
+
return splitter.element;
|
|
328
|
+
}
|
|
329
|
+
applyLayout(state) {
|
|
330
|
+
const paneElements = {
|
|
331
|
+
explorer: this.explorer,
|
|
332
|
+
settings: this.settings,
|
|
333
|
+
bottom: this.bottom,
|
|
334
|
+
agent: this.agentShell?.root ?? null,
|
|
335
|
+
};
|
|
336
|
+
for (const pane of ['explorer', 'settings', 'bottom', 'agent']) {
|
|
337
|
+
const paneElement = paneElements[pane];
|
|
338
|
+
const visible = state.visible[pane] && paneElement !== null;
|
|
339
|
+
if (paneElement) {
|
|
340
|
+
paneElement.hidden = !visible;
|
|
341
|
+
if (pane === 'bottom') {
|
|
342
|
+
paneElement.style.height = `${state.sizes[pane]}px`;
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
paneElement.style.width = `${state.sizes[pane]}px`;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const splitter = this.splitters.get(pane);
|
|
349
|
+
if (splitter) {
|
|
350
|
+
splitter.element.hidden = !visible;
|
|
351
|
+
splitter.refresh();
|
|
352
|
+
}
|
|
353
|
+
const railControl = this.railButtons.get(pane);
|
|
354
|
+
if (railControl) {
|
|
355
|
+
railControl.setAttribute('aria-pressed', String(visible));
|
|
356
|
+
railControl.dataset.active = String(visible);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
this.syncPanelPolling();
|
|
360
|
+
}
|
|
361
|
+
revealPanel(name) {
|
|
362
|
+
this.layout.setVisible('bottom', true);
|
|
363
|
+
this.activatePanel(name);
|
|
364
|
+
}
|
|
365
|
+
autoRevealFailures(state) {
|
|
366
|
+
const phase = state.runtime.phase;
|
|
367
|
+
if (phase !== this.lastPhase && FAILURE_PHASES.has(phase)) {
|
|
368
|
+
const diagnostics = state.authoritativeDiagnostics.length + state.localDiagnostics.length;
|
|
369
|
+
this.revealPanel(diagnostics > 0 ? 'problems' : 'build');
|
|
370
|
+
}
|
|
371
|
+
this.lastPhase = phase;
|
|
372
|
+
}
|
|
373
|
+
// ----- Menus -----------------------------------------------------------
|
|
374
|
+
createMenu(label, options = {}) {
|
|
375
|
+
const wrap = element('div', 'ck-crowdy-studio-menu-wrap');
|
|
376
|
+
const control = button(label);
|
|
377
|
+
control.className = options.buttonClass
|
|
378
|
+
? `ck-crowdy-studio-menu-button ${options.buttonClass}`
|
|
379
|
+
: 'ck-crowdy-studio-menu-button';
|
|
380
|
+
if (options.ariaLabel)
|
|
381
|
+
control.setAttribute('aria-label', options.ariaLabel);
|
|
382
|
+
control.setAttribute('aria-haspopup', 'true');
|
|
383
|
+
control.setAttribute('aria-expanded', 'false');
|
|
384
|
+
const menu = element('div', 'ck-crowdy-studio-menu');
|
|
385
|
+
menu.hidden = true;
|
|
386
|
+
menu.setAttribute('role', 'menu');
|
|
387
|
+
wrap.append(control, menu);
|
|
388
|
+
const outsideClick = (event) => {
|
|
389
|
+
if (!wrap.contains(event.target))
|
|
390
|
+
close();
|
|
391
|
+
};
|
|
392
|
+
const escape = (event) => {
|
|
393
|
+
if (event.key !== 'Escape')
|
|
394
|
+
return;
|
|
395
|
+
event.stopPropagation();
|
|
396
|
+
close();
|
|
397
|
+
control.focus();
|
|
398
|
+
};
|
|
399
|
+
const close = () => {
|
|
400
|
+
if (menu.hidden)
|
|
401
|
+
return;
|
|
402
|
+
menu.hidden = true;
|
|
403
|
+
control.setAttribute('aria-expanded', 'false');
|
|
404
|
+
document.removeEventListener('pointerdown', outsideClick, true);
|
|
405
|
+
wrap.removeEventListener('keydown', escape);
|
|
406
|
+
};
|
|
407
|
+
const open = () => {
|
|
408
|
+
if (!menu.hidden)
|
|
409
|
+
return;
|
|
410
|
+
menu.hidden = false;
|
|
411
|
+
control.setAttribute('aria-expanded', 'true');
|
|
412
|
+
document.addEventListener('pointerdown', outsideClick, true);
|
|
413
|
+
wrap.addEventListener('keydown', escape);
|
|
414
|
+
};
|
|
415
|
+
control.addEventListener('click', () => {
|
|
416
|
+
if (menu.hidden)
|
|
417
|
+
open();
|
|
418
|
+
else
|
|
419
|
+
close();
|
|
420
|
+
});
|
|
421
|
+
return {
|
|
422
|
+
button: control,
|
|
423
|
+
menu,
|
|
424
|
+
wrap,
|
|
425
|
+
close,
|
|
426
|
+
dispose() {
|
|
427
|
+
close();
|
|
428
|
+
wrap.remove();
|
|
429
|
+
},
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
// ----- Panels ------------------------------------------------------------
|
|
194
433
|
createPanel(name, label, tabs) {
|
|
195
434
|
const tab = button(label);
|
|
196
435
|
tab.className = 'ck-crowdy-studio-panel-tab';
|
|
@@ -203,7 +442,6 @@ export class CrowdyStudioDomShell {
|
|
|
203
442
|
return panel;
|
|
204
443
|
}
|
|
205
444
|
activatePanel(name) {
|
|
206
|
-
const previous = this.activePanel;
|
|
207
445
|
this.activePanel = name;
|
|
208
446
|
for (const [key, buttonElement] of this.panelButtons) {
|
|
209
447
|
buttonElement.dataset.active = String(key === name);
|
|
@@ -211,32 +449,43 @@ export class CrowdyStudioDomShell {
|
|
|
211
449
|
for (const [key, panel] of this.panels) {
|
|
212
450
|
panel.dataset.active = String(key === name);
|
|
213
451
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
452
|
+
this.syncPanelPolling();
|
|
453
|
+
}
|
|
454
|
+
/** Poll logs/runs only while their panel is both selected and shown. */
|
|
455
|
+
syncPanelPolling() {
|
|
456
|
+
const bottomVisible = this.layout.isVisible('bottom');
|
|
457
|
+
for (const surface of ['logs', 'runs']) {
|
|
458
|
+
this.controller.setSurfaceVisible(surface, bottomVisible && this.activePanel === surface);
|
|
218
459
|
}
|
|
219
|
-
if (nextPoll)
|
|
220
|
-
this.controller.setSurfaceVisible(nextPoll, true);
|
|
221
460
|
}
|
|
461
|
+
// ----- Rendering -----------------------------------------------------------
|
|
222
462
|
renderProjects(state) {
|
|
223
|
-
const selected = state.project
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
463
|
+
const selected = state.project;
|
|
464
|
+
this.projectMenu.button.textContent = selected
|
|
465
|
+
? `${selected.metadata.name} ▾`
|
|
466
|
+
: state.projects.length === 0
|
|
467
|
+
? 'No projects yet ▾'
|
|
468
|
+
: 'Select project ▾';
|
|
469
|
+
const items = state.projects.map((project) => {
|
|
470
|
+
const item = menuItem(`${project.name} · ${project.kind.toLowerCase().replace('_', ' ')}`);
|
|
471
|
+
item.dataset.active = String(project.projectId === selected?.projectId);
|
|
472
|
+
item.addEventListener('click', () => {
|
|
473
|
+
this.projectMenu.close();
|
|
474
|
+
void this.run(() => this.controller.switchProject(project.projectId));
|
|
475
|
+
});
|
|
476
|
+
return item;
|
|
231
477
|
});
|
|
232
|
-
if (
|
|
233
|
-
|
|
234
|
-
empty.value = '';
|
|
235
|
-
empty.textContent = 'No projects yet';
|
|
236
|
-
options.push(empty);
|
|
478
|
+
if (items.length === 0) {
|
|
479
|
+
items.push(empty('No projects yet.'));
|
|
237
480
|
}
|
|
238
|
-
|
|
239
|
-
|
|
481
|
+
const divider = element('div', 'ck-crowdy-studio-menu-divider');
|
|
482
|
+
const newProject = menuItem('New project…');
|
|
483
|
+
newProject.addEventListener('click', () => {
|
|
484
|
+
this.projectMenu.close();
|
|
485
|
+
this.newForm.dataset.open = 'true';
|
|
486
|
+
this.newName.focus();
|
|
487
|
+
});
|
|
488
|
+
this.projectMenuList.replaceChildren(...items, divider, newProject);
|
|
240
489
|
}
|
|
241
490
|
renderSaveState(state) {
|
|
242
491
|
const label = {
|
|
@@ -245,16 +494,25 @@ export class CrowdyStudioDomShell {
|
|
|
245
494
|
CONFLICT: 'Conflict',
|
|
246
495
|
OFFLINE: 'Offline',
|
|
247
496
|
}[state.saveState];
|
|
248
|
-
this.
|
|
249
|
-
this.
|
|
497
|
+
this.saveMenu.button.dataset.state = state.saveState;
|
|
498
|
+
this.saveMenu.button.textContent = label;
|
|
499
|
+
this.saveStatus.textContent = state.saveMessage
|
|
250
500
|
? `${label}: ${state.saveMessage}`
|
|
251
501
|
: label;
|
|
252
|
-
this.
|
|
502
|
+
this.saveMessageText.textContent = state.saveMessage
|
|
503
|
+
? state.saveMessage
|
|
504
|
+
: state.saveState === 'SAVED'
|
|
505
|
+
? 'Edits autosave to your private project.'
|
|
506
|
+
: label;
|
|
507
|
+
const attention = ['CONFLICT', 'OFFLINE'].includes(state.saveState);
|
|
508
|
+
this.saveAction.hidden = !attention;
|
|
253
509
|
this.saveAction.textContent =
|
|
254
510
|
state.saveState === 'CONFLICT' ? 'Keep my version' : 'Retry save';
|
|
255
511
|
this.remoteAction.hidden = state.saveState !== 'CONFLICT';
|
|
512
|
+
this.saveMenu.button.dataset.attention = String(attention);
|
|
256
513
|
}
|
|
257
514
|
renderExplorer(state) {
|
|
515
|
+
const focused = focusedExplorerField(this.explorer);
|
|
258
516
|
this.explorer.replaceChildren();
|
|
259
517
|
if (!state.project) {
|
|
260
518
|
this.explorer.append(empty('Create a project to start authoring.'));
|
|
@@ -269,6 +527,7 @@ export class CrowdyStudioDomShell {
|
|
|
269
527
|
}))));
|
|
270
528
|
}
|
|
271
529
|
this.explorer.append(this.referenceSection('Personal library', state.personalLibraryFiles), this.referenceSection('Common files', state.commonFiles));
|
|
530
|
+
restoreExplorerFocus(this.explorer, focused);
|
|
272
531
|
}
|
|
273
532
|
projectSection(target, refs) {
|
|
274
533
|
const section = element('section', 'ck-crowdy-studio-section');
|
|
@@ -279,13 +538,29 @@ export class CrowdyStudioDomShell {
|
|
|
279
538
|
add.setAttribute('aria-label', `Add ${target} file`);
|
|
280
539
|
add.disabled = !this.controller.canTarget(target, 'write');
|
|
281
540
|
add.addEventListener('click', () => {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
541
|
+
this.explorerForm =
|
|
542
|
+
this.explorerForm?.kind === 'add' && this.explorerForm.target === target
|
|
543
|
+
? null
|
|
544
|
+
: { kind: 'add', target, value: 'src/new.rs' };
|
|
545
|
+
this.rerenderExplorer();
|
|
286
546
|
});
|
|
287
547
|
header.append(title, add);
|
|
288
548
|
section.append(header);
|
|
549
|
+
if (this.explorerForm?.kind === 'add' && this.explorerForm.target === target) {
|
|
550
|
+
section.append(this.inlineForm({
|
|
551
|
+
value: this.explorerForm.value,
|
|
552
|
+
placeholder: `New ${target} file path`,
|
|
553
|
+
submitLabel: 'Add file',
|
|
554
|
+
onInput: (value) => {
|
|
555
|
+
if (this.explorerForm?.kind === 'add')
|
|
556
|
+
this.explorerForm.value = value;
|
|
557
|
+
},
|
|
558
|
+
onSubmit: (value) => {
|
|
559
|
+
this.explorerForm = null;
|
|
560
|
+
void this.run(() => Promise.resolve(this.controller.addFile(target, value)));
|
|
561
|
+
},
|
|
562
|
+
}));
|
|
563
|
+
}
|
|
289
564
|
for (const ref of refs) {
|
|
290
565
|
section.append(this.projectFileRow(ref));
|
|
291
566
|
}
|
|
@@ -294,38 +569,97 @@ export class CrowdyStudioDomShell {
|
|
|
294
569
|
return section;
|
|
295
570
|
}
|
|
296
571
|
projectFileRow(ref) {
|
|
572
|
+
const form = this.explorerForm;
|
|
573
|
+
if (form &&
|
|
574
|
+
form.kind !== 'add' &&
|
|
575
|
+
form.kind !== 'import' &&
|
|
576
|
+
form.target === ref.target &&
|
|
577
|
+
form.path === ref.path) {
|
|
578
|
+
return this.projectFileFormRow(ref, form);
|
|
579
|
+
}
|
|
297
580
|
const row = element('div', 'ck-crowdy-studio-file');
|
|
298
581
|
const open = button(ref.path);
|
|
299
582
|
open.title = `${ref.target}:${ref.path}`;
|
|
300
583
|
open.addEventListener('click', () => this.controller.openFile(ref));
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const next = window.prompt('Rename file', ref.path);
|
|
305
|
-
if (!next || !ref.target)
|
|
306
|
-
return;
|
|
307
|
-
void this.run(() => Promise.resolve(this.controller.renameFile(ref.target, ref.path, next)));
|
|
584
|
+
const actions = this.createMenu('…', {
|
|
585
|
+
ariaLabel: `Actions for ${ref.path}`,
|
|
586
|
+
buttonClass: 'ck-crowdy-studio-file-action',
|
|
308
587
|
});
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
if (!ref.target
|
|
313
|
-
!window.confirm(`Delete ${ref.target}:${ref.path}?`)) {
|
|
588
|
+
const library = menuItem('Save to library');
|
|
589
|
+
library.addEventListener('click', () => {
|
|
590
|
+
actions.close();
|
|
591
|
+
if (!ref.target)
|
|
314
592
|
return;
|
|
315
|
-
|
|
316
|
-
|
|
593
|
+
this.explorerForm = {
|
|
594
|
+
kind: 'library',
|
|
595
|
+
target: ref.target,
|
|
596
|
+
path: ref.path,
|
|
597
|
+
value: ref.path.split('/').at(-1) ?? ref.path,
|
|
598
|
+
};
|
|
599
|
+
this.rerenderExplorer();
|
|
317
600
|
});
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
601
|
+
const rename = menuItem('Rename');
|
|
602
|
+
rename.addEventListener('click', () => {
|
|
603
|
+
actions.close();
|
|
321
604
|
if (!ref.target)
|
|
322
605
|
return;
|
|
323
|
-
|
|
324
|
-
|
|
606
|
+
this.explorerForm = {
|
|
607
|
+
kind: 'rename',
|
|
608
|
+
target: ref.target,
|
|
609
|
+
path: ref.path,
|
|
610
|
+
value: ref.path,
|
|
611
|
+
};
|
|
612
|
+
this.rerenderExplorer();
|
|
613
|
+
});
|
|
614
|
+
const remove = menuItem('Delete');
|
|
615
|
+
remove.addEventListener('click', () => {
|
|
616
|
+
actions.close();
|
|
617
|
+
if (!ref.target)
|
|
325
618
|
return;
|
|
326
|
-
|
|
619
|
+
this.explorerForm = { kind: 'delete', target: ref.target, path: ref.path };
|
|
620
|
+
this.rerenderExplorer();
|
|
327
621
|
});
|
|
328
|
-
|
|
622
|
+
actions.menu.append(library, rename, remove);
|
|
623
|
+
row.append(open, actions.wrap);
|
|
624
|
+
return row;
|
|
625
|
+
}
|
|
626
|
+
projectFileFormRow(ref, form) {
|
|
627
|
+
const row = element('div', 'ck-crowdy-studio-file-form-row');
|
|
628
|
+
if (form.kind === 'delete') {
|
|
629
|
+
const text = document.createElement('span');
|
|
630
|
+
text.textContent = `Delete ${ref.target}:${ref.path}?`;
|
|
631
|
+
const confirm = button('Delete');
|
|
632
|
+
confirm.addEventListener('click', () => {
|
|
633
|
+
this.explorerForm = null;
|
|
634
|
+
void this.run(() => Promise.resolve(this.controller.deleteFile(form.target, form.path)));
|
|
635
|
+
});
|
|
636
|
+
const cancel = button('Cancel');
|
|
637
|
+
cancel.addEventListener('click', () => {
|
|
638
|
+
this.explorerForm = null;
|
|
639
|
+
this.rerenderExplorer();
|
|
640
|
+
});
|
|
641
|
+
row.append(text, confirm, cancel);
|
|
642
|
+
return row;
|
|
643
|
+
}
|
|
644
|
+
row.append(this.inlineForm({
|
|
645
|
+
value: form.value,
|
|
646
|
+
placeholder: form.kind === 'rename' ? 'New file path' : 'Library file title',
|
|
647
|
+
submitLabel: form.kind === 'rename' ? 'Rename' : 'Save to library',
|
|
648
|
+
onInput: (value) => {
|
|
649
|
+
if (this.explorerForm && this.explorerForm.kind === form.kind) {
|
|
650
|
+
this.explorerForm.value = value;
|
|
651
|
+
}
|
|
652
|
+
},
|
|
653
|
+
onSubmit: (value) => {
|
|
654
|
+
this.explorerForm = null;
|
|
655
|
+
if (form.kind === 'rename') {
|
|
656
|
+
void this.run(() => Promise.resolve(this.controller.renameFile(form.target, form.path, value)));
|
|
657
|
+
}
|
|
658
|
+
else {
|
|
659
|
+
void this.run(() => this.controller.saveProjectFileToLibrary(form.target, form.path, value));
|
|
660
|
+
}
|
|
661
|
+
},
|
|
662
|
+
}));
|
|
329
663
|
return row;
|
|
330
664
|
}
|
|
331
665
|
referenceSection(titleText, files) {
|
|
@@ -346,18 +680,78 @@ export class CrowdyStudioDomShell {
|
|
|
346
680
|
const add = button('Add');
|
|
347
681
|
add.setAttribute('aria-label', `Add ${file.title} to project`);
|
|
348
682
|
add.addEventListener('click', () => {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
683
|
+
this.explorerForm =
|
|
684
|
+
this.explorerForm?.kind === 'import' &&
|
|
685
|
+
this.explorerForm.referenceId === file.id
|
|
686
|
+
? null
|
|
687
|
+
: { kind: 'import', referenceId: file.id, value: file.path };
|
|
688
|
+
this.rerenderExplorer();
|
|
353
689
|
});
|
|
354
690
|
row.append(open, add);
|
|
355
691
|
section.append(row);
|
|
692
|
+
if (this.explorerForm?.kind === 'import' &&
|
|
693
|
+
this.explorerForm.referenceId === file.id) {
|
|
694
|
+
section.append(this.inlineForm({
|
|
695
|
+
value: this.explorerForm.value,
|
|
696
|
+
placeholder: 'Destination project path',
|
|
697
|
+
submitLabel: 'Add to project',
|
|
698
|
+
onInput: (value) => {
|
|
699
|
+
if (this.explorerForm?.kind === 'import') {
|
|
700
|
+
this.explorerForm.value = value;
|
|
701
|
+
}
|
|
702
|
+
},
|
|
703
|
+
onSubmit: (value) => {
|
|
704
|
+
this.explorerForm = null;
|
|
705
|
+
void this.run(() => this.controller.importReferenceFile(file, value));
|
|
706
|
+
},
|
|
707
|
+
}));
|
|
708
|
+
}
|
|
356
709
|
}
|
|
357
710
|
if (files.length === 0)
|
|
358
711
|
section.append(empty('No files'));
|
|
359
712
|
return section;
|
|
360
713
|
}
|
|
714
|
+
inlineForm(options) {
|
|
715
|
+
const form = document.createElement('form');
|
|
716
|
+
form.className = 'ck-crowdy-studio-inline-form';
|
|
717
|
+
const field = document.createElement('input');
|
|
718
|
+
field.value = options.value;
|
|
719
|
+
field.placeholder = options.placeholder;
|
|
720
|
+
field.setAttribute('aria-label', options.placeholder);
|
|
721
|
+
field.dataset.explorerField = 'true';
|
|
722
|
+
field.addEventListener('input', () => options.onInput(field.value));
|
|
723
|
+
const submit = button(options.submitLabel, 'submit');
|
|
724
|
+
const cancel = button('Cancel');
|
|
725
|
+
cancel.addEventListener('click', () => {
|
|
726
|
+
this.explorerForm = null;
|
|
727
|
+
this.rerenderExplorer();
|
|
728
|
+
});
|
|
729
|
+
form.append(field, submit, cancel);
|
|
730
|
+
form.addEventListener('submit', (event) => {
|
|
731
|
+
event.preventDefault();
|
|
732
|
+
const value = field.value.trim();
|
|
733
|
+
if (!value)
|
|
734
|
+
return;
|
|
735
|
+
options.onSubmit(value);
|
|
736
|
+
});
|
|
737
|
+
queueMicrotask(() => {
|
|
738
|
+
try {
|
|
739
|
+
if (field.isConnected &&
|
|
740
|
+
!this.explorer.contains(document.activeElement)) {
|
|
741
|
+
field.focus();
|
|
742
|
+
field.select();
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
catch {
|
|
746
|
+
// Focus management is an enhancement only.
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
return form;
|
|
750
|
+
}
|
|
751
|
+
rerenderExplorer() {
|
|
752
|
+
if (this.lastState)
|
|
753
|
+
this.renderExplorer(this.lastState);
|
|
754
|
+
}
|
|
361
755
|
renderTabs(state) {
|
|
362
756
|
this.tabs.replaceChildren();
|
|
363
757
|
for (const ref of state.openFiles) {
|
|
@@ -406,6 +800,11 @@ export class CrowdyStudioDomShell {
|
|
|
406
800
|
...state.authoritativeDiagnostics,
|
|
407
801
|
...state.localDiagnostics,
|
|
408
802
|
];
|
|
803
|
+
const problemsTab = this.panelButtons.get('problems');
|
|
804
|
+
if (problemsTab) {
|
|
805
|
+
problemsTab.textContent =
|
|
806
|
+
diagnostics.length > 0 ? `Problems (${diagnostics.length})` : 'Problems';
|
|
807
|
+
}
|
|
409
808
|
if (diagnostics.length === 0) {
|
|
410
809
|
this.problemsPanel.append(empty('No problems.'));
|
|
411
810
|
return;
|
|
@@ -457,11 +856,6 @@ export class CrowdyStudioDomShell {
|
|
|
457
856
|
}
|
|
458
857
|
}
|
|
459
858
|
}
|
|
460
|
-
function polledSurface(panel) {
|
|
461
|
-
if (panel === 'logs' || panel === 'runs')
|
|
462
|
-
return panel;
|
|
463
|
-
return null;
|
|
464
|
-
}
|
|
465
859
|
function budgetText(state) {
|
|
466
860
|
const usage = state.usage
|
|
467
861
|
? `units ${state.usage.hourUnitsUsed}/${state.usage.unitsPerHour ?? '∞'} · compiles ${state.usage.compilesThisHour}/${state.usage.maxCompilesPerHour}`
|
|
@@ -486,6 +880,32 @@ function sameRef(a, b) {
|
|
|
486
880
|
a.path === b.path &&
|
|
487
881
|
a.referenceId === b.referenceId);
|
|
488
882
|
}
|
|
883
|
+
function focusedExplorerField(explorer) {
|
|
884
|
+
if (typeof HTMLInputElement === 'undefined')
|
|
885
|
+
return null;
|
|
886
|
+
const active = document.activeElement;
|
|
887
|
+
if (active instanceof HTMLInputElement &&
|
|
888
|
+
explorer.contains(active) &&
|
|
889
|
+
active.dataset.explorerField === 'true') {
|
|
890
|
+
return active.value;
|
|
891
|
+
}
|
|
892
|
+
return null;
|
|
893
|
+
}
|
|
894
|
+
function restoreExplorerFocus(explorer, value) {
|
|
895
|
+
if (value === null)
|
|
896
|
+
return;
|
|
897
|
+
const field = explorer.querySelector('input[data-explorer-field="true"]');
|
|
898
|
+
if (!field)
|
|
899
|
+
return;
|
|
900
|
+
field.focus();
|
|
901
|
+
const end = field.value.length;
|
|
902
|
+
try {
|
|
903
|
+
field.setSelectionRange(end, end);
|
|
904
|
+
}
|
|
905
|
+
catch {
|
|
906
|
+
// Selection APIs are an enhancement only.
|
|
907
|
+
}
|
|
908
|
+
}
|
|
489
909
|
function element(tag, className) {
|
|
490
910
|
const value = document.createElement(tag);
|
|
491
911
|
if (className)
|
|
@@ -498,6 +918,12 @@ function button(label, type = 'button') {
|
|
|
498
918
|
value.textContent = label;
|
|
499
919
|
return value;
|
|
500
920
|
}
|
|
921
|
+
function menuItem(label) {
|
|
922
|
+
const value = button(label);
|
|
923
|
+
value.className = 'ck-crowdy-studio-menu-item';
|
|
924
|
+
value.setAttribute('role', 'menuitem');
|
|
925
|
+
return value;
|
|
926
|
+
}
|
|
501
927
|
function input(placeholder) {
|
|
502
928
|
const value = document.createElement('input');
|
|
503
929
|
value.placeholder = placeholder;
|