lobsterboard 0.8.3 → 0.8.4
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 +2 -0
- package/dist/lobsterboard.css +1 -1
- package/dist/lobsterboard.esm.js +206 -1
- package/dist/lobsterboard.esm.js.map +1 -1
- package/dist/lobsterboard.esm.min.js +2 -2
- package/dist/lobsterboard.esm.min.js.map +1 -1
- package/dist/lobsterboard.umd.js +206 -1
- package/dist/lobsterboard.umd.js.map +1 -1
- package/dist/lobsterboard.umd.min.js +2 -2
- package/dist/lobsterboard.umd.min.js.map +1 -1
- package/js/widgets/finance.js +145 -0
- package/js/widgets/search.js +125 -0
- package/js/widgets.js +230 -1
- package/package.json +1 -1
- package/server.cjs +4 -0
- package/src/widgets.js +205 -0
package/app.html
CHANGED
|
@@ -761,6 +761,8 @@
|
|
|
761
761
|
<script src="js/widgets/layout.js"></script>
|
|
762
762
|
<script src="js/widgets/releases.js"></script>
|
|
763
763
|
<script src="js/widgets/misc.js"></script>
|
|
764
|
+
<script src="js/widgets/search.js"></script>
|
|
765
|
+
<script src="js/widgets/finance.js"></script>
|
|
764
766
|
<!-- Editor modules (order matters: state first, index last) -->
|
|
765
767
|
<script src="js/editor/state.js"></script>
|
|
766
768
|
<script src="js/editor/canvas.js"></script>
|
package/dist/lobsterboard.css
CHANGED
package/dist/lobsterboard.esm.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* LobsterBoard v0.8.
|
|
2
|
+
* LobsterBoard v0.8.4
|
|
3
3
|
* Dashboard builder with customizable widgets
|
|
4
4
|
* https://github.com/curbob/LobsterBoard
|
|
5
5
|
* @license MIT
|
|
@@ -619,6 +619,211 @@ const WIDGETS = {
|
|
|
619
619
|
async function update_${props.id.replace(/-/g,'_')}() { const body = document.getElementById('${props.id}-body'); const subEl = document.getElementById('${props.id}-sub'); try { const res = await fetch('/api/pages/claude-usage/usage'); const d = await res.json(); if (d.error) { body.innerHTML='<div style="color:#f85149;">'+d.error+'</div>'; return; } if (subEl) { subEl.textContent = {max:'Max (5×)',pro:'Pro',free:'Free'}[d.subscription]||d.subscription||''; } let html=''; if(d.five_hour) html+=usageBar('5h Session',d.five_hour.utilization,d.five_hour.resets_at); if(d.seven_day) html+=usageBar('7d Weekly',d.seven_day.utilization,d.seven_day.resets_at); if(d.seven_day_opus) html+=usageBar('Opus (7d)',d.seven_day_opus.utilization,d.seven_day_opus.resets_at); if(d.seven_day_sonnet&&d.seven_day_sonnet.utilization>0) html+=usageBar('Sonnet (7d)',d.seven_day_sonnet.utilization,d.seven_day_sonnet.resets_at); if(d.extra_usage&&d.extra_usage.is_enabled){const used=(d.extra_usage.used_credits/100).toFixed(2),limit=d.extra_usage.monthly_limit>0?(d.extra_usage.monthly_limit/100).toFixed(2):'∞';html+='<div style="margin-top:4px;padding-top:6px;border-top:1px solid #30363d;"><div style="display:flex;justify-content:space-between;font-size:11px;"><span style="color:#8b949e;">Extra Usage</span><span style="font-weight:600;">$'+used+' / $'+limit+'</span></div></div>';} if(!html) html='<div style="color:#8b949e;">No usage data</div>'; body.innerHTML=html; } catch(e) { console.error('Claude usage error:',e); body.innerHTML='<div style="color:#f85149;">Failed to load</div>'; } }
|
|
620
620
|
update_${props.id.replace(/-/g,'_')}(); setInterval(update_${props.id.replace(/-/g,'_')}, ${(props.refreshInterval||120)*1000});
|
|
621
621
|
`
|
|
622
|
+
},
|
|
623
|
+
|
|
624
|
+
// ── Finance ──────────────────────────────────────────────────────────────
|
|
625
|
+
|
|
626
|
+
'stock-ticker': {
|
|
627
|
+
name: 'Stock Ticker',
|
|
628
|
+
icon: '📈',
|
|
629
|
+
category: 'large',
|
|
630
|
+
description: 'Live stock prices via Yahoo Finance. No API key required.',
|
|
631
|
+
defaultWidth: 320,
|
|
632
|
+
defaultHeight: 280,
|
|
633
|
+
hasApiKey: false,
|
|
634
|
+
properties: {
|
|
635
|
+
title: 'Stocks',
|
|
636
|
+
symbols: 'AAPL,MSFT,GOOGL,AMZN',
|
|
637
|
+
refreshInterval: 300
|
|
638
|
+
},
|
|
639
|
+
preview: `<div style="padding:6px;font-size:11px;">
|
|
640
|
+
<div style="display:flex;justify-content:space-between;"><span>AAPL</span><span>$182.50 <span style="color:#3fb950;">+1.24%</span></span></div>
|
|
641
|
+
<div style="display:flex;justify-content:space-between;"><span>MSFT</span><span>$375.20 <span style="color:#f85149;">-0.38%</span></span></div>
|
|
642
|
+
</div>`,
|
|
643
|
+
generateHtml: (props) => `
|
|
644
|
+
<div class="dash-card" id="widget-${props.id}" style="height:100%;">
|
|
645
|
+
<div class="dash-card-head">
|
|
646
|
+
<span class="dash-card-title">📈 ${props.title || 'Stocks'}</span>
|
|
647
|
+
<span class="dash-card-badge" id="${props.id}-badge">—</span>
|
|
648
|
+
</div>
|
|
649
|
+
<div class="dash-card-body" id="${props.id}-list" style="overflow-y:auto;padding:4px 8px;">
|
|
650
|
+
<div style="color:#8b949e;font-size:12px;">Loading...</div>
|
|
651
|
+
</div>
|
|
652
|
+
</div>`,
|
|
653
|
+
generateJs: (props) => `
|
|
654
|
+
function registerWidgetInterval(widgetId, intervalId) {
|
|
655
|
+
window.__lobsterboardWidgetIntervals = window.__lobsterboardWidgetIntervals || {};
|
|
656
|
+
if (window.__lobsterboardWidgetIntervals[widgetId]) clearInterval(window.__lobsterboardWidgetIntervals[widgetId]);
|
|
657
|
+
window.__lobsterboardWidgetIntervals[widgetId] = intervalId;
|
|
658
|
+
}
|
|
659
|
+
async function update_${props.id.replace(/-/g, '_')}() {
|
|
660
|
+
var list = document.getElementById('${props.id}-list');
|
|
661
|
+
var badge = document.getElementById('${props.id}-badge');
|
|
662
|
+
if (!list) return;
|
|
663
|
+
try {
|
|
664
|
+
var res = await fetch('/api/finance/stocks?symbols=' + encodeURIComponent('${props.symbols || 'AAPL,MSFT'}'));
|
|
665
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
666
|
+
var data = await res.json();
|
|
667
|
+
var ok = data.filter(function(d) { return !d.error; });
|
|
668
|
+
list.innerHTML = data.map(function(d) {
|
|
669
|
+
if (d.error) return '<div style="padding:5px 0;border-bottom:1px solid #21262d;color:#8b949e;font-size:12px;">' + _esc(d.symbol) + ' — ' + _esc(d.error) + '</div>';
|
|
670
|
+
var color = d.up ? '#3fb950' : '#f85149';
|
|
671
|
+
return '<div style="display:flex;justify-content:space-between;align-items:center;padding:5px 0;border-bottom:1px solid #21262d;">'
|
|
672
|
+
+ '<span style="font-weight:600;font-size:13px;">' + _esc(d.symbol) + '</span>'
|
|
673
|
+
+ '<span style="text-align:right;">'
|
|
674
|
+
+ '<span style="font-size:13px;font-weight:700;margin-right:8px;">$' + _esc(d.price) + '</span>'
|
|
675
|
+
+ '<span style="font-size:11px;color:' + color + ';font-weight:600;">' + _esc(d.pctChange) + '%</span>'
|
|
676
|
+
+ '</span></div>';
|
|
677
|
+
}).join('');
|
|
678
|
+
if (badge) badge.textContent = ok.length + ' stocks';
|
|
679
|
+
} catch (e) {
|
|
680
|
+
console.error('Stock ticker error:', e);
|
|
681
|
+
if (list) list.innerHTML = '<div style="color:#f85149;font-size:12px;">Failed to load</div>';
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
update_${props.id.replace(/-/g, '_')}();
|
|
685
|
+
registerWidgetInterval('${props.id}', setInterval(update_${props.id.replace(/-/g, '_')}, ${(props.refreshInterval || 300) * 1000}));
|
|
686
|
+
`
|
|
687
|
+
},
|
|
688
|
+
|
|
689
|
+
'crypto-price': {
|
|
690
|
+
name: 'Crypto Price',
|
|
691
|
+
icon: '₿',
|
|
692
|
+
category: 'large',
|
|
693
|
+
description: 'Live crypto prices via CoinGecko. No API key required.',
|
|
694
|
+
defaultWidth: 320,
|
|
695
|
+
defaultHeight: 280,
|
|
696
|
+
hasApiKey: false,
|
|
697
|
+
properties: {
|
|
698
|
+
title: 'Crypto',
|
|
699
|
+
coins: 'bitcoin,ethereum,solana',
|
|
700
|
+
currency: 'usd',
|
|
701
|
+
refreshInterval: 300
|
|
702
|
+
},
|
|
703
|
+
preview: `<div style="padding:6px;font-size:11px;">
|
|
704
|
+
<div style="display:flex;justify-content:space-between;"><span>Bitcoin</span><span>$67,240 <span style="color:#3fb950;">+2.1%</span></span></div>
|
|
705
|
+
<div style="display:flex;justify-content:space-between;"><span>Ethereum</span><span>$3,521 <span style="color:#f85149;">-0.8%</span></span></div>
|
|
706
|
+
</div>`,
|
|
707
|
+
generateHtml: (props) => `
|
|
708
|
+
<div class="dash-card" id="widget-${props.id}" style="height:100%;">
|
|
709
|
+
<div class="dash-card-head">
|
|
710
|
+
<span class="dash-card-title">₿ ${props.title || 'Crypto'}</span>
|
|
711
|
+
<span class="dash-card-badge" id="${props.id}-badge">—</span>
|
|
712
|
+
</div>
|
|
713
|
+
<div class="dash-card-body" id="${props.id}-list" style="overflow-y:auto;padding:4px 8px;">
|
|
714
|
+
<div style="color:#8b949e;font-size:12px;">Loading...</div>
|
|
715
|
+
</div>
|
|
716
|
+
</div>`,
|
|
717
|
+
generateJs: (props) => `
|
|
718
|
+
function registerWidgetInterval(widgetId, intervalId) {
|
|
719
|
+
window.__lobsterboardWidgetIntervals = window.__lobsterboardWidgetIntervals || {};
|
|
720
|
+
if (window.__lobsterboardWidgetIntervals[widgetId]) clearInterval(window.__lobsterboardWidgetIntervals[widgetId]);
|
|
721
|
+
window.__lobsterboardWidgetIntervals[widgetId] = intervalId;
|
|
722
|
+
}
|
|
723
|
+
async function update_${props.id.replace(/-/g, '_')}() {
|
|
724
|
+
var list = document.getElementById('${props.id}-list');
|
|
725
|
+
var badge = document.getElementById('${props.id}-badge');
|
|
726
|
+
if (!list) return;
|
|
727
|
+
try {
|
|
728
|
+
var res = await fetch('/api/finance/crypto?coins=' + encodeURIComponent('${props.coins || 'bitcoin,ethereum'}') + '¤cy=${props.currency || 'usd'}');
|
|
729
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
730
|
+
var data = await res.json();
|
|
731
|
+
list.innerHTML = data.map(function(d) {
|
|
732
|
+
var color = d.up ? '#3fb950' : '#f85149';
|
|
733
|
+
return '<div style="display:flex;justify-content:space-between;align-items:center;padding:5px 0;border-bottom:1px solid #21262d;">'
|
|
734
|
+
+ '<span style="font-weight:600;font-size:13px;">' + _esc(d.name) + '</span>'
|
|
735
|
+
+ '<span style="text-align:right;">'
|
|
736
|
+
+ '<span style="font-size:13px;font-weight:700;margin-right:8px;">${(props.currency || 'usd').toUpperCase()} ' + _esc(d.price) + '</span>'
|
|
737
|
+
+ '<span style="font-size:11px;color:' + color + ';font-weight:600;">' + _esc(d.pctChange) + '%</span>'
|
|
738
|
+
+ '</span></div>';
|
|
739
|
+
}).join('');
|
|
740
|
+
if (badge) badge.textContent = data.length + ' coins';
|
|
741
|
+
} catch (e) {
|
|
742
|
+
console.error('Crypto price error:', e);
|
|
743
|
+
if (list) list.innerHTML = '<div style="color:#f85149;font-size:12px;">Failed to load</div>';
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
update_${props.id.replace(/-/g, '_')}();
|
|
747
|
+
registerWidgetInterval('${props.id}', setInterval(update_${props.id.replace(/-/g, '_')}, ${(props.refreshInterval || 300) * 1000}));
|
|
748
|
+
`
|
|
749
|
+
},
|
|
750
|
+
|
|
751
|
+
// ── Search ────────────────────────────────────────────────────────────────
|
|
752
|
+
|
|
753
|
+
'search': {
|
|
754
|
+
name: 'Search',
|
|
755
|
+
icon: '🔍',
|
|
756
|
+
category: 'large',
|
|
757
|
+
description: 'Search box with DuckDuckGo-style !bangs. Press S to focus, ↑ for last query.',
|
|
758
|
+
defaultWidth: 460,
|
|
759
|
+
defaultHeight: 120,
|
|
760
|
+
hasApiKey: false,
|
|
761
|
+
properties: {
|
|
762
|
+
title: 'Search',
|
|
763
|
+
placeholder: 'Search or use !bangs',
|
|
764
|
+
searchEngine: 'duckduckgo',
|
|
765
|
+
openInNewTab: true,
|
|
766
|
+
showBangHints: true
|
|
767
|
+
},
|
|
768
|
+
preview: `<div style="padding:10px;">
|
|
769
|
+
<input style="width:100%;padding:6px 10px;background:#21262d;border:1px solid #30363d;border-radius:6px;color:#e6edf3;font-size:13px;" placeholder="Search or use !bangs" readonly />
|
|
770
|
+
<div style="font-size:10px;color:#8b949e;margin-top:6px;">!yt YouTube !gh GitHub !r Reddit !so Stack Overflow</div>
|
|
771
|
+
</div>`,
|
|
772
|
+
generateHtml: (props) => `
|
|
773
|
+
<div class="dash-card" id="widget-${props.id}" style="height:100%;">
|
|
774
|
+
<div class="dash-card-head">
|
|
775
|
+
<span class="dash-card-title">🔍 ${props.title || 'Search'}</span>
|
|
776
|
+
</div>
|
|
777
|
+
<div class="dash-card-body" style="padding:8px 10px;">
|
|
778
|
+
<form id="${props.id}-form" style="display:flex;gap:6px;" onsubmit="return false;">
|
|
779
|
+
<input id="${props.id}-input" type="text"
|
|
780
|
+
placeholder="${props.placeholder || 'Search or use !bangs'}"
|
|
781
|
+
autocomplete="off" spellcheck="false"
|
|
782
|
+
style="flex:1;padding:7px 11px;background:#21262d;border:1px solid #30363d;border-radius:6px;color:#e6edf3;font-size:13px;outline:none;" />
|
|
783
|
+
<button type="submit"
|
|
784
|
+
style="padding:7px 14px;background:#238636;border:none;border-radius:6px;color:#fff;font-size:13px;cursor:pointer;">Go</button>
|
|
785
|
+
</form>
|
|
786
|
+
${props.showBangHints !== false ? `<div style="font-size:10px;color:#8b949e;margin-top:5px;"><span style="opacity:.7;">!g Google !yt YouTube !gh GitHub !r Reddit !so Stack Overflow !w Wikipedia !npm npm</span></div>` : ''}
|
|
787
|
+
</div>
|
|
788
|
+
</div>`,
|
|
789
|
+
generateJs: (props) => {
|
|
790
|
+
const providers = { duckduckgo:'https://duckduckgo.com/?q={q}', google:'https://www.google.com/search?q={q}', bing:'https://www.bing.com/search?q={q}', brave:'https://search.brave.com/search?q={q}' };
|
|
791
|
+
const bangs = { g:'https://www.google.com/search?q={q}',b:'https://www.bing.com/search?q={q}',yt:'https://www.youtube.com/results?search_query={q}',gh:'https://github.com/search?q={q}',r:'https://www.reddit.com/search/?q={q}',so:'https://stackoverflow.com/search?q={q}',w:'https://en.wikipedia.org/wiki/Special:Search/{q}',img:'https://www.google.com/search?tbm=isch&q={q}',maps:'https://www.google.com/maps/search/{q}',ddg:'https://duckduckgo.com/?q={q}',npm:'https://www.npmjs.com/search?q={q}',mdn:'https://developer.mozilla.org/en-US/search?q={q}',x:'https://x.com/search?q={q}',tw:'https://x.com/search?q={q}' };
|
|
792
|
+
const defaultUrl = providers[props.searchEngine] || providers.duckduckgo;
|
|
793
|
+
return `
|
|
794
|
+
(function() {
|
|
795
|
+
var BANGS = ${JSON.stringify(bangs)};
|
|
796
|
+
var DEFAULT_URL = '${defaultUrl}';
|
|
797
|
+
var widgetId = '${props.id}';
|
|
798
|
+
var lastQuery = '';
|
|
799
|
+
function navigate(q) {
|
|
800
|
+
q = q.trim(); if (!q) return; lastQuery = q;
|
|
801
|
+
var url = DEFAULT_URL;
|
|
802
|
+
var m = q.match(/^!(\\S+)\\s*(.*)/);
|
|
803
|
+
if (m) { var bang = m[1].toLowerCase(), rest = m[2]; if (BANGS[bang]) { url = BANGS[bang]; q = rest || q; } }
|
|
804
|
+
var target = url.replace('{q}', encodeURIComponent(q));
|
|
805
|
+
if (${props.openInNewTab !== false}) { window.open(target, '_blank', 'noopener'); } else { window.location.href = target; }
|
|
806
|
+
}
|
|
807
|
+
var form = document.getElementById('${props.id}-form');
|
|
808
|
+
var input = document.getElementById('${props.id}-input');
|
|
809
|
+
if (form) form.addEventListener('submit', function() { navigate(input.value); });
|
|
810
|
+
if (input) input.addEventListener('keydown', function(e) { if (e.key === 'ArrowUp') { e.preventDefault(); input.value = lastQuery; } });
|
|
811
|
+
window.__lobsterboardSearchHandlers = window.__lobsterboardSearchHandlers || {};
|
|
812
|
+
if (window.__lobsterboardSearchHandlers[widgetId]) {
|
|
813
|
+
document.removeEventListener('keydown', window.__lobsterboardSearchHandlers[widgetId]);
|
|
814
|
+
}
|
|
815
|
+
var searchFocusHandler = function(e) {
|
|
816
|
+
var tag = document.activeElement && document.activeElement.tagName;
|
|
817
|
+
if (e.key === 's' && tag !== 'INPUT' && tag !== 'TEXTAREA' && !e.ctrlKey && !e.metaKey) {
|
|
818
|
+
var inp = document.getElementById(widgetId + '-input');
|
|
819
|
+
if (inp) { inp.focus(); inp.select(); e.preventDefault(); }
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
window.__lobsterboardSearchHandlers[widgetId] = searchFocusHandler;
|
|
823
|
+
document.addEventListener('keydown', searchFocusHandler);
|
|
824
|
+
})();
|
|
825
|
+
`;
|
|
826
|
+
}
|
|
622
827
|
}
|
|
623
828
|
};
|
|
624
829
|
|