lobsterboard 0.3.2 โ†’ 0.4.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/app.html CHANGED
@@ -223,9 +223,60 @@
223
223
  <div class="widget-section">
224
224
  <h4>๐Ÿค– AI / LLM</h4>
225
225
  <div class="widget-list">
226
- <div class="widget-item" draggable="true" data-widget="ai-usage-claude">
226
+ <div class="widget-item" draggable="true" data-widget="ai-usage">
227
+ <span class="widget-icon">๐Ÿค–</span>
228
+ <span class="widget-name">AI Usage</span>
229
+ <span class="widget-verified" title="Tested & Verified">โœ“</span>
230
+ </div>
231
+ <div class="widget-item" draggable="true" data-widget="claude-code">
227
232
  <span class="widget-icon">๐ŸŸฃ</span>
228
- <span class="widget-name">Claude Usage</span>
233
+ <span class="widget-name">Claude Code</span>
234
+ <span class="widget-verified" title="Tested & Verified">โœ“</span>
235
+ </div>
236
+ <div class="widget-item" draggable="true" data-widget="codex-cli">
237
+ <span class="widget-icon">๐ŸŸข</span>
238
+ <span class="widget-name">Codex CLI</span>
239
+ <span class="widget-verified" title="Tested & Verified">โœ“</span>
240
+ </div>
241
+ <div class="widget-item" draggable="true" data-widget="github-copilot">
242
+ <span class="widget-icon">โšซ</span>
243
+ <span class="widget-name">GitHub Copilot</span>
244
+ </div>
245
+ <div class="widget-item" draggable="true" data-widget="cursor">
246
+ <span class="widget-icon">๐Ÿ”ต</span>
247
+ <span class="widget-name">Cursor</span>
248
+ </div>
249
+ <div class="widget-item" draggable="true" data-widget="gemini-cli">
250
+ <span class="widget-icon">๐Ÿ”ท</span>
251
+ <span class="widget-name">Gemini CLI</span>
252
+ </div>
253
+ <div class="widget-item" draggable="true" data-widget="amp-code">
254
+ <span class="widget-icon">โšก</span>
255
+ <span class="widget-name">Amp Code</span>
256
+ </div>
257
+ <div class="widget-item" draggable="true" data-widget="factory">
258
+ <span class="widget-icon">๐Ÿญ</span>
259
+ <span class="widget-name">Factory</span>
260
+ </div>
261
+ <div class="widget-item" draggable="true" data-widget="kimi-code">
262
+ <span class="widget-icon">๐ŸŒ™</span>
263
+ <span class="widget-name">Kimi Code</span>
264
+ </div>
265
+ <div class="widget-item" draggable="true" data-widget="jetbrains-ai">
266
+ <span class="widget-icon">๐Ÿง </span>
267
+ <span class="widget-name">JetBrains AI</span>
268
+ </div>
269
+ <div class="widget-item" draggable="true" data-widget="minimax">
270
+ <span class="widget-icon">๐Ÿ”ถ</span>
271
+ <span class="widget-name">MiniMax</span>
272
+ </div>
273
+ <div class="widget-item" draggable="true" data-widget="zai">
274
+ <span class="widget-icon">๐Ÿ‡ฟ</span>
275
+ <span class="widget-name">Z.ai</span>
276
+ </div>
277
+ <div class="widget-item" draggable="true" data-widget="antigravity-local">
278
+ <span class="widget-icon">๐Ÿช</span>
279
+ <span class="widget-name">Antigravity</span>
229
280
  </div>
230
281
  <div class="widget-item" draggable="true" data-widget="ai-cost-tracker">
231
282
  <span class="widget-icon">๐Ÿ’ฐ</span>
@@ -1,4 +1,4 @@
1
- /* LobsterBoard v0.3.2 - Dashboard Styles */
1
+ /* LobsterBoard v0.4.0 - Dashboard Styles */
2
2
  /* LobsterBoard Dashboard - Generated Styles */
3
3
 
