@kolbo/mcp 1.103.0 → 1.104.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/package.json +1 -1
- package/src/apps/html.js +150 -38
- package/src/apps/theme.js +46 -30
- package/src/apps/widgets/generation.js +9 -79
- package/src/apps/widgets/list.js +0 -1
- package/src/apps/widgets/mediaGrid.js +0 -1
package/package.json
CHANGED
package/src/apps/html.js
CHANGED
|
@@ -62,7 +62,8 @@ var ICONS = {
|
|
|
62
62
|
chevronDown: _svg('<path d="M6 9l6 6 6-6"/>'),
|
|
63
63
|
chevronLeft: _svg('<path d="M15 18l-6-6 6-6"/>'),
|
|
64
64
|
chevronRight: _svg('<path d="M9 18l6-6-6-6"/>'),
|
|
65
|
-
chevronUp: _svg('<path d="M18 15l-6-6-6 6"/>')
|
|
65
|
+
chevronUp: _svg('<path d="M18 15l-6-6-6 6"/>'),
|
|
66
|
+
expand: _svg('<path d="M15 3h6v6"/><path d="M9 21H3v-6"/><path d="M21 3l-7 7"/><path d="M3 21l7-7"/>')
|
|
66
67
|
};
|
|
67
68
|
// Media-kind → icon (accepts model kind / media_type strings).
|
|
68
69
|
function kindIcon(kind) {
|
|
@@ -141,9 +142,12 @@ function monogram(name) {
|
|
|
141
142
|
var ch = String(name || '').replace(/^[@#]+/, '').trim().charAt(0) || '?';
|
|
142
143
|
return '<span class="k-mono-icon">' + esc(ch.toUpperCase()) + '</span>';
|
|
143
144
|
}
|
|
144
|
-
// In-widget
|
|
145
|
-
// rows)
|
|
146
|
-
//
|
|
145
|
+
// In-widget lightbox — every card (generation results, chips, media grid, list
|
|
146
|
+
// rows) opens media here. It shows inside the card at once, then asks the host
|
|
147
|
+
// for fullscreen: granted, it fills the host viewport; refused or unanswered,
|
|
148
|
+
// the in-card popup still works (a click never depends on the host answering).
|
|
149
|
+
// Any [data-peek] node opens it through ONE delegated listener, and the
|
|
150
|
+
// lightbox pages through the other [data-peek] items in the same #stage.
|
|
147
151
|
function ensurePeek() {
|
|
148
152
|
if (el('peek')) return el('peek');
|
|
149
153
|
var card = document.querySelector('.k-card') || document.body;
|
|
@@ -151,55 +155,116 @@ function ensurePeek() {
|
|
|
151
155
|
box.id = 'peek';
|
|
152
156
|
box.className = 'k-peek';
|
|
153
157
|
box.hidden = true;
|
|
154
|
-
box.innerHTML = '<
|
|
155
|
-
+ '<
|
|
156
|
-
+ '<
|
|
157
|
-
+ '<
|
|
158
|
+
box.innerHTML = '<div class="k-peek-bar"><span class="k-peek-cap" id="peek-cap"></span>'
|
|
159
|
+
+ '<span class="k-peek-count" id="peek-count"></span>'
|
|
160
|
+
+ '<button type="button" class="k-peek-btn" id="peek-dl" aria-label="Download"></button>'
|
|
161
|
+
+ '<button type="button" class="k-peek-btn" id="peek-close" aria-label="Close"></button></div>'
|
|
162
|
+
+ '<div class="k-peek-stage" id="peek-stage"><img id="peek-img" alt="">'
|
|
163
|
+
// nofullscreen: host iframes are never granted allow="fullscreen", so the
|
|
164
|
+
// native button is always greyed out. The lightbox IS the fullscreen view.
|
|
165
|
+
+ '<video id="peek-video" playsinline controls controlslist="nofullscreen"></video></div>'
|
|
166
|
+
+ '<button type="button" class="k-peek-nav prev" id="peek-prev" aria-label="Previous"></button>'
|
|
167
|
+
+ '<button type="button" class="k-peek-nav next" id="peek-next" aria-label="Next"></button>';
|
|
158
168
|
card.appendChild(box);
|
|
159
169
|
el('peek-close').innerHTML = ICONS.x;
|
|
170
|
+
el('peek-dl').innerHTML = ICONS.download;
|
|
171
|
+
el('peek-prev').innerHTML = ICONS.chevronLeft;
|
|
172
|
+
el('peek-next').innerHTML = ICONS.chevronRight;
|
|
160
173
|
el('peek-close').onclick = function (e) { e.stopPropagation(); closePeek(); };
|
|
161
|
-
|
|
174
|
+
el('peek-prev').onclick = function (e) { e.stopPropagation(); stepPeek(-1); };
|
|
175
|
+
el('peek-next').onclick = function (e) { e.stopPropagation(); stepPeek(1); };
|
|
176
|
+
el('peek-dl').onclick = function (e) {
|
|
177
|
+
e.stopPropagation();
|
|
178
|
+
var it = peekList[peekIndex];
|
|
179
|
+
if (it) window.kolbo.openLink(downloadUrl(it.dl || it.url));
|
|
180
|
+
};
|
|
181
|
+
box.onclick = function (e) { if (e.target === box || e.target === el('peek-stage')) closePeek(); };
|
|
162
182
|
return box;
|
|
163
183
|
}
|
|
164
184
|
function peekAttrs(url, kind, cap) {
|
|
165
185
|
if (!url) return '';
|
|
166
186
|
return ' data-peek="' + esc(url) + '" data-peek-kind="' + esc(kind || 'image') + '" data-peek-cap="' + esc(cap || '') + '"';
|
|
167
187
|
}
|
|
168
|
-
function
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
node.
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
};
|
|
176
|
-
});
|
|
188
|
+
function peekItem(node) {
|
|
189
|
+
return {
|
|
190
|
+
url: node.getAttribute('data-peek'),
|
|
191
|
+
kind: node.getAttribute('data-peek-kind') || 'image',
|
|
192
|
+
cap: node.getAttribute('data-peek-cap') || '',
|
|
193
|
+
dl: node.getAttribute('data-peek-dl') || ''
|
|
194
|
+
};
|
|
177
195
|
}
|
|
178
|
-
|
|
179
|
-
|
|
196
|
+
document.addEventListener('click', function (e) {
|
|
197
|
+
var node = e.target && e.target.closest && e.target.closest('[data-peek]');
|
|
198
|
+
if (!node || node.closest('#peek')) return;
|
|
199
|
+
// A control INSIDE a previewable tile (download, use, a player) keeps its own job.
|
|
200
|
+
var ctl = e.target.closest('button, a, audio, video[controls]');
|
|
201
|
+
if (ctl && ctl !== node && node.contains(ctl)) return;
|
|
202
|
+
e.preventDefault();
|
|
203
|
+
e.stopPropagation();
|
|
204
|
+
var scope = node.closest('#stage') || node.parentNode;
|
|
205
|
+
var seen = {};
|
|
206
|
+
var list = [];
|
|
207
|
+
Array.prototype.forEach.call(scope.querySelectorAll('[data-peek]'), function (n) {
|
|
208
|
+
var it = peekItem(n);
|
|
209
|
+
if (!it.url || seen[it.url]) return;
|
|
210
|
+
seen[it.url] = 1;
|
|
211
|
+
list.push(it);
|
|
212
|
+
});
|
|
213
|
+
var at = peekItem(node);
|
|
214
|
+
openPeek(at.url, at.kind, at.cap, { list: list, time: +node.getAttribute('data-peek-time') || 0 });
|
|
215
|
+
}, true);
|
|
216
|
+
var peekList = [];
|
|
217
|
+
var peekIndex = 0;
|
|
218
|
+
// Whether THIS peek took the iframe fullscreen — only then does closing it
|
|
219
|
+
// hand the display mode back.
|
|
180
220
|
var peekWentFullscreen = false;
|
|
181
|
-
function
|
|
182
|
-
|
|
183
|
-
var box = ensurePeek();
|
|
221
|
+
function showPeekItem(time) {
|
|
222
|
+
var it = peekList[peekIndex];
|
|
184
223
|
var img = el('peek-img');
|
|
185
224
|
var vid = el('peek-video');
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
225
|
+
if (it.kind === 'video') {
|
|
226
|
+
img.removeAttribute('src');
|
|
227
|
+
vid.setAttribute('src', it.url);
|
|
228
|
+
if (time) vid.addEventListener('loadedmetadata', function () { try { vid.currentTime = time; } catch (e) {} }, { once: true });
|
|
229
|
+
var p = vid.play && vid.play();
|
|
230
|
+
if (p && p.catch) p.catch(function () {});
|
|
231
|
+
} else {
|
|
232
|
+
try { vid.pause(); } catch (e) {}
|
|
233
|
+
vid.removeAttribute('src');
|
|
234
|
+
img.setAttribute('src', it.url);
|
|
235
|
+
}
|
|
236
|
+
el('peek-cap').textContent = it.cap || '';
|
|
237
|
+
var many = peekList.length > 1;
|
|
238
|
+
el('peek-count').textContent = many ? (peekIndex + 1) + ' / ' + peekList.length : '';
|
|
239
|
+
el('peek-prev').hidden = !many;
|
|
240
|
+
el('peek-next').hidden = !many;
|
|
241
|
+
}
|
|
242
|
+
function stepPeek(d) {
|
|
243
|
+
var box = el('peek');
|
|
244
|
+
if (!box || box.hidden || peekList.length < 2) return;
|
|
245
|
+
peekIndex = (peekIndex + d + peekList.length) % peekList.length;
|
|
246
|
+
showPeekItem(0);
|
|
247
|
+
}
|
|
248
|
+
function openPeek(url, kind, cap, opts) {
|
|
249
|
+
if (!url) return;
|
|
250
|
+
opts = opts || {};
|
|
251
|
+
var box = ensurePeek();
|
|
252
|
+
peekList = [{ url: url, kind: kind || 'image', cap: cap || '' }];
|
|
253
|
+
peekIndex = 0;
|
|
254
|
+
(opts.list || []).forEach(function (it, i) { if (it.url === url) { peekList = opts.list; peekIndex = i; } });
|
|
255
|
+
// Pause inline players so two soundtracks never overlap.
|
|
256
|
+
Array.prototype.forEach.call(document.querySelectorAll('video'), function (v) {
|
|
257
|
+
if (v.id !== 'peek-video') { try { v.pause(); } catch (e) {} }
|
|
258
|
+
});
|
|
259
|
+
showPeekItem(opts.time || 0);
|
|
190
260
|
box.hidden = false;
|
|
261
|
+
document.documentElement.classList.add('k-peek-open');
|
|
191
262
|
if (window.kolbo && window.kolbo.notifySize) window.kolbo.notifySize();
|
|
192
|
-
|
|
193
|
-
// card, so a host that refuses fullscreen (or never answers the request)
|
|
194
|
-
// still shows the preview — the failure mode is "smaller", never "nothing".
|
|
195
|
-
// Skip the request when the card itself is fullscreen; the overlay covers it.
|
|
196
|
-
if (!window.kolbo || !window.kolbo.requestDisplayMode) return;
|
|
197
|
-
if (document.documentElement.classList.contains('k-fullscreen')) return;
|
|
263
|
+
if (peekWentFullscreen || !window.kolbo || !window.kolbo.requestDisplayMode) return;
|
|
198
264
|
try {
|
|
199
265
|
window.kolbo.requestDisplayMode('fullscreen').then(function (res) {
|
|
200
|
-
// The user may have closed the peek before the host answered — hand the
|
|
201
|
-
// grant straight back instead of fullscreening an empty card.
|
|
202
266
|
if (!(res && res.mode === 'fullscreen')) return;
|
|
267
|
+
// Closed before the host answered — hand the grant straight back.
|
|
203
268
|
if (box.hidden) { window.kolbo.requestDisplayMode('inline').catch(function () {}); return; }
|
|
204
269
|
peekWentFullscreen = true;
|
|
205
270
|
document.documentElement.classList.add('k-peek-fs');
|
|
@@ -207,22 +272,69 @@ function openPeek(url, kind, cap) {
|
|
|
207
272
|
}).catch(function () {});
|
|
208
273
|
} catch (e) {}
|
|
209
274
|
}
|
|
210
|
-
function closePeek() {
|
|
275
|
+
function closePeek(hostExited) {
|
|
211
276
|
var box = el('peek');
|
|
212
277
|
if (!box || box.hidden) return;
|
|
213
278
|
box.hidden = true;
|
|
279
|
+
document.documentElement.classList.remove('k-peek-open');
|
|
214
280
|
el('peek-img').removeAttribute('src');
|
|
215
281
|
var vid = el('peek-video');
|
|
216
282
|
if (vid) { try { vid.pause(); } catch (e) {} vid.removeAttribute('src'); }
|
|
217
283
|
if (peekWentFullscreen) {
|
|
218
284
|
peekWentFullscreen = false;
|
|
219
285
|
document.documentElement.classList.remove('k-peek-fs');
|
|
220
|
-
try { window.kolbo.requestDisplayMode('inline').catch(function () {}); } catch (e) {}
|
|
286
|
+
if (hostExited !== true) { try { window.kolbo.requestDisplayMode('inline').catch(function () {}); } catch (e) {} }
|
|
221
287
|
if (window.kolbo.setFullscreen) window.kolbo.setFullscreen(false);
|
|
222
288
|
}
|
|
223
289
|
if (window.kolbo && window.kolbo.notifySize) window.kolbo.notifySize();
|
|
224
290
|
}
|
|
225
|
-
|
|
291
|
+
// The host's own exit control (claude.ai's fullscreen X / Esc) drops the card
|
|
292
|
+
// back to inline without asking us — close the lightbox along with it.
|
|
293
|
+
window.kolbo.onThemeChange(function (ctx) {
|
|
294
|
+
if (peekWentFullscreen && ctx && ctx.displayMode && ctx.displayMode !== 'fullscreen') closePeek(true);
|
|
295
|
+
});
|
|
296
|
+
document.addEventListener('keydown', function (e) {
|
|
297
|
+
if (e.key === 'Escape') closePeek();
|
|
298
|
+
else if (e.key === 'ArrowLeft') stepPeek(-1);
|
|
299
|
+
else if (e.key === 'ArrowRight') stepPeek(1);
|
|
300
|
+
});
|
|
301
|
+
// Every inline <video controls> gets an Expand button (and double-click) that
|
|
302
|
+
// opens the lightbox at the current time; the native fullscreen button is
|
|
303
|
+
// hidden because the host iframe can never honour it. A MutationObserver
|
|
304
|
+
// covers every render path (results, batch/status grids, scenes, media-grid
|
|
305
|
+
// play swap) with no per-widget wiring.
|
|
306
|
+
function wireVideoExpand(root) {
|
|
307
|
+
if (!root || !root.querySelectorAll) return;
|
|
308
|
+
var vids = root.tagName === 'VIDEO' ? [root] : root.querySelectorAll('video[controls]');
|
|
309
|
+
Array.prototype.forEach.call(vids, function (v) {
|
|
310
|
+
if (v.id === 'peek-video' || !v.controls || v.getAttribute('data-expand') || !v.parentNode) return;
|
|
311
|
+
var src = v.getAttribute('src');
|
|
312
|
+
if (!src) return;
|
|
313
|
+
v.setAttribute('data-expand', '1');
|
|
314
|
+
v.setAttribute('controlslist', 'nofullscreen');
|
|
315
|
+
var b = document.createElement('button');
|
|
316
|
+
b.type = 'button';
|
|
317
|
+
b.className = 'k-vexpand';
|
|
318
|
+
b.title = 'Expand';
|
|
319
|
+
b.setAttribute('aria-label', 'Expand video');
|
|
320
|
+
b.innerHTML = ICONS.expand;
|
|
321
|
+
b.setAttribute('data-peek', src);
|
|
322
|
+
b.setAttribute('data-peek-kind', 'video');
|
|
323
|
+
var holder = v.parentNode.closest && v.parentNode.closest('[title]');
|
|
324
|
+
if (holder) b.setAttribute('data-peek-cap', holder.getAttribute('title'));
|
|
325
|
+
// Resume where the inline player was.
|
|
326
|
+
function mark() { b.setAttribute('data-peek-time', String(v.currentTime || 0)); }
|
|
327
|
+
b.addEventListener('pointerdown', mark);
|
|
328
|
+
b.addEventListener('keydown', mark);
|
|
329
|
+
v.addEventListener('dblclick', function (e) { e.preventDefault(); mark(); b.click(); });
|
|
330
|
+
v.parentNode.insertBefore(b, v.nextSibling);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
if (window.MutationObserver) {
|
|
334
|
+
new MutationObserver(function (muts) {
|
|
335
|
+
muts.forEach(function (m) { Array.prototype.forEach.call(m.addedNodes, wireVideoExpand); });
|
|
336
|
+
}).observe(document.documentElement, { childList: true, subtree: true });
|
|
337
|
+
}
|
|
226
338
|
// Pull structuredContent out of a tools/call result (host bridge shape).
|
|
227
339
|
function structured(res) {
|
|
228
340
|
if (!res) return null;
|
package/src/apps/theme.js
CHANGED
|
@@ -85,7 +85,7 @@ body {
|
|
|
85
85
|
.k-prompt { color: var(--text-muted); font-size: 12.5px; margin-bottom: 4px; word-break: break-word;
|
|
86
86
|
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
|
|
87
87
|
user-select: text; -webkit-user-select: text; cursor: text; }
|
|
88
|
-
.k-prompt.expanded { -webkit-line-clamp: unset; white-space: pre-wrap; }
|
|
88
|
+
.k-prompt.expanded { -webkit-line-clamp: unset; white-space: pre-wrap; }
|
|
89
89
|
.k-text-tools { display: flex; gap: 2px; justify-content: flex-end; margin: 0 0 10px; }
|
|
90
90
|
.k-text-btn {
|
|
91
91
|
display: inline-flex; align-items: center; gap: 4px;
|
|
@@ -138,27 +138,58 @@ body {
|
|
|
138
138
|
.k-peek-hit { cursor: zoom-in; }
|
|
139
139
|
.k-peek {
|
|
140
140
|
position: absolute; inset: 0; z-index: 20;
|
|
141
|
-
display: flex; flex-direction: column;
|
|
142
|
-
|
|
143
|
-
background: rgba(8, 10, 16, 0.92);
|
|
141
|
+
display: flex; flex-direction: column;
|
|
142
|
+
background: rgba(6, 8, 12, 0.96);
|
|
144
143
|
}
|
|
145
144
|
.k-peek[hidden] { display: none !important; }
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
145
|
+
/* An inline lightbox needs room: a short card (one list row, a 3-tile grid)
|
|
146
|
+
used to clip the media to a sliver. The card grows while it is open and
|
|
147
|
+
notifySize() asks the host for the height. */
|
|
148
|
+
html.k-peek-open .k-card { min-height: 460px; }
|
|
149
|
+
.k-peek-bar { flex: none; display: flex; align-items: center; gap: 8px; padding: 8px 8px 8px 14px; min-height: 44px; }
|
|
150
|
+
.k-peek-cap { flex: 1; min-width: 0; font-size: 12.5px; color: rgba(255,255,255,0.78);
|
|
151
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
152
|
+
.k-peek-count { flex: none; font-size: 11.5px; color: rgba(255,255,255,0.55); font-family: 'JetBrains Mono', ui-monospace, monospace; }
|
|
153
|
+
.k-peek-btn {
|
|
154
|
+
flex: none; width: 32px; height: 32px; border: 1px solid rgba(255,255,255,0.14); border-radius: 999px;
|
|
155
|
+
background: rgba(255,255,255,0.08); color: #fff; cursor: pointer; font-size: 15px;
|
|
156
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
157
|
+
}
|
|
158
|
+
.k-peek-btn:hover { background: var(--brand); border-color: var(--brand); }
|
|
159
|
+
/* min-height:0 + max 100% on BOTH axes: the media is contained in whatever box
|
|
160
|
+
the lightbox has, never cropped and never overflowing it. */
|
|
161
|
+
.k-peek-stage { flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center; padding: 0 12px 12px; }
|
|
162
|
+
.k-peek-stage img, .k-peek-stage video {
|
|
163
|
+
display: none; max-width: 100%; max-height: 100%; width: auto; height: auto;
|
|
164
|
+
object-fit: contain; border-radius: 8px; background: #000;
|
|
149
165
|
}
|
|
150
|
-
.k-peek img[src], .k-peek video[src] { display: block; }
|
|
151
|
-
.k-peek-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
166
|
+
.k-peek-stage img[src], .k-peek-stage video[src] { display: block; }
|
|
167
|
+
.k-peek-stage video[src] { width: 100%; height: 100%; }
|
|
168
|
+
.k-peek-nav {
|
|
169
|
+
position: absolute; top: 50%; transform: translateY(-50%); z-index: 2;
|
|
170
|
+
width: 40px; height: 40px; border: 1px solid rgba(255,255,255,0.14); border-radius: 999px;
|
|
171
|
+
background: rgba(0,0,0,0.55); color: #fff; cursor: pointer; font-size: 20px;
|
|
155
172
|
display: inline-flex; align-items: center; justify-content: center;
|
|
156
173
|
}
|
|
157
|
-
.k-peek-
|
|
158
|
-
|
|
174
|
+
.k-peek-nav[hidden] { display: none; }
|
|
175
|
+
.k-peek-nav:hover { background: var(--brand); border-color: var(--brand); }
|
|
176
|
+
.k-peek-nav.prev { left: 10px; }
|
|
177
|
+
.k-peek-nav.next { right: 10px; }
|
|
178
|
+
/* Host granted fullscreen: the lightbox owns the whole viewport, not the card. */
|
|
159
179
|
html.k-peek-fs, html.k-peek-fs body { height: 100%; overflow: hidden; }
|
|
160
180
|
html.k-peek-fs .k-peek { position: fixed; inset: 0; z-index: 50; }
|
|
161
|
-
html.k-peek-fs .k-
|
|
181
|
+
html.k-peek-fs .k-card { min-height: 0; }
|
|
182
|
+
/* Expand button on every inline video — replaces the native fullscreen button,
|
|
183
|
+
which a host iframe without allow="fullscreen" always greys out. */
|
|
184
|
+
.k-vexpand {
|
|
185
|
+
position: absolute; top: 8px; left: 8px; z-index: 3;
|
|
186
|
+
width: 30px; height: 30px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.18);
|
|
187
|
+
background: rgba(0, 0, 0, 0.62); color: #fff; font-size: 14px; cursor: pointer;
|
|
188
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
189
|
+
}
|
|
190
|
+
.k-vexpand:hover { background: var(--brand); border-color: var(--brand); }
|
|
191
|
+
/* Recent Chromium still draws the native button despite controlslist. */
|
|
192
|
+
video::-webkit-media-controls-fullscreen-button { display: none !important; }
|
|
162
193
|
/* ---- Plans / upgrade card ---- */
|
|
163
194
|
.k-plan-toggle { display: inline-flex; gap: 2px; padding: 3px; margin-bottom: 12px;
|
|
164
195
|
background: var(--surface-2); border: 1px solid var(--border); border-radius: 999px; }
|
|
@@ -293,21 +324,6 @@ html.k-peek-fs .k-peek img, html.k-peek-fs .k-peek video { max-height: calc(100v
|
|
|
293
324
|
border-radius: 12px; background: #000; border: 1px solid var(--border); cursor: zoom-in; }
|
|
294
325
|
.k-viewer video { cursor: default; }
|
|
295
326
|
|
|
296
|
-
/* ---- Fullscreen (ui/request-display-mode granted) ----
|
|
297
|
-
position:fixed pins the card to the iframe viewport regardless of document
|
|
298
|
-
flow/host sizing quirks — the media physically cannot exceed the screen. */
|
|
299
|
-
html.k-fullscreen, html.k-fullscreen body { height: 100%; overflow: hidden; }
|
|
300
|
-
html.k-fullscreen .k-card { position: fixed; inset: 0; display: flex; flex-direction: column; border-radius: 0; }
|
|
301
|
-
html.k-fullscreen .k-body { flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
|
|
302
|
-
html.k-fullscreen .k-prompt, html.k-fullscreen .k-chips { flex: none; }
|
|
303
|
-
html.k-fullscreen #stage { flex: 1; min-height: 0; display: flex; flex-direction: column; }
|
|
304
|
-
html.k-fullscreen .k-viewer { flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center; margin-bottom: 6px; }
|
|
305
|
-
html.k-fullscreen .k-viewer img, html.k-fullscreen .k-viewer video {
|
|
306
|
-
max-height: 100%; max-width: 100%; height: auto;
|
|
307
|
-
width: auto; margin: 0 auto; cursor: zoom-out; object-fit: contain; }
|
|
308
|
-
html.k-fullscreen .k-thumbs { flex: none; }
|
|
309
|
-
html.k-fullscreen .k-thumbs .k-thumb { width: 56px; height: 56px; }
|
|
310
|
-
html.k-fullscreen .k-actions { flex: none; padding-top: 8px; }
|
|
311
327
|
.k-expand-hint { display: none; }
|
|
312
328
|
.k-thumbs { display: flex; gap: 6px; margin: 10px 0 2px; }
|
|
313
329
|
.k-thumbs .k-thumb { width: 48px; height: 48px; border-radius: 8px; overflow: hidden; cursor: pointer;
|
|
@@ -335,7 +335,6 @@ function renderChips(sc) {
|
|
|
335
335
|
if (sc.count > 1) h += chip('×' + sc.count);
|
|
336
336
|
h += referenceHTML(sc);
|
|
337
337
|
el('chips').innerHTML = h;
|
|
338
|
-
bindPeekHits(el('chips'));
|
|
339
338
|
}
|
|
340
339
|
|
|
341
340
|
var REF_VIDEO_RE = /\\.(mp4|mov|webm|mkv|avi|m4v)(\\?|#|$)/i;
|
|
@@ -920,13 +919,6 @@ function renderImages(sc, urls) {
|
|
|
920
919
|
else renderLinks(urls);
|
|
921
920
|
window.kolbo.notifySize();
|
|
922
921
|
};
|
|
923
|
-
// Click → expand into an in-Claude fullscreen viewer (all actions stay
|
|
924
|
-
// available); click again (or Exit) collapses back. Hosts that refuse
|
|
925
|
-
// fullscreen fall back to opening the original in a new tab.
|
|
926
|
-
setTimeout(function () {
|
|
927
|
-
var img = el('main-img');
|
|
928
|
-
if (img) img.onclick = toggleFullscreen;
|
|
929
|
-
}, 0);
|
|
930
922
|
var thumbs = '';
|
|
931
923
|
if (urls.length > 1) {
|
|
932
924
|
thumbs = '<div class="k-thumbs">' + urls.map(function (u, i) {
|
|
@@ -935,6 +927,10 @@ function renderImages(sc, urls) {
|
|
|
935
927
|
}
|
|
936
928
|
el('stage').innerHTML = viewer + thumbs;
|
|
937
929
|
wireDlButtons(el('stage'));
|
|
930
|
+
// Click → lightbox (host fullscreen when granted), paging through every result.
|
|
931
|
+
el('main-img').onclick = function () {
|
|
932
|
+
openPeek(state.urls[selected], 'image', '', { list: state.urls.map(function (u) { return { url: u, kind: 'image' }; }) });
|
|
933
|
+
};
|
|
938
934
|
Array.prototype.forEach.call(el('stage').querySelectorAll('.k-thumb'), function (t) {
|
|
939
935
|
t.onclick = function () {
|
|
940
936
|
selected = +t.getAttribute('data-i');
|
|
@@ -1094,20 +1090,11 @@ function renderBatchGrid(sc) {
|
|
|
1094
1090
|
(it.label ? ' title="' + esc(it.label) + '"' : '') + '>' +
|
|
1095
1091
|
(it.type === 'video'
|
|
1096
1092
|
? '<video class="k-cell-fill" src="' + esc(it.url) + '" controls playsinline preload="metadata"></video>'
|
|
1097
|
-
: '<img class="k-cell-fill" src="' + esc(it.url) + '" alt="" loading="lazy"
|
|
1093
|
+
: '<img class="k-cell-fill k-peek-hit" src="' + esc(it.url) + '" alt="" loading="lazy"' + peekAttrs(it.url, 'image', it.label) + '>') +
|
|
1098
1094
|
(it.label ? '<span class="k-skel-cap" title="' + esc(it.label) + '">' + esc(it.label) + '</span>' : '') +
|
|
1099
1095
|
dlBtnHTML(it.url) + '</div>';
|
|
1100
1096
|
}).join('') + '</div>';
|
|
1101
1097
|
wireDlButtons(el('stage'));
|
|
1102
|
-
// In-widget popup, not the host round-trip focusMedia() uses — a batch grid
|
|
1103
|
-
// tile has no visible full-size image otherwise, so a host that never resolves
|
|
1104
|
-
// requestDisplayMode() (or drops window.open after the async round trip eats
|
|
1105
|
-
// the click's user-activation window) leaves the click doing nothing at all.
|
|
1106
|
-
Array.prototype.forEach.call(el('stage').querySelectorAll('[data-focus]'), function (cell) {
|
|
1107
|
-
var it = items[+cell.getAttribute('data-focus')];
|
|
1108
|
-
if (it.type !== 'image') return; // <video controls> owns its own clicks
|
|
1109
|
-
cell.onclick = function () { openPeek(it.url, 'image', it.label); };
|
|
1110
|
-
});
|
|
1111
1098
|
renderActions(sc);
|
|
1112
1099
|
window.kolbo.notifySize();
|
|
1113
1100
|
}
|
|
@@ -1142,7 +1129,7 @@ function renderStatusGrid(sc) {
|
|
|
1142
1129
|
? '<video class="k-cell-fill" src="' + esc(it.url) + '" controls playsinline preload="metadata"></video>'
|
|
1143
1130
|
: it.kind === 'audio'
|
|
1144
1131
|
? '<div class="k-cell-fill" style="display:flex;align-items:center;justify-content:center">' + ICONS.sound + '</div>'
|
|
1145
|
-
: '<img class="k-cell-fill" src="' + esc(it.url) + '" alt="" loading="lazy"
|
|
1132
|
+
: '<img class="k-cell-fill k-peek-hit" src="' + esc(it.url) + '" alt="" loading="lazy"' + peekAttrs(it.url, 'image', it.title) + '>') +
|
|
1146
1133
|
cap + dlBtnHTML(it.url) + '</div>';
|
|
1147
1134
|
}
|
|
1148
1135
|
var failed = it.state === 'failed' || it.state === 'cancelled';
|
|
@@ -1152,11 +1139,6 @@ function renderStatusGrid(sc) {
|
|
|
1152
1139
|
return '<div class="k-skel square">' + badge + cap + '</div>';
|
|
1153
1140
|
}).join('') + '</div>';
|
|
1154
1141
|
wireDlButtons(el('stage'));
|
|
1155
|
-
Array.prototype.forEach.call(el('stage').querySelectorAll('[data-focus]'), function (cell) {
|
|
1156
|
-
var it = items[+cell.getAttribute('data-focus')];
|
|
1157
|
-
if (it.kind !== 'image') return; // <video controls> owns its own clicks
|
|
1158
|
-
cell.onclick = function () { openPeek(it.url, 'image', it.title); };
|
|
1159
|
-
});
|
|
1160
1142
|
renderActions(sc);
|
|
1161
1143
|
window.kolbo.notifySize();
|
|
1162
1144
|
}
|
|
@@ -1182,8 +1164,9 @@ function renderScenes(sc) {
|
|
|
1182
1164
|
makeExpandable(el('scene-cap'), it.label);
|
|
1183
1165
|
wireDlButtons(el('stage'));
|
|
1184
1166
|
if (it.type === 'image') {
|
|
1185
|
-
|
|
1186
|
-
|
|
1167
|
+
el('scene-main').onclick = function () {
|
|
1168
|
+
openPeek(it.url, 'image', it.label, { list: items.map(function (x) { return { url: x.url, kind: x.type === 'video' ? 'video' : 'image', cap: x.label }; }) });
|
|
1169
|
+
};
|
|
1187
1170
|
}
|
|
1188
1171
|
Array.prototype.forEach.call(el('stage').querySelectorAll('.k-thumb'), function (t) {
|
|
1189
1172
|
t.onclick = function () { selected = +t.getAttribute('data-i'); renderScenes(sc); window.kolbo.notifySize(); };
|
|
@@ -1191,27 +1174,6 @@ function renderScenes(sc) {
|
|
|
1191
1174
|
renderActions(sc);
|
|
1192
1175
|
}
|
|
1193
1176
|
|
|
1194
|
-
// Fullscreen a single item out of a multi-item grid (Creative Director
|
|
1195
|
-
// scenes). Exit restores the grid.
|
|
1196
|
-
function focusMedia(url) {
|
|
1197
|
-
window.kolbo.requestDisplayMode('fullscreen').then(function (res) {
|
|
1198
|
-
if (!(res && res.mode === 'fullscreen')) return window.kolbo.openLink(url);
|
|
1199
|
-
isFullscreen = true;
|
|
1200
|
-
el('stage').innerHTML = '<div class="k-viewer"><img id="focus-img" src="' + esc(url) + '" alt="" style="cursor:zoom-out">' + dlBtnHTML(url) + '</div>';
|
|
1201
|
-
wireDlButtons(el('stage'));
|
|
1202
|
-
el('focus-img').onclick = exitFocus;
|
|
1203
|
-
applyFullscreen(true, exitFocus);
|
|
1204
|
-
window.kolbo.notifySize();
|
|
1205
|
-
}).catch(function () { window.kolbo.openLink(url); });
|
|
1206
|
-
}
|
|
1207
|
-
function exitFocus() {
|
|
1208
|
-
window.kolbo.requestDisplayMode('inline').catch(function () {});
|
|
1209
|
-
isFullscreen = false;
|
|
1210
|
-
applyFullscreen(false);
|
|
1211
|
-
renderResult(state); // restore whichever multi-item view we came from
|
|
1212
|
-
window.kolbo.notifySize();
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
1177
|
function renderError(msg) {
|
|
1216
1178
|
clearTimeout(pollTimer);
|
|
1217
1179
|
|
|
@@ -1241,38 +1203,6 @@ function renderTrackingIssue(msg) {
|
|
|
1241
1203
|
window.kolbo.notifySize();
|
|
1242
1204
|
}
|
|
1243
1205
|
|
|
1244
|
-
/* ---------- fullscreen viewer ---------- */
|
|
1245
|
-
var isFullscreen = false;
|
|
1246
|
-
function toggleFullscreen() {
|
|
1247
|
-
var want = isFullscreen ? 'inline' : 'fullscreen';
|
|
1248
|
-
window.kolbo.requestDisplayMode(want).then(function (res) {
|
|
1249
|
-
var granted = res && res.mode;
|
|
1250
|
-
if (granted === 'fullscreen') { isFullscreen = true; applyFullscreen(true); }
|
|
1251
|
-
else if (granted === 'inline' || isFullscreen) { isFullscreen = false; applyFullscreen(false); }
|
|
1252
|
-
else if (!isFullscreen) {
|
|
1253
|
-
// Host refused fullscreen — degrade to opening the original file.
|
|
1254
|
-
window.kolbo.openLink(state.urls && state.urls[selected]);
|
|
1255
|
-
}
|
|
1256
|
-
}).catch(function () {
|
|
1257
|
-
if (!isFullscreen) window.kolbo.openLink(state.urls && state.urls[selected]);
|
|
1258
|
-
});
|
|
1259
|
-
}
|
|
1260
|
-
function applyFullscreen(on, exitHandler) {
|
|
1261
|
-
document.documentElement.classList.toggle('k-fullscreen', on);
|
|
1262
|
-
if (window.kolbo.setFullscreen) window.kolbo.setFullscreen(on);
|
|
1263
|
-
var c = el('phase-chip');
|
|
1264
|
-
if (on) {
|
|
1265
|
-
c.style.display = '';
|
|
1266
|
-
c.innerHTML = ICONS.x + ' ' + esc('Exit');
|
|
1267
|
-
c.style.cursor = 'pointer';
|
|
1268
|
-
c.onclick = exitHandler || toggleFullscreen;
|
|
1269
|
-
} else {
|
|
1270
|
-
c.style.display = 'none';
|
|
1271
|
-
c.onclick = null;
|
|
1272
|
-
c.style.cursor = '';
|
|
1273
|
-
}
|
|
1274
|
-
window.kolbo.notifySize();
|
|
1275
|
-
}
|
|
1276
1206
|
|
|
1277
1207
|
function setPhaseChip(text, spinning) {
|
|
1278
1208
|
var c = el('phase-chip');
|
package/src/apps/widgets/list.js
CHANGED
|
@@ -180,7 +180,6 @@ function audioRowHTML(item) {
|
|
|
180
180
|
|
|
181
181
|
function wire() {
|
|
182
182
|
var stage = el('stage');
|
|
183
|
-
bindPeekHits(stage);
|
|
184
183
|
Array.prototype.forEach.call(stage.querySelectorAll('.k-tile, .k-audio-row'), function (c) {
|
|
185
184
|
c.onclick = function (e) {
|
|
186
185
|
if (e.target && e.target.closest && e.target.closest('[data-peek],[data-use],[data-dl],[data-video-play]')) return;
|