@crowdedkingdoms/crowdyjs 11.1.0 → 12.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION.md +152 -0
- package/README.md +262 -0
- package/dist/crowdy-agent/browser-dispatcher.d.ts +41 -0
- package/dist/crowdy-agent/browser-dispatcher.d.ts.map +1 -0
- package/dist/crowdy-agent/browser-dispatcher.js +264 -0
- package/dist/crowdy-agent/controller.d.ts +144 -0
- package/dist/crowdy-agent/controller.d.ts.map +1 -0
- package/dist/crowdy-agent/controller.js +1239 -0
- package/dist/crowdy-agent/errors.d.ts +37 -0
- package/dist/crowdy-agent/errors.d.ts.map +1 -0
- package/dist/crowdy-agent/errors.js +107 -0
- package/dist/crowdy-agent/graphql-transport.d.ts +154 -0
- package/dist/crowdy-agent/graphql-transport.d.ts.map +1 -0
- package/dist/crowdy-agent/graphql-transport.js +854 -0
- package/dist/crowdy-agent/index.d.ts +16 -0
- package/dist/crowdy-agent/index.d.ts.map +1 -0
- package/dist/crowdy-agent/index.js +15 -0
- package/dist/crowdy-agent/registry.d.ts +29 -0
- package/dist/crowdy-agent/registry.d.ts.map +1 -0
- package/dist/crowdy-agent/registry.js +281 -0
- package/dist/crowdy-agent/schema.d.ts +72 -0
- package/dist/crowdy-agent/schema.d.ts.map +1 -0
- package/dist/crowdy-agent/schema.js +467 -0
- package/dist/crowdy-agent/studio-tools.d.ts +15 -0
- package/dist/crowdy-agent/studio-tools.d.ts.map +1 -0
- package/dist/crowdy-agent/studio-tools.js +280 -0
- package/dist/crowdy-agent/tool-descriptors.d.ts +4 -0
- package/dist/crowdy-agent/tool-descriptors.d.ts.map +1 -0
- package/dist/crowdy-agent/tool-descriptors.js +1049 -0
- package/dist/crowdy-agent/transport.d.ts +152 -0
- package/dist/crowdy-agent/transport.d.ts.map +1 -0
- package/dist/crowdy-agent/transport.js +27 -0
- package/dist/crowdy-agent/types.d.ts +328 -0
- package/dist/crowdy-agent/types.d.ts.map +1 -0
- package/dist/crowdy-agent/types.js +37 -0
- package/dist/crowdy-client.d.ts +3 -0
- package/dist/crowdy-client.d.ts.map +1 -1
- package/dist/crowdy-client.js +6 -0
- package/dist/crowdy-studio/agent-dom-shell.d.ts +68 -0
- package/dist/crowdy-studio/agent-dom-shell.d.ts.map +1 -0
- package/dist/crowdy-studio/agent-dom-shell.js +418 -0
- package/dist/crowdy-studio/controller.d.ts +74 -4
- package/dist/crowdy-studio/controller.d.ts.map +1 -1
- package/dist/crowdy-studio/controller.js +450 -17
- package/dist/crowdy-studio/dom-shell.d.ts +50 -4
- package/dist/crowdy-studio/dom-shell.d.ts.map +1 -1
- package/dist/crowdy-studio/dom-shell.js +554 -123
- 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 +9 -3
- package/dist/crowdy-studio/index.d.ts.map +1 -1
- package/dist/crowdy-studio/index.js +6 -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/models.d.ts +67 -0
- package/dist/crowdy-studio/models.d.ts.map +1 -1
- package/dist/crowdy-studio/mount.d.ts +15 -0
- package/dist/crowdy-studio/mount.d.ts.map +1 -1
- package/dist/crowdy-studio/mount.js +132 -3
- 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 -11
- package/dist/generated/graphql.d.ts +3753 -1
- package/dist/generated/graphql.d.ts.map +1 -1
- package/dist/generated/graphql.js +296 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- 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 +7 -0
- package/dist/player-host/index.d.ts.map +1 -0
- package/dist/player-host/index.js +5 -0
- package/dist/player-host/lease-manager.d.ts +66 -0
- package/dist/player-host/lease-manager.d.ts.map +1 -0
- package/dist/player-host/lease-manager.js +428 -0
- package/dist/player-host/schemas.d.ts +9 -0
- package/dist/player-host/schemas.d.ts.map +1 -0
- package/dist/player-host/schemas.js +347 -0
- package/dist/player-host/tools.d.ts +13 -0
- package/dist/player-host/tools.d.ts.map +1 -0
- package/dist/player-host/tools.js +79 -0
- package/dist/player-host/types.d.ts +203 -0
- package/dist/player-host/types.d.ts.map +1 -0
- package/dist/player-host/types.js +1 -0
- package/package.json +17 -2
|
@@ -1,36 +1,69 @@
|
|
|
1
|
+
import { CrowdyStudioAgentDomShell, } from './agent-dom-shell.js';
|
|
2
|
+
import { StudioLayoutController, studioPaneSizeRange, } from './layout.js';
|
|
3
|
+
import { createPaneSplitter } from './splitter.js';
|
|
1
4
|
import { projectTargets, } from './models.js';
|
|
2
5
|
import { CROWDY_STUDIO_STYLES } from './styles.js';
|
|
3
|
-
|
|
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
|
+
*/
|
|
4
19
|
export class CrowdyStudioDomShell {
|
|
5
|
-
constructor(host, controller) {
|
|
20
|
+
constructor(host, controller, agentController, agentOptions = {}) {
|
|
6
21
|
this.controller = controller;
|
|
7
22
|
this.panelButtons = new Map();
|
|
8
23
|
this.panels = new Map();
|
|
24
|
+
this.railButtons = new Map();
|
|
25
|
+
this.splitters = new Map();
|
|
9
26
|
this.activePanel = 'problems';
|
|
27
|
+
this.explorerForm = null;
|
|
28
|
+
this.lastState = null;
|
|
29
|
+
this.lastPhase = 'IDLE';
|
|
10
30
|
this.disposed = false;
|
|
11
31
|
const style = document.createElement('style');
|
|
12
32
|
style.textContent = CROWDY_STUDIO_STYLES;
|
|
13
33
|
this.root = element('div', 'ck-crowdy-studio');
|
|
14
34
|
this.root.append(style);
|
|
35
|
+
this.layout = new StudioLayoutController();
|
|
36
|
+
// ----- Top bar -----------------------------------------------------
|
|
15
37
|
const toolbar = element('div', 'ck-crowdy-studio-toolbar');
|
|
16
38
|
const projectTools = element('div', 'ck-crowdy-studio-project-tools');
|
|
17
|
-
this.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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');
|
|
21
49
|
this.saveAction = button('Retry save');
|
|
22
50
|
this.remoteAction = button('Use cloud version');
|
|
23
|
-
this.
|
|
24
|
-
this.
|
|
25
|
-
|
|
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);
|
|
26
55
|
const runtimeTools = element('div', 'ck-crowdy-studio-runtime-tools');
|
|
27
56
|
this.testAction = button('Test draft');
|
|
28
|
-
this.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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);
|
|
33
65
|
toolbar.append(projectTools, runtimeTools);
|
|
66
|
+
// ----- New-project popover -----------------------------------------
|
|
34
67
|
this.newForm = document.createElement('form');
|
|
35
68
|
this.newForm.className = 'ck-crowdy-studio-new';
|
|
36
69
|
this.newForm.dataset.open = 'false';
|
|
@@ -56,13 +89,34 @@ export class CrowdyStudioDomShell {
|
|
|
56
89
|
const create = button('Create project', 'submit');
|
|
57
90
|
const cancel = button('Cancel');
|
|
58
91
|
this.newForm.append(this.newName, this.newKind, create, cancel);
|
|
59
|
-
|
|
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'));
|
|
60
97
|
this.explorer = element('aside', 'ck-crowdy-studio-explorer');
|
|
61
|
-
|
|
98
|
+
this.explorer.setAttribute('aria-label', 'Project files');
|
|
99
|
+
this.editorColumn = element('section', 'ck-crowdy-studio-editor-column');
|
|
62
100
|
this.tabs = element('div', 'ck-crowdy-studio-tabs');
|
|
63
101
|
this.editorHost = element('div', 'ck-crowdy-studio-editor');
|
|
64
|
-
|
|
65
|
-
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');
|
|
66
120
|
const settingsTitle = element('h3');
|
|
67
121
|
settingsTitle.textContent = 'Project settings';
|
|
68
122
|
this.settingsName = input('Project name');
|
|
@@ -74,40 +128,38 @@ export class CrowdyStudioDomShell {
|
|
|
74
128
|
['OPTIONAL', 'Optional companion'],
|
|
75
129
|
['REQUIRED', 'Require client companion'],
|
|
76
130
|
]);
|
|
77
|
-
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));
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
this.
|
|
82
|
-
this.
|
|
83
|
-
this.
|
|
84
|
-
this.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
this.invokeExport = input('Export name');
|
|
91
|
-
this.invokeExport.value = 'invoke';
|
|
92
|
-
this.invokeParams = document.createElement('textarea');
|
|
93
|
-
this.invokeParams.placeholder = '{"example": true}';
|
|
94
|
-
this.invokeParams.setAttribute('aria-label', 'Invoke JSON parameters');
|
|
95
|
-
const invokeButton = button('Invoke server export');
|
|
96
|
-
this.invokeResult = document.createElement('pre');
|
|
97
|
-
const invokeControls = element('div', 'ck-crowdy-studio-invoke');
|
|
98
|
-
invokeControls.append(this.invokeExport, this.invokeParams, invokeButton);
|
|
99
|
-
this.invokePanel.append(invokeControls, this.invokeResult);
|
|
100
|
-
this.root.append(toolbar, this.newForm, main, bottom);
|
|
101
|
-
host.appendChild(this.root);
|
|
102
|
-
newButton.addEventListener('click', () => {
|
|
103
|
-
this.newForm.dataset.open =
|
|
104
|
-
this.newForm.dataset.open === 'true' ? 'false' : 'true';
|
|
105
|
-
if (this.newForm.dataset.open === 'true')
|
|
106
|
-
this.newName.focus();
|
|
107
|
-
});
|
|
108
|
-
cancel.addEventListener('click', () => {
|
|
109
|
-
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);
|
|
110
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 ---------------------------------------------------------
|
|
111
163
|
this.newForm.addEventListener('submit', (event) => {
|
|
112
164
|
event.preventDefault();
|
|
113
165
|
void this.run(async () => {
|
|
@@ -119,18 +171,32 @@ export class CrowdyStudioDomShell {
|
|
|
119
171
|
this.newForm.dataset.open = 'false';
|
|
120
172
|
});
|
|
121
173
|
});
|
|
122
|
-
|
|
123
|
-
|
|
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());
|
|
124
189
|
});
|
|
125
|
-
this.testAction.addEventListener('click', () => void this.run(() => this.controller.testDraft()));
|
|
126
|
-
this.deployAction.addEventListener('click', () => void this.run(() => this.controller.deployLive()));
|
|
127
|
-
stop.addEventListener('click', () => void this.run(() => this.controller.stopProject()));
|
|
128
190
|
this.saveAction.addEventListener('click', () => {
|
|
191
|
+
this.saveMenu.close();
|
|
129
192
|
void this.run(() => this.controller.getState().saveState === 'CONFLICT'
|
|
130
193
|
? this.controller.overwriteConflict()
|
|
131
194
|
: this.controller.retrySave());
|
|
132
195
|
});
|
|
133
|
-
this.remoteAction.addEventListener('click', () =>
|
|
196
|
+
this.remoteAction.addEventListener('click', () => {
|
|
197
|
+
this.saveMenu.close();
|
|
198
|
+
void this.run(() => this.controller.acceptRemoteConflict());
|
|
199
|
+
});
|
|
134
200
|
this.settingsName.addEventListener('change', () => this.controller.updateSettings({ name: this.settingsName.value }));
|
|
135
201
|
this.settingsDescription.addEventListener('change', () => this.controller.updateSettings({
|
|
136
202
|
description: this.settingsDescription.value,
|
|
@@ -142,15 +208,28 @@ export class CrowdyStudioDomShell {
|
|
|
142
208
|
clientModuleName: this.clientModuleName.value,
|
|
143
209
|
}));
|
|
144
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);
|
|
145
221
|
invokeButton.addEventListener('click', () => {
|
|
146
222
|
void this.run(() => this.controller.invoke(this.invokeExport.value, this.invokeParams.value));
|
|
147
223
|
});
|
|
148
224
|
this.controller.setSurfaceVisible('usage', true);
|
|
225
|
+
this.unsubscribeLayout = this.layout.subscribe((state) => this.applyLayout(state));
|
|
226
|
+
this.applyLayout(this.layout.getState());
|
|
149
227
|
this.activatePanel('problems');
|
|
150
228
|
}
|
|
151
229
|
render(state) {
|
|
152
230
|
if (this.disposed)
|
|
153
231
|
return;
|
|
232
|
+
this.lastState = state;
|
|
154
233
|
this.renderProjects(state);
|
|
155
234
|
this.renderSaveState(state);
|
|
156
235
|
this.renderExplorer(state);
|
|
@@ -168,14 +247,13 @@ export class CrowdyStudioDomShell {
|
|
|
168
247
|
this.invokeResult.textContent = state.invokeResult
|
|
169
248
|
? formatInvokeResult(state.invokeResult)
|
|
170
249
|
: '';
|
|
171
|
-
this.runtimeStatus.textContent =
|
|
172
|
-
state.runtime.phase,
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
.filter(Boolean)
|
|
177
|
-
.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;
|
|
178
255
|
this.budgetStatus.textContent = budgetText(state);
|
|
256
|
+
this.autoRevealFailures(state);
|
|
179
257
|
}
|
|
180
258
|
dispose() {
|
|
181
259
|
if (this.disposed)
|
|
@@ -184,8 +262,174 @@ export class CrowdyStudioDomShell {
|
|
|
184
262
|
this.controller.setSurfaceVisible('usage', false);
|
|
185
263
|
this.controller.setSurfaceVisible('logs', false);
|
|
186
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();
|
|
271
|
+
this.agentShell?.dispose();
|
|
187
272
|
this.root.remove();
|
|
188
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 ------------------------------------------------------------
|
|
189
433
|
createPanel(name, label, tabs) {
|
|
190
434
|
const tab = button(label);
|
|
191
435
|
tab.className = 'ck-crowdy-studio-panel-tab';
|
|
@@ -198,7 +442,6 @@ export class CrowdyStudioDomShell {
|
|
|
198
442
|
return panel;
|
|
199
443
|
}
|
|
200
444
|
activatePanel(name) {
|
|
201
|
-
const previous = this.activePanel;
|
|
202
445
|
this.activePanel = name;
|
|
203
446
|
for (const [key, buttonElement] of this.panelButtons) {
|
|
204
447
|
buttonElement.dataset.active = String(key === name);
|
|
@@ -206,32 +449,43 @@ export class CrowdyStudioDomShell {
|
|
|
206
449
|
for (const [key, panel] of this.panels) {
|
|
207
450
|
panel.dataset.active = String(key === name);
|
|
208
451
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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);
|
|
213
459
|
}
|
|
214
|
-
if (nextPoll)
|
|
215
|
-
this.controller.setSurfaceVisible(nextPoll, true);
|
|
216
460
|
}
|
|
461
|
+
// ----- Rendering -----------------------------------------------------------
|
|
217
462
|
renderProjects(state) {
|
|
218
|
-
const selected = state.project
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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;
|
|
226
477
|
});
|
|
227
|
-
if (
|
|
228
|
-
|
|
229
|
-
empty.value = '';
|
|
230
|
-
empty.textContent = 'No projects yet';
|
|
231
|
-
options.push(empty);
|
|
478
|
+
if (items.length === 0) {
|
|
479
|
+
items.push(empty('No projects yet.'));
|
|
232
480
|
}
|
|
233
|
-
|
|
234
|
-
|
|
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);
|
|
235
489
|
}
|
|
236
490
|
renderSaveState(state) {
|
|
237
491
|
const label = {
|
|
@@ -240,16 +494,25 @@ export class CrowdyStudioDomShell {
|
|
|
240
494
|
CONFLICT: 'Conflict',
|
|
241
495
|
OFFLINE: 'Offline',
|
|
242
496
|
}[state.saveState];
|
|
243
|
-
this.
|
|
244
|
-
this.
|
|
497
|
+
this.saveMenu.button.dataset.state = state.saveState;
|
|
498
|
+
this.saveMenu.button.textContent = label;
|
|
499
|
+
this.saveStatus.textContent = state.saveMessage
|
|
245
500
|
? `${label}: ${state.saveMessage}`
|
|
246
501
|
: label;
|
|
247
|
-
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;
|
|
248
509
|
this.saveAction.textContent =
|
|
249
510
|
state.saveState === 'CONFLICT' ? 'Keep my version' : 'Retry save';
|
|
250
511
|
this.remoteAction.hidden = state.saveState !== 'CONFLICT';
|
|
512
|
+
this.saveMenu.button.dataset.attention = String(attention);
|
|
251
513
|
}
|
|
252
514
|
renderExplorer(state) {
|
|
515
|
+
const focused = focusedExplorerField(this.explorer);
|
|
253
516
|
this.explorer.replaceChildren();
|
|
254
517
|
if (!state.project) {
|
|
255
518
|
this.explorer.append(empty('Create a project to start authoring.'));
|
|
@@ -264,6 +527,7 @@ export class CrowdyStudioDomShell {
|
|
|
264
527
|
}))));
|
|
265
528
|
}
|
|
266
529
|
this.explorer.append(this.referenceSection('Personal library', state.personalLibraryFiles), this.referenceSection('Common files', state.commonFiles));
|
|
530
|
+
restoreExplorerFocus(this.explorer, focused);
|
|
267
531
|
}
|
|
268
532
|
projectSection(target, refs) {
|
|
269
533
|
const section = element('section', 'ck-crowdy-studio-section');
|
|
@@ -274,13 +538,29 @@ export class CrowdyStudioDomShell {
|
|
|
274
538
|
add.setAttribute('aria-label', `Add ${target} file`);
|
|
275
539
|
add.disabled = !this.controller.canTarget(target, 'write');
|
|
276
540
|
add.addEventListener('click', () => {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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();
|
|
281
546
|
});
|
|
282
547
|
header.append(title, add);
|
|
283
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
|
+
}
|
|
284
564
|
for (const ref of refs) {
|
|
285
565
|
section.append(this.projectFileRow(ref));
|
|
286
566
|
}
|
|
@@ -289,38 +569,97 @@ export class CrowdyStudioDomShell {
|
|
|
289
569
|
return section;
|
|
290
570
|
}
|
|
291
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
|
+
}
|
|
292
580
|
const row = element('div', 'ck-crowdy-studio-file');
|
|
293
581
|
const open = button(ref.path);
|
|
294
582
|
open.title = `${ref.target}:${ref.path}`;
|
|
295
583
|
open.addEventListener('click', () => this.controller.openFile(ref));
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const next = window.prompt('Rename file', ref.path);
|
|
300
|
-
if (!next || !ref.target)
|
|
301
|
-
return;
|
|
302
|
-
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',
|
|
303
587
|
});
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (!ref.target
|
|
308
|
-
!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)
|
|
309
592
|
return;
|
|
310
|
-
|
|
311
|
-
|
|
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();
|
|
312
600
|
});
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
601
|
+
const rename = menuItem('Rename');
|
|
602
|
+
rename.addEventListener('click', () => {
|
|
603
|
+
actions.close();
|
|
316
604
|
if (!ref.target)
|
|
317
605
|
return;
|
|
318
|
-
|
|
319
|
-
|
|
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)
|
|
320
618
|
return;
|
|
321
|
-
|
|
619
|
+
this.explorerForm = { kind: 'delete', target: ref.target, path: ref.path };
|
|
620
|
+
this.rerenderExplorer();
|
|
322
621
|
});
|
|
323
|
-
|
|
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
|
+
}));
|
|
324
663
|
return row;
|
|
325
664
|
}
|
|
326
665
|
referenceSection(titleText, files) {
|
|
@@ -341,18 +680,78 @@ export class CrowdyStudioDomShell {
|
|
|
341
680
|
const add = button('Add');
|
|
342
681
|
add.setAttribute('aria-label', `Add ${file.title} to project`);
|
|
343
682
|
add.addEventListener('click', () => {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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();
|
|
348
689
|
});
|
|
349
690
|
row.append(open, add);
|
|
350
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
|
+
}
|
|
351
709
|
}
|
|
352
710
|
if (files.length === 0)
|
|
353
711
|
section.append(empty('No files'));
|
|
354
712
|
return section;
|
|
355
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
|
+
}
|
|
356
755
|
renderTabs(state) {
|
|
357
756
|
this.tabs.replaceChildren();
|
|
358
757
|
for (const ref of state.openFiles) {
|
|
@@ -401,6 +800,11 @@ export class CrowdyStudioDomShell {
|
|
|
401
800
|
...state.authoritativeDiagnostics,
|
|
402
801
|
...state.localDiagnostics,
|
|
403
802
|
];
|
|
803
|
+
const problemsTab = this.panelButtons.get('problems');
|
|
804
|
+
if (problemsTab) {
|
|
805
|
+
problemsTab.textContent =
|
|
806
|
+
diagnostics.length > 0 ? `Problems (${diagnostics.length})` : 'Problems';
|
|
807
|
+
}
|
|
404
808
|
if (diagnostics.length === 0) {
|
|
405
809
|
this.problemsPanel.append(empty('No problems.'));
|
|
406
810
|
return;
|
|
@@ -452,11 +856,6 @@ export class CrowdyStudioDomShell {
|
|
|
452
856
|
}
|
|
453
857
|
}
|
|
454
858
|
}
|
|
455
|
-
function polledSurface(panel) {
|
|
456
|
-
if (panel === 'logs' || panel === 'runs')
|
|
457
|
-
return panel;
|
|
458
|
-
return null;
|
|
459
|
-
}
|
|
460
859
|
function budgetText(state) {
|
|
461
860
|
const usage = state.usage
|
|
462
861
|
? `units ${state.usage.hourUnitsUsed}/${state.usage.unitsPerHour ?? '∞'} · compiles ${state.usage.compilesThisHour}/${state.usage.maxCompilesPerHour}`
|
|
@@ -481,6 +880,32 @@ function sameRef(a, b) {
|
|
|
481
880
|
a.path === b.path &&
|
|
482
881
|
a.referenceId === b.referenceId);
|
|
483
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
|
+
}
|
|
484
909
|
function element(tag, className) {
|
|
485
910
|
const value = document.createElement(tag);
|
|
486
911
|
if (className)
|
|
@@ -493,6 +918,12 @@ function button(label, type = 'button') {
|
|
|
493
918
|
value.textContent = label;
|
|
494
919
|
return value;
|
|
495
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
|
+
}
|
|
496
927
|
function input(placeholder) {
|
|
497
928
|
const value = document.createElement('input');
|
|
498
929
|
value.placeholder = placeholder;
|