4
4
  :root {
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * LobsterBoard v0.3.2
2
+ * LobsterBoard v0.4.0
3
3
  * Dashboard builder with customizable widgets
4
4
  * https://github.com/curbob/LobsterBoard
5
5
  * @license MIT
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * LobsterBoard v0.3.2
2
+ * LobsterBoard v0.4.0
3
3
  * Dashboard builder with customizable widgets
4
4
  * https://github.com/curbob/LobsterBoard
5
5
  * @license MIT
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * LobsterBoard v0.3.2
2
+ * LobsterBoard v0.4.0
3
3
  * Dashboard builder with customizable widgets
4
4
  * https://github.com/curbob/LobsterBoard
5
5
  * @license MIT
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * LobsterBoard v0.3.2
2
+ * LobsterBoard v0.4.0
3
3
  * Dashboard builder with customizable widgets
4
4
  * https://github.com/curbob/LobsterBoard
5
5
  * @license MIT
package/js/builder.js CHANGED
@@ -3290,32 +3290,35 @@ async function openDirBrowser(startDir) {
3290
3290
  try {
3291
3291
  const res = await fetch('/api/browse-dirs?dir=' + encodeURIComponent(dir));
3292
3292
  const data = await res.json();
3293
- if (data.status !== 'ok') { browser.innerHTML = `<span style="color:#f85149;">${data.message}</span>`; return; }
3294
- let html = `<div style="margin-bottom:6px;color:var(--text-secondary);font-size:11px;word-break:break-all;">${data.path}</div>`;
3293
+ if (data.status !== 'ok') { browser.innerHTML = `<span style="color:#f85149;">${escapeHtml(data.message)}</span>`; return; }
3294
+ let html = `<div style="margin-bottom:6px;color:var(--text-secondary);font-size:11px;word-break:break-all;">${escapeHtml(data.path)}</div>`;
3295
3295
  if (data.imageCount > 0) {
3296
- html += `<div style="margin-bottom:6px;padding:4px 8px;background:var(--bg-secondary);border-radius:4px;color:#3fb950;font-size:11px;">๐Ÿ“ท ${data.imageCount} image${data.imageCount !== 1 ? 's' : ''} in this folder</div>`;
3296
+ html += `<div style="margin-bottom:6px;padding:4px 8px;background:var(--bg-secondary);border-radius:4px;color:#3fb950;font-size:11px;">๐Ÿ“ท ${escapeHtml(String(data.imageCount))} image${data.imageCount !== 1 ? 's' : ''} in this folder</div>`;
3297
3297
  }
3298
3298
  // Up one level
3299
3299
  const parent = data.path.replace(/\/[^/]+\/?$/, '') || '/';
3300
3300
  if (data.path !== parent) {
3301
- html += `<div class="dir-entry" data-path="${parent}" style="cursor:pointer;padding:3px 6px;border-radius:4px;color:var(--text-primary);" onmouseover="this.style.background='var(--bg-secondary)'" onmouseout="this.style.background='none'">๐Ÿ“ ..</div>`;
3301
+ html += `<div class="dir-entry" data-path="${escapeHtml(parent)}" style="cursor:pointer;padding:3px 6px;border-radius:4px;color:var(--text-primary);" onmouseover="this.style.background='var(--bg-secondary)'" onmouseout="this.style.background='none'">๐Ÿ“ ..</div>`;
3302
3302
  }
3303
3303
  for (const d of data.dirs) {
3304
3304
  const full = data.path + '/' + d;
3305
- html += `<div class="dir-entry" data-path="${full}" style="cursor:pointer;padding:3px 6px;border-radius:4px;color:var(--text-primary);" onmouseover="this.style.background='var(--bg-secondary)'" onmouseout="this.style.background='none'">๐Ÿ“ ${d}</div>`;
3305
+ html += `<div class="dir-entry" data-path="${escapeHtml(full)}" style="cursor:pointer;padding:3px 6px;border-radius:4px;color:var(--text-primary);" onmouseover="this.style.background='var(--bg-secondary)'" onmouseout="this.style.background='none'">๐Ÿ“ ${escapeHtml(d)}</div>`;
3306
3306
  }
3307
3307
  if (data.dirs.length === 0 && data.imageCount === 0) {
3308
3308
  html += `<div style="color:var(--text-muted);font-size:11px;padding:4px;">Empty directory</div>`;
3309
3309
  }
3310
3310
  html += `<div style="margin-top:8px;display:flex;gap:4px;">`;
3311
- html += `<button type="button" onclick="selectDir('${data.path.replace(/'/g, "\\'")}')" style="flex:1;padding:4px 8px;background:var(--accent-blue);color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:11px;">โœ“ Select this folder</button>`;
3311
+ html += `<button type="button" style="flex:1;padding:4px 8px;background:var(--accent-blue);color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:11px;">โœ“ Select this folder</button>`;
3312
3312
  html += `<button type="button" onclick="document.getElementById('dir-browser').style.display='none'" style="padding:4px 8px;background:var(--bg-secondary);color:var(--text-primary);border:1px solid var(--border-color);border-radius:4px;cursor:pointer;font-size:11px;">Cancel</button>`;
3313
3313
  html += `</div>`;
3314
3314
  browser.innerHTML = html;
3315
+ // Attach select button handler safely (avoid inline onclick with path data)
3316
+ const selectBtn = browser.querySelector('button');
3317
+ if (selectBtn) selectBtn.addEventListener('click', () => selectDir(data.path));
3315
3318
  browser.querySelectorAll('.dir-entry').forEach(el => {
3316
3319
  el.addEventListener('click', () => openDirBrowser(el.dataset.path));
3317
3320
  });
3318
- } catch (e) { browser.innerHTML = `<span style="color:#f85149;">Error: ${e.message}</span>`; }
3321
+ } catch (e) { browser.innerHTML = `<span style="color:#f85149;">Error: ${escapeHtml(e.message)}</span>`; }
3319
3322
  }
