@cluesmith/codev 2.0.2 → 2.0.3
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/dashboard/dist/assets/{index-b38SaXk5.js → index-UsH9ixz1.js} +20 -20
- package/dashboard/dist/assets/index-UsH9ixz1.js.map +1 -0
- package/dashboard/dist/index.html +1 -1
- package/dist/agent-farm/cli.js +1 -1
- package/dist/agent-farm/cli.js.map +1 -1
- package/dist/agent-farm/commands/architect.d.ts +1 -1
- package/dist/agent-farm/commands/architect.js +3 -3
- package/dist/agent-farm/commands/architect.js.map +1 -1
- package/dist/agent-farm/commands/attach.js +1 -1
- package/dist/agent-farm/commands/attach.js.map +1 -1
- package/dist/agent-farm/commands/cleanup.js +6 -6
- package/dist/agent-farm/commands/cleanup.js.map +1 -1
- package/dist/agent-farm/commands/open.js +5 -5
- package/dist/agent-farm/commands/open.js.map +1 -1
- package/dist/agent-farm/commands/send.js +5 -5
- package/dist/agent-farm/commands/send.js.map +1 -1
- package/dist/agent-farm/commands/shell.js +5 -5
- package/dist/agent-farm/commands/shell.js.map +1 -1
- package/dist/agent-farm/commands/spawn-worktree.d.ts +1 -1
- package/dist/agent-farm/commands/spawn-worktree.d.ts.map +1 -1
- package/dist/agent-farm/commands/spawn-worktree.js +8 -8
- package/dist/agent-farm/commands/spawn-worktree.js.map +1 -1
- package/dist/agent-farm/commands/spawn.js +3 -3
- package/dist/agent-farm/commands/spawn.js.map +1 -1
- package/dist/agent-farm/commands/start.d.ts +4 -4
- package/dist/agent-farm/commands/start.js +16 -16
- package/dist/agent-farm/commands/start.js.map +1 -1
- package/dist/agent-farm/commands/status.d.ts +1 -1
- package/dist/agent-farm/commands/status.js +16 -16
- package/dist/agent-farm/commands/status.js.map +1 -1
- package/dist/agent-farm/commands/stop.d.ts +4 -4
- package/dist/agent-farm/commands/stop.js +9 -9
- package/dist/agent-farm/commands/stop.js.map +1 -1
- package/dist/agent-farm/db/index.d.ts.map +1 -1
- package/dist/agent-farm/db/index.js +82 -7
- package/dist/agent-farm/db/index.js.map +1 -1
- package/dist/agent-farm/db/schema.d.ts +2 -2
- package/dist/agent-farm/db/schema.d.ts.map +1 -1
- package/dist/agent-farm/db/schema.js +21 -4
- package/dist/agent-farm/db/schema.js.map +1 -1
- package/dist/agent-farm/lib/tower-client.d.ts +20 -20
- package/dist/agent-farm/lib/tower-client.d.ts.map +1 -1
- package/dist/agent-farm/lib/tower-client.js +25 -25
- package/dist/agent-farm/lib/tower-client.js.map +1 -1
- package/dist/agent-farm/lib/tunnel-client.d.ts +12 -2
- package/dist/agent-farm/lib/tunnel-client.d.ts.map +1 -1
- package/dist/agent-farm/lib/tunnel-client.js +59 -1
- package/dist/agent-farm/lib/tunnel-client.js.map +1 -1
- package/dist/agent-farm/servers/tower-instances.d.ts +18 -18
- package/dist/agent-farm/servers/tower-instances.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-instances.js +89 -89
- package/dist/agent-farm/servers/tower-instances.js.map +1 -1
- package/dist/agent-farm/servers/tower-routes.d.ts +1 -1
- package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-routes.js +184 -162
- package/dist/agent-farm/servers/tower-routes.js.map +1 -1
- package/dist/agent-farm/servers/tower-server.js +23 -19
- package/dist/agent-farm/servers/tower-server.js.map +1 -1
- package/dist/agent-farm/servers/tower-terminals.d.ts +27 -29
- package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-terminals.js +95 -116
- package/dist/agent-farm/servers/tower-terminals.js.map +1 -1
- package/dist/agent-farm/servers/tower-tunnel.d.ts +2 -2
- package/dist/agent-farm/servers/tower-tunnel.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-tunnel.js +12 -12
- package/dist/agent-farm/servers/tower-tunnel.js.map +1 -1
- package/dist/agent-farm/servers/tower-types.d.ts +8 -10
- package/dist/agent-farm/servers/tower-types.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-utils.d.ts +9 -9
- package/dist/agent-farm/servers/tower-utils.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-utils.js +18 -18
- package/dist/agent-farm/servers/tower-utils.js.map +1 -1
- package/dist/agent-farm/servers/tower-websocket.d.ts +2 -2
- package/dist/agent-farm/servers/tower-websocket.js +14 -14
- package/dist/agent-farm/servers/tower-websocket.js.map +1 -1
- package/dist/agent-farm/types.d.ts +2 -2
- package/dist/agent-farm/types.d.ts.map +1 -1
- package/dist/agent-farm/utils/config.d.ts +1 -1
- package/dist/agent-farm/utils/config.d.ts.map +1 -1
- package/dist/agent-farm/utils/config.js +16 -16
- package/dist/agent-farm/utils/config.js.map +1 -1
- package/dist/agent-farm/utils/file-tabs.d.ts +3 -3
- package/dist/agent-farm/utils/file-tabs.d.ts.map +1 -1
- package/dist/agent-farm/utils/file-tabs.js +9 -9
- package/dist/agent-farm/utils/file-tabs.js.map +1 -1
- package/dist/agent-farm/utils/gate-status.d.ts +2 -2
- package/dist/agent-farm/utils/gate-status.d.ts.map +1 -1
- package/dist/agent-farm/utils/gate-status.js +3 -3
- package/dist/agent-farm/utils/gate-status.js.map +1 -1
- package/dist/agent-farm/utils/index.d.ts +0 -1
- package/dist/agent-farm/utils/index.d.ts.map +1 -1
- package/dist/agent-farm/utils/index.js +0 -1
- package/dist/agent-farm/utils/index.js.map +1 -1
- package/dist/agent-farm/utils/notifications.d.ts +4 -4
- package/dist/agent-farm/utils/notifications.d.ts.map +1 -1
- package/dist/agent-farm/utils/notifications.js +18 -18
- package/dist/agent-farm/utils/notifications.js.map +1 -1
- package/dist/commands/adopt.d.ts +2 -2
- package/dist/commands/adopt.d.ts.map +1 -1
- package/dist/commands/adopt.js +13 -3
- package/dist/commands/adopt.js.map +1 -1
- package/dist/commands/consult/index.d.ts +1 -1
- package/dist/commands/consult/index.d.ts.map +1 -1
- package/dist/commands/consult/index.js +52 -51
- package/dist/commands/consult/index.js.map +1 -1
- package/dist/commands/doctor.js +6 -6
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/import.js +4 -4
- package/dist/commands/import.js.map +1 -1
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +13 -3
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/porch/index.d.ts +6 -6
- package/dist/commands/porch/index.d.ts.map +1 -1
- package/dist/commands/porch/index.js +37 -37
- package/dist/commands/porch/index.js.map +1 -1
- package/dist/commands/porch/next.d.ts +1 -1
- package/dist/commands/porch/next.d.ts.map +1 -1
- package/dist/commands/porch/next.js +43 -40
- package/dist/commands/porch/next.js.map +1 -1
- package/dist/commands/porch/notify.d.ts +11 -0
- package/dist/commands/porch/notify.d.ts.map +1 -0
- package/dist/commands/porch/notify.js +30 -0
- package/dist/commands/porch/notify.js.map +1 -0
- package/dist/commands/porch/plan.d.ts +1 -1
- package/dist/commands/porch/plan.d.ts.map +1 -1
- package/dist/commands/porch/plan.js +3 -3
- package/dist/commands/porch/plan.js.map +1 -1
- package/dist/commands/porch/prompts.d.ts +1 -1
- package/dist/commands/porch/prompts.d.ts.map +1 -1
- package/dist/commands/porch/prompts.js +13 -13
- package/dist/commands/porch/prompts.js.map +1 -1
- package/dist/commands/porch/protocol.d.ts +1 -1
- package/dist/commands/porch/protocol.d.ts.map +1 -1
- package/dist/commands/porch/protocol.js +6 -6
- package/dist/commands/porch/protocol.js.map +1 -1
- package/dist/commands/porch/state.d.ts +6 -6
- package/dist/commands/porch/state.d.ts.map +1 -1
- package/dist/commands/porch/state.js +11 -11
- package/dist/commands/porch/state.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +10 -1
- package/dist/commands/update.js.map +1 -1
- package/dist/lib/scaffold.d.ts +13 -0
- package/dist/lib/scaffold.d.ts.map +1 -1
- package/dist/lib/scaffold.js +34 -0
- package/dist/lib/scaffold.js.map +1 -1
- package/dist/lib/skeleton.d.ts +7 -7
- package/dist/lib/skeleton.d.ts.map +1 -1
- package/dist/lib/skeleton.js +10 -10
- package/dist/lib/skeleton.js.map +1 -1
- package/dist/terminal/pty-manager.d.ts +1 -1
- package/dist/terminal/pty-manager.d.ts.map +1 -1
- package/dist/terminal/pty-manager.js +3 -3
- package/dist/terminal/pty-manager.js.map +1 -1
- package/package.json +1 -1
- package/templates/open.html +13 -13
- package/templates/tower.html +54 -54
- package/templates/vendor/marked.min.js +6 -0
- package/templates/vendor/prism-bash.min.js +1 -0
- package/templates/vendor/prism-css.min.js +1 -0
- package/templates/vendor/prism-javascript.min.js +1 -0
- package/templates/vendor/prism-json.min.js +1 -0
- package/templates/vendor/prism-markdown.min.js +1 -0
- package/templates/vendor/prism-markup.min.js +1 -0
- package/templates/vendor/prism-python.min.js +1 -0
- package/templates/vendor/prism-tomorrow.min.css +1 -0
- package/templates/vendor/prism-typescript.min.js +1 -0
- package/templates/vendor/prism-yaml.min.js +1 -0
- package/templates/vendor/prism.min.js +1 -0
- package/templates/vendor/purify.min.js +3 -0
- package/dashboard/dist/assets/index-b38SaXk5.js.map +0 -1
- package/dist/agent-farm/hq-connector.d.ts +0 -19
- package/dist/agent-farm/hq-connector.d.ts.map +0 -1
- package/dist/agent-farm/hq-connector.js +0 -351
- package/dist/agent-farm/hq-connector.js.map +0 -1
- package/dist/agent-farm/utils/deps.d.ts +0 -51
- package/dist/agent-farm/utils/deps.d.ts.map +0 -1
- package/dist/agent-farm/utils/deps.js +0 -162
- package/dist/agent-farm/utils/deps.js.map +0 -1
- package/dist/agent-farm/utils/gate-watcher.d.ts +0 -38
- package/dist/agent-farm/utils/gate-watcher.d.ts.map +0 -1
- package/dist/agent-farm/utils/gate-watcher.js +0 -122
- package/dist/agent-farm/utils/gate-watcher.js.map +0 -1
- package/dist/agent-farm/utils/session.d.ts +0 -32
- package/dist/agent-farm/utils/session.d.ts.map +0 -1
- package/dist/agent-farm/utils/session.js +0 -57
- package/dist/agent-farm/utils/session.js.map +0 -1
- package/dist/lib/projectlist-parser.d.ts +0 -70
- package/dist/lib/projectlist-parser.d.ts.map +0 -1
- package/dist/lib/projectlist-parser.js +0 -200
- package/dist/lib/projectlist-parser.js.map +0 -1
- package/templates/dashboard/css/dialogs.css +0 -149
- package/templates/dashboard/css/files.css +0 -558
- package/templates/dashboard/css/layout.css +0 -133
- package/templates/dashboard/css/projects.css +0 -501
- package/templates/dashboard/css/statusbar.css +0 -23
- package/templates/dashboard/css/tabs.css +0 -314
- package/templates/dashboard/css/utilities.css +0 -50
- package/templates/dashboard/css/variables.css +0 -45
- package/templates/dashboard/index.html +0 -149
- package/templates/dashboard/js/dialogs.js +0 -368
- package/templates/dashboard/js/files.js +0 -448
- package/templates/dashboard/js/main.js +0 -476
- package/templates/dashboard/js/projects.js +0 -544
- package/templates/dashboard/js/state.js +0 -91
- package/templates/dashboard/js/tabs.js +0 -518
- package/templates/dashboard/js/utils.js +0 -191
|
@@ -1,518 +0,0 @@
|
|
|
1
|
-
// Tab Management - Rendering, Selection, Overflow
|
|
2
|
-
|
|
3
|
-
// Get the base URL for server connections (uses current hostname for remote access)
|
|
4
|
-
// DEPRECATED: Use getTerminalUrl() for terminal tabs (Spec 0062)
|
|
5
|
-
function getBaseUrl(port) {
|
|
6
|
-
return `http://${window.location.hostname}:${port}`;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Get the terminal URL for a tab (Spec 0062 - Secure Remote Access)
|
|
11
|
-
* Uses the reverse proxy instead of direct port access, enabling SSH tunnel support.
|
|
12
|
-
*
|
|
13
|
-
* @param {Object} tab - The tab object
|
|
14
|
-
* @returns {string} The URL to load in the iframe
|
|
15
|
-
*/
|
|
16
|
-
function getTerminalUrl(tab) {
|
|
17
|
-
// Architect terminal
|
|
18
|
-
if (tab.type === 'architect') {
|
|
19
|
-
return '/terminal/architect';
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// Builder terminal - use the builder's ID (e.g., builder-0055)
|
|
23
|
-
if (tab.type === 'builder') {
|
|
24
|
-
return `/terminal/builder-${tab.projectId}`;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Shell/utility terminal - use the utility's ID (e.g., util-U12345)
|
|
28
|
-
if (tab.type === 'shell') {
|
|
29
|
-
return `/terminal/util-${tab.utilId}`;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// File/annotation tabs - use the annotation ID for proxy routing
|
|
33
|
-
if (tab.type === 'file' && tab.annotationId) {
|
|
34
|
-
return `/annotation/${tab.annotationId}/`;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Fallback for backward compatibility
|
|
38
|
-
if (tab.port) {
|
|
39
|
-
return getBaseUrl(tab.port);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Build tabs from initial state
|
|
46
|
-
function buildTabsFromState() {
|
|
47
|
-
const previousTabIds = new Set(tabs.map(t => t.id));
|
|
48
|
-
// Preserve client-side-only tabs (like activity)
|
|
49
|
-
const clientSideTabs = tabs.filter(t => t.type === 'activity');
|
|
50
|
-
tabs = [];
|
|
51
|
-
|
|
52
|
-
// Dashboard tab is ALWAYS first and uncloseable (Spec 0045, 0057)
|
|
53
|
-
tabs.push({
|
|
54
|
-
id: 'dashboard',
|
|
55
|
-
type: 'dashboard',
|
|
56
|
-
name: 'Dashboard',
|
|
57
|
-
closeable: false
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
// Add file tabs from annotations
|
|
61
|
-
for (const annotation of state.annotations || []) {
|
|
62
|
-
tabs.push({
|
|
63
|
-
id: `file-${annotation.id}`,
|
|
64
|
-
type: 'file',
|
|
65
|
-
name: getFileName(annotation.file),
|
|
66
|
-
path: annotation.file,
|
|
67
|
-
port: annotation.port,
|
|
68
|
-
annotationId: annotation.id
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Add builder tabs
|
|
73
|
-
for (const builder of state.builders || []) {
|
|
74
|
-
tabs.push({
|
|
75
|
-
id: `builder-${builder.id}`,
|
|
76
|
-
type: 'builder',
|
|
77
|
-
name: builder.name || `Builder ${builder.id}`,
|
|
78
|
-
projectId: builder.id,
|
|
79
|
-
port: builder.port,
|
|
80
|
-
status: builder.status
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Add shell tabs
|
|
85
|
-
for (const util of state.utils || []) {
|
|
86
|
-
tabs.push({
|
|
87
|
-
id: `shell-${util.id}`,
|
|
88
|
-
type: 'shell',
|
|
89
|
-
name: util.name,
|
|
90
|
-
port: util.port,
|
|
91
|
-
utilId: util.id
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Re-add preserved client-side tabs
|
|
96
|
-
for (const tab of clientSideTabs) {
|
|
97
|
-
tabs.push(tab);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Detect new tabs and auto-switch to them (skip projects tab)
|
|
101
|
-
for (const tab of tabs) {
|
|
102
|
-
if (tab.id !== 'dashboard' && tab.id !== 'files' && !knownTabIds.has(tab.id) && previousTabIds.size > 0) {
|
|
103
|
-
// This is a new tab - switch to it
|
|
104
|
-
activeTabId = tab.id;
|
|
105
|
-
break;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Update known tab IDs
|
|
110
|
-
knownTabIds = new Set(tabs.map(t => t.id));
|
|
111
|
-
|
|
112
|
-
// Set active tab to Dashboard on first load if none selected
|
|
113
|
-
if (!activeTabId) {
|
|
114
|
-
activeTabId = 'dashboard';
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Render architect pane
|
|
119
|
-
function renderArchitect() {
|
|
120
|
-
const content = document.getElementById('architect-content');
|
|
121
|
-
const statusDot = document.getElementById('architect-status');
|
|
122
|
-
|
|
123
|
-
if (state.architect && state.architect.port) {
|
|
124
|
-
statusDot.classList.remove('inactive');
|
|
125
|
-
// Only update iframe if port changed (avoid flashing on poll)
|
|
126
|
-
if (currentArchitectPort !== state.architect.port) {
|
|
127
|
-
currentArchitectPort = state.architect.port;
|
|
128
|
-
// Use proxied URL for remote access support (Spec 0062)
|
|
129
|
-
content.innerHTML = `<iframe src="/terminal/architect" title="Architect Terminal" allow="clipboard-read; clipboard-write"></iframe>`;
|
|
130
|
-
}
|
|
131
|
-
} else {
|
|
132
|
-
if (currentArchitectPort !== null) {
|
|
133
|
-
currentArchitectPort = null;
|
|
134
|
-
content.innerHTML = `
|
|
135
|
-
<div class="architect-placeholder">
|
|
136
|
-
<p>Architect not running</p>
|
|
137
|
-
<p>Run <code>agent-farm start</code> to begin</p>
|
|
138
|
-
</div>
|
|
139
|
-
`;
|
|
140
|
-
}
|
|
141
|
-
statusDot.classList.add('inactive');
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Render tabs
|
|
146
|
-
function renderTabs() {
|
|
147
|
-
const container = document.getElementById('tabs-container');
|
|
148
|
-
|
|
149
|
-
if (tabs.length === 0) {
|
|
150
|
-
container.innerHTML = '';
|
|
151
|
-
checkTabOverflow(); // Update overflow state when tabs cleared
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
container.innerHTML = tabs.map(tab => {
|
|
156
|
-
const isActive = tab.id === activeTabId;
|
|
157
|
-
const icon = getTabIcon(tab.type);
|
|
158
|
-
const statusDot = tab.type === 'builder' ? getStatusDot(tab.status) : '';
|
|
159
|
-
const tooltip = getTabTooltip(tab);
|
|
160
|
-
const isUncloseable = tab.closeable === false;
|
|
161
|
-
|
|
162
|
-
return `
|
|
163
|
-
<div class="tab ${isActive ? 'active' : ''} ${isUncloseable ? 'tab-uncloseable' : ''}"
|
|
164
|
-
onclick="selectTab('${tab.id}')"
|
|
165
|
-
oncontextmenu="showContextMenu(event, '${tab.id}')"
|
|
166
|
-
data-tab-id="${tab.id}"
|
|
167
|
-
title="${tooltip}">
|
|
168
|
-
<span class="icon">${icon}</span>
|
|
169
|
-
<span class="name">${tab.name}</span>
|
|
170
|
-
${statusDot}
|
|
171
|
-
${!isUncloseable ? `<span class="close"
|
|
172
|
-
onclick="event.stopPropagation(); closeTab('${tab.id}', event)"
|
|
173
|
-
role="button"
|
|
174
|
-
tabindex="0"
|
|
175
|
-
aria-label="Close ${tab.name}"
|
|
176
|
-
onkeydown="if(event.key==='Enter'||event.key===' '){event.stopPropagation();closeTab('${tab.id}',event)}">×</span>` : ''}
|
|
177
|
-
</div>
|
|
178
|
-
`;
|
|
179
|
-
}).join('');
|
|
180
|
-
|
|
181
|
-
// Check overflow after tabs are rendered
|
|
182
|
-
checkTabOverflow();
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Get tab icon
|
|
186
|
-
function getTabIcon(type) {
|
|
187
|
-
switch (type) {
|
|
188
|
-
case 'dashboard': return '🏠';
|
|
189
|
-
case 'files': return '📁';
|
|
190
|
-
case 'file': return '📄';
|
|
191
|
-
case 'builder': return '🔨';
|
|
192
|
-
case 'shell': return '>_';
|
|
193
|
-
default: return '?';
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Status configuration - hoisted for performance (per Codex review)
|
|
198
|
-
const STATUS_CONFIG = {
|
|
199
|
-
'spawning': { color: 'var(--status-active)', label: 'Spawning', shape: 'circle', animation: 'pulse' },
|
|
200
|
-
'implementing': { color: 'var(--status-active)', label: 'Implementing', shape: 'circle', animation: 'pulse' },
|
|
201
|
-
'blocked': { color: 'var(--status-error)', label: 'Blocked', shape: 'diamond', animation: 'blink-fast' },
|
|
202
|
-
'pr-ready': { color: 'var(--status-waiting)', label: 'PR Ready', shape: 'ring', animation: 'blink-slow' },
|
|
203
|
-
'complete': { color: 'var(--status-complete)', label: 'Complete', shape: 'circle', animation: null }
|
|
204
|
-
};
|
|
205
|
-
const DEFAULT_STATUS_CONFIG = { color: 'var(--text-muted)', label: 'Unknown', shape: 'circle', animation: null };
|
|
206
|
-
|
|
207
|
-
// Get status dot HTML with accessibility support
|
|
208
|
-
function getStatusDot(status) {
|
|
209
|
-
const config = STATUS_CONFIG[status] || { ...DEFAULT_STATUS_CONFIG, label: status || 'Unknown' };
|
|
210
|
-
const classes = ['status-dot'];
|
|
211
|
-
if (config.shape === 'diamond') classes.push('status-dot--diamond');
|
|
212
|
-
if (config.shape === 'ring') classes.push('status-dot--ring');
|
|
213
|
-
if (config.animation === 'pulse') classes.push('status-dot--pulse');
|
|
214
|
-
if (config.animation === 'blink-slow') classes.push('status-dot--blink-slow');
|
|
215
|
-
if (config.animation === 'blink-fast') classes.push('status-dot--blink-fast');
|
|
216
|
-
return `<span class="${classes.join(' ')}" style="background: ${config.color}" title="${config.label}" role="img" aria-label="${config.label}"></span>`;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Generate tooltip text for tab hover
|
|
220
|
-
function getTabTooltip(tab) {
|
|
221
|
-
const lines = [tab.name];
|
|
222
|
-
|
|
223
|
-
if (tab.type === 'builder') {
|
|
224
|
-
if (tab.port) lines.push(`Port: ${tab.port}`);
|
|
225
|
-
lines.push(`Status: ${tab.status || 'unknown'}`);
|
|
226
|
-
const projectId = tab.id.replace('builder-', '');
|
|
227
|
-
lines.push(`Worktree: .builders/${projectId}`);
|
|
228
|
-
} else if (tab.type === 'file') {
|
|
229
|
-
lines.push(`Path: ${tab.path}`);
|
|
230
|
-
if (tab.port) lines.push(`Port: ${tab.port}`);
|
|
231
|
-
} else if (tab.type === 'shell') {
|
|
232
|
-
if (tab.port) lines.push(`Port: ${tab.port}`);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
return escapeHtml(lines.join('\n'));
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Render tab content
|
|
239
|
-
function renderTabContent() {
|
|
240
|
-
const content = document.getElementById('tab-content');
|
|
241
|
-
|
|
242
|
-
if (!activeTabId || tabs.length === 0) {
|
|
243
|
-
if (currentTabPort !== null || currentTabType !== null) {
|
|
244
|
-
currentTabPort = null;
|
|
245
|
-
currentTabType = null;
|
|
246
|
-
content.innerHTML = `
|
|
247
|
-
<div class="empty-state">
|
|
248
|
-
<p>No tabs open</p>
|
|
249
|
-
<p class="hint">Click the + buttons above or ask the architect to open files/builders</p>
|
|
250
|
-
</div>
|
|
251
|
-
`;
|
|
252
|
-
}
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const tab = tabs.find(t => t.id === activeTabId);
|
|
257
|
-
if (!tab) {
|
|
258
|
-
if (currentTabPort !== null || currentTabType !== null) {
|
|
259
|
-
currentTabPort = null;
|
|
260
|
-
currentTabType = null;
|
|
261
|
-
content.innerHTML = '<div class="empty-state"><p>Tab not found</p></div>';
|
|
262
|
-
}
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Handle dashboard tab specially (no iframe, inline content)
|
|
267
|
-
if (tab.type === 'dashboard') {
|
|
268
|
-
if (currentTabType !== 'dashboard') {
|
|
269
|
-
currentTabType = 'dashboard';
|
|
270
|
-
currentTabPort = null;
|
|
271
|
-
renderDashboardTab();
|
|
272
|
-
}
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Handle activity tab specially (no iframe, inline content)
|
|
277
|
-
if (tab.type === 'activity') {
|
|
278
|
-
if (currentTabType !== 'activity') {
|
|
279
|
-
currentTabType = 'activity';
|
|
280
|
-
currentTabPort = null;
|
|
281
|
-
renderActivityTab();
|
|
282
|
-
}
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// For other tabs, only update iframe if port changed (avoid flashing on poll)
|
|
287
|
-
if (currentTabPort !== tab.port || currentTabType !== tab.type) {
|
|
288
|
-
currentTabPort = tab.port;
|
|
289
|
-
currentTabType = tab.type;
|
|
290
|
-
// Use proxied URL for terminal tabs (Spec 0062 - Secure Remote Access)
|
|
291
|
-
const url = getTerminalUrl(tab);
|
|
292
|
-
if (url) {
|
|
293
|
-
content.innerHTML = `<iframe src="${url}" title="${tab.name}" allow="clipboard-read; clipboard-write"></iframe>`;
|
|
294
|
-
} else {
|
|
295
|
-
content.innerHTML = `<div class="empty-state"><p>Terminal unavailable</p></div>`;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Force refresh the iframe for a file tab (reloads content from server)
|
|
301
|
-
function refreshFileTab(tabId) {
|
|
302
|
-
const tab = tabs.find(t => t.id === tabId);
|
|
303
|
-
if (!tab || tab.type !== 'file') return;
|
|
304
|
-
|
|
305
|
-
if (activeTabId === tabId) {
|
|
306
|
-
const content = document.getElementById('tab-content');
|
|
307
|
-
const iframe = content.querySelector('iframe');
|
|
308
|
-
if (iframe) {
|
|
309
|
-
const baseUrl = getTerminalUrl(tab);
|
|
310
|
-
iframe.src = baseUrl ? `${baseUrl}?t=${Date.now()}` : iframe.src;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// Update status bar
|
|
316
|
-
function updateStatusBar() {
|
|
317
|
-
const archStatus = document.getElementById('status-architect');
|
|
318
|
-
if (state.architect) {
|
|
319
|
-
archStatus.innerHTML = `
|
|
320
|
-
<span class="dot" style="background: var(--status-active)"></span>
|
|
321
|
-
<span>Architect: running</span>
|
|
322
|
-
`;
|
|
323
|
-
} else {
|
|
324
|
-
archStatus.innerHTML = `
|
|
325
|
-
<span class="dot" style="background: var(--text-muted)"></span>
|
|
326
|
-
<span>Architect: stopped</span>
|
|
327
|
-
`;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const builderCount = (state.builders || []).length;
|
|
331
|
-
const shellCount = (state.utils || []).length;
|
|
332
|
-
const fileCount = (state.annotations || []).length;
|
|
333
|
-
|
|
334
|
-
document.getElementById('status-builders').innerHTML = `<span>${builderCount} builder${builderCount !== 1 ? 's' : ''}</span>`;
|
|
335
|
-
document.getElementById('status-shells').innerHTML = `<span>${shellCount} shell${shellCount !== 1 ? 's' : ''}</span>`;
|
|
336
|
-
document.getElementById('status-files').innerHTML = `<span>${fileCount} file${fileCount !== 1 ? 's' : ''}</span>`;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// Select tab
|
|
340
|
-
function selectTab(tabId) {
|
|
341
|
-
activeTabId = tabId;
|
|
342
|
-
renderTabs();
|
|
343
|
-
renderTabContent();
|
|
344
|
-
scrollActiveTabIntoView();
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// Scroll the active tab into view
|
|
348
|
-
function scrollActiveTabIntoView() {
|
|
349
|
-
const container = document.getElementById('tabs-container');
|
|
350
|
-
const activeTab = container.querySelector('.tab.active');
|
|
351
|
-
if (activeTab) {
|
|
352
|
-
activeTab.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// Set up overflow detection for the tab bar
|
|
357
|
-
function setupOverflowDetection() {
|
|
358
|
-
const container = document.getElementById('tabs-container');
|
|
359
|
-
|
|
360
|
-
checkTabOverflow();
|
|
361
|
-
|
|
362
|
-
let resizeTimeout;
|
|
363
|
-
window.addEventListener('resize', () => {
|
|
364
|
-
clearTimeout(resizeTimeout);
|
|
365
|
-
resizeTimeout = setTimeout(checkTabOverflow, 100);
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
if (container) {
|
|
369
|
-
let scrollTimeout;
|
|
370
|
-
container.addEventListener('scroll', () => {
|
|
371
|
-
clearTimeout(scrollTimeout);
|
|
372
|
-
scrollTimeout = setTimeout(checkTabOverflow, 50);
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
if (typeof ResizeObserver !== 'undefined') {
|
|
377
|
-
if (container) {
|
|
378
|
-
const observer = new ResizeObserver(() => {
|
|
379
|
-
checkTabOverflow();
|
|
380
|
-
});
|
|
381
|
-
observer.observe(container);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// Check if tabs are overflowing and update the overflow button
|
|
387
|
-
function checkTabOverflow() {
|
|
388
|
-
const container = document.getElementById('tabs-container');
|
|
389
|
-
const overflowBtn = document.getElementById('overflow-btn');
|
|
390
|
-
const overflowCount = document.getElementById('overflow-count');
|
|
391
|
-
|
|
392
|
-
if (!container || !overflowBtn) return;
|
|
393
|
-
|
|
394
|
-
const isOverflowing = container.scrollWidth > container.clientWidth;
|
|
395
|
-
overflowBtn.style.display = isOverflowing ? 'flex' : 'none';
|
|
396
|
-
|
|
397
|
-
if (isOverflowing) {
|
|
398
|
-
const tabElements = container.querySelectorAll('.tab');
|
|
399
|
-
const containerRect = container.getBoundingClientRect();
|
|
400
|
-
let hiddenCount = 0;
|
|
401
|
-
|
|
402
|
-
tabElements.forEach(tab => {
|
|
403
|
-
const rect = tab.getBoundingClientRect();
|
|
404
|
-
if (rect.right > containerRect.right + 1) {
|
|
405
|
-
hiddenCount++;
|
|
406
|
-
} else if (rect.left < containerRect.left - 1) {
|
|
407
|
-
hiddenCount++;
|
|
408
|
-
}
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
overflowCount.textContent = `+${hiddenCount}`;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// Toggle the overflow menu
|
|
416
|
-
function toggleOverflowMenu() {
|
|
417
|
-
const menu = document.getElementById('overflow-menu');
|
|
418
|
-
const isHidden = menu.classList.contains('hidden');
|
|
419
|
-
|
|
420
|
-
if (isHidden) {
|
|
421
|
-
showOverflowMenu();
|
|
422
|
-
} else {
|
|
423
|
-
hideOverflowMenu();
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// Show the overflow menu
|
|
428
|
-
function showOverflowMenu() {
|
|
429
|
-
const menu = document.getElementById('overflow-menu');
|
|
430
|
-
const btn = document.getElementById('overflow-btn');
|
|
431
|
-
|
|
432
|
-
menu.innerHTML = tabs.map((tab, index) => {
|
|
433
|
-
const icon = getTabIcon(tab.type);
|
|
434
|
-
const isActive = tab.id === activeTabId;
|
|
435
|
-
return `
|
|
436
|
-
<div class="overflow-menu-item ${isActive ? 'active' : ''}"
|
|
437
|
-
role="menuitem"
|
|
438
|
-
tabindex="${index === 0 ? 0 : -1}"
|
|
439
|
-
data-tab-id="${tab.id}"
|
|
440
|
-
onclick="selectTabFromMenu('${tab.id}')"
|
|
441
|
-
onkeydown="handleOverflowMenuKeydown(event, '${tab.id}')">
|
|
442
|
-
<span class="icon">${icon}</span>
|
|
443
|
-
<span class="name">${tab.name}</span>
|
|
444
|
-
<span class="open-external"
|
|
445
|
-
onclick="event.stopPropagation(); openInNewTabFromMenu('${tab.id}')"
|
|
446
|
-
onkeydown="if(event.key==='Enter'||event.key===' '){event.stopPropagation();openInNewTabFromMenu('${tab.id}')}"
|
|
447
|
-
title="Open in new tab"
|
|
448
|
-
role="button"
|
|
449
|
-
tabindex="0"
|
|
450
|
-
aria-label="Open ${tab.name} in new tab">↗</span>
|
|
451
|
-
</div>
|
|
452
|
-
`;
|
|
453
|
-
}).join('');
|
|
454
|
-
|
|
455
|
-
menu.classList.remove('hidden');
|
|
456
|
-
btn.setAttribute('aria-expanded', 'true');
|
|
457
|
-
|
|
458
|
-
const firstItem = menu.querySelector('.overflow-menu-item');
|
|
459
|
-
if (firstItem) firstItem.focus();
|
|
460
|
-
|
|
461
|
-
setTimeout(() => {
|
|
462
|
-
document.addEventListener('click', handleOverflowClickOutside);
|
|
463
|
-
}, 0);
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// Hide the overflow menu
|
|
467
|
-
function hideOverflowMenu() {
|
|
468
|
-
const menu = document.getElementById('overflow-menu');
|
|
469
|
-
const btn = document.getElementById('overflow-btn');
|
|
470
|
-
menu.classList.add('hidden');
|
|
471
|
-
btn.setAttribute('aria-expanded', 'false');
|
|
472
|
-
document.removeEventListener('click', handleOverflowClickOutside);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// Handle click outside overflow menu
|
|
476
|
-
function handleOverflowClickOutside(event) {
|
|
477
|
-
const menu = document.getElementById('overflow-menu');
|
|
478
|
-
const btn = document.getElementById('overflow-btn');
|
|
479
|
-
if (!menu.contains(event.target) && !btn.contains(event.target)) {
|
|
480
|
-
hideOverflowMenu();
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
// Select tab from overflow menu
|
|
485
|
-
function selectTabFromMenu(tabId) {
|
|
486
|
-
hideOverflowMenu();
|
|
487
|
-
selectTab(tabId);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
// Open tab in new window from overflow menu
|
|
491
|
-
function openInNewTabFromMenu(tabId) {
|
|
492
|
-
hideOverflowMenu();
|
|
493
|
-
openInNewTab(tabId);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
// Handle keyboard navigation in overflow menu
|
|
497
|
-
// Uses shared handleMenuKeydown from utils.js (Maintenance Run 0004)
|
|
498
|
-
function handleOverflowMenuKeydown(event, tabId) {
|
|
499
|
-
handleMenuKeydown(event, 'overflow-menu', 'overflow-menu-item', hideOverflowMenu, {
|
|
500
|
-
onEnter: () => selectTabFromMenu(tabId),
|
|
501
|
-
focusOnEscape: 'overflow-btn'
|
|
502
|
-
});
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
// Open tab content in a new browser tab
|
|
506
|
-
function openInNewTab(tabId) {
|
|
507
|
-
const tab = tabs.find(t => t.id === tabId);
|
|
508
|
-
if (!tab) return;
|
|
509
|
-
|
|
510
|
-
// Use proxied URL for terminal tabs (Spec 0062 - Secure Remote Access)
|
|
511
|
-
const url = getTerminalUrl(tab);
|
|
512
|
-
if (!url) {
|
|
513
|
-
showToast('Tab not ready', 'error');
|
|
514
|
-
return;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
window.open(url, '_blank', 'noopener,noreferrer');
|
|
518
|
-
}
|
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
// Dashboard Utility Functions
|
|
2
|
-
|
|
3
|
-
// Get the base path for API calls (handles tower proxy context)
|
|
4
|
-
// When accessed via tower proxy at /project/<encoded>/, API calls need to go through the proxy
|
|
5
|
-
// When accessed directly at /, API calls go to root
|
|
6
|
-
function getApiBase() {
|
|
7
|
-
const path = window.location.pathname;
|
|
8
|
-
// Check if we're in tower proxy context: /project/<encoded>/
|
|
9
|
-
const match = path.match(/^(\/project\/[^/]+\/)/);
|
|
10
|
-
if (match) {
|
|
11
|
-
return match[1]; // Returns /project/<encoded>/
|
|
12
|
-
}
|
|
13
|
-
return '/'; // Direct access
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// Build API URL that works both directly and through tower proxy
|
|
17
|
-
function apiUrl(endpoint) {
|
|
18
|
-
const base = getApiBase();
|
|
19
|
-
// Remove leading slash from endpoint if present
|
|
20
|
-
const cleanEndpoint = endpoint.startsWith('/') ? endpoint.slice(1) : endpoint;
|
|
21
|
-
return base + cleanEndpoint;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Escape HTML special characters to prevent XSS
|
|
25
|
-
function escapeHtml(text) {
|
|
26
|
-
return String(text)
|
|
27
|
-
.replace(/&/g, '&')
|
|
28
|
-
.replace(/</g, '<')
|
|
29
|
-
.replace(/>/g, '>')
|
|
30
|
-
.replace(/"/g, '"')
|
|
31
|
-
.replace(/'/g, ''');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// XSS-safe HTML escaping (used by projects module)
|
|
35
|
-
function escapeProjectHtml(text) {
|
|
36
|
-
if (!text) return '';
|
|
37
|
-
const div = document.createElement('div');
|
|
38
|
-
div.textContent = String(text);
|
|
39
|
-
return div.innerHTML;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Escape a string for use inside a JavaScript string literal in onclick handlers
|
|
43
|
-
function escapeJsString(str) {
|
|
44
|
-
return str
|
|
45
|
-
.replace(/\\/g, '\\\\')
|
|
46
|
-
.replace(/'/g, "\\'")
|
|
47
|
-
.replace(/"/g, '\\"')
|
|
48
|
-
.replace(/\n/g, '\\n')
|
|
49
|
-
.replace(/\r/g, '\\r');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Get filename from path
|
|
53
|
-
function getFileName(path) {
|
|
54
|
-
const parts = path.split('/');
|
|
55
|
-
return parts[parts.length - 1];
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Simple DJB2 hash for change detection
|
|
59
|
-
function hashString(str) {
|
|
60
|
-
let hash = 5381;
|
|
61
|
-
for (let i = 0; i < str.length; i++) {
|
|
62
|
-
hash = ((hash << 5) + hash) + str.charCodeAt(i);
|
|
63
|
-
}
|
|
64
|
-
return hash >>> 0;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Toast notifications
|
|
68
|
-
function showToast(message, type = 'info') {
|
|
69
|
-
const container = document.getElementById('toast-container');
|
|
70
|
-
const toast = document.createElement('div');
|
|
71
|
-
toast.className = `toast ${type}`;
|
|
72
|
-
toast.textContent = message;
|
|
73
|
-
container.appendChild(toast);
|
|
74
|
-
|
|
75
|
-
setTimeout(() => {
|
|
76
|
-
toast.remove();
|
|
77
|
-
}, 3000);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// ========================================
|
|
81
|
-
// Shared Utilities (Maintenance Run 0004)
|
|
82
|
-
// ========================================
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Open a file in a new tab (or switch to existing)
|
|
86
|
-
* Consolidates duplicate code from main.js, files.js, dialogs.js
|
|
87
|
-
*
|
|
88
|
-
* @param {string} filePath - Path to the file to open
|
|
89
|
-
* @param {Object} options - Optional settings
|
|
90
|
-
* @param {number} options.lineNumber - Line number to show in toast
|
|
91
|
-
* @param {boolean} options.showSwitchToast - Show toast when switching to existing tab
|
|
92
|
-
* @param {Function} options.onSuccess - Callback after successful open
|
|
93
|
-
*/
|
|
94
|
-
async function openFileTab(filePath, options = {}) {
|
|
95
|
-
const { lineNumber, showSwitchToast = true, onSuccess } = options;
|
|
96
|
-
|
|
97
|
-
try {
|
|
98
|
-
// Check for existing tab
|
|
99
|
-
const existingTab = tabs.find(t => t.type === 'file' && t.path === filePath);
|
|
100
|
-
if (existingTab) {
|
|
101
|
-
selectTab(existingTab.id);
|
|
102
|
-
refreshFileTab(existingTab.id);
|
|
103
|
-
if (showSwitchToast) {
|
|
104
|
-
showToast(`Switched to ${getFileName(filePath)}`, 'success');
|
|
105
|
-
}
|
|
106
|
-
if (onSuccess) onSuccess();
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Create new tab
|
|
111
|
-
const response = await fetch(apiUrl('api/tabs/file'), {
|
|
112
|
-
method: 'POST',
|
|
113
|
-
headers: { 'Content-Type': 'application/json' },
|
|
114
|
-
body: JSON.stringify({ path: filePath })
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
if (!response.ok) {
|
|
118
|
-
throw new Error(await response.text());
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const result = await response.json();
|
|
122
|
-
await refresh();
|
|
123
|
-
|
|
124
|
-
// Select the new tab
|
|
125
|
-
const newTab = tabs.find(t => t.type === 'file' && (t.path === filePath || t.annotationId === result.id));
|
|
126
|
-
if (newTab) {
|
|
127
|
-
selectTab(newTab.id);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const lineInfo = lineNumber ? `:${lineNumber}` : '';
|
|
131
|
-
showToast(`Opened ${getFileName(filePath)}${lineInfo}`, 'success');
|
|
132
|
-
if (onSuccess) onSuccess();
|
|
133
|
-
} catch (err) {
|
|
134
|
-
showToast('Failed to open file: ' + err.message, 'error');
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Handle keyboard navigation in dropdown menus
|
|
140
|
-
* Consolidates duplicate code from dialogs.js and tabs.js
|
|
141
|
-
*
|
|
142
|
-
* @param {KeyboardEvent} event - The keyboard event
|
|
143
|
-
* @param {string} menuId - ID of the menu element
|
|
144
|
-
* @param {string} itemClass - CSS class of menu items
|
|
145
|
-
* @param {Function} hideFunction - Function to hide the menu
|
|
146
|
-
* @param {Object} options - Optional settings
|
|
147
|
-
* @param {Function} options.onEnter - Custom handler for Enter/Space (default: call data-action)
|
|
148
|
-
* @param {string} options.focusOnEscape - Element ID to focus after Escape
|
|
149
|
-
*/
|
|
150
|
-
function handleMenuKeydown(event, menuId, itemClass, hideFunction, options = {}) {
|
|
151
|
-
const { onEnter, focusOnEscape } = options;
|
|
152
|
-
const menu = document.getElementById(menuId);
|
|
153
|
-
const items = Array.from(menu.querySelectorAll(`.${itemClass}`));
|
|
154
|
-
const currentIndex = items.findIndex(item => item === document.activeElement);
|
|
155
|
-
|
|
156
|
-
switch (event.key) {
|
|
157
|
-
case 'ArrowDown':
|
|
158
|
-
event.preventDefault();
|
|
159
|
-
const nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
|
|
160
|
-
items[nextIndex].focus();
|
|
161
|
-
break;
|
|
162
|
-
case 'ArrowUp':
|
|
163
|
-
event.preventDefault();
|
|
164
|
-
const prevIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
|
|
165
|
-
items[prevIndex].focus();
|
|
166
|
-
break;
|
|
167
|
-
case 'Enter':
|
|
168
|
-
case ' ':
|
|
169
|
-
event.preventDefault();
|
|
170
|
-
if (onEnter) {
|
|
171
|
-
onEnter(event);
|
|
172
|
-
} else {
|
|
173
|
-
const actionName = event.target.dataset.action;
|
|
174
|
-
if (actionName && typeof window[actionName] === 'function') {
|
|
175
|
-
window[actionName]();
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
break;
|
|
179
|
-
case 'Escape':
|
|
180
|
-
event.preventDefault();
|
|
181
|
-
hideFunction();
|
|
182
|
-
if (focusOnEscape) {
|
|
183
|
-
document.getElementById(focusOnEscape).focus();
|
|
184
|
-
}
|
|
185
|
-
break;
|
|
186
|
-
case 'Tab':
|
|
187
|
-
hideFunction();
|
|
188
|
-
break;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|