@@ -19,12 +19,15 @@ class BahmcloudStorePanel extends HTMLElement {
this . _detailRepoId = null ;
this . _detailRepo = null ;
this . _readmeLoading = false ;
this . _readmeText = null ;
this . _readmeHtml = null ;
this . _readmeError = null ;
// README UX (E2)
this . _readmeExpanded = false ;
this . _readmeCanToggle = false ;
this . _refreshing = false ;
this . _installingRepoId = null ;
@@ -201,6 +204,7 @@ class BahmcloudStorePanel extends HTMLElement {
this . _readmeText = null ;
this . _readmeHtml = null ;
this . _readmeError = null ;
this . _readmeExpanded = false ;
this . _update ( ) ;
return ;
}
@@ -265,6 +269,8 @@ class BahmcloudStorePanel extends HTMLElement {
this . _readmeText = null ;
this . _readmeHtml = null ;
this . _readmeError = null ;
this . _readmeExpanded = false ;
this . _readmeCanToggle = false ;
this . _update ( ) ;
this . _loadReadme ( repoId ) ;
@@ -282,15 +288,23 @@ class BahmcloudStorePanel extends HTMLElement {
if ( resp ? . ok && typeof resp . readme === "string" && resp . readme . trim ( ) ) {
this . _readmeText = resp . readme ;
this . _readmeHtml = typeof resp . html === "string" && resp . html . trim ( ) ? resp . html : null ;
const lines = resp . readme . split ( /\r?\n/ ) . length ;
this . _readmeCanToggle = lines > 24 || resp . readme . length > 1600 ;
this . _readmeExpanded = ! this . _readmeCanToggle ;
} else {
this . _readmeText = null ;
this . _readmeHtml = null ;
this . _readmeError = this . _safeText ( resp ? . message ) || "README not found." ;
this . _readmeCanToggle = false ;
this . _readmeExpanded = false ;
}
} catch ( e ) {
this . _readmeText = null ;
this . _readmeHtml = null ;
this . _readmeError = e ? . message ? String ( e . message ) : String ( e ) ;
this . _readmeCanToggle = false ;
this . _readmeExpanded = false ;
} finally {
this . _readmeLoading = false ;
this . _update ( ) ;
@@ -302,7 +316,7 @@ class BahmcloudStorePanel extends HTMLElement {
root . innerHTML = `
<style>
:host { display:block; min-height:100%; --bcs-accent:#1E88E5; }
:host { display:block; min-height:100%; --bcs-accent:#1E88E5; max-width:100%; overflow-x:hidden; }
.mobilebar{
position:sticky; top:0; z-index:50;
@@ -317,13 +331,15 @@ class BahmcloudStorePanel extends HTMLElement {
.iconbtn{
width:40px; height:40px; border-radius:14px;
display:flex; align-items:center; justify-content:center;
border:1px solid var(--divider-color);
background: var(--card-background -color);
/* Ensure icon visibility on app header (light & dark modes) */
color: var(--app-header-text-color, var(--primary-text -color)) ;
border: 1px solid rgba(255,255,255,0.35);
background: rgba(255,255,255,0.16);
cursor:pointer; user-select:none;
}
.iconbtn:hover{ filter:brightness(0.98); }
.wrap{ max-width:1200px; margin:0 auto; padding:16px; }
.wrap{ max-width:1200px; margin:0 auto; padding:16px; overflow-x:hidden; }
.tabs{ display:flex; gap:10px; flex-wrap:wrap; margin:8px 0 16px; }
.tab{
@@ -336,9 +352,12 @@ class BahmcloudStorePanel extends HTMLElement {
.grid{ display:grid; gap:12px; grid-template-columns: repeat(1, minmax(0, 1fr)); }
@media (min-width: 900px){ .grid{ grid-template-columns: repeat(2, minmax(0, 1fr)); } }
/* Prevent grid children from forcing horizontal overflow (mobile) */
.grid > *{ min-width:0; }
.grid2{ display:grid; gap:12px; grid-template-columns: 1fr; }
@media (min-width: 1024px){ .grid2{ grid-template-columns: 1.2fr .8fr; } }
.grid2 > *{ min-width:0; }
.card{
padding:14px 14px;
@@ -346,10 +365,11 @@ class BahmcloudStorePanel extends HTMLElement {
background: var(--card-background-color);
border:1px solid var(--divider-color);
box-shadow: 0 1px 0 rgba(0,0,0,.04);
overflow:hidden;
}
.row{ display:flex; align-items:flex-start; justify-content:space-between; gap:12px; }
.muted{ color: var(--secondary-text-color); }
.row{ display:flex; align-items:flex-start; justify-content:space-between; gap:12px; flex-wrap:wrap; }
.muted{ color: var(--secondary-text-color); overflow-wrap:anywhere; word-break:break-word; }
.small{ font-size: 12px; }
.badge{
padding:6px 10px;
@@ -358,7 +378,10 @@ class BahmcloudStorePanel extends HTMLElement {
background: rgba(30,136,229,.06);
color: var(--primary-text-color);
font-size: 12px;
white-space:nowrap ;
white-space:normal ;
max-width:55%;
overflow:hidden;
text-overflow:ellipsis;
}
.filters{ display:flex; gap:10px; flex-wrap:wrap; margin-bottom:12px; }
@@ -397,36 +420,6 @@ class BahmcloudStorePanel extends HTMLElement {
background: rgba(255, 82, 82, .08);
}
.fabs{
position: fixed;
right: 16px;
bottom: 16px;
display:flex;
flex-direction:column;
gap:10px;
z-index: 60;
}
.fabbtn{
width:54px; height:54px;
border-radius:18px;
border:1px solid var(--divider-color);
background: var(--card-background-color);
display:flex; align-items:center; justify-content:center;
cursor:pointer;
box-shadow: 0 8px 18px rgba(0,0,0,.12);
user-select:none;
font-size: 18px;
padding: 0;
}
.fabbtn.primary{
border-color: rgba(30,136,229,.35);
background: rgba(30,136,229,.10);
}
.fabbtn:disabled{
opacity: .55;
cursor: not-allowed;
}
pre.readme{
padding: 12px;
border-radius: 14px;
@@ -437,7 +430,10 @@ class BahmcloudStorePanel extends HTMLElement {
line-height: 1.4;
}
/* Markdown can contain very wide content (tables/images). Keep it within the viewport. */
.md{ overflow-wrap:anywhere; word-break:break-word; max-width:100%; min-width:0; overflow-x:auto; -webkit-overflow-scrolling:touch; }
.md :is(h1,h2,h3){ margin-top: 12px; }
.md img{ max-width:100%; height:auto; }
.md code{
padding: 2px 5px;
border-radius: 8px;
@@ -454,14 +450,65 @@ class BahmcloudStorePanel extends HTMLElement {
.md table{
width:100%;
border-collapse: collapse;
overflow:auto;
overflow-x :auto;
overflow-y:hidden;
-webkit-overflow-scrolling: touch;
display:block;
max-width:100%;
}
.readmeWrap{
margin-top:12px;
max-width:100%;
border:1px solid var(--divider-color);
border-radius:14px;
padding:12px;
background: rgba(0,0,0,.02);
position:relative;
overflow:hidden;
}
.readmeWrap.collapsed{
max-height:260px;
}
@media (min-width: 1024px){
.readmeWrap.collapsed{ max-height:340px; }
}
.readmeWrap.collapsed::after{
content:"";
position:absolute;
left:0; right:0; bottom:0;
height:64px;
background: linear-gradient(to bottom, rgba(0,0,0,0), var(--card-background-color));
pointer-events:none;
}
.readmeWrap.expanded{
max-height:70vh;
overflow:auto;
-webkit-overflow-scrolling:touch;
}
.readmeActions{
display:flex; justify-content:flex-end;
margin-top:10px;
}
button.link{
border:1px solid transparent;
background: transparent;
color: var(--bcs-accent);
padding:8px 10px;
}
button.link:hover{
border-color: rgba(30,136,229,.25);
background: rgba(30,136,229,.06);
}
.md th, .md td{
border: 1px solid var(--divider-color);
padding: 8px;
text-align:left;
max-width:100%;
overflow-wrap:anywhere;
word-break:break-word;
}
</style>
<div class="mobilebar">
@@ -488,7 +535,7 @@ class BahmcloudStorePanel extends HTMLElement {
<div id="content"></div>
</div>
<div id="fabs"></div>
` ;
root . getElementById ( "menuBtn" ) . addEventListener ( "click" , ( ) => this . _toggleMenu ( ) ) ;
@@ -528,8 +575,7 @@ class BahmcloudStorePanel extends HTMLElement {
setActive ( "tabAbout" , this . _view === "about" ) ;
const content = root . getElementById ( "content" ) ;
const fabs = root . getElementById ( "fabs" ) ;
if ( ! content || ! fabs ) return ;
if ( ! content ) return ;
const err = this . _error
? ` <div class="err"><strong>Error:</strong> ${ this . _esc ( this . _error ) } </div> `
@@ -537,13 +583,11 @@ class BahmcloudStorePanel extends HTMLElement {
if ( this . _loading ) {
content . innerHTML = ` ${ err } <div class="card">Loading…</div> ` ;
fabs . innerHTML = "" ;
return ;
}
if ( ! this . _data ? . ok ) {
content . innerHTML = ` ${ err } <div class="card">No data. Please refresh.</div> ` ;
fabs . innerHTML = "" ;
return ;
}
@@ -554,13 +598,11 @@ class BahmcloudStorePanel extends HTMLElement {
else if ( this . _view === "detail" ) html = this . _renderDetail ( ) ;
content . innerHTML = ` ${ err } ${ html } ` ;
fabs . innerHTML = this . _view === "detail" ? this . _renderFabs ( ) : "" ;
if ( this . _view === "store" ) this . _wireStore ( ) ;
if ( this . _view === "manage" ) this . _wireManage ( ) ;
if ( this . _view === "detail" ) {
this . _wireDetail ( ) ; // now always wires buttons
this . _wireFabs ( ) ;
}
}
@@ -752,7 +794,14 @@ class BahmcloudStorePanel extends HTMLElement {
<div class="muted small">Rendered Markdown</div>
</div>
<div id ="readmePretty" class="md" style="margin-top:12px;"></div >
<div class ="readmeWrap ${ this . _readmeExpanded ? "expanded" : "collapsed" } " >
<div id="readmePretty" class="md"></div>
</div>
${ this . _readmeCanToggle ? `
<div class="readmeActions">
<button class="link" id="btnReadmeToggle"> ${ this . _readmeExpanded ? "Show less" : "Show more" } </button>
</div>
` : ` ` }
<details>
<summary>Show raw Markdown</summary>
@@ -855,6 +904,7 @@ class BahmcloudStorePanel extends HTMLElement {
const btnUpdate = root . getElementById ( "btnUpdate" ) ;
const btnUninstall = root . getElementById ( "btnUninstall" ) ;
const btnRestart = root . getElementById ( "btnRestart" ) ;
const btnReadmeToggle = root . getElementById ( "btnReadmeToggle" ) ;
if ( btnInstall ) {
btnInstall . addEventListener ( "click" , ( ) => {
@@ -881,6 +931,13 @@ class BahmcloudStorePanel extends HTMLElement {
btnRestart . addEventListener ( "click" , ( ) => this . _restartHA ( ) ) ;
}
if ( btnReadmeToggle ) {
btnReadmeToggle . addEventListener ( "click" , ( ) => {
this . _readmeExpanded = ! this . _readmeExpanded ;
this . _update ( ) ;
} ) ;
}
const mount = root . getElementById ( "readmePretty" ) ;
if ( ! mount ) return ;
@@ -907,75 +964,6 @@ class BahmcloudStorePanel extends HTMLElement {
} catch ( _ ) { }
}
_renderFabs ( ) {
const r = this . _detailRepo ;
if ( ! r ) return "" ;
const repoId = this . _safeId ( r ? . id ) ;
const installed = this . _asBoolStrict ( r ? . installed ) ;
const latest = this . _safeText ( r ? . latest _version ) ;
const installedVersion = this . _safeText ( r ? . installed _version ) ;
const busy = this . _installingRepoId === repoId || this . _updatingRepoId === repoId || this . _uninstallingRepoId === repoId ;
const updateAvailable = installed && ! ! latest && ( ! installedVersion || latest !== installedVersion ) ;
const installDisabled = installed || busy ;
const updateDisabled = ! updateAvailable || busy ;
const uninstallDisabled = ! installed || busy ;
return `
<div class="fabs">
<button class="fabbtn primary" id="fabOpen" title="Open repository">↗</button>
<button class="fabbtn" id="fabReload" title="Reload README">⟳</button>
<button class="fabbtn" id="fabInstall" title=" ${ installDisabled ? ( installed ? "Already installed" : "Installing…" ) : "Install" } " ${ installDisabled ? "disabled" : "" } >+ </button>
<button class="fabbtn" id="fabUpdate" title=" ${ updateDisabled ? ( ! installed ? "Not installed" : "No update available" ) : "Update" } " ${ updateDisabled ? "disabled" : "" } >↑</button>
<button class="fabbtn" id="fabUninstall" title=" ${ uninstallDisabled ? ( ! installed ? "Not installed" : "Busy" ) : "Uninstall" } " ${ uninstallDisabled ? "disabled" : "" } >✕</button>
<button class="fabbtn" id="fabInfo" title="About">i</button>
</div>
` ;
}
_wireFabs ( ) {
const root = this . shadowRoot ;
const r = this . _detailRepo ;
if ( ! r ) return ;
const url = this . _safeText ( r ? . url ) ;
const repoId = this . _safeId ( r ? . id ) ;
const open = root . getElementById ( "fabOpen" ) ;
const reload = root . getElementById ( "fabReload" ) ;
const install = root . getElementById ( "fabInstall" ) ;
const update = root . getElementById ( "fabUpdate" ) ;
const uninstall = root . getElementById ( "fabUninstall" ) ;
const info = root . getElementById ( "fabInfo" ) ;
if ( open ) open . addEventListener ( "click" , ( ) => url && window . open ( url , "_blank" , "noreferrer" ) ) ;
if ( reload ) reload . addEventListener ( "click" , ( ) => this . _detailRepoId && this . _loadReadme ( this . _detailRepoId ) ) ;
if ( install ) {
install . addEventListener ( "click" , ( ) => {
if ( install . disabled ) return ;
this . _installRepo ( repoId ) ;
} ) ;
}
if ( update ) {
update . addEventListener ( "click" , ( ) => {
if ( update . disabled ) return ;
this . _updateRepo ( repoId ) ;
} ) ;
}
if ( uninstall ) {
uninstall . addEventListener ( "click" , ( ) => {
if ( uninstall . disabled ) return ;
this . _uninstallRepo ( repoId ) ;
} ) ;
}
if ( info ) info . addEventListener ( "click" , ( ) => { this . _view = "about" ; this . _update ( ) ; } ) ;
}
_renderManage ( ) {
const repos = Array . isArray ( this . _data . repos ) ? this . _data . repos : [ ] ;