3320
3323
 
3321
3324
  function selectDir(dirPath) {
package/js/templates.js CHANGED
@@ -2,6 +2,13 @@
2
2
  * LobsterBoard Template Gallery System
3
3
  */
4
4
  (function() {
5
+ function _esc(str) {
6
+ if (str == null) return '';
7
+ const div = document.createElement('div');
8
+ div.textContent = String(str);
9
+ return div.innerHTML;
10
+ }
11
+
5
12
  const galleryModal = document.getElementById('template-gallery-modal');
6
13
  const exportModal = document.getElementById('template-export-modal');
7
14
  const tplGrid = document.getElementById('tpl-grid');
@@ -88,19 +95,19 @@
88
95
  return;
89
96
  }
90
97
  tplGrid.innerHTML = templates.map(t => `
91
- <div class="tpl-card" data-id="${t.id}">
98
+ <div class="tpl-card" data-id="${_esc(t.id)}">
92
99
  <div class="tpl-card-img">
93
- <img src="/api/templates/${t.id}/preview" alt="${t.name}" onerror="this.parentElement.innerHTML='<div class=\\'tpl-no-preview\\'>๐Ÿฆž</div>'">
100
+ <img src="/api/templates/${_esc(t.id)}/preview" alt="${_esc(t.name)}" onerror="this.parentElement.innerHTML='<div class=\\'tpl-no-preview\\'>๐Ÿฆž</div>'">
94
101
  </div>
95
102
  <div class="tpl-card-body">
96
- <h3>${t.name}</h3>
97
- <p>${t.description || ''}</p>
103
+ <h3>${_esc(t.name)}</h3>
104
+ <p>${_esc(t.description || '')}</p>
98
105
  <div class="tpl-card-meta">
99
- <span>${t.widgetCount || 0} widgets</span>
100
- <span>${t.canvasSize || ''}</span>
106
+ <span>${_esc(String(t.widgetCount || 0))} widgets</span>
107
+ <span>${_esc(t.canvasSize || '')}</span>
101
108
  </div>
102
- ${(t.widgetTypes || []).length ? `<div style="margin-top:4px;font-size:10px;color:var(--text-muted);">${t.widgetTypes.slice(0,6).map(w => (w.icon || '') + ' ' + w.name).join(' ยท ')}${t.widgetTypes.length > 6 ? ' ยท +' + (t.widgetTypes.length - 6) + ' more' : ''}</div>` : ''}
103
- <div class="tpl-card-tags">${(t.tags || []).map(tag => `<span class="tpl-tag">${tag}</span>`).join('')}</div>
109
+ ${(t.widgetTypes || []).length ? `<div style="margin-top:4px;font-size:10px;color:var(--text-muted);">${t.widgetTypes.slice(0,6).map(w => _esc((w.icon || '') + ' ' + w.name)).join(' ยท ')}${t.widgetTypes.length > 6 ? ' ยท +' + (t.widgetTypes.length - 6) + ' more' : ''}</div>` : ''}
110
+ <div class="tpl-card-tags">${(t.tags || []).map(tag => `<span class="tpl-tag">${_esc(tag)}</span>`).join('')}</div>
104
111
  </div>
105
112
  </div>
106
113
  `).join('');
@@ -122,13 +129,13 @@
122
129
  document.getElementById('tpl-detail-name').textContent = selectedTemplate.name;
123
130
  document.getElementById('tpl-detail-desc').textContent = selectedTemplate.description || '';
124
131
  document.getElementById('tpl-detail-meta').innerHTML = `
125
- <div><strong>Author:</strong> ${selectedTemplate.author || 'anonymous'}</div>
126
- <div><strong>Canvas:</strong> ${selectedTemplate.canvasSize || 'unknown'}</div>
127
- <div><strong>Widgets:</strong> ${selectedTemplate.widgetCount || 0}</div>
128
- ${(selectedTemplate.requiresSetup || []).length ? `<div><strong>Requires:</strong> ${selectedTemplate.requiresSetup.join(', ')}</div>` : ''}
129
- ${(selectedTemplate.widgetTypes || []).length ? `<div style="margin-top:8px;"><strong>Widget Types:</strong><div style="margin-top:4px;">${selectedTemplate.widgetTypes.map(w => `<span style="display:inline-block;padding:2px 8px;margin:2px;background:var(--bg-tertiary);border-radius:4px;font-size:11px;">${w.icon || ''} ${w.name}${w.count > 1 ? ' ร—' + w.count : ''}</span>`).join('')}</div></div>` : ''}
132
+ <div><strong>Author:</strong> ${_esc(selectedTemplate.author || 'anonymous')}</div>
133
+ <div><strong>Canvas:</strong> ${_esc(selectedTemplate.canvasSize || 'unknown')}</div>
134
+ <div><strong>Widgets:</strong> ${_esc(String(selectedTemplate.widgetCount || 0))}</div>
135
+ ${(selectedTemplate.requiresSetup || []).length ? `<div><strong>Requires:</strong> ${(selectedTemplate.requiresSetup || []).map(s => _esc(s)).join(', ')}</div>` : ''}
136
+ ${(selectedTemplate.widgetTypes || []).length ? `<div style="margin-top:8px;"><strong>Widget Types:</strong><div style="margin-top:4px;">${selectedTemplate.widgetTypes.map(w => `<span style="display:inline-block;padding:2px 8px;margin:2px;background:var(--bg-tertiary);border-radius:4px;font-size:11px;">${_esc((w.icon || '') + ' ' + w.name)}${w.count > 1 ? ' ร—' + _esc(String(w.count)) : ''}</span>`).join('')}</div></div>` : ''}
130
137
  `;
131
- document.getElementById('tpl-detail-tags').innerHTML = (selectedTemplate.tags || []).map(t => `<span class="tpl-tag">${t}</span>`).join('');
138
+ document.getElementById('tpl-detail-tags').innerHTML = (selectedTemplate.tags || []).map(t => `<span class="tpl-tag">${_esc(t)}</span>`).join('');
132
139
  }
133
140
 
134
141
  document.getElementById('tpl-back').addEventListener('click', () => {
@@ -267,16 +274,16 @@
267
274
  body: JSON.stringify({ data: screenshotData })
268
275
  });
269
276
  }
270
- resultEl.innerHTML = `โœ… Template exported as <strong>${data.id}</strong>${screenshotData ? '<br>Screenshot captured!' : '<br>โš ๏ธ No screenshot (auto-capture failed).'}`;
277
+ resultEl.innerHTML = `โœ… Template exported as <strong>${_esc(data.id)}</strong>${screenshotData ? '<br>Screenshot captured!' : '<br>โš ๏ธ No screenshot (auto-capture failed).'}`;
271
278
  resultEl.className = 'tpl-export-result tpl-export-success';
272
279
  } else {
273
- resultEl.innerHTML = `โŒ ${data.error || 'Export failed'}`;
280
+ resultEl.innerHTML = `โŒ ${_esc(data.error || 'Export failed')}`;
274
281
  resultEl.className = 'tpl-export-result tpl-export-error';
275
282
  }
276
283
  resultEl.style.display = 'block';
277
284
  } catch (e) {
278
285
  const resultEl = document.getElementById('tpl-export-result');
279
- resultEl.innerHTML = `โŒ Export failed: ${e.message}`;
286
+ resultEl.innerHTML = `โŒ Export failed: ${_esc(e.message)}`;
280
287
  resultEl.className = 'tpl-export-result tpl-export-error';
281
288
  resultEl.style.display = 'block';
282
289
